From 84222254647bd44226e6ddea93019db2a0995a4b Mon Sep 17 00:00:00 2001 From: darmody Date: Thu, 30 Jun 2022 14:52:13 +0800 Subject: [PATCH] works! --- .../ParagraphView/ParagraphView.tsx | 2 + .../bubbleMenu/bubbleMenuViewPlugin.ts | 3 +- .../selection/MultipleNodeSelection.ts | 8 +- .../extensions/selection/selection.ts | 184 ++++++++++++++++-- 4 files changed, 181 insertions(+), 16 deletions(-) diff --git a/packages/editor/src/components/blockViews/ParagraphView/ParagraphView.tsx b/packages/editor/src/components/blockViews/ParagraphView/ParagraphView.tsx index d0d95e5b7..17bd6fba8 100644 --- a/packages/editor/src/components/blockViews/ParagraphView/ParagraphView.tsx +++ b/packages/editor/src/components/blockViews/ParagraphView/ParagraphView.tsx @@ -28,6 +28,8 @@ export const ParagraphView: FC = props => { const placeholderClassName = placeholderStyle() const paragraphClassName = paragraphStyles() + console.log(props) + return ( { - ranges.push(new SelectionRange(doc.resolve(pos + 1), doc.resolve(pos + node.nodeSize))) + const from = Math.min($anchorPos.pos, $headPos.pos) + const to = Math.max($anchorPos.pos, $headPos.pos) + + doc.nodesBetween(from, to, (node, pos) => { + ranges.push(new SelectionRange(doc.resolve(pos), doc.resolve(pos + node.nodeSize))) return false }) @@ -150,6 +153,7 @@ export class MultipleNodeSelection extends Selection { * @returns */ static create(doc: Node, anchor: number, head: number = anchor): MultipleNodeSelection { + console.log(anchor, head) return new MultipleNodeSelection(doc.resolve(anchor), doc.resolve(head)) } } diff --git a/packages/editor/src/extensions/extensions/selection/selection.ts b/packages/editor/src/extensions/extensions/selection/selection.ts index c52695912..99ee434ae 100644 --- a/packages/editor/src/extensions/extensions/selection/selection.ts +++ b/packages/editor/src/extensions/extensions/selection/selection.ts @@ -1,6 +1,6 @@ import { Editor } from '@tiptap/core' import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' -import { Decoration, DecorationSet } from 'prosemirror-view' +import { Decoration, DecorationSet, EditorView } from 'prosemirror-view' import { findNodesInSelection } from '../../../helpers' import { createExtension } from '../../common' import { meta, SelectAttributes, SelectionOptions } from './meta' @@ -13,6 +13,18 @@ import { meta as taskItemMeta } from '../../blocks/taskItem/meta' import { meta as taskListMeta } from '../../blocks/taskList/meta' import { meta as calloutMeta } from '../../blocks/callout/meta' import { meta as blockquoteMeta } from '../../blocks/blockquote/meta' +import { MultipleNodeSelection } from './MultipleNodeSelection' + +declare module '@tiptap/core' { + interface Commands { + multipleNodeSelection: { + /** + * Set a multiple node selection + */ + setMultipleNodeSelection: (anchor: number, head: number) => ReturnType + } + } +} const allowedNodeTypes = [ paragraphMeta.name, @@ -56,15 +68,126 @@ export const isTextContentSelected = ({ editor, from, to }: { editor: Editor; fr export const DEFAULT_SELECTION_CLASS = 'editor-selection' +function resolveCoordinates( + anchor: { x: number; y: number }, + head: { + x: number + y: number + }, + docRect: { + top: number + left: number + bottom: number + right: number + } +): null | { + anchor: { + x: number + y: number + } + head: { + x: number + y: number + } +} { + if (anchor.y < docRect.top && head.y < docRect.top) return null + if (anchor.y > docRect.bottom && head.y > docRect.bottom) return null + if (anchor.x < docRect.left && head.x < docRect.left) return null + if (anchor.x > docRect.right && head.x > docRect.right) return null + + return { + anchor: { + x: Math.max(Math.min(docRect.right, anchor.x), docRect.left), + y: Math.max(Math.min(docRect.bottom, anchor.y), docRect.top) + }, + head: { + x: Math.max(Math.min(docRect.right, head.x), docRect.left), + y: Math.max(Math.min(docRect.bottom, head.y), docRect.top) + } + } +} + +function resolveSelection(editor: Editor, view: EditorView, pluginState: SelectionState, event: MouseEvent): void { + if (!pluginState.multiNodeSelecting) return + + const { x, y } = pluginState.multiNodeSelecting + const rect = view.dom.getBoundingClientRect() + + const coordinates = resolveCoordinates({ x, y }, { x: event.x, y: event.y }, rect) + + if (!coordinates) return + + const anchor = view.posAtCoords({ left: coordinates.anchor.x, top: coordinates.anchor.y }) + const head = view.posAtCoords({ left: coordinates.head.x, top: coordinates.head.y }) + + if (!anchor || !head) return + + if (anchor.pos === head.pos) return + + editor.commands.setMultipleNodeSelection(anchor.pos, head.pos) +} + interface SelectionState { - multiNodeSelecting: boolean + multiNodeSelecting: + | false + | { + x: number + y: number + } } -export const SelectionPluginKey = new PluginKey(meta.name) +export const SelectionPluginKey = new PluginKey(meta.name) + +class SelectionView { + view: EditorView + editor: Editor + + constructor(editor: Editor, view: EditorView) { + this.editor = editor + this.view = view + + window.addEventListener('mouseup', this.mouseup) + } + + mouseup = (event: MouseEvent) => { + const pluginState = SelectionPluginKey.getState(this.view.state) + if (pluginState?.multiNodeSelecting) { + resolveSelection(this.editor, this.view, pluginState, event) + this.view.dispatch(this.view.state.tr.setMeta('multipleNodeSelectingEnd', true)) + this.editor.commands.focus() + } + } + + destroy(): void { + window.removeEventListener('mouseup', this.mouseup) + } +} export const Selection = createExtension({ name: meta.name, + addCommands() { + return { + setMultipleNodeSelection: + (anchor, head) => + ({ dispatch, tr }) => { + if (dispatch) { + const { doc } = tr + const minPos = TextSelection.atStart(doc).from + const maxPos = TextSelection.atEnd(doc).to + + const resolvedAnchor = Math.max(Math.min(anchor, maxPos), minPos) + const resolvedHead = Math.max(Math.min(head, maxPos), minPos) + const selection = MultipleNodeSelection.create(doc, resolvedAnchor, resolvedHead) + + tr.setSelection(selection) + } + + return true + } + } + }, + addProseMirrorPlugins() { return [ new Plugin({ @@ -76,9 +199,19 @@ export const Selection = createExtension({ } }, apply(tr, value, oldState, newState) { + const coordinates = tr.getMeta('multipleNodeSelectingStart') + if (coordinates) { + return { ...value, multiNodeSelecting: coordinates } + } + + if (tr.getMeta('multipleNodeSelectingEnd')) { + return { ...value, multiNodeSelecting: false } + } + return { ...value } } }, + view: view => new SelectionView(this.editor, view), props: { handleDOMEvents: { mousedown: (view, event) => { @@ -87,12 +220,21 @@ export const Selection = createExtension({ // if click happens outside the doc nodes, assume a Multiple Node Selection will happen. // so call blur to avoid Text Selection happen. if (result?.inside === -1) { + view.dispatch( + view.state.tr.setMeta('multipleNodeSelectingStart', { + x: event.x, + y: event.y + }) + ) this.editor.commands.blur() } return false }, - mouseup: () => { - this.editor.commands.focus() + mousemove: (view, event) => { + const pluginState = SelectionPluginKey.getState(view.state) + if (!pluginState) return false + + resolveSelection(this.editor, view, pluginState, event) return false } @@ -101,18 +243,34 @@ export const Selection = createExtension({ const { from, to } = state.selection const decorations: Decoration[] = [] - if (!this.editor.isEditable || from === to) return + if (!this.editor.isEditable) return + if (state.selection instanceof TextSelection && from === to) return + + if (state.selection instanceof TextSelection) { + const inlineDecoration = Decoration.inline(from, to, { + class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS, + style: this.options.HTMLAttributes?.style + }) + decorations.push(inlineDecoration) - const inlineDecoration = Decoration.inline(from, to, { - class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS, - style: this.options.HTMLAttributes?.style - }) - decorations.push(inlineDecoration) + return DecorationSet.create(this.editor.state.doc, decorations) + } + + if (state.selection instanceof MultipleNodeSelection) { + state.selection.ranges.forEach(range => { + const nodeDecoration = Decoration.node(range.$from.pos, range.$to.pos, { + class: this.options.HTMLAttributes?.class ?? DEFAULT_SELECTION_CLASS, + style: this.options.HTMLAttributes?.style + }) + decorations.push(nodeDecoration) + }) - return DecorationSet.create(this.editor.state.doc, decorations) + return DecorationSet.create(this.editor.state.doc, decorations) + } }, createSelectionBetween: (view, $anchor, $head) => { - if (!this.editor.isEditable || $anchor.pos === $head.pos) return + if (!this.editor.isEditable) return + if ($anchor.pos === $head.pos) return // remove non-text node and empty text node selection by check: to - from > 1 // because we don't want non-text nodes at the end of Text Selection