@malva-ui/editor 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/fesm2022/malva-ui-editor-ai.mjs +17 -0
- package/fesm2022/malva-ui-editor-ai.mjs.map +1 -0
- package/fesm2022/malva-ui-editor.mjs +10227 -0
- package/fesm2022/malva-ui-editor.mjs.map +1 -0
- package/package.json +35 -26
- package/types/malva-ui-editor-ai.d.ts +1 -0
- package/types/malva-ui-editor.d.ts +2995 -0
- package/ai/index.d.ts +0 -12
- package/ai/malva-ui-editor-ai.d.ts +0 -5
- package/esm2022/ai/index.js +0 -12
- package/esm2022/ai/index.js.map +0 -1
- package/esm2022/ai/malva-ui-editor-ai.js +0 -5
- package/esm2022/ai/malva-ui-editor-ai.js.map +0 -1
- package/esm2022/index.js +0 -37
- package/esm2022/index.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-actions.js +0 -69
- package/esm2022/lib/ai/editor-ai-actions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-context.js +0 -612
- package/esm2022/lib/ai/editor-ai-context.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-menu.js +0 -288
- package/esm2022/lib/ai/editor-ai-menu.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-review-bar.js +0 -223
- package/esm2022/lib/ai/editor-ai-review-bar.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-stream.js +0 -677
- package/esm2022/lib/ai/editor-ai-stream.js.map +0 -1
- package/esm2022/lib/ai/editor-ai-suggestions.js +0 -751
- package/esm2022/lib/ai/editor-ai-suggestions.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.tokens.js +0 -10
- package/esm2022/lib/ai/editor-ai.tokens.js.map +0 -1
- package/esm2022/lib/ai/editor-ai.types.js +0 -1
- package/esm2022/lib/ai/editor-ai.types.js.map +0 -1
- package/esm2022/lib/editor/editor.js +0 -985
- package/esm2022/lib/editor/editor.js.map +0 -1
- package/esm2022/lib/editor-extension-preflight.js +0 -83
- package/esm2022/lib/editor-extension-preflight.js.map +0 -1
- package/esm2022/lib/editor-serialization.js +0 -152
- package/esm2022/lib/editor-serialization.js.map +0 -1
- package/esm2022/lib/editor-toolbar-context.js +0 -259
- package/esm2022/lib/editor-toolbar-context.js.map +0 -1
- package/esm2022/lib/editor.tokens.js +0 -10
- package/esm2022/lib/editor.tokens.js.map +0 -1
- package/esm2022/lib/editor.types.js +0 -48
- package/esm2022/lib/editor.types.js.map +0 -1
- package/esm2022/lib/extensions/editor-block-handle.js +0 -760
- package/esm2022/lib/extensions/editor-block-handle.js.map +0 -1
- package/esm2022/lib/extensions/editor-extensions.js +0 -321
- package/esm2022/lib/extensions/editor-extensions.js.map +0 -1
- package/esm2022/lib/extensions/editor-markdown-compatibility.js +0 -162
- package/esm2022/lib/extensions/editor-markdown-compatibility.js.map +0 -1
- package/esm2022/lib/extensions/editor-upload-placeholder.js +0 -156
- package/esm2022/lib/extensions/editor-upload-placeholder.js.map +0 -1
- package/esm2022/lib/status/editor-status.js +0 -64
- package/esm2022/lib/status/editor-status.js.map +0 -1
- package/esm2022/lib/toolbar/editor-alignment.js +0 -203
- package/esm2022/lib/toolbar/editor-alignment.js.map +0 -1
- package/esm2022/lib/toolbar/editor-block-insert.js +0 -101
- package/esm2022/lib/toolbar/editor-block-insert.js.map +0 -1
- package/esm2022/lib/toolbar/editor-color-control.js +0 -71
- package/esm2022/lib/toolbar/editor-color-control.js.map +0 -1
- package/esm2022/lib/toolbar/editor-command-button.js +0 -115
- package/esm2022/lib/toolbar/editor-command-button.js.map +0 -1
- package/esm2022/lib/toolbar/editor-heading.js +0 -266
- package/esm2022/lib/toolbar/editor-heading.js.map +0 -1
- package/esm2022/lib/toolbar/editor-highlight.js +0 -84
- package/esm2022/lib/toolbar/editor-highlight.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js +0 -289
- package/esm2022/lib/toolbar/editor-image-upload-dialog.js.map +0 -1
- package/esm2022/lib/toolbar/editor-image-upload.js +0 -315
- package/esm2022/lib/toolbar/editor-image-upload.js.map +0 -1
- package/esm2022/lib/toolbar/editor-inline-marks.js +0 -124
- package/esm2022/lib/toolbar/editor-inline-marks.js.map +0 -1
- package/esm2022/lib/toolbar/editor-link.js +0 -265
- package/esm2022/lib/toolbar/editor-link.js.map +0 -1
- package/esm2022/lib/toolbar/editor-list.js +0 -249
- package/esm2022/lib/toolbar/editor-list.js.map +0 -1
- package/esm2022/lib/toolbar/editor-table.js +0 -292
- package/esm2022/lib/toolbar/editor-table.js.map +0 -1
- package/esm2022/lib/toolbar/editor-text-color.js +0 -82
- package/esm2022/lib/toolbar/editor-text-color.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar-widget.js +0 -41
- package/esm2022/lib/toolbar/editor-toolbar-widget.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.defs.js +0 -60
- package/esm2022/lib/toolbar/editor-toolbar.defs.js.map +0 -1
- package/esm2022/lib/toolbar/editor-toolbar.js +0 -826
- package/esm2022/lib/toolbar/editor-toolbar.js.map +0 -1
- package/esm2022/lib/toolbar/editor-undo-redo.js +0 -72
- package/esm2022/lib/toolbar/editor-undo-redo.js.map +0 -1
- package/esm2022/lib/toolbar/editor-zoom.js +0 -330
- package/esm2022/lib/toolbar/editor-zoom.js.map +0 -1
- package/esm2022/lib/upload/editor-image-upload-coordinator.js +0 -480
- package/esm2022/lib/upload/editor-image-upload-coordinator.js.map +0 -1
- package/esm2022/malva-ui-editor.js +0 -5
- package/esm2022/malva-ui-editor.js.map +0 -1
- package/index.d.ts +0 -35
- package/lib/ai/editor-ai-actions.d.ts +0 -27
- package/lib/ai/editor-ai-context.d.ts +0 -307
- package/lib/ai/editor-ai-menu.d.ts +0 -142
- package/lib/ai/editor-ai-review-bar.d.ts +0 -116
- package/lib/ai/editor-ai-stream.d.ts +0 -207
- package/lib/ai/editor-ai-suggestions.d.ts +0 -219
- package/lib/ai/editor-ai.tokens.d.ts +0 -10
- package/lib/ai/editor-ai.types.d.ts +0 -176
- package/lib/editor/editor.d.ts +0 -243
- package/lib/editor-extension-preflight.d.ts +0 -23
- package/lib/editor-serialization.d.ts +0 -74
- package/lib/editor-toolbar-context.d.ts +0 -116
- package/lib/editor.tokens.d.ts +0 -6
- package/lib/editor.types.d.ts +0 -194
- package/lib/extensions/editor-block-handle.d.ts +0 -59
- package/lib/extensions/editor-extensions.d.ts +0 -165
- package/lib/extensions/editor-markdown-compatibility.d.ts +0 -11
- package/lib/extensions/editor-upload-placeholder.d.ts +0 -75
- package/lib/status/editor-status.d.ts +0 -24
- package/lib/toolbar/editor-alignment.d.ts +0 -61
- package/lib/toolbar/editor-block-insert.d.ts +0 -26
- package/lib/toolbar/editor-color-control.d.ts +0 -32
- package/lib/toolbar/editor-command-button.d.ts +0 -29
- package/lib/toolbar/editor-heading.d.ts +0 -68
- package/lib/toolbar/editor-highlight.d.ts +0 -21
- package/lib/toolbar/editor-image-upload-dialog.d.ts +0 -96
- package/lib/toolbar/editor-image-upload.d.ts +0 -71
- package/lib/toolbar/editor-inline-marks.d.ts +0 -30
- package/lib/toolbar/editor-link.d.ts +0 -78
- package/lib/toolbar/editor-list.d.ts +0 -67
- package/lib/toolbar/editor-table.d.ts +0 -133
- package/lib/toolbar/editor-text-color.d.ts +0 -21
- package/lib/toolbar/editor-toolbar-widget.d.ts +0 -19
- package/lib/toolbar/editor-toolbar.d.ts +0 -157
- package/lib/toolbar/editor-toolbar.defs.d.ts +0 -35
- package/lib/toolbar/editor-undo-redo.d.ts +0 -26
- package/lib/toolbar/editor-zoom.d.ts +0 -108
- package/lib/upload/editor-image-upload-coordinator.d.ts +0 -55
- package/malva-ui-editor.d.ts +0 -5
|
@@ -1,760 +0,0 @@
|
|
|
1
|
-
import { Extension } from '@tiptap/core';
|
|
2
|
-
import { NodeSelection, Plugin, TextSelection } from '@tiptap/pm/state';
|
|
3
|
-
/** @internal Class of the floating drop indicator. */
|
|
4
|
-
const MLV_EDITOR_DROP_INDICATOR_CLASS = 'mlv-editor__drop-indicator';
|
|
5
|
-
/** @internal Class of the off-screen wrapper handed to `setDragImage`. */
|
|
6
|
-
const MLV_EDITOR_DRAG_GHOST_CLASS = 'mlv-editor__drag-ghost';
|
|
7
|
-
/**
|
|
8
|
-
* @internal Marks the editor DOM while a block drag is in flight, so the
|
|
9
|
-
* stylesheet — not this file — owns the partition's transition and its
|
|
10
|
-
* reduced-motion fallback. JavaScript writes only the `transform` itself.
|
|
11
|
-
*/
|
|
12
|
-
const MLV_EDITOR_DRAGGING_CLASS = 'ProseMirror--block-dragging';
|
|
13
|
-
/** @internal Marks the block currently on the cursor, dimmed in place. */
|
|
14
|
-
const MLV_EDITOR_DRAG_SOURCE_CLASS = 'mlv-editor__block--dragging';
|
|
15
|
-
/** @internal Duration of the post-drop settle, matching `--mlv-duration-normal`. */
|
|
16
|
-
const MLV_EDITOR_SETTLE_DURATION = 200;
|
|
17
|
-
/** @internal Sub-pixel movement not worth animating after a drop. */
|
|
18
|
-
const MLV_EDITOR_SETTLE_EPSILON = 0.5;
|
|
19
|
-
/** @internal Document position where the given top-level child begins. */
|
|
20
|
-
function childStart(doc, index) {
|
|
21
|
-
let position = 0;
|
|
22
|
-
for (let i = 0; i < index; i += 1) {
|
|
23
|
-
position += doc.child(i).nodeSize;
|
|
24
|
-
}
|
|
25
|
-
return position;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* @internal Document position `moveBlock({ from, to })` inserts at, expressed
|
|
29
|
-
* against the document as it stands **before** the move.
|
|
30
|
-
*
|
|
31
|
-
* Shared by the command and the drop indicator so the line the user sees can
|
|
32
|
-
* never point at a gap other than the one the drop actually uses.
|
|
33
|
-
*/
|
|
34
|
-
function insertAnchor(doc, from, to) {
|
|
35
|
-
return to > from
|
|
36
|
-
? childStart(doc, to) + doc.child(to).nodeSize
|
|
37
|
-
: childStart(doc, to);
|
|
38
|
-
}
|
|
39
|
-
/** @internal Reads the live scale of the mount layer instead of trusting a CSS variable. */
|
|
40
|
-
function layerScale(mount) {
|
|
41
|
-
const width = mount.offsetWidth;
|
|
42
|
-
if (!width)
|
|
43
|
-
return 1;
|
|
44
|
-
const scaled = mount.getBoundingClientRect().width;
|
|
45
|
-
return scaled > 0 ? scaled / width : 1;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* @internal Top-level child index for one rendered child of the editor DOM, or
|
|
49
|
-
* null when that child is not a top-level node at all.
|
|
50
|
-
*
|
|
51
|
-
* Indexing `view.dom.children` positionally would not do: widget decorations
|
|
52
|
-
* are rendered children that own no document node, so the two sequences do not
|
|
53
|
-
* line up. This library alone puts three of them at the top level — the drop
|
|
54
|
-
* indicator below, `MlvEditorUploadPlaceholder`'s pending image, and
|
|
55
|
-
* StarterKit's gap cursor — and any future block-level decoration joins them.
|
|
56
|
-
*
|
|
57
|
-
* `posAtDOM` on its own does not discriminate either, which is the whole
|
|
58
|
-
* difficulty: a widget anchored in a top-level gap maps to a perfectly valid
|
|
59
|
-
* index — the one belonging to the block it sits beside. So the resolved index
|
|
60
|
-
* is handed back to `nodeDOM`, and only the element ProseMirror actually
|
|
61
|
-
* rendered *for* that child may claim it. Containment rather than identity,
|
|
62
|
-
* because an outer node decoration wraps a node's own DOM in another element.
|
|
63
|
-
*/
|
|
64
|
-
function topLevelIndexOfDom(view, element) {
|
|
65
|
-
let position;
|
|
66
|
-
try {
|
|
67
|
-
position = view.posAtDOM(element, 0);
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
// `posAtDOM` throws for DOM the editor view does not own. A foreign child
|
|
71
|
-
// must not take a pointer handler down with it.
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
const doc = view.state.doc;
|
|
75
|
-
const index = doc.resolve(position).index(0);
|
|
76
|
-
if (index >= doc.childCount)
|
|
77
|
-
return null;
|
|
78
|
-
return element.contains(view.nodeDOM(childStart(doc, index))) ? index : null;
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* @internal Top-level block the given viewport y falls on, or the nearest one
|
|
82
|
-
* above it. Null for an empty document, or for a point the view cannot map.
|
|
83
|
-
*
|
|
84
|
-
* Resolves geometrically instead of through `EditorView.posAtCoords`, which
|
|
85
|
-
* cannot serve this affordance at all. `.ProseMirror` is the
|
|
86
|
-
* `max-inline-size: var(--mlv-editor-measure)` text column, so the gutter the
|
|
87
|
-
* handle lives in is outside `view.dom`'s rect by construction — and
|
|
88
|
-
* `posAtCoords` falls back to an `inRect(coords, view.dom.getBoundingClientRect())`
|
|
89
|
-
* test and returns null for every point outside it, in a real browser as much
|
|
90
|
-
* as under a test DOM.
|
|
91
|
-
*
|
|
92
|
-
* Rendered children lay out in document order, so the scan stops at the first
|
|
93
|
-
* one starting below the pointer, costing one `getBoundingClientRect()` and
|
|
94
|
-
* one index mapping per child down to the hovered one.
|
|
95
|
-
*
|
|
96
|
-
* Mapping inside the loop is what makes widget decorations skippable as a
|
|
97
|
-
* category rather than by class name, and it is not free: `posAtDOM`,
|
|
98
|
-
* `nodeDOM`, and `childStart` each walk linearly to the child they address, so
|
|
99
|
-
* the scan down to index `k` is quadratic in `k` — measured at roughly 4x per
|
|
100
|
-
* doubling, against 2x for the map-only-the-winner scan this replaced. That
|
|
101
|
-
* one was abandoned because it could not tell a widget from a block, and
|
|
102
|
-
* pairing a block's index with a widget's box drops the block on the wrong
|
|
103
|
-
* side of its neighbour. Correctness first; the cost is only paid down to the
|
|
104
|
-
* *hovered* child, so it is deep hovering in a long document that degrades.
|
|
105
|
-
* Restoring the linear scan without losing the category skip means deferring
|
|
106
|
-
* the mapping until a winner is picked and walking back over the few
|
|
107
|
-
* consecutive widgets — the clamp above the first block is what makes that
|
|
108
|
-
* more than a one-liner, so it is deliberately left as follow-up.
|
|
109
|
-
*/
|
|
110
|
-
function blockAtPoint(view, clientY) {
|
|
111
|
-
const children = view.dom.children;
|
|
112
|
-
let candidate = null;
|
|
113
|
-
for (let i = 0; i < children.length; i += 1) {
|
|
114
|
-
const element = children[i];
|
|
115
|
-
// A widget decoration owns no top-level node, and its box is a sliver
|
|
116
|
-
// rather than a block's. Letting one become the candidate would pair a
|
|
117
|
-
// neighbouring block's index with the widget's geometry, so
|
|
118
|
-
// `dropTargetIndex` would take that block's midpoint from the wrong box
|
|
119
|
-
// and land the drop on the wrong side of it — silently, in one undo step.
|
|
120
|
-
const index = topLevelIndexOfDom(view, element);
|
|
121
|
-
if (index === null)
|
|
122
|
-
continue;
|
|
123
|
-
const rect = element.getBoundingClientRect();
|
|
124
|
-
if (clientY < rect.top) {
|
|
125
|
-
// Above the first block clamps to it; in the gap between two blocks the
|
|
126
|
-
// preceding one was already recorded on the previous iteration.
|
|
127
|
-
candidate ??= { index, top: rect.top, bottom: rect.bottom };
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
candidate = { index, top: rect.top, bottom: rect.bottom };
|
|
131
|
-
if (clientY <= rect.bottom)
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
return candidate;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* @internal Pre-move `to` index for dropping the block at `from` onto `hit`,
|
|
138
|
-
* or null when that drop would leave the document unchanged.
|
|
139
|
-
*
|
|
140
|
-
* `blockAtPoint` clamps to the nearest block and carries no before/after bias,
|
|
141
|
-
* so the hovered block's own midpoint decides which of its two edges the
|
|
142
|
-
* dragged block would land on. That yields one of the `childCount + 1` gaps,
|
|
143
|
-
* which is then folded back onto `moveBlock`'s pre-move index convention:
|
|
144
|
-
* `to` names the block the moved one ends up *after* when moving down and
|
|
145
|
-
* *before* when moving up, so both gaps bordering the source are no-ops.
|
|
146
|
-
*/
|
|
147
|
-
function dropTargetIndex(hit, clientY, from) {
|
|
148
|
-
const gap = clientY < (hit.top + hit.bottom) / 2 ? hit.index : hit.index + 1;
|
|
149
|
-
const to = gap <= from ? gap : gap - 1;
|
|
150
|
-
return to === from ? null : to;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* @internal Measures every top-level block once, in the mount's unscaled space.
|
|
154
|
-
*
|
|
155
|
-
* Taken at `dragstart` and read by everything downstream, which is a
|
|
156
|
-
* correctness requirement rather than an optimisation. The partition applies
|
|
157
|
-
* `transform` to blocks, and a transform *is* reflected in
|
|
158
|
-
* `getBoundingClientRect()`. Resolving the target from live rects after moving
|
|
159
|
-
* them would feed this plugin's own output back into its input: the gap opens,
|
|
160
|
-
* the pointer is now over a different block, the target changes, the gap moves.
|
|
161
|
-
* A snapshot taken before any transform breaks that loop.
|
|
162
|
-
*
|
|
163
|
-
* Mount-relative rather than viewport-relative so drag-autoscroll cannot
|
|
164
|
-
* invalidate it — only the mount's own rect is re-read per event, one layout
|
|
165
|
-
* read instead of one per block. Unscaled for the same reason the handle's
|
|
166
|
-
* `top` is: a CSS length on a child of the zoom layer applies in that layer's
|
|
167
|
-
* space, while `getBoundingClientRect()` reports scaled screen pixels.
|
|
168
|
-
*/
|
|
169
|
-
function snapshotBlocks(view, mount) {
|
|
170
|
-
const scale = layerScale(mount);
|
|
171
|
-
const mountTop = mount.getBoundingClientRect().top;
|
|
172
|
-
const slots = [];
|
|
173
|
-
const children = view.dom.children;
|
|
174
|
-
for (let i = 0; i < children.length; i += 1) {
|
|
175
|
-
const element = children[i];
|
|
176
|
-
// Same category check the hover scan uses: a widget decoration owns no
|
|
177
|
-
// top-level node and must not occupy a slot, or the partition would shift
|
|
178
|
-
// it as though it were a block.
|
|
179
|
-
const index = topLevelIndexOfDom(view, element);
|
|
180
|
-
if (index === null)
|
|
181
|
-
continue;
|
|
182
|
-
const rect = element.getBoundingClientRect();
|
|
183
|
-
slots.push({
|
|
184
|
-
index,
|
|
185
|
-
top: (rect.top - mountTop) / scale,
|
|
186
|
-
bottom: (rect.bottom - mountTop) / scale,
|
|
187
|
-
element: element,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return slots;
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* @internal Slot the given mount-space y falls on, or the nearest one above it.
|
|
194
|
-
*
|
|
195
|
-
* Reproduces `blockAtPoint`'s clamping exactly — above the first block resolves
|
|
196
|
-
* to the first, a point in the gap between two blocks resolves to the
|
|
197
|
-
* preceding one, below the last resolves to the last — but against a static
|
|
198
|
-
* array, so it is a binary search rather than a scan that maps every child it
|
|
199
|
-
* passes.
|
|
200
|
-
*/
|
|
201
|
-
function slotAt(slots, y) {
|
|
202
|
-
if (slots.length === 0)
|
|
203
|
-
return null;
|
|
204
|
-
if (y < slots[0].top)
|
|
205
|
-
return slots[0];
|
|
206
|
-
let low = 0;
|
|
207
|
-
let high = slots.length - 1;
|
|
208
|
-
while (low < high) {
|
|
209
|
-
const middle = Math.ceil((low + high) / 2);
|
|
210
|
-
if (slots[middle].top <= y)
|
|
211
|
-
low = middle;
|
|
212
|
-
else
|
|
213
|
-
high = middle - 1;
|
|
214
|
-
}
|
|
215
|
-
return slots[low];
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* @internal Mount-space y of the boundary the given gap sits on, where `gap`
|
|
219
|
-
* counts the `slots.length + 1` positions a block can be inserted at.
|
|
220
|
-
*
|
|
221
|
-
* Deliberately the *pre-partition* boundary: the indicator is offset into the
|
|
222
|
-
* opened space by half the gap in CSS, so the gap's size stays a single token
|
|
223
|
-
* and this never has to resolve a rem into pixels.
|
|
224
|
-
*/
|
|
225
|
-
function gapOffset(slots, gap) {
|
|
226
|
-
if (gap <= 0)
|
|
227
|
-
return slots[0].top;
|
|
228
|
-
if (gap >= slots.length)
|
|
229
|
-
return slots[slots.length - 1].bottom;
|
|
230
|
-
return (slots[gap - 1].bottom + slots[gap].top) / 2;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* @internal Deep-clones an element with every computed style written inline.
|
|
234
|
-
*
|
|
235
|
-
* The clone is handed to `setDragImage` from `document.body`, outside every
|
|
236
|
-
* `.mlv-editor`/`.ProseMirror` selector that styles it in place. Inlining the
|
|
237
|
-
* resolved styles makes it self-contained, which is sturdier than rebuilding
|
|
238
|
-
* an ancestor class chain that would have to track whichever selectors happen
|
|
239
|
-
* to exist. Adapted from `@tiptap/extension-drag-handle`'s `cloneElement`.
|
|
240
|
-
*
|
|
241
|
-
* Note what this cannot carry: computed styles are used values, and an
|
|
242
|
-
* ancestor `transform: scale()` never reaches them. The editor's zoom is
|
|
243
|
-
* applied to the wrapper instead — see `createGhostElement`.
|
|
244
|
-
*/
|
|
245
|
-
function cloneWithComputedStyles(source) {
|
|
246
|
-
const clone = source.cloneNode(true);
|
|
247
|
-
const sources = [
|
|
248
|
-
source,
|
|
249
|
-
...source.querySelectorAll('*'),
|
|
250
|
-
];
|
|
251
|
-
const targets = [
|
|
252
|
-
clone,
|
|
253
|
-
...clone.querySelectorAll('*'),
|
|
254
|
-
];
|
|
255
|
-
sources.forEach((element, index) => {
|
|
256
|
-
const target = targets[index];
|
|
257
|
-
if (!target)
|
|
258
|
-
return;
|
|
259
|
-
const computed = getComputedStyle(element);
|
|
260
|
-
let declarations = '';
|
|
261
|
-
for (let i = 0; i < computed.length; i += 1) {
|
|
262
|
-
declarations += `${computed[i]}:${computed.getPropertyValue(computed[i])};`;
|
|
263
|
-
}
|
|
264
|
-
target.style.cssText = declarations;
|
|
265
|
-
});
|
|
266
|
-
// The block's own margins would otherwise pad the drag image with empty
|
|
267
|
-
// space, since the clone has no siblings to collapse against.
|
|
268
|
-
clone.style.margin = '0';
|
|
269
|
-
return clone;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* @internal Builds the off-screen element handed to `setDragImage`.
|
|
273
|
-
*
|
|
274
|
-
* `position: absolute; top: -10000px` keeps it rendered — which the drag image
|
|
275
|
-
* requires — without ever being seen. `zoom` rather than `transform` carries
|
|
276
|
-
* the editor's scale because it affects layout, so the wrapper's own box
|
|
277
|
-
* reflects it before the browser rasterizes; a `transform` is honoured
|
|
278
|
-
* inconsistently across engines for drag images.
|
|
279
|
-
*/
|
|
280
|
-
function createGhostElement(source, scale) {
|
|
281
|
-
const wrapper = document.createElement('div');
|
|
282
|
-
wrapper.className = MLV_EDITOR_DRAG_GHOST_CLASS;
|
|
283
|
-
wrapper.setAttribute('aria-hidden', 'true');
|
|
284
|
-
wrapper.style.position = 'absolute';
|
|
285
|
-
wrapper.style.top = '-10000px';
|
|
286
|
-
wrapper.style.insetInlineStart = '0';
|
|
287
|
-
wrapper.style.inlineSize = `${source.offsetWidth}px`;
|
|
288
|
-
if (scale !== 1)
|
|
289
|
-
wrapper.style.setProperty('zoom', String(scale));
|
|
290
|
-
wrapper.appendChild(cloneWithComputedStyles(source));
|
|
291
|
-
document.body.appendChild(wrapper);
|
|
292
|
-
return wrapper;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* @internal Whether the user asked for reduced motion.
|
|
296
|
-
*
|
|
297
|
-
* Guarded because jsdom implements no `matchMedia`; absent the query, motion
|
|
298
|
-
* is allowed and the animation itself is separately guarded on `animate`.
|
|
299
|
-
*/
|
|
300
|
-
function prefersReducedMotion() {
|
|
301
|
-
return (typeof matchMedia === 'function' &&
|
|
302
|
-
matchMedia('(prefers-reduced-motion: reduce)').matches);
|
|
303
|
-
}
|
|
304
|
-
/** @internal Creates the single floating drop indicator element. */
|
|
305
|
-
function createIndicatorElement() {
|
|
306
|
-
const element = document.createElement('div');
|
|
307
|
-
element.className = MLV_EDITOR_DROP_INDICATOR_CLASS;
|
|
308
|
-
element.setAttribute('aria-hidden', 'true');
|
|
309
|
-
element.setAttribute('data-visible', 'false');
|
|
310
|
-
return element;
|
|
311
|
-
}
|
|
312
|
-
/** @internal Creates the single floating handle element. */
|
|
313
|
-
function createHandleElement(label) {
|
|
314
|
-
const element = document.createElement('div');
|
|
315
|
-
element.className = 'mlv-editor__block-handle';
|
|
316
|
-
element.setAttribute('draggable', 'true');
|
|
317
|
-
element.setAttribute('aria-hidden', 'true');
|
|
318
|
-
element.setAttribute('data-visible', 'false');
|
|
319
|
-
element.title = label;
|
|
320
|
-
element.innerHTML =
|
|
321
|
-
// Sized from `.mlv-editor__block-handle svg`; no intrinsic width/height
|
|
322
|
-
// attributes, so the icon scales with the block's tokens.
|
|
323
|
-
'<svg viewBox="0 0 24 24" focusable="false" aria-hidden="true">' +
|
|
324
|
-
'<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/>' +
|
|
325
|
-
'<circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/>' +
|
|
326
|
-
'<circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/></svg>';
|
|
327
|
-
return element;
|
|
328
|
-
}
|
|
329
|
-
/** @internal Top-level index containing the selection, or null for an empty document. */
|
|
330
|
-
function topLevelIndexAtSelection(state) {
|
|
331
|
-
if (state.doc.childCount === 0)
|
|
332
|
-
return null;
|
|
333
|
-
const index = state.selection.$from.index(0);
|
|
334
|
-
return index < state.doc.childCount ? index : null;
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Tiptap extension supplying top-level block reordering.
|
|
338
|
-
*
|
|
339
|
-
* The pointer handle and the keyboard shortcuts both route through `moveBlock`,
|
|
340
|
-
* so the two paths cannot drift apart. The handle itself is rendered by this
|
|
341
|
-
* extension's ProseMirror plugin into the host-supplied mount container and is
|
|
342
|
-
* never part of the document, so no serialized representation can contain it.
|
|
343
|
-
*/
|
|
344
|
-
export const MlvEditorBlockHandle = Extension.create({
|
|
345
|
-
name: 'mlvEditorBlockHandle',
|
|
346
|
-
addOptions() {
|
|
347
|
-
return {
|
|
348
|
-
mount: () => null,
|
|
349
|
-
label: () => 'Drag block',
|
|
350
|
-
announceMove: () => undefined,
|
|
351
|
-
enabled: () => true,
|
|
352
|
-
};
|
|
353
|
-
},
|
|
354
|
-
addCommands() {
|
|
355
|
-
return {
|
|
356
|
-
moveBlock: ({ from, to }) => ({ state, tr, dispatch }) => {
|
|
357
|
-
if (!this.options.enabled())
|
|
358
|
-
return false;
|
|
359
|
-
if (!Number.isInteger(from) || !Number.isInteger(to))
|
|
360
|
-
return false;
|
|
361
|
-
const total = state.doc.childCount;
|
|
362
|
-
if (from < 0 || from >= total)
|
|
363
|
-
return false;
|
|
364
|
-
if (to < 0 || to >= total)
|
|
365
|
-
return false;
|
|
366
|
-
if (from === to)
|
|
367
|
-
return false;
|
|
368
|
-
if (!dispatch)
|
|
369
|
-
return true;
|
|
370
|
-
const node = state.doc.child(from);
|
|
371
|
-
const start = childStart(state.doc, from);
|
|
372
|
-
const end = start + node.nodeSize;
|
|
373
|
-
const anchor = insertAnchor(state.doc, from, to);
|
|
374
|
-
tr.delete(start, end);
|
|
375
|
-
const insertAt = tr.mapping.map(anchor);
|
|
376
|
-
tr.insert(insertAt, node);
|
|
377
|
-
// The selection must land *inside* the moved node so a following
|
|
378
|
-
// moveBlockUp/Down keeps acting on it. An atom has no position
|
|
379
|
-
// inside it — `insertAt + 1` is already past a leaf such as
|
|
380
|
-
// horizontalRule, and TextSelection.near's forward bias would then
|
|
381
|
-
// walk into the next block — so select the node itself instead.
|
|
382
|
-
tr.setSelection(node.isAtom && NodeSelection.isSelectable(node)
|
|
383
|
-
? NodeSelection.create(tr.doc, insertAt)
|
|
384
|
-
: TextSelection.near(tr.doc.resolve(Math.min(insertAt + 1, tr.doc.content.size))));
|
|
385
|
-
dispatch(tr);
|
|
386
|
-
// Fires before the document actually changes: props `dispatch` is a
|
|
387
|
-
// no-op and CommandManager runs the real `view.dispatch(tr)` only
|
|
388
|
-
// after this command returns. A host callback therefore still sees
|
|
389
|
-
// the pre-move `editor.state` and must rely on this payload alone.
|
|
390
|
-
this.options.announceMove({
|
|
391
|
-
type: node.type.name,
|
|
392
|
-
position: to + 1,
|
|
393
|
-
total,
|
|
394
|
-
});
|
|
395
|
-
return true;
|
|
396
|
-
},
|
|
397
|
-
moveBlockUp: () => ({ state, commands }) => {
|
|
398
|
-
const index = topLevelIndexAtSelection(state);
|
|
399
|
-
return (index !== null &&
|
|
400
|
-
index > 0 &&
|
|
401
|
-
commands.moveBlock({ from: index, to: index - 1 }));
|
|
402
|
-
},
|
|
403
|
-
moveBlockDown: () => ({ state, commands }) => {
|
|
404
|
-
const index = topLevelIndexAtSelection(state);
|
|
405
|
-
return (index !== null &&
|
|
406
|
-
index < state.doc.childCount - 1 &&
|
|
407
|
-
commands.moveBlock({ from: index, to: index + 1 }));
|
|
408
|
-
},
|
|
409
|
-
};
|
|
410
|
-
},
|
|
411
|
-
addKeyboardShortcuts() {
|
|
412
|
-
return {
|
|
413
|
-
'Alt-Shift-ArrowUp': () => this.editor.commands.moveBlockUp(),
|
|
414
|
-
'Alt-Shift-ArrowDown': () => this.editor.commands.moveBlockDown(),
|
|
415
|
-
};
|
|
416
|
-
},
|
|
417
|
-
addProseMirrorPlugins() {
|
|
418
|
-
const options = this.options;
|
|
419
|
-
// The plugin view receives an `EditorView`, which carries no command
|
|
420
|
-
// interface; the drop path has to reach `moveBlock` through Tiptap.
|
|
421
|
-
const editor = this.editor;
|
|
422
|
-
return [
|
|
423
|
-
new Plugin({
|
|
424
|
-
view: (view) => {
|
|
425
|
-
const mount = options.mount();
|
|
426
|
-
if (!mount)
|
|
427
|
-
return { destroy: () => undefined };
|
|
428
|
-
const handle = createHandleElement(options.label());
|
|
429
|
-
mount.appendChild(handle);
|
|
430
|
-
// The indicator lives beside the handle rather than inside the
|
|
431
|
-
// document. As a `Decoration.widget` it could never move: a widget
|
|
432
|
-
// at a new position is a *different* decoration, so ProseMirror
|
|
433
|
-
// destroys and rebuilds its DOM on every change, and an element in
|
|
434
|
-
// normal flow has no `top` to transition anyway. Out here it is
|
|
435
|
-
// also further from the document than a decoration ever was — the
|
|
436
|
-
// mount is a sibling of `view.dom`, so no serializer can reach it.
|
|
437
|
-
const indicator = createIndicatorElement();
|
|
438
|
-
mount.appendChild(indicator);
|
|
439
|
-
const hide = () => handle.setAttribute('data-visible', 'false');
|
|
440
|
-
const onMouseMove = (event) => {
|
|
441
|
-
if (!options.enabled())
|
|
442
|
-
return hide();
|
|
443
|
-
const hit = blockAtPoint(view, event.clientY);
|
|
444
|
-
if (!hit)
|
|
445
|
-
return hide();
|
|
446
|
-
// `getBoundingClientRect()` reports scaled screen pixels, but a
|
|
447
|
-
// CSS `top` on a child of the scaled layer is applied in the
|
|
448
|
-
// layer's own unscaled space. Convert once, deriving the scale
|
|
449
|
-
// from the DOM rather than `--mlv-editor-zoom`, so the handle
|
|
450
|
-
// stays correct if anything else ever transforms this layer.
|
|
451
|
-
const scale = layerScale(mount);
|
|
452
|
-
const mountTop = mount.getBoundingClientRect().top;
|
|
453
|
-
handle.style.top = `${(hit.top - mountTop) / scale}px`;
|
|
454
|
-
handle.dataset['index'] = String(hit.index);
|
|
455
|
-
handle.title = options.label();
|
|
456
|
-
handle.setAttribute('data-visible', 'true');
|
|
457
|
-
};
|
|
458
|
-
/** Top-level index being dragged, or null when no drag is active. */
|
|
459
|
-
let source = null;
|
|
460
|
-
/** The dimmed element the drag started from. */
|
|
461
|
-
let sourceElement = null;
|
|
462
|
-
/** Geometry measured once at `dragstart`; empty while no drag runs. */
|
|
463
|
-
let slots = [];
|
|
464
|
-
/** The off-screen element handed to `setDragImage`. */
|
|
465
|
-
let ghost = null;
|
|
466
|
-
/** Gap the partition is currently opened at, or null when closed. */
|
|
467
|
-
let gap = null;
|
|
468
|
-
/** Converts a viewport y into the mount's own unscaled space. */
|
|
469
|
-
const toMountSpace = (clientY) => (clientY - mount.getBoundingClientRect().top) / layerScale(mount);
|
|
470
|
-
/**
|
|
471
|
-
* Gap a resolved target index sits on. `dropTargetIndex` folds the
|
|
472
|
-
* `slots.length + 1` gaps onto `moveBlock`'s pre-move convention,
|
|
473
|
-
* where `to` names the block the moved one ends up after when
|
|
474
|
-
* moving down; this is that fold inverted, so the previewed space
|
|
475
|
-
* and the committed move come from one resolved index.
|
|
476
|
-
*/
|
|
477
|
-
const gapForTarget = (to, from) => to <= from ? to : to + 1;
|
|
478
|
-
/** Opens the partition at `next`, or closes it when null. */
|
|
479
|
-
const setGap = (next) => {
|
|
480
|
-
if (gap === next)
|
|
481
|
-
return;
|
|
482
|
-
gap = next;
|
|
483
|
-
for (const slot of slots) {
|
|
484
|
-
// The gap is a token rather than a number here, so the size
|
|
485
|
-
// stays owned by the stylesheet.
|
|
486
|
-
slot.element.style.transform =
|
|
487
|
-
next !== null && slot.index >= next
|
|
488
|
-
? 'translateY(var(--mlv-editor-drop-gap))'
|
|
489
|
-
: '';
|
|
490
|
-
}
|
|
491
|
-
if (next === null) {
|
|
492
|
-
indicator.setAttribute('data-visible', 'false');
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
// The *pre-partition* boundary: the stylesheet offsets the line
|
|
496
|
-
// into the opened space by half the gap, so this never has to
|
|
497
|
-
// resolve the token into pixels.
|
|
498
|
-
indicator.style.top = `${gapOffset(slots, next)}px`;
|
|
499
|
-
indicator.setAttribute('data-visible', 'true');
|
|
500
|
-
};
|
|
501
|
-
const endDrag = () => {
|
|
502
|
-
for (const slot of slots)
|
|
503
|
-
slot.element.style.transform = '';
|
|
504
|
-
indicator.setAttribute('data-visible', 'false');
|
|
505
|
-
view.dom.classList.remove(MLV_EDITOR_DRAGGING_CLASS);
|
|
506
|
-
sourceElement?.classList.remove(MLV_EDITOR_DRAG_SOURCE_CLASS);
|
|
507
|
-
ghost?.remove();
|
|
508
|
-
slots = [];
|
|
509
|
-
gap = null;
|
|
510
|
-
source = null;
|
|
511
|
-
sourceElement = null;
|
|
512
|
-
ghost = null;
|
|
513
|
-
};
|
|
514
|
-
/** Target index for the pointer, or null when the drop is a no-op. */
|
|
515
|
-
const targetAt = (clientY) => {
|
|
516
|
-
if (source === null)
|
|
517
|
-
return null;
|
|
518
|
-
const y = toMountSpace(clientY);
|
|
519
|
-
const slot = slotAt(slots, y);
|
|
520
|
-
return slot === null ? null : dropTargetIndex(slot, y, source);
|
|
521
|
-
};
|
|
522
|
-
/**
|
|
523
|
-
* FLIP the blocks the move displaced: ProseMirror has already
|
|
524
|
-
* re-rendered them at their new positions, so each is offset back
|
|
525
|
-
* to where it was and released. Read after `moveBlock` because the
|
|
526
|
-
* DOM update is applied synchronously inside `dispatch`.
|
|
527
|
-
*
|
|
528
|
-
* Which block was where is derived from the move's own permutation
|
|
529
|
-
* rather than by matching DOM elements against their old boxes.
|
|
530
|
-
* `moveBlock` deletes the dragged node and reinserts it, so its
|
|
531
|
-
* element is not the one that was measured — identity matching
|
|
532
|
-
* would silently skip the one block the user actually dragged, and
|
|
533
|
-
* leave every other block sliding around it.
|
|
534
|
-
*
|
|
535
|
-
* Only the span between the two indices moved, so the walk is
|
|
536
|
-
* bounded by the distance dragged rather than by document length.
|
|
537
|
-
*
|
|
538
|
-
* Uses the Web Animations API rather than a transition: nothing has
|
|
539
|
-
* to force a reflow between setting and clearing the offset, the
|
|
540
|
-
* animation cleans itself up, and it never collides with the inline
|
|
541
|
-
* `transform` the partition writes.
|
|
542
|
-
*/
|
|
543
|
-
const settle = (tops, from, to) => {
|
|
544
|
-
if (prefersReducedMotion())
|
|
545
|
-
return;
|
|
546
|
-
// Pre-move index now occupying each post-move index.
|
|
547
|
-
const order = tops.map((_, index) => index);
|
|
548
|
-
order.splice(from, 1);
|
|
549
|
-
order.splice(to, 0, from);
|
|
550
|
-
const doc = view.state.doc;
|
|
551
|
-
const scale = layerScale(mount);
|
|
552
|
-
const mountTop = mount.getBoundingClientRect().top;
|
|
553
|
-
const first = Math.min(from, to);
|
|
554
|
-
const last = Math.max(from, to);
|
|
555
|
-
for (let index = first; index <= last; index += 1) {
|
|
556
|
-
const element = view.nodeDOM(childStart(doc, index));
|
|
557
|
-
if (!(element instanceof HTMLElement))
|
|
558
|
-
continue;
|
|
559
|
-
// jsdom implements no WAAPI, and neither did older engines.
|
|
560
|
-
if (typeof element.animate !== 'function')
|
|
561
|
-
return;
|
|
562
|
-
const previous = tops[order[index]];
|
|
563
|
-
if (previous === undefined)
|
|
564
|
-
continue;
|
|
565
|
-
const current = (element.getBoundingClientRect().top - mountTop) / scale;
|
|
566
|
-
const delta = previous - current;
|
|
567
|
-
if (Math.abs(delta) < MLV_EDITOR_SETTLE_EPSILON)
|
|
568
|
-
continue;
|
|
569
|
-
element.animate([
|
|
570
|
-
{ transform: `translateY(${delta}px)` },
|
|
571
|
-
{ transform: 'translateY(0)' },
|
|
572
|
-
], {
|
|
573
|
-
duration: MLV_EDITOR_SETTLE_DURATION,
|
|
574
|
-
easing: 'cubic-bezier(0.2, 0, 0, 1)',
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
const onDragStart = (event) => {
|
|
579
|
-
// `data-index` is absent until the first successful hover, and a
|
|
580
|
-
// revoked `enabled()` must not start a drag that could never be
|
|
581
|
-
// applied. Either way, refuse the drag outright.
|
|
582
|
-
const attribute = handle.dataset['index'];
|
|
583
|
-
const index = attribute === undefined ? NaN : Number(attribute);
|
|
584
|
-
if (!options.enabled() || !Number.isInteger(index)) {
|
|
585
|
-
event.preventDefault();
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
const measured = snapshotBlocks(view, mount);
|
|
589
|
-
const slot = measured.find((candidate) => candidate.index === index);
|
|
590
|
-
// The hovered index outliving its block means the snapshot and
|
|
591
|
-
// `data-index` disagree; refusing is the same answer as a drag
|
|
592
|
-
// that never hovered.
|
|
593
|
-
if (!slot) {
|
|
594
|
-
event.preventDefault();
|
|
595
|
-
return;
|
|
596
|
-
}
|
|
597
|
-
slots = measured;
|
|
598
|
-
source = index;
|
|
599
|
-
// Firefox refuses to begin a drag whose dataTransfer carries no
|
|
600
|
-
// data. The payload stays empty on purpose: the reorder replays
|
|
601
|
-
// `source` through `moveBlock`, never a parsed slice, so this
|
|
602
|
-
// drag cannot reparent into a list or a table cell and cannot be
|
|
603
|
-
// dropped into a different editor.
|
|
604
|
-
event.dataTransfer?.setData('text/plain', '');
|
|
605
|
-
if (event.dataTransfer)
|
|
606
|
-
event.dataTransfer.effectAllowed = 'move';
|
|
607
|
-
// A `dragstart` that arrives while one is already recorded would
|
|
608
|
-
// otherwise strand the previous drag image on `document.body`,
|
|
609
|
-
// where nothing else reaches it.
|
|
610
|
-
ghost?.remove();
|
|
611
|
-
// Cloned *before* the source is dimmed: the clone carries
|
|
612
|
-
// resolved computed styles, so dimming first would bake the
|
|
613
|
-
// reduced opacity into the drag image.
|
|
614
|
-
ghost = createGhostElement(slot.element, layerScale(mount));
|
|
615
|
-
event.dataTransfer?.setDragImage(ghost, getComputedStyle(view.dom).direction === 'rtl'
|
|
616
|
-
? ghost.offsetWidth
|
|
617
|
-
: 0, 0);
|
|
618
|
-
sourceElement = slot.element;
|
|
619
|
-
sourceElement.classList.add(MLV_EDITOR_DRAG_SOURCE_CLASS);
|
|
620
|
-
view.dom.classList.add(MLV_EDITOR_DRAGGING_CLASS);
|
|
621
|
-
};
|
|
622
|
-
const onDragOver = (event) => {
|
|
623
|
-
if (source === null)
|
|
624
|
-
return;
|
|
625
|
-
// Claimed in the capture phase on the mount, so ProseMirror's own
|
|
626
|
-
// `view.dom` drop handler never sees the event and cannot fall
|
|
627
|
-
// back to its native slice path.
|
|
628
|
-
event.stopPropagation();
|
|
629
|
-
event.preventDefault();
|
|
630
|
-
if (event.dataTransfer)
|
|
631
|
-
event.dataTransfer.dropEffect = 'move';
|
|
632
|
-
const to = targetAt(event.clientY);
|
|
633
|
-
setGap(to === null ? null : gapForTarget(to, source));
|
|
634
|
-
};
|
|
635
|
-
/**
|
|
636
|
-
* Whether a `dragleave` is the pointer leaving the mount outright,
|
|
637
|
-
* rather than crossing between two elements inside it — the event
|
|
638
|
-
* fires for both, and only the first should retract the indicator.
|
|
639
|
-
*/
|
|
640
|
-
const leavesMount = (event) => {
|
|
641
|
-
const entered = event.relatedTarget;
|
|
642
|
-
// Where the browser populates it, `relatedTarget` names the
|
|
643
|
-
// element being entered; one inside the mount means the drag
|
|
644
|
-
// never left.
|
|
645
|
-
if (entered instanceof Node)
|
|
646
|
-
return !mount.contains(entered);
|
|
647
|
-
// Chromium leaves it null on every drag event, so fall back to
|
|
648
|
-
// the pointer itself. Leaving the window reports (0, 0) in some
|
|
649
|
-
// browsers, which may read as inside; the drag's own `dragend`
|
|
650
|
-
// still clears the indicator there, exactly as it did before.
|
|
651
|
-
const rect = mount.getBoundingClientRect();
|
|
652
|
-
return (event.clientX < rect.left ||
|
|
653
|
-
event.clientX > rect.right ||
|
|
654
|
-
event.clientY < rect.top ||
|
|
655
|
-
event.clientY > rect.bottom);
|
|
656
|
-
};
|
|
657
|
-
const onDragLeave = (event) => {
|
|
658
|
-
// Only the preview goes — the partition closes and the line
|
|
659
|
-
// retracts. The drag itself is still in flight, and re-entering
|
|
660
|
-
// the editor must resolve a target again rather than leave the
|
|
661
|
-
// user dragging something that can no longer be dropped.
|
|
662
|
-
if (source !== null && leavesMount(event))
|
|
663
|
-
setGap(null);
|
|
664
|
-
};
|
|
665
|
-
const onDrop = (event) => {
|
|
666
|
-
if (source === null)
|
|
667
|
-
return;
|
|
668
|
-
event.stopPropagation();
|
|
669
|
-
event.preventDefault();
|
|
670
|
-
const from = source;
|
|
671
|
-
// Resolved from the drop's own coordinates rather than the last
|
|
672
|
-
// `dragover`, so the release position always wins.
|
|
673
|
-
const to = targetAt(event.clientY);
|
|
674
|
-
// Captured before `endDrag` discards the snapshot. These are the
|
|
675
|
-
// pre-partition tops, which is exactly the FLIP's "before": the
|
|
676
|
-
// partition's transforms are cleared by `endDrag` in the same
|
|
677
|
-
// turn, so nothing the user saw shifted is measured as movement.
|
|
678
|
-
const tops = slots.map((slot) => slot.top);
|
|
679
|
-
endDrag();
|
|
680
|
-
if (to === null)
|
|
681
|
-
return;
|
|
682
|
-
editor.commands.moveBlock({ from, to });
|
|
683
|
-
settle(tops, from, to);
|
|
684
|
-
};
|
|
685
|
-
const onKeyDown = (event) => {
|
|
686
|
-
if (event.key === 'Escape' && source !== null)
|
|
687
|
-
endDrag();
|
|
688
|
-
};
|
|
689
|
-
mount.addEventListener('mousemove', onMouseMove);
|
|
690
|
-
mount.addEventListener('mouseleave', hide);
|
|
691
|
-
handle.addEventListener('dragstart', onDragStart);
|
|
692
|
-
handle.addEventListener('dragend', endDrag);
|
|
693
|
-
// On the mount rather than `view.dom`: `.ProseMirror` is the text
|
|
694
|
-
// column, so a drag that never leaves the gutter the handle lives
|
|
695
|
-
// in would otherwise produce no `dragover` and no droppable target.
|
|
696
|
-
mount.addEventListener('dragover', onDragOver, true);
|
|
697
|
-
mount.addEventListener('dragleave', onDragLeave, true);
|
|
698
|
-
mount.addEventListener('drop', onDrop, true);
|
|
699
|
-
// Escape aborts a native drag in a real browser, which fires
|
|
700
|
-
// `dragend` anyway; this keeps the cancellation deterministic and
|
|
701
|
-
// independent of that.
|
|
702
|
-
document.addEventListener('keydown', onKeyDown);
|
|
703
|
-
return {
|
|
704
|
-
// `enabled()` is checked on every pointer move too, but a host
|
|
705
|
-
// can revoke it *between* moves. Disabling the editor puts
|
|
706
|
-
// `pointer-events: none` on the surface containing the mount, so
|
|
707
|
-
// no further mousemove or mouseleave can ever arrive to retract
|
|
708
|
-
// a handle that was left visible. This runs on every state
|
|
709
|
-
// update, which is what a `disabled`/`readonly` flip produces.
|
|
710
|
-
update: (updated, previous) => {
|
|
711
|
-
// `source` is a pre-move top-level index, exactly like the
|
|
712
|
-
// anchor `apply` drops on `docChanged`, and it is the one that
|
|
713
|
-
// decides *which* block moves. An insertion before it makes it
|
|
714
|
-
// name a different block, so the indicator would repaint at the
|
|
715
|
-
// gap the user aimed at while the drop silently moved the wrong
|
|
716
|
-
// block — one undo step, no error. Abort rather than map: a
|
|
717
|
-
// source block that was itself deleted has no mapped index, and
|
|
718
|
-
// aborting matches what already happens to the anchor.
|
|
719
|
-
//
|
|
720
|
-
// Identity, not `Node.eq`: ProseMirror only replaces
|
|
721
|
-
// `state.doc` when a step is applied — a meta- or
|
|
722
|
-
// selection-only transaction carries the same reference — so
|
|
723
|
-
// this is exactly `docChanged` at O(1), where a structural
|
|
724
|
-
// compare would walk the whole document on every keystroke.
|
|
725
|
-
if (previous.doc !== updated.state.doc && source !== null) {
|
|
726
|
-
endDrag();
|
|
727
|
-
}
|
|
728
|
-
if (options.enabled())
|
|
729
|
-
return;
|
|
730
|
-
hide();
|
|
731
|
-
// Retracts the preview and drops the in-flight source, so
|
|
732
|
-
// releasing the button after the revocation cannot reorder
|
|
733
|
-
// anything. Previously the line went through `decorations`,
|
|
734
|
-
// which read `enabled()` itself; a mount-owned element has to
|
|
735
|
-
// be told.
|
|
736
|
-
if (source !== null)
|
|
737
|
-
endDrag();
|
|
738
|
-
},
|
|
739
|
-
destroy: () => {
|
|
740
|
-
mount.removeEventListener('mousemove', onMouseMove);
|
|
741
|
-
mount.removeEventListener('mouseleave', hide);
|
|
742
|
-
handle.removeEventListener('dragstart', onDragStart);
|
|
743
|
-
handle.removeEventListener('dragend', endDrag);
|
|
744
|
-
mount.removeEventListener('dragover', onDragOver, true);
|
|
745
|
-
mount.removeEventListener('dragleave', onDragLeave, true);
|
|
746
|
-
mount.removeEventListener('drop', onDrop, true);
|
|
747
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
748
|
-
handle.remove();
|
|
749
|
-
indicator.remove();
|
|
750
|
-
// A drag interrupted by teardown leaves its drag image on
|
|
751
|
-
// `document.body`, outside everything else this removes.
|
|
752
|
-
ghost?.remove();
|
|
753
|
-
},
|
|
754
|
-
};
|
|
755
|
-
},
|
|
756
|
-
}),
|
|
757
|
-
];
|
|
758
|
-
},
|
|
759
|
-
});
|
|
760
|
-
//# sourceMappingURL=editor-block-handle.js.map
|