|
1 | | -import {Plugin, PluginKey} from 'prosemirror-state'; |
| 1 | +import throttle from 'lodash/throttle'; |
| 2 | +import {Plugin, PluginKey, PluginView} from 'prosemirror-state'; |
2 | 3 | import type {EditorView} from 'prosemirror-view'; |
3 | 4 | import type {ResolvedPos} from 'prosemirror-model'; |
| 5 | +import {findDomRefAtPos} from 'prosemirror-utils'; |
4 | 6 | import {isTextSelection} from '../../../../utils/selection'; |
5 | 7 | import {cutContentType, cutType} from '../const'; |
6 | 8 |
|
7 | 9 | const key = new PluginKey('yfm-cut-auto-open'); |
8 | 10 |
|
9 | | -export const cutAutoOpenPlugin = () => |
10 | | - new Plugin({ |
| 11 | +export const cutAutoOpenPlugin = () => { |
| 12 | + return new Plugin({ |
11 | 13 | key, |
12 | 14 | view(view) { |
13 | 15 | update(view); |
| 16 | + const dragHandler = new CutAutoOpenOnDragOver(view); |
14 | 17 | return { |
15 | 18 | update: (view) => update(view), |
| 19 | + destroy: () => dragHandler.destroy(), |
16 | 20 | }; |
17 | 21 | }, |
18 | 22 | }); |
| 23 | +}; |
19 | 24 |
|
20 | 25 | function update(view: EditorView) { |
21 | 26 | const sel = view.state.selection; |
@@ -44,3 +49,61 @@ function openParentYfmCuts($pos: ResolvedPos, domAtPos: EditorView['domAtPos']): |
44 | 49 | depth--; |
45 | 50 | } |
46 | 51 | } |
| 52 | + |
| 53 | +class CutAutoOpenOnDragOver implements PluginView { |
| 54 | + private static readonly YFM_CUT_SELECTOR = '.yfm-cut:not(.open)'; |
| 55 | + private static readonly OPEN_TIMEOUT = 500; //ms |
| 56 | + private static readonly THROTTLE_WAIT = 50; //ms |
| 57 | + |
| 58 | + private _cutElem: HTMLElement | null = null; |
| 59 | + private _editorView: EditorView; |
| 60 | + private _timeout: ReturnType<typeof setTimeout> | null = null; |
| 61 | + private readonly _docListener; |
| 62 | + |
| 63 | + constructor(view: EditorView) { |
| 64 | + this._editorView = view; |
| 65 | + this._docListener = throttle( |
| 66 | + this._onDocEvent.bind(this), |
| 67 | + CutAutoOpenOnDragOver.THROTTLE_WAIT, |
| 68 | + ); |
| 69 | + document.addEventListener('mousemove', this._docListener); |
| 70 | + document.addEventListener('dragover', this._docListener); |
| 71 | + } |
| 72 | + |
| 73 | + destroy(): void { |
| 74 | + this._clear(); |
| 75 | + this._docListener.cancel(); |
| 76 | + document.removeEventListener('mousemove', this._docListener); |
| 77 | + document.removeEventListener('dragover', this._docListener); |
| 78 | + } |
| 79 | + |
| 80 | + private _onDocEvent(event: MouseEvent) { |
| 81 | + const view = this._editorView; |
| 82 | + if (!view.dragging) return; |
| 83 | + const pos = view.posAtCoords({left: event.clientX, top: event.clientY}); |
| 84 | + if (!pos) return; |
| 85 | + const elem = findDomRefAtPos(pos.pos, view.domAtPos.bind(view)) as HTMLElement; |
| 86 | + const cutElem = elem.closest(CutAutoOpenOnDragOver.YFM_CUT_SELECTOR); |
| 87 | + if (cutElem === this._cutElem) return; |
| 88 | + this._clear(); |
| 89 | + if (cutElem) this._setCutElem(cutElem as HTMLElement); |
| 90 | + } |
| 91 | + |
| 92 | + private _clear() { |
| 93 | + if (this._timeout !== null) clearTimeout(this._timeout); |
| 94 | + this._timeout = null; |
| 95 | + this._cutElem = null; |
| 96 | + } |
| 97 | + |
| 98 | + private _setCutElem(elem: HTMLElement) { |
| 99 | + this._cutElem = elem; |
| 100 | + this._timeout = setTimeout(this._openCut.bind(this), CutAutoOpenOnDragOver.OPEN_TIMEOUT); |
| 101 | + } |
| 102 | + |
| 103 | + private _openCut() { |
| 104 | + if (this._editorView.dragging) { |
| 105 | + this._cutElem?.classList.add('open'); |
| 106 | + } |
| 107 | + this._clear(); |
| 108 | + } |
| 109 | +} |
0 commit comments