@domternal/extension-block-menu 0.7.0
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/LICENSE +21 -0
- package/README.md +44 -0
- package/dist/index.cjs +2622 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +607 -0
- package/dist/index.d.ts +607 -0
- package/dist/index.js +2602 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2622 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@domternal/core');
|
|
4
|
+
var state = require('@domternal/pm/state');
|
|
5
|
+
var model = require('@domternal/pm/model');
|
|
6
|
+
var view = require('@domternal/pm/view');
|
|
7
|
+
var transform = require('@domternal/pm/transform');
|
|
8
|
+
|
|
9
|
+
// src/FloatingMenu.ts
|
|
10
|
+
var floatingMenuPluginKey = new state.PluginKey("floatingMenu");
|
|
11
|
+
function defaultShouldShow({
|
|
12
|
+
editor,
|
|
13
|
+
state
|
|
14
|
+
}) {
|
|
15
|
+
if (!editor.isEditable) return false;
|
|
16
|
+
const { selection } = state;
|
|
17
|
+
const { $from, empty } = selection;
|
|
18
|
+
if (!empty) return false;
|
|
19
|
+
if ($from.parent.type.name !== "paragraph") return false;
|
|
20
|
+
if ($from.parent.content.size !== 0) return false;
|
|
21
|
+
if ($from.parentOffset !== 0) return false;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
var DEFAULT_ENTER_MENU_SHORTCUTS = ["Alt-F10", "Mod-/"];
|
|
25
|
+
var FLOATING_MENU_META = "dm:floatingMenuTrigger";
|
|
26
|
+
function showFloatingMenu(view) {
|
|
27
|
+
view.dispatch(view.state.tr.setMeta(FLOATING_MENU_META, "show"));
|
|
28
|
+
}
|
|
29
|
+
var IS_MAC = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
30
|
+
function matchShortcut(event, shortcut) {
|
|
31
|
+
const parts = shortcut.split("-");
|
|
32
|
+
let keyPart = parts.pop();
|
|
33
|
+
if (!keyPart) return false;
|
|
34
|
+
if (keyPart === "Space") keyPart = " ";
|
|
35
|
+
let needCtrl = false;
|
|
36
|
+
let needMeta = false;
|
|
37
|
+
let needAlt = false;
|
|
38
|
+
let needShift = false;
|
|
39
|
+
for (const mod of parts) {
|
|
40
|
+
switch (mod) {
|
|
41
|
+
case "Mod":
|
|
42
|
+
case "mod":
|
|
43
|
+
if (IS_MAC) needMeta = true;
|
|
44
|
+
else needCtrl = true;
|
|
45
|
+
break;
|
|
46
|
+
case "Cmd":
|
|
47
|
+
case "cmd":
|
|
48
|
+
case "Meta":
|
|
49
|
+
case "meta":
|
|
50
|
+
case "m":
|
|
51
|
+
needMeta = true;
|
|
52
|
+
break;
|
|
53
|
+
case "Ctrl":
|
|
54
|
+
case "ctrl":
|
|
55
|
+
case "Control":
|
|
56
|
+
case "control":
|
|
57
|
+
case "c":
|
|
58
|
+
needCtrl = true;
|
|
59
|
+
break;
|
|
60
|
+
case "Alt":
|
|
61
|
+
case "alt":
|
|
62
|
+
case "a":
|
|
63
|
+
needAlt = true;
|
|
64
|
+
break;
|
|
65
|
+
case "Shift":
|
|
66
|
+
case "shift":
|
|
67
|
+
case "s":
|
|
68
|
+
needShift = true;
|
|
69
|
+
break;
|
|
70
|
+
default:
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (event.altKey !== needAlt) return false;
|
|
75
|
+
if (event.ctrlKey !== needCtrl) return false;
|
|
76
|
+
if (event.metaKey !== needMeta) return false;
|
|
77
|
+
if (keyPart.length > 1 && event.shiftKey !== needShift) return false;
|
|
78
|
+
return event.key === keyPart || event.key.toLowerCase() === keyPart.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
function createFloatingMenuPlugin(options) {
|
|
81
|
+
const {
|
|
82
|
+
pluginKey,
|
|
83
|
+
editor,
|
|
84
|
+
element,
|
|
85
|
+
shouldShow = defaultShouldShow,
|
|
86
|
+
offset = 0,
|
|
87
|
+
keymap,
|
|
88
|
+
requireExplicitTrigger = false
|
|
89
|
+
} = options;
|
|
90
|
+
const enterShortcuts = keymap?.enterMenu ?? DEFAULT_ENTER_MENU_SHORTCUTS;
|
|
91
|
+
if (!element.getAttribute("role")) {
|
|
92
|
+
element.setAttribute("role", "menu");
|
|
93
|
+
element.setAttribute("aria-label", "Insert block");
|
|
94
|
+
}
|
|
95
|
+
let cleanupFloating = null;
|
|
96
|
+
let editorEl = null;
|
|
97
|
+
let clickOutsideHandler = null;
|
|
98
|
+
let dismissOverlayHandler = null;
|
|
99
|
+
const isVisible = () => element.hasAttribute("data-show");
|
|
100
|
+
const updatePosition = (view) => {
|
|
101
|
+
const { selection } = view.state;
|
|
102
|
+
const { $from } = selection;
|
|
103
|
+
const depth = $from.depth;
|
|
104
|
+
const startPos = $from.start(depth);
|
|
105
|
+
const domNode = view.nodeDOM(startPos - 1);
|
|
106
|
+
if (domNode instanceof HTMLElement) {
|
|
107
|
+
cleanupFloating?.();
|
|
108
|
+
cleanupFloating = core.positionFloatingOnce(domNode, element, {
|
|
109
|
+
placement: "bottom-start",
|
|
110
|
+
offsetValue: offset
|
|
111
|
+
});
|
|
112
|
+
element.setAttribute("data-show", "");
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const hideMenu = () => {
|
|
116
|
+
cleanupFloating?.();
|
|
117
|
+
cleanupFloating = null;
|
|
118
|
+
element.removeAttribute("data-show");
|
|
119
|
+
};
|
|
120
|
+
const focusFirstItem = () => {
|
|
121
|
+
const first = element.querySelector('[data-floating-menu-item]:not([aria-disabled="true"])') ?? element.querySelector('button, [tabindex]:not([tabindex="-1"])');
|
|
122
|
+
if (!first) return false;
|
|
123
|
+
first.focus();
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
hideMenu();
|
|
127
|
+
const isVisibleNow = (view) => {
|
|
128
|
+
const wantsShow = shouldShow({ editor, view, state: view.state });
|
|
129
|
+
if (!requireExplicitTrigger) return wantsShow;
|
|
130
|
+
const triggered = pluginKey.getState(view.state)?.triggered ?? false;
|
|
131
|
+
return triggered && wantsShow;
|
|
132
|
+
};
|
|
133
|
+
return new state.Plugin({
|
|
134
|
+
key: pluginKey,
|
|
135
|
+
state: {
|
|
136
|
+
init: () => ({ triggered: false }),
|
|
137
|
+
apply: (tr, prev) => {
|
|
138
|
+
const meta = tr.getMeta(FLOATING_MENU_META);
|
|
139
|
+
if (meta === "show") return { triggered: true };
|
|
140
|
+
if (meta === "hide") return { triggered: false };
|
|
141
|
+
return prev;
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
props: {
|
|
145
|
+
// Keyboard entry from the editor. ProseMirror's handleKeyDown fires
|
|
146
|
+
// before browser defaults, so we can intercept without risking
|
|
147
|
+
// interference with typing. We only act while the menu is visible.
|
|
148
|
+
handleKeyDown(_view, event) {
|
|
149
|
+
if (!isVisible()) return false;
|
|
150
|
+
for (const shortcut of enterShortcuts) {
|
|
151
|
+
if (matchShortcut(event, shortcut)) {
|
|
152
|
+
if (focusFirstItem()) {
|
|
153
|
+
event.preventDefault();
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
view: (editorView) => {
|
|
163
|
+
editorEl = editorView.dom.closest(".dm-editor");
|
|
164
|
+
if (editorEl && element.parentElement !== editorEl) {
|
|
165
|
+
editorEl.appendChild(element);
|
|
166
|
+
}
|
|
167
|
+
const dismiss = () => {
|
|
168
|
+
hideMenu();
|
|
169
|
+
if (requireExplicitTrigger) {
|
|
170
|
+
const triggered = pluginKey.getState(editor.view.state)?.triggered ?? false;
|
|
171
|
+
if (triggered) {
|
|
172
|
+
editor.view.dispatch(editor.view.state.tr.setMeta(FLOATING_MENU_META, "hide"));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const onFocus = () => {
|
|
177
|
+
if (isVisibleNow(editor.view)) updatePosition(editor.view);
|
|
178
|
+
else dismiss();
|
|
179
|
+
};
|
|
180
|
+
const onBlur = ({ event }) => {
|
|
181
|
+
const related = event.relatedTarget;
|
|
182
|
+
if (related instanceof Node && element.contains(related)) return;
|
|
183
|
+
dismiss();
|
|
184
|
+
};
|
|
185
|
+
clickOutsideHandler = (e) => {
|
|
186
|
+
if (!isVisible()) return;
|
|
187
|
+
const target = e.target;
|
|
188
|
+
if (!(target instanceof Node)) return;
|
|
189
|
+
if (element.contains(target)) return;
|
|
190
|
+
if (editor.view.dom.contains(target)) return;
|
|
191
|
+
dismiss();
|
|
192
|
+
};
|
|
193
|
+
document.addEventListener("mousedown", clickOutsideHandler, true);
|
|
194
|
+
if (editorEl) {
|
|
195
|
+
dismissOverlayHandler = () => {
|
|
196
|
+
dismiss();
|
|
197
|
+
};
|
|
198
|
+
editorEl.addEventListener("dm:dismiss-overlays", dismissOverlayHandler);
|
|
199
|
+
}
|
|
200
|
+
editor.on("focus", onFocus);
|
|
201
|
+
editor.on("blur", onBlur);
|
|
202
|
+
return {
|
|
203
|
+
update: (view) => {
|
|
204
|
+
if (isVisibleNow(view)) updatePosition(view);
|
|
205
|
+
else {
|
|
206
|
+
hideMenu();
|
|
207
|
+
if (requireExplicitTrigger) {
|
|
208
|
+
const triggered = pluginKey.getState(view.state)?.triggered ?? false;
|
|
209
|
+
if (triggered) {
|
|
210
|
+
view.dispatch(view.state.tr.setMeta(FLOATING_MENU_META, "hide"));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
destroy: () => {
|
|
216
|
+
hideMenu();
|
|
217
|
+
editor.off("focus", onFocus);
|
|
218
|
+
editor.off("blur", onBlur);
|
|
219
|
+
if (clickOutsideHandler) {
|
|
220
|
+
document.removeEventListener("mousedown", clickOutsideHandler, true);
|
|
221
|
+
clickOutsideHandler = null;
|
|
222
|
+
}
|
|
223
|
+
if (dismissOverlayHandler && editorEl) {
|
|
224
|
+
editorEl.removeEventListener("dm:dismiss-overlays", dismissOverlayHandler);
|
|
225
|
+
dismissOverlayHandler = null;
|
|
226
|
+
}
|
|
227
|
+
editorEl = null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
var FloatingMenu = core.Extension.create({
|
|
234
|
+
name: "floatingMenu",
|
|
235
|
+
addOptions() {
|
|
236
|
+
return {
|
|
237
|
+
element: null,
|
|
238
|
+
shouldShow: defaultShouldShow,
|
|
239
|
+
offset: 0,
|
|
240
|
+
requireExplicitTrigger: false
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
addProseMirrorPlugins() {
|
|
244
|
+
const { element, shouldShow, offset, keymap, requireExplicitTrigger } = this.options;
|
|
245
|
+
if (!element) return [];
|
|
246
|
+
const editor = this.editor;
|
|
247
|
+
if (!editor) return [];
|
|
248
|
+
return [
|
|
249
|
+
createFloatingMenuPlugin({
|
|
250
|
+
pluginKey: floatingMenuPluginKey,
|
|
251
|
+
editor,
|
|
252
|
+
element,
|
|
253
|
+
shouldShow,
|
|
254
|
+
offset,
|
|
255
|
+
keymap,
|
|
256
|
+
...requireExplicitTrigger !== void 0 && { requireExplicitTrigger }
|
|
257
|
+
})
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// src/helpers/findTopLevelBlock.ts
|
|
263
|
+
function findTopLevelBlock(doc, pos) {
|
|
264
|
+
if (pos < 0 || pos > doc.content.size) return null;
|
|
265
|
+
const $pos = doc.resolve(pos);
|
|
266
|
+
if ($pos.depth === 0) {
|
|
267
|
+
const node2 = doc.nodeAt(pos);
|
|
268
|
+
if (!node2) return null;
|
|
269
|
+
return {
|
|
270
|
+
node: node2,
|
|
271
|
+
pos,
|
|
272
|
+
end: pos + node2.nodeSize,
|
|
273
|
+
index: $pos.index(0)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const node = $pos.node(1);
|
|
277
|
+
const blockPos = $pos.before(1);
|
|
278
|
+
const index = $pos.index(0);
|
|
279
|
+
return {
|
|
280
|
+
node,
|
|
281
|
+
pos: blockPos,
|
|
282
|
+
end: blockPos + node.nodeSize,
|
|
283
|
+
index
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function findDeepestBlockAtY(view, clientY, allowedTypes, matchers = []) {
|
|
287
|
+
if (allowedTypes.length === 0) return null;
|
|
288
|
+
let best = null;
|
|
289
|
+
view.state.doc.descendants((node, pos, parent, index) => {
|
|
290
|
+
const dom = view.nodeDOM(pos);
|
|
291
|
+
if (!(dom instanceof HTMLElement)) return true;
|
|
292
|
+
const rect = dom.getBoundingClientRect();
|
|
293
|
+
if (clientY < rect.top || clientY > rect.bottom) return false;
|
|
294
|
+
if (allowedTypes.includes(node.type.name)) {
|
|
295
|
+
if (matchers.length > 0 && isRejectedByMatchers(view, node, pos, parent, index, matchers)) {
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
if (best === null || rect.height < best.rect.height) {
|
|
299
|
+
best = { node, pos, dom, rect };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return true;
|
|
303
|
+
});
|
|
304
|
+
return best;
|
|
305
|
+
}
|
|
306
|
+
function isRejectedByMatchers(view, node, pos, parent, index, matchers) {
|
|
307
|
+
const $pos = view.state.doc.resolve(pos);
|
|
308
|
+
const candidate = {
|
|
309
|
+
block: node,
|
|
310
|
+
documentPos: pos,
|
|
311
|
+
// `pos` sits right BEFORE `node`, so $pos.depth is the parent's depth
|
|
312
|
+
// and `node` itself lives one level deeper.
|
|
313
|
+
treeDepth: $pos.depth + 1,
|
|
314
|
+
container: parent,
|
|
315
|
+
positionInContainer: index,
|
|
316
|
+
isFirstChild: index === 0,
|
|
317
|
+
isLastChild: parent !== null ? index === parent.childCount - 1 : true,
|
|
318
|
+
resolvedPos: $pos,
|
|
319
|
+
editorView: view
|
|
320
|
+
};
|
|
321
|
+
for (const matcher of matchers) {
|
|
322
|
+
if (matcher.test(candidate) === "reject") return true;
|
|
323
|
+
}
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/helpers/expandToEmptyWrappers.ts
|
|
328
|
+
function expandToEmptyWrappers(doc, from, to) {
|
|
329
|
+
let curFrom = from;
|
|
330
|
+
let curTo = to;
|
|
331
|
+
let $pos = doc.resolve(curFrom);
|
|
332
|
+
while ($pos.depth > 0 && isCollapsibleParent($pos.node($pos.depth), $pos.index($pos.depth))) {
|
|
333
|
+
curFrom = $pos.before($pos.depth);
|
|
334
|
+
curTo = $pos.after($pos.depth);
|
|
335
|
+
$pos = doc.resolve(curFrom);
|
|
336
|
+
}
|
|
337
|
+
return { from: curFrom, to: curTo };
|
|
338
|
+
}
|
|
339
|
+
function isCollapsibleParent(parent, sourceIndex) {
|
|
340
|
+
if (parent.childCount === 1) return true;
|
|
341
|
+
if (parent.childCount !== 2) return false;
|
|
342
|
+
const otherIndex = sourceIndex === 0 ? 1 : 0;
|
|
343
|
+
const other = parent.child(otherIndex);
|
|
344
|
+
return other.type.name === "paragraph" && other.content.size === 0;
|
|
345
|
+
}
|
|
346
|
+
var TASK_LIST_TYPE = "taskList";
|
|
347
|
+
var BULLET_LIST_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList"]);
|
|
348
|
+
var TASK_ITEM_TYPE = "taskItem";
|
|
349
|
+
var LIST_ITEM_TYPE = "listItem";
|
|
350
|
+
function convertListItemForParent(schema, content, parentType) {
|
|
351
|
+
const expectsTaskItem = parentType.name === TASK_LIST_TYPE;
|
|
352
|
+
const expectsListItem = BULLET_LIST_TYPES.has(parentType.name);
|
|
353
|
+
if (!expectsTaskItem && !expectsListItem) return content;
|
|
354
|
+
const targetTypeName = expectsTaskItem ? TASK_ITEM_TYPE : LIST_ITEM_TYPE;
|
|
355
|
+
const oppositeTypeName = expectsTaskItem ? LIST_ITEM_TYPE : TASK_ITEM_TYPE;
|
|
356
|
+
const targetItemType = schema.nodes[targetTypeName];
|
|
357
|
+
if (!targetItemType) return content;
|
|
358
|
+
const children = [];
|
|
359
|
+
content.forEach((child) => {
|
|
360
|
+
children.push(child);
|
|
361
|
+
});
|
|
362
|
+
const allMatch = children.every((c) => c.type.name === targetTypeName);
|
|
363
|
+
if (allMatch) return content;
|
|
364
|
+
const replaced = children.map((child) => {
|
|
365
|
+
if (child.type.name === targetTypeName) return child;
|
|
366
|
+
if (child.type.name === oppositeTypeName) {
|
|
367
|
+
const newAttrs = expectsTaskItem ? { ...child.attrs, checked: false } : { ...child.attrs };
|
|
368
|
+
return targetItemType.create(newAttrs, child.content, child.marks);
|
|
369
|
+
}
|
|
370
|
+
const wrapperAttrs = expectsTaskItem ? { checked: false } : {};
|
|
371
|
+
if (child.type.name === "paragraph") {
|
|
372
|
+
return targetItemType.create(wrapperAttrs, [child]);
|
|
373
|
+
}
|
|
374
|
+
const paragraphType = schema.nodes["paragraph"];
|
|
375
|
+
const labelParagraph = paragraphType?.createAndFill();
|
|
376
|
+
const content2 = labelParagraph ? [labelParagraph, child] : [child];
|
|
377
|
+
return targetItemType.create(wrapperAttrs, content2);
|
|
378
|
+
});
|
|
379
|
+
return model.Fragment.from(replaced);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/helpers/moveBlock.ts
|
|
383
|
+
function moveBlock(tr, sourcePos, targetPos) {
|
|
384
|
+
if (sourcePos < 0 || sourcePos >= tr.doc.content.size) return tr;
|
|
385
|
+
const sourceNode = tr.doc.nodeAt(sourcePos);
|
|
386
|
+
if (!sourceNode) return tr;
|
|
387
|
+
const sourceEnd = sourcePos + sourceNode.nodeSize;
|
|
388
|
+
const { from, to } = expandToEmptyWrappers(tr.doc, sourcePos, sourceEnd);
|
|
389
|
+
if (targetPos >= from && targetPos <= to) return tr;
|
|
390
|
+
const slice = tr.doc.slice(sourcePos, sourceEnd);
|
|
391
|
+
tr.delete(from, to);
|
|
392
|
+
const adjustedTarget = targetPos > from ? targetPos - (to - from) : targetPos;
|
|
393
|
+
const targetParent = tr.doc.resolve(adjustedTarget).parent;
|
|
394
|
+
const adaptedContent = convertListItemForParent(
|
|
395
|
+
tr.doc.type.schema,
|
|
396
|
+
slice.content,
|
|
397
|
+
targetParent.type
|
|
398
|
+
);
|
|
399
|
+
tr.insert(adjustedTarget, adaptedContent);
|
|
400
|
+
return tr;
|
|
401
|
+
}
|
|
402
|
+
var LIST_ITEM_TYPES = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
|
|
403
|
+
function moveBlockAsNestedChild(tr, sourcePos, wrapperPos, targetItemPos) {
|
|
404
|
+
if (sourcePos < 0 || sourcePos >= tr.doc.content.size) return false;
|
|
405
|
+
const sourceNode = tr.doc.nodeAt(sourcePos);
|
|
406
|
+
if (!sourceNode) return false;
|
|
407
|
+
const sourceEnd = sourcePos + sourceNode.nodeSize;
|
|
408
|
+
if (targetItemPos >= sourcePos && targetItemPos < sourceEnd) return false;
|
|
409
|
+
if (wrapperPos >= sourcePos && wrapperPos < sourceEnd) return false;
|
|
410
|
+
const { from: expandedFrom, to: expandedTo } = expandToEmptyWrappers(tr.doc, sourcePos, sourceEnd);
|
|
411
|
+
let blockNode;
|
|
412
|
+
if (LIST_ITEM_TYPES.has(sourceNode.type.name)) {
|
|
413
|
+
const wrapperNode = tr.doc.nodeAt(wrapperPos);
|
|
414
|
+
if (!wrapperNode) return false;
|
|
415
|
+
const adaptedFragment = convertListItemForParent(
|
|
416
|
+
tr.doc.type.schema,
|
|
417
|
+
model.Fragment.from(sourceNode),
|
|
418
|
+
wrapperNode.type
|
|
419
|
+
);
|
|
420
|
+
if (adaptedFragment.childCount === 0) return false;
|
|
421
|
+
blockNode = wrapperNode.type.create(null, adaptedFragment);
|
|
422
|
+
} else {
|
|
423
|
+
blockNode = sourceNode;
|
|
424
|
+
}
|
|
425
|
+
const result = core.insertAsListItemChild({
|
|
426
|
+
tr,
|
|
427
|
+
wrapperPos,
|
|
428
|
+
targetItemPos,
|
|
429
|
+
blockNode,
|
|
430
|
+
sourceRange: { from: expandedFrom, to: expandedTo }
|
|
431
|
+
});
|
|
432
|
+
return result.ok;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/helpers/dragGhost.ts
|
|
436
|
+
function createDragGhost(source) {
|
|
437
|
+
const wrapper = makeWrapper(source);
|
|
438
|
+
const clone = deepCloneWithStyles(source);
|
|
439
|
+
wrapper.appendChild(clone);
|
|
440
|
+
document.body.appendChild(wrapper);
|
|
441
|
+
return { wrapper };
|
|
442
|
+
}
|
|
443
|
+
function makeWrapper(source) {
|
|
444
|
+
const w = document.createElement("div");
|
|
445
|
+
w.setAttribute("aria-hidden", "true");
|
|
446
|
+
Object.assign(w.style, {
|
|
447
|
+
position: "absolute",
|
|
448
|
+
top: "-10000px",
|
|
449
|
+
left: "-10000px",
|
|
450
|
+
pointerEvents: "none",
|
|
451
|
+
width: `${String(source.getBoundingClientRect().width)}px`
|
|
452
|
+
});
|
|
453
|
+
return w;
|
|
454
|
+
}
|
|
455
|
+
function deepCloneWithStyles(source) {
|
|
456
|
+
const clone = source.cloneNode(true);
|
|
457
|
+
freezeStylesIntoClone(source, clone);
|
|
458
|
+
return clone;
|
|
459
|
+
}
|
|
460
|
+
function freezeStylesIntoClone(src, dst) {
|
|
461
|
+
if (src instanceof HTMLElement && dst instanceof HTMLElement) {
|
|
462
|
+
dst.style.cssText = getComputedStyle(src).cssText;
|
|
463
|
+
}
|
|
464
|
+
const srcKids = src.children;
|
|
465
|
+
const dstKids = dst.children;
|
|
466
|
+
const n = Math.min(srcKids.length, dstKids.length);
|
|
467
|
+
for (let i = 0; i < n; i++) {
|
|
468
|
+
const s = srcKids.item(i);
|
|
469
|
+
const d = dstKids.item(i);
|
|
470
|
+
if (s !== null && d !== null) freezeStylesIntoClone(s, d);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/helpers/clampCoords.ts
|
|
475
|
+
var DEFAULT_INSET_PX = 5;
|
|
476
|
+
function clampToContent(view, clientX, clientY, inset = DEFAULT_INSET_PX) {
|
|
477
|
+
const first = view.dom.firstElementChild;
|
|
478
|
+
const last = view.dom.lastElementChild;
|
|
479
|
+
if (!first || !last) return null;
|
|
480
|
+
const topRect = first.getBoundingClientRect();
|
|
481
|
+
const bottomRect = last.getBoundingClientRect();
|
|
482
|
+
let clampedY = clientY;
|
|
483
|
+
if (clientY < topRect.top) clampedY = topRect.top + inset;
|
|
484
|
+
else if (clientY > bottomRect.bottom) clampedY = bottomRect.bottom - inset;
|
|
485
|
+
const minX = topRect.left + inset;
|
|
486
|
+
const maxX = topRect.right - inset;
|
|
487
|
+
const clampedX = Math.max(minX, Math.min(clientX, maxX));
|
|
488
|
+
return { x: clampedX, y: clampedY };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// src/helpers/gutterBias.ts
|
|
492
|
+
var PRESET_DEFAULT = {
|
|
493
|
+
edges: ["left", "top"],
|
|
494
|
+
threshold: 12,
|
|
495
|
+
strength: 500
|
|
496
|
+
};
|
|
497
|
+
var PRESET_RIGHT = { ...PRESET_DEFAULT, edges: ["right", "top"] };
|
|
498
|
+
var PRESET_BOTH = { ...PRESET_DEFAULT, edges: ["left", "right", "top"] };
|
|
499
|
+
function resolveGutterBias(input) {
|
|
500
|
+
switch (input) {
|
|
501
|
+
case void 0:
|
|
502
|
+
case false:
|
|
503
|
+
case "none":
|
|
504
|
+
return null;
|
|
505
|
+
case true:
|
|
506
|
+
case "left":
|
|
507
|
+
return { ...PRESET_DEFAULT };
|
|
508
|
+
case "right":
|
|
509
|
+
return { ...PRESET_RIGHT };
|
|
510
|
+
case "both":
|
|
511
|
+
return { ...PRESET_BOTH };
|
|
512
|
+
default:
|
|
513
|
+
return {
|
|
514
|
+
edges: input.edges ?? PRESET_DEFAULT.edges,
|
|
515
|
+
threshold: input.threshold ?? PRESET_DEFAULT.threshold,
|
|
516
|
+
strength: input.strength ?? PRESET_DEFAULT.strength
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function isInGutter(x, y, rect, config) {
|
|
521
|
+
const t = config.threshold;
|
|
522
|
+
for (const edge of config.edges) {
|
|
523
|
+
if (edge === "left" && x - rect.left <= t) return true;
|
|
524
|
+
if (edge === "right" && rect.right - x <= t) return true;
|
|
525
|
+
if (edge === "top" && y - rect.top <= t) return true;
|
|
526
|
+
if (edge === "bottom" && rect.bottom - y <= t) return true;
|
|
527
|
+
}
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
function gutterBiasWeight(x, y, rect, depth, config) {
|
|
531
|
+
return isInGutter(x, y, rect, config) ? config.strength * depth : 0;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/helpers/defaultMatchers.ts
|
|
535
|
+
var LIST_ITEM_NODE_TYPES = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
|
|
536
|
+
var TABLE_PLUMBING_TYPES = /* @__PURE__ */ new Set(["tableRow", "tableCell", "tableHeader"]);
|
|
537
|
+
var firstChildOfListItem = {
|
|
538
|
+
name: "firstChildOfListItem",
|
|
539
|
+
test(candidate) {
|
|
540
|
+
if (!candidate.isFirstChild) return "allow";
|
|
541
|
+
const parent = candidate.container;
|
|
542
|
+
if (parent === null) return "allow";
|
|
543
|
+
return LIST_ITEM_NODE_TYPES.has(parent.type.name) ? "reject" : "allow";
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
var listContainerSkip = {
|
|
547
|
+
name: "listContainerSkip",
|
|
548
|
+
test(candidate) {
|
|
549
|
+
const firstChild = candidate.block.firstChild;
|
|
550
|
+
if (firstChild === null) return "allow";
|
|
551
|
+
return LIST_ITEM_NODE_TYPES.has(firstChild.type.name) ? "reject" : "allow";
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
var tableInternals = {
|
|
555
|
+
name: "tableInternals",
|
|
556
|
+
test(candidate) {
|
|
557
|
+
if (TABLE_PLUMBING_TYPES.has(candidate.block.type.name)) return "reject";
|
|
558
|
+
if (candidate.container?.type.name === "tableHeader") return "reject";
|
|
559
|
+
return "allow";
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var inlineNodes = {
|
|
563
|
+
name: "inlineNodes",
|
|
564
|
+
test(candidate) {
|
|
565
|
+
const { block } = candidate;
|
|
566
|
+
return block.isText || block.isInline ? "reject" : "allow";
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
var DEFAULT_BLOCK_MATCHERS = Object.freeze([
|
|
570
|
+
firstChildOfListItem,
|
|
571
|
+
listContainerSkip,
|
|
572
|
+
tableInternals,
|
|
573
|
+
inlineNodes
|
|
574
|
+
]);
|
|
575
|
+
|
|
576
|
+
// src/helpers/resolveDragTarget.ts
|
|
577
|
+
function resolveDragTarget(view, x, y, options) {
|
|
578
|
+
const coord = view.posAtCoords({ left: x, top: y });
|
|
579
|
+
if (coord === null) return null;
|
|
580
|
+
const $pos = view.state.doc.resolve(coord.pos);
|
|
581
|
+
const survivors = [];
|
|
582
|
+
collectAncestors($pos, view, options, survivors);
|
|
583
|
+
collectAtomLeaf($pos, view, options, survivors);
|
|
584
|
+
if (survivors.length === 0) return null;
|
|
585
|
+
applyGutterBias(survivors, x, y, options.gutterBias);
|
|
586
|
+
return chooseWinner(survivors);
|
|
587
|
+
}
|
|
588
|
+
function collectAncestors($pos, view, options, out) {
|
|
589
|
+
for (let depth = $pos.depth; depth >= 1; depth--) {
|
|
590
|
+
const block = $pos.node(depth);
|
|
591
|
+
const documentPos = $pos.before(depth);
|
|
592
|
+
const container = depth > 0 ? $pos.node(depth - 1) : null;
|
|
593
|
+
const positionInContainer = depth > 0 ? $pos.index(depth - 1) : 0;
|
|
594
|
+
const containerSize = container?.childCount ?? 1;
|
|
595
|
+
if (!passesScopeFilters(block, $pos, depth, options)) continue;
|
|
596
|
+
const candidate = {
|
|
597
|
+
block,
|
|
598
|
+
documentPos,
|
|
599
|
+
treeDepth: depth,
|
|
600
|
+
container,
|
|
601
|
+
positionInContainer,
|
|
602
|
+
isFirstChild: positionInContainer === 0,
|
|
603
|
+
isLastChild: positionInContainer === containerSize - 1,
|
|
604
|
+
resolvedPos: $pos,
|
|
605
|
+
editorView: view
|
|
606
|
+
};
|
|
607
|
+
if (rejectedByMatchers(candidate, options.matchers)) continue;
|
|
608
|
+
const dom = view.nodeDOM(documentPos);
|
|
609
|
+
if (!(dom instanceof HTMLElement)) continue;
|
|
610
|
+
out.push({
|
|
611
|
+
pos: documentPos,
|
|
612
|
+
rect: dom.getBoundingClientRect(),
|
|
613
|
+
dom,
|
|
614
|
+
depth,
|
|
615
|
+
rank: depth,
|
|
616
|
+
node: block
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function collectAtomLeaf($pos, view, options, out) {
|
|
621
|
+
const leaf = $pos.nodeAfter;
|
|
622
|
+
if (leaf === null) return;
|
|
623
|
+
if (!leaf.isAtom || leaf.isInline) return;
|
|
624
|
+
const synthDepth = $pos.depth + 1;
|
|
625
|
+
if (!passesScopeFilters(leaf, $pos, synthDepth, options)) return;
|
|
626
|
+
const parent = $pos.parent;
|
|
627
|
+
const positionInContainer = $pos.index();
|
|
628
|
+
const containerSize = parent.childCount;
|
|
629
|
+
const candidate = {
|
|
630
|
+
block: leaf,
|
|
631
|
+
documentPos: $pos.pos,
|
|
632
|
+
treeDepth: synthDepth,
|
|
633
|
+
container: parent,
|
|
634
|
+
positionInContainer,
|
|
635
|
+
isFirstChild: positionInContainer === 0,
|
|
636
|
+
isLastChild: positionInContainer === containerSize - 1,
|
|
637
|
+
resolvedPos: $pos,
|
|
638
|
+
editorView: view
|
|
639
|
+
};
|
|
640
|
+
if (rejectedByMatchers(candidate, options.matchers)) return;
|
|
641
|
+
const dom = view.nodeDOM($pos.pos);
|
|
642
|
+
if (!(dom instanceof HTMLElement)) return;
|
|
643
|
+
out.push({
|
|
644
|
+
pos: $pos.pos,
|
|
645
|
+
rect: dom.getBoundingClientRect(),
|
|
646
|
+
dom,
|
|
647
|
+
depth: synthDepth,
|
|
648
|
+
rank: synthDepth,
|
|
649
|
+
node: leaf
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
function passesScopeFilters(block, $pos, depth, options) {
|
|
653
|
+
const { allowedTypes, requiredAncestorTypes } = options;
|
|
654
|
+
if (allowedTypes !== void 0 && allowedTypes.length > 0) {
|
|
655
|
+
if (!allowedTypes.includes(block.type.name)) return false;
|
|
656
|
+
}
|
|
657
|
+
if (requiredAncestorTypes !== void 0 && requiredAncestorTypes.length > 0) {
|
|
658
|
+
if (!hasMatchingAncestor($pos, depth, requiredAncestorTypes)) return false;
|
|
659
|
+
}
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
function hasMatchingAncestor($pos, depth, acceptableTypes) {
|
|
663
|
+
for (let d = depth - 1; d >= 1; d--) {
|
|
664
|
+
const ancestor = $pos.node(d);
|
|
665
|
+
if (acceptableTypes.includes(ancestor.type.name)) return true;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
function rejectedByMatchers(candidate, matchers) {
|
|
670
|
+
for (const matcher of matchers) {
|
|
671
|
+
const verdict = matcher.test(candidate);
|
|
672
|
+
if (verdict === "reject") return true;
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
function applyGutterBias(candidates, x, y, config) {
|
|
677
|
+
if (config === null) return;
|
|
678
|
+
for (const c of candidates) {
|
|
679
|
+
const penalty = gutterBiasWeight(x, y, c.rect, c.depth, config);
|
|
680
|
+
c.rank = c.depth - penalty;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function chooseWinner(candidates) {
|
|
684
|
+
return candidates.reduce((best, c) => {
|
|
685
|
+
if (c.rank > best.rank) return c;
|
|
686
|
+
if (c.rank === best.rank && c.depth > best.depth) return c;
|
|
687
|
+
return best;
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/BlockHandle.ts
|
|
692
|
+
var DEFAULT_NESTED_NODES = ["listItem", "taskItem"];
|
|
693
|
+
var LIST_ITEM_TYPES2 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
|
|
694
|
+
var DEFAULT_NEST_THRESHOLD = 28;
|
|
695
|
+
var NESTED_INDICATOR_INDENT_PX = 24;
|
|
696
|
+
var DROP_ZONE_TOL_LEFT = 80;
|
|
697
|
+
var DROP_ZONE_TOL = 16;
|
|
698
|
+
var blockHandlePluginKey = new state.PluginKey("blockHandle");
|
|
699
|
+
function asDragView(view) {
|
|
700
|
+
return view;
|
|
701
|
+
}
|
|
702
|
+
function findScrollableAncestor(el) {
|
|
703
|
+
let cur = el;
|
|
704
|
+
while (cur) {
|
|
705
|
+
const style = getComputedStyle(cur);
|
|
706
|
+
const overflowY = style.overflowY;
|
|
707
|
+
if ((overflowY === "auto" || overflowY === "scroll") && cur.scrollHeight > cur.clientHeight) {
|
|
708
|
+
return cur;
|
|
709
|
+
}
|
|
710
|
+
cur = cur.parentElement;
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function resolveBlockAtCoords(view, clientX, clientY, nested) {
|
|
715
|
+
if (nested.allowedNodes.length === 0) {
|
|
716
|
+
return resolveTopLevelByY(view, clientY);
|
|
717
|
+
}
|
|
718
|
+
const clamped = clampToContent(view, clientX, clientY);
|
|
719
|
+
if (!clamped) return null;
|
|
720
|
+
if (nested.gutterBias) {
|
|
721
|
+
const target = resolveDragTarget(view, clamped.x, clamped.y, {
|
|
722
|
+
matchers: nested.matchers,
|
|
723
|
+
gutterBias: nested.gutterBias,
|
|
724
|
+
allowedTypes: nested.allowedNodes,
|
|
725
|
+
requiredAncestorTypes: nested.allowedContainers
|
|
726
|
+
});
|
|
727
|
+
if (target) {
|
|
728
|
+
return { pos: target.pos, rect: target.rect, dom: target.dom };
|
|
729
|
+
}
|
|
730
|
+
return resolveTopLevelByY(view, clamped.y);
|
|
731
|
+
}
|
|
732
|
+
const found = findDeepestBlockAtY(view, clamped.y, nested.allowedNodes, nested.matchers);
|
|
733
|
+
if (found) {
|
|
734
|
+
return { pos: found.pos, rect: found.rect, dom: found.dom };
|
|
735
|
+
}
|
|
736
|
+
return resolveTopLevelByY(view, clamped.y);
|
|
737
|
+
}
|
|
738
|
+
function resolveTopLevelByY(view, clientY) {
|
|
739
|
+
const doc = view.state.doc;
|
|
740
|
+
let offset = 0;
|
|
741
|
+
let closest = null;
|
|
742
|
+
for (let i = 0; i < doc.childCount; i++) {
|
|
743
|
+
const child = doc.child(i);
|
|
744
|
+
const dom = view.nodeDOM(offset);
|
|
745
|
+
if (dom instanceof HTMLElement) {
|
|
746
|
+
const rect = dom.getBoundingClientRect();
|
|
747
|
+
if (clientY >= rect.top && clientY <= rect.bottom) {
|
|
748
|
+
return { pos: offset, rect, dom };
|
|
749
|
+
}
|
|
750
|
+
const dist = clientY < rect.top ? rect.top - clientY : clientY - rect.bottom;
|
|
751
|
+
if (closest === null || dist < closest.dist) {
|
|
752
|
+
closest = { pos: offset, rect, dom, dist };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
offset += child.nodeSize;
|
|
756
|
+
}
|
|
757
|
+
if (closest) return { pos: closest.pos, rect: closest.rect, dom: closest.dom };
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
var LIST_WRAPPER_TYPE_NAMES = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
|
|
761
|
+
function adjustDropTargetForListWrapper(view, resolved, clientY) {
|
|
762
|
+
const node = view.state.doc.nodeAt(resolved.pos);
|
|
763
|
+
if (!node) return resolved;
|
|
764
|
+
if (!LIST_WRAPPER_TYPE_NAMES.has(node.type.name)) return resolved;
|
|
765
|
+
if (node.childCount === 0) return resolved;
|
|
766
|
+
const isAbove = clientY < resolved.rect.top;
|
|
767
|
+
const isBelow = clientY > resolved.rect.bottom;
|
|
768
|
+
if (!isAbove && !isBelow) return resolved;
|
|
769
|
+
const targetIndex = isBelow ? node.childCount - 1 : 0;
|
|
770
|
+
let childPos = resolved.pos + 1;
|
|
771
|
+
for (let i = 0; i < targetIndex; i++) {
|
|
772
|
+
childPos += node.child(i).nodeSize;
|
|
773
|
+
}
|
|
774
|
+
const childDom = view.nodeDOM(childPos);
|
|
775
|
+
if (!(childDom instanceof HTMLElement)) return resolved;
|
|
776
|
+
return { pos: childPos, rect: childDom.getBoundingClientRect(), dom: childDom };
|
|
777
|
+
}
|
|
778
|
+
function computeDropPlacement(view, clientX, clientY, nested, nestThreshold = DEFAULT_NEST_THRESHOLD) {
|
|
779
|
+
const initial = resolveBlockAtCoords(view, clientX, clientY, nested);
|
|
780
|
+
if (!initial) return null;
|
|
781
|
+
const resolved = adjustDropTargetForListWrapper(view, initial, clientY);
|
|
782
|
+
if (nestThreshold > 0) {
|
|
783
|
+
const targetNode = view.state.doc.nodeAt(resolved.pos);
|
|
784
|
+
if (targetNode && LIST_ITEM_TYPES2.has(targetNode.type.name)) {
|
|
785
|
+
const targetRect = resolved.rect;
|
|
786
|
+
const xInTarget = clientX - targetRect.left;
|
|
787
|
+
const isInsideX = xInTarget >= 0 && xInTarget <= targetRect.width;
|
|
788
|
+
const isInNestZone = xInTarget >= nestThreshold;
|
|
789
|
+
const isInsideY = clientY >= targetRect.top && clientY <= targetRect.bottom;
|
|
790
|
+
if (isInsideX && isInNestZone && isInsideY) {
|
|
791
|
+
const $resolved = view.state.doc.resolve(resolved.pos);
|
|
792
|
+
const wrapperPos = $resolved.before();
|
|
793
|
+
const midY2 = targetRect.top + targetRect.height / 2;
|
|
794
|
+
return {
|
|
795
|
+
pos: resolved.pos,
|
|
796
|
+
rect: targetRect,
|
|
797
|
+
insertAfter: clientY > midY2,
|
|
798
|
+
mode: "nested",
|
|
799
|
+
targetItemPos: resolved.pos,
|
|
800
|
+
wrapperPos
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const rect = resolved.rect;
|
|
806
|
+
const midY = rect.top + rect.height / 2;
|
|
807
|
+
const insertAfter = clientY > midY;
|
|
808
|
+
if (!insertAfter) {
|
|
809
|
+
const prev = findPreviousSiblingAtSameDepth(view, resolved.pos);
|
|
810
|
+
if (prev) {
|
|
811
|
+
return { pos: prev.pos, rect: prev.rect, insertAfter: true, mode: "sibling" };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return { pos: resolved.pos, rect, insertAfter, mode: "sibling" };
|
|
815
|
+
}
|
|
816
|
+
function findPreviousSiblingAtSameDepth(view, pos) {
|
|
817
|
+
const doc = view.state.doc;
|
|
818
|
+
if (pos <= 0) return null;
|
|
819
|
+
const $pos = doc.resolve(pos);
|
|
820
|
+
if ($pos.index() === 0) return null;
|
|
821
|
+
const prevNode = $pos.parent.child($pos.index() - 1);
|
|
822
|
+
const prevPos = pos - prevNode.nodeSize;
|
|
823
|
+
const prevDom = view.nodeDOM(prevPos);
|
|
824
|
+
if (!(prevDom instanceof HTMLElement)) return null;
|
|
825
|
+
return { pos: prevPos, rect: prevDom.getBoundingClientRect() };
|
|
826
|
+
}
|
|
827
|
+
function createBlockHandlePlugin(options) {
|
|
828
|
+
const { pluginKey, editor, hideDelay, disableDrag, autoScroll, autoScrollThreshold, autoScrollMaxSpeed, nested, dropIndicator, nestThreshold } = options;
|
|
829
|
+
const root = document.createElement("div");
|
|
830
|
+
root.className = "dm-block-handle";
|
|
831
|
+
root.setAttribute("data-dm-editor-ui", "");
|
|
832
|
+
const dragBtn = document.createElement("button");
|
|
833
|
+
dragBtn.type = "button";
|
|
834
|
+
dragBtn.className = "dm-block-handle-btn dm-block-handle-drag";
|
|
835
|
+
dragBtn.setAttribute("aria-label", "Drag to reorder, click for options");
|
|
836
|
+
dragBtn.setAttribute("draggable", "true");
|
|
837
|
+
dragBtn.innerHTML = core.defaultIcons["dotsSixVertical"] ?? "";
|
|
838
|
+
const plusBtn = document.createElement("button");
|
|
839
|
+
plusBtn.type = "button";
|
|
840
|
+
plusBtn.className = "dm-block-handle-btn dm-block-handle-plus";
|
|
841
|
+
plusBtn.setAttribute("aria-label", "Add block below");
|
|
842
|
+
plusBtn.innerHTML = core.defaultIcons["plus"] ?? "";
|
|
843
|
+
root.appendChild(dragBtn);
|
|
844
|
+
root.appendChild(plusBtn);
|
|
845
|
+
const indicator = document.createElement("div");
|
|
846
|
+
indicator.className = "dm-block-drop-indicator";
|
|
847
|
+
indicator.setAttribute("data-dm-editor-ui", "");
|
|
848
|
+
let editorEl = null;
|
|
849
|
+
let hoverEl = null;
|
|
850
|
+
let hideTimer = null;
|
|
851
|
+
let hoverRaf = null;
|
|
852
|
+
let pendingHoverCoords = null;
|
|
853
|
+
let currentHoveredPos = null;
|
|
854
|
+
let dragPressActive = false;
|
|
855
|
+
let dragPreview = null;
|
|
856
|
+
let pendingDraggedFrom = null;
|
|
857
|
+
const DRAGOVER_SILENCE_MS = 1500;
|
|
858
|
+
const autoScrollState = { raf: null, target: null, lastClientY: null, lastAt: 0 };
|
|
859
|
+
const resetAutoScroll = () => {
|
|
860
|
+
if (autoScrollState.raf !== null) {
|
|
861
|
+
cancelAnimationFrame(autoScrollState.raf);
|
|
862
|
+
}
|
|
863
|
+
autoScrollState.raf = null;
|
|
864
|
+
autoScrollState.target = null;
|
|
865
|
+
autoScrollState.lastClientY = null;
|
|
866
|
+
autoScrollState.lastAt = 0;
|
|
867
|
+
};
|
|
868
|
+
const clearHideTimer = () => {
|
|
869
|
+
if (hideTimer !== null) {
|
|
870
|
+
window.clearTimeout(hideTimer);
|
|
871
|
+
hideTimer = null;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
const hide = () => {
|
|
875
|
+
root.removeAttribute("data-show");
|
|
876
|
+
currentHoveredPos = null;
|
|
877
|
+
};
|
|
878
|
+
const scheduleHide = () => {
|
|
879
|
+
if (editorEl?.hasAttribute("data-block-context-menu-open")) return;
|
|
880
|
+
clearHideTimer();
|
|
881
|
+
hideTimer = window.setTimeout(() => {
|
|
882
|
+
hide();
|
|
883
|
+
hideTimer = null;
|
|
884
|
+
}, hideDelay);
|
|
885
|
+
};
|
|
886
|
+
const show = (blockEl, blockRect, editorRect) => {
|
|
887
|
+
const cs = getComputedStyle(blockEl);
|
|
888
|
+
let lineHeight = parseFloat(cs.lineHeight);
|
|
889
|
+
if (!Number.isFinite(lineHeight)) {
|
|
890
|
+
const fontSize = parseFloat(cs.fontSize) || 16;
|
|
891
|
+
lineHeight = fontSize * 1.2;
|
|
892
|
+
}
|
|
893
|
+
const handleHeight = root.offsetHeight || 24;
|
|
894
|
+
const offsetIntoBlock = Math.max(0, (lineHeight - handleHeight) / 2);
|
|
895
|
+
const top = blockRect.top - editorRect.top + offsetIntoBlock;
|
|
896
|
+
root.style.top = `${String(top)}px`;
|
|
897
|
+
root.setAttribute("data-show", "");
|
|
898
|
+
};
|
|
899
|
+
const updateHoverState = (view, pos) => {
|
|
900
|
+
const current = pluginKey.getState(view.state)?.hoveredPos ?? null;
|
|
901
|
+
if (current === pos) return;
|
|
902
|
+
view.dispatch(view.state.tr.setMeta(pluginKey, { hoveredPos: pos }));
|
|
903
|
+
};
|
|
904
|
+
const setDraggedFrom = (view, pos) => {
|
|
905
|
+
view.dispatch(view.state.tr.setMeta(pluginKey, { draggedFrom: pos }));
|
|
906
|
+
};
|
|
907
|
+
const onMouseMove = (event) => {
|
|
908
|
+
if (!editorEl) return;
|
|
909
|
+
if (dragPressActive) return;
|
|
910
|
+
if (editorEl.hasAttribute("data-block-context-menu-open")) return;
|
|
911
|
+
pendingHoverCoords = { x: event.clientX, y: event.clientY };
|
|
912
|
+
if (hoverRaf !== null) return;
|
|
913
|
+
hoverRaf = requestAnimationFrame(() => {
|
|
914
|
+
hoverRaf = null;
|
|
915
|
+
const coords = pendingHoverCoords;
|
|
916
|
+
pendingHoverCoords = null;
|
|
917
|
+
if (!coords || !editorEl) return;
|
|
918
|
+
if (editorEl.hasAttribute("data-block-context-menu-open")) return;
|
|
919
|
+
const resolved = resolveBlockAtCoords(editor.view, coords.x, coords.y, nested);
|
|
920
|
+
if (!resolved) {
|
|
921
|
+
scheduleHide();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
clearHideTimer();
|
|
925
|
+
updateHoverState(editor.view, resolved.pos);
|
|
926
|
+
if (resolved.pos === currentHoveredPos && root.hasAttribute("data-show")) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
currentHoveredPos = resolved.pos;
|
|
930
|
+
const editorRect = editorEl.getBoundingClientRect();
|
|
931
|
+
show(resolved.dom, resolved.rect, editorRect);
|
|
932
|
+
});
|
|
933
|
+
};
|
|
934
|
+
const onMouseLeave = () => {
|
|
935
|
+
scheduleHide();
|
|
936
|
+
};
|
|
937
|
+
const onMouseEnter = () => {
|
|
938
|
+
clearHideTimer();
|
|
939
|
+
};
|
|
940
|
+
const onDismissOverlays = () => {
|
|
941
|
+
if (editorEl?.hasAttribute("data-block-context-menu-open")) return;
|
|
942
|
+
hide();
|
|
943
|
+
updateHoverState(editor.view, null);
|
|
944
|
+
};
|
|
945
|
+
const onPlusClick = (event) => {
|
|
946
|
+
event.preventDefault();
|
|
947
|
+
event.stopPropagation();
|
|
948
|
+
if (!editor.isEditable) return;
|
|
949
|
+
const state$1 = pluginKey.getState(editor.view.state);
|
|
950
|
+
const pos = state$1?.hoveredPos ?? null;
|
|
951
|
+
if (pos === null) return;
|
|
952
|
+
const node = editor.view.state.doc.nodeAt(pos);
|
|
953
|
+
if (!node) return;
|
|
954
|
+
const paragraphType = editor.view.state.schema.nodes["paragraph"];
|
|
955
|
+
if (!paragraphType) return;
|
|
956
|
+
editorEl?.dispatchEvent(new Event("dm:dismiss-overlays", { bubbles: false }));
|
|
957
|
+
const blockEnd = pos + node.nodeSize;
|
|
958
|
+
const tr = editor.view.state.tr;
|
|
959
|
+
tr.insert(blockEnd, paragraphType.create());
|
|
960
|
+
const sel = state.TextSelection.near(tr.doc.resolve(blockEnd + 1));
|
|
961
|
+
tr.setSelection(sel);
|
|
962
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
963
|
+
editor.view.focus();
|
|
964
|
+
showFloatingMenu(editor.view);
|
|
965
|
+
};
|
|
966
|
+
const onPlusBtnMouseDown = (event) => {
|
|
967
|
+
event.preventDefault();
|
|
968
|
+
};
|
|
969
|
+
const onDragBtnMouseDown = () => {
|
|
970
|
+
dragPressActive = true;
|
|
971
|
+
document.addEventListener("mouseup", releaseDragPress, { once: true });
|
|
972
|
+
};
|
|
973
|
+
const releaseDragPress = () => {
|
|
974
|
+
dragPressActive = false;
|
|
975
|
+
document.removeEventListener("mouseup", releaseDragPress);
|
|
976
|
+
};
|
|
977
|
+
const onDragBtnClick = (event) => {
|
|
978
|
+
event.preventDefault();
|
|
979
|
+
event.stopPropagation();
|
|
980
|
+
if (!editor.isEditable) return;
|
|
981
|
+
const state = pluginKey.getState(editor.view.state);
|
|
982
|
+
const pos = state?.hoveredPos ?? null;
|
|
983
|
+
if (pos === null) return;
|
|
984
|
+
editorEl?.dispatchEvent(new CustomEvent("dm:block-context-menu-open", {
|
|
985
|
+
bubbles: false,
|
|
986
|
+
detail: { blockPos: pos, anchorElement: dragBtn }
|
|
987
|
+
}));
|
|
988
|
+
};
|
|
989
|
+
const onDocumentDragover = (event) => {
|
|
990
|
+
autoScrollState.lastClientY = event.clientY;
|
|
991
|
+
autoScrollState.lastAt = performance.now();
|
|
992
|
+
const inZone = isCursorOverDropZone(event.clientX, event.clientY);
|
|
993
|
+
if (inZone) event.preventDefault();
|
|
994
|
+
if (dropIndicator) {
|
|
995
|
+
if (!inZone) {
|
|
996
|
+
indicator.removeAttribute("data-show");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
updateDropIndicator(event.clientX, event.clientY);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
const onDocumentDrop = (event) => {
|
|
1003
|
+
if (event.defaultPrevented) return;
|
|
1004
|
+
const view = editor.view;
|
|
1005
|
+
const dragging = asDragView(view).dragging;
|
|
1006
|
+
if (!dragging) return;
|
|
1007
|
+
if (!isCursorOverDropZone(event.clientX, event.clientY)) return;
|
|
1008
|
+
if (performBlockDrop(event.clientX, event.clientY)) {
|
|
1009
|
+
event.preventDefault();
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
const performBlockDrop = (clientX, clientY) => {
|
|
1013
|
+
const view = editor.view;
|
|
1014
|
+
const state = pluginKey.getState(view.state);
|
|
1015
|
+
const draggedFrom = state?.draggedFrom ?? pendingDraggedFrom ?? asDragView(view).dragging?.node?.from ?? null;
|
|
1016
|
+
if (draggedFrom === null) return false;
|
|
1017
|
+
const sourceNode = view.state.doc.nodeAt(draggedFrom);
|
|
1018
|
+
if (!sourceNode) return false;
|
|
1019
|
+
const placement = computeDropPlacement(view, clientX, clientY, nested, nestThreshold);
|
|
1020
|
+
if (!placement) return false;
|
|
1021
|
+
const tr = view.state.tr;
|
|
1022
|
+
if (placement.mode === "nested" && placement.targetItemPos !== void 0 && placement.wrapperPos !== void 0) {
|
|
1023
|
+
const ok = moveBlockAsNestedChild(tr, draggedFrom, placement.wrapperPos, placement.targetItemPos);
|
|
1024
|
+
if (ok) {
|
|
1025
|
+
view.dispatch(tr.scrollIntoView());
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
const targetNode = view.state.doc.nodeAt(placement.pos);
|
|
1030
|
+
const targetEnd = targetNode ? placement.pos + targetNode.nodeSize : placement.pos;
|
|
1031
|
+
const targetPos = placement.insertAfter ? targetEnd : placement.pos;
|
|
1032
|
+
moveBlock(tr, draggedFrom, targetPos);
|
|
1033
|
+
view.dispatch(tr.scrollIntoView());
|
|
1034
|
+
return true;
|
|
1035
|
+
};
|
|
1036
|
+
const isCursorOverDropZone = (clientX, clientY) => {
|
|
1037
|
+
if (!editorEl) return false;
|
|
1038
|
+
const rect = editorEl.getBoundingClientRect();
|
|
1039
|
+
return clientX >= rect.left - DROP_ZONE_TOL_LEFT && clientX <= rect.right + DROP_ZONE_TOL && clientY >= rect.top - DROP_ZONE_TOL && clientY <= rect.bottom + DROP_ZONE_TOL;
|
|
1040
|
+
};
|
|
1041
|
+
const updateDropIndicator = (clientX, clientY) => {
|
|
1042
|
+
if (!editorEl) return;
|
|
1043
|
+
const placement = computeDropPlacement(editor.view, clientX, clientY, nested, nestThreshold);
|
|
1044
|
+
if (!placement) {
|
|
1045
|
+
indicator.removeAttribute("data-show");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const editorRect = editorEl.getBoundingClientRect();
|
|
1049
|
+
let lineY;
|
|
1050
|
+
let left;
|
|
1051
|
+
let width;
|
|
1052
|
+
if (placement.mode === "nested") {
|
|
1053
|
+
const indent = NESTED_INDICATOR_INDENT_PX;
|
|
1054
|
+
lineY = placement.rect.bottom - editorRect.top;
|
|
1055
|
+
left = placement.rect.left - editorRect.left + indent;
|
|
1056
|
+
width = Math.max(0, placement.rect.width - indent);
|
|
1057
|
+
} else {
|
|
1058
|
+
lineY = (placement.insertAfter ? placement.rect.bottom : placement.rect.top) - editorRect.top;
|
|
1059
|
+
left = placement.rect.left - editorRect.left;
|
|
1060
|
+
width = placement.rect.width;
|
|
1061
|
+
}
|
|
1062
|
+
indicator.style.top = `${String(lineY)}px`;
|
|
1063
|
+
indicator.style.left = `${String(left)}px`;
|
|
1064
|
+
indicator.style.width = `${String(width)}px`;
|
|
1065
|
+
indicator.setAttribute("data-show", "");
|
|
1066
|
+
indicator.setAttribute("data-mode", placement.mode);
|
|
1067
|
+
};
|
|
1068
|
+
const stopAutoScroll = () => {
|
|
1069
|
+
resetAutoScroll();
|
|
1070
|
+
};
|
|
1071
|
+
const hideDropIndicator = () => {
|
|
1072
|
+
indicator.removeAttribute("data-show");
|
|
1073
|
+
};
|
|
1074
|
+
let dragoverAttached = false;
|
|
1075
|
+
const startDragListeners = () => {
|
|
1076
|
+
if (dragoverAttached) return;
|
|
1077
|
+
document.addEventListener("dragover", onDocumentDragover);
|
|
1078
|
+
document.addEventListener("drop", onDocumentDrop);
|
|
1079
|
+
dragoverAttached = true;
|
|
1080
|
+
};
|
|
1081
|
+
const stopDragListeners = () => {
|
|
1082
|
+
if (!dragoverAttached) return;
|
|
1083
|
+
document.removeEventListener("dragover", onDocumentDragover);
|
|
1084
|
+
document.removeEventListener("drop", onDocumentDrop);
|
|
1085
|
+
dragoverAttached = false;
|
|
1086
|
+
};
|
|
1087
|
+
const autoScrollTick = () => {
|
|
1088
|
+
const target = autoScrollState.target;
|
|
1089
|
+
if (target === null) {
|
|
1090
|
+
autoScrollState.raf = null;
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (autoScrollState.lastAt > 0 && performance.now() - autoScrollState.lastAt > DRAGOVER_SILENCE_MS) {
|
|
1094
|
+
stopAutoScroll();
|
|
1095
|
+
stopDragListeners();
|
|
1096
|
+
hideDropIndicator();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (autoScrollState.lastClientY !== null) {
|
|
1100
|
+
const rect = target.getBoundingClientRect();
|
|
1101
|
+
const distFromTop = autoScrollState.lastClientY - rect.top;
|
|
1102
|
+
const distFromBottom = rect.bottom - autoScrollState.lastClientY;
|
|
1103
|
+
let delta = 0;
|
|
1104
|
+
if (distFromTop < autoScrollThreshold && distFromTop >= 0) {
|
|
1105
|
+
delta = -Math.round(autoScrollMaxSpeed * (1 - distFromTop / autoScrollThreshold));
|
|
1106
|
+
} else if (distFromBottom < autoScrollThreshold && distFromBottom >= 0) {
|
|
1107
|
+
delta = Math.round(autoScrollMaxSpeed * (1 - distFromBottom / autoScrollThreshold));
|
|
1108
|
+
}
|
|
1109
|
+
if (delta !== 0) {
|
|
1110
|
+
target.scrollBy(0, delta);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
autoScrollState.raf = requestAnimationFrame(autoScrollTick);
|
|
1114
|
+
};
|
|
1115
|
+
const startAutoScroll = () => {
|
|
1116
|
+
if (!autoScroll) return;
|
|
1117
|
+
if (!editorEl) return;
|
|
1118
|
+
const target = findScrollableAncestor(editorEl);
|
|
1119
|
+
if (!target) return;
|
|
1120
|
+
autoScrollState.target = target;
|
|
1121
|
+
autoScrollState.lastClientY = null;
|
|
1122
|
+
autoScrollState.lastAt = performance.now();
|
|
1123
|
+
autoScrollState.raf = requestAnimationFrame(autoScrollTick);
|
|
1124
|
+
};
|
|
1125
|
+
const onDragStart = (event) => {
|
|
1126
|
+
if (disableDrag || !editor.isEditable) {
|
|
1127
|
+
event.preventDefault();
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const state$1 = pluginKey.getState(editor.view.state);
|
|
1131
|
+
const pos = state$1?.hoveredPos ?? null;
|
|
1132
|
+
if (pos === null) {
|
|
1133
|
+
event.preventDefault();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
pendingDraggedFrom = pos;
|
|
1137
|
+
const node = editor.view.state.doc.nodeAt(pos);
|
|
1138
|
+
if (!node) {
|
|
1139
|
+
event.preventDefault();
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
const sourceEnd = pos + node.nodeSize;
|
|
1143
|
+
const slice = editor.view.state.doc.slice(pos, sourceEnd);
|
|
1144
|
+
const nodeSelection = state.NodeSelection.create(editor.view.state.doc, pos);
|
|
1145
|
+
if (event.dataTransfer) {
|
|
1146
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1147
|
+
event.dataTransfer.setData("text/plain", node.textContent);
|
|
1148
|
+
const dom = editor.view.nodeDOM(pos);
|
|
1149
|
+
if (dom instanceof HTMLElement) {
|
|
1150
|
+
const ghost = createDragGhost(dom);
|
|
1151
|
+
dragPreview = ghost.wrapper;
|
|
1152
|
+
event.dataTransfer.setDragImage(ghost.wrapper, 10, 10);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
asDragView(editor.view).dragging = {
|
|
1156
|
+
slice,
|
|
1157
|
+
move: true,
|
|
1158
|
+
node: nodeSelection
|
|
1159
|
+
};
|
|
1160
|
+
window.setTimeout(() => {
|
|
1161
|
+
if (pendingDraggedFrom === null) return;
|
|
1162
|
+
editor.view.dispatch(
|
|
1163
|
+
editor.view.state.tr.setSelection(nodeSelection).setMeta(pluginKey, { draggedFrom: pos })
|
|
1164
|
+
);
|
|
1165
|
+
editorEl?.dispatchEvent(new Event("dm:dismiss-overlays", { bubbles: false }));
|
|
1166
|
+
hide();
|
|
1167
|
+
}, 0);
|
|
1168
|
+
startAutoScroll();
|
|
1169
|
+
startDragListeners();
|
|
1170
|
+
};
|
|
1171
|
+
const teardownDragPreview = () => {
|
|
1172
|
+
if (dragPreview) {
|
|
1173
|
+
dragPreview.remove();
|
|
1174
|
+
dragPreview = null;
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
const onDragEnd = () => {
|
|
1178
|
+
asDragView(editor.view).dragging = null;
|
|
1179
|
+
setDraggedFrom(editor.view, null);
|
|
1180
|
+
pendingDraggedFrom = null;
|
|
1181
|
+
stopAutoScroll();
|
|
1182
|
+
stopDragListeners();
|
|
1183
|
+
hideDropIndicator();
|
|
1184
|
+
teardownDragPreview();
|
|
1185
|
+
releaseDragPress();
|
|
1186
|
+
};
|
|
1187
|
+
return new state.Plugin({
|
|
1188
|
+
key: pluginKey,
|
|
1189
|
+
state: {
|
|
1190
|
+
init: () => ({ hoveredPos: null, draggedFrom: null }),
|
|
1191
|
+
apply(tr, prev) {
|
|
1192
|
+
const meta = tr.getMeta(pluginKey);
|
|
1193
|
+
let next = prev;
|
|
1194
|
+
if (meta) {
|
|
1195
|
+
if ("hoveredPos" in meta) {
|
|
1196
|
+
next = { ...next, hoveredPos: meta.hoveredPos ?? null };
|
|
1197
|
+
}
|
|
1198
|
+
if ("draggedFrom" in meta) {
|
|
1199
|
+
next = { ...next, draggedFrom: meta.draggedFrom ?? null };
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
if (tr.docChanged) {
|
|
1203
|
+
if (next.hoveredPos !== null) {
|
|
1204
|
+
const mapped = tr.mapping.mapResult(next.hoveredPos, 1);
|
|
1205
|
+
next = { ...next, hoveredPos: mapped.deleted ? null : mapped.pos };
|
|
1206
|
+
}
|
|
1207
|
+
if (next.draggedFrom !== null) {
|
|
1208
|
+
const mapped = tr.mapping.mapResult(next.draggedFrom, 1);
|
|
1209
|
+
next = { ...next, draggedFrom: mapped.deleted ? null : mapped.pos };
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
return next;
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
props: {
|
|
1216
|
+
// Custom drop handler - gives Notion-like "above-mid → insert before,
|
|
1217
|
+
// below-mid → insert after" block-level semantics instead of PM's
|
|
1218
|
+
// default `posAtCoords + dropPoint` which for short paragraphs
|
|
1219
|
+
// rounds to an arbitrary neighbour boundary and surprises users.
|
|
1220
|
+
// The `moveBlock` helper handles the delete+insert + position
|
|
1221
|
+
// adjustment so the block keeps its attrs (UniqueID, colors) and
|
|
1222
|
+
// the doc ends up in a predictable state.
|
|
1223
|
+
handleDrop(_view, event, _slice, moved) {
|
|
1224
|
+
if (!moved) return false;
|
|
1225
|
+
if (performBlockDrop(event.clientX, event.clientY)) {
|
|
1226
|
+
event.preventDefault();
|
|
1227
|
+
return true;
|
|
1228
|
+
}
|
|
1229
|
+
event.preventDefault();
|
|
1230
|
+
return true;
|
|
1231
|
+
}
|
|
1232
|
+
},
|
|
1233
|
+
view: (editorView) => {
|
|
1234
|
+
editorEl = editorView.dom.closest(".dm-editor");
|
|
1235
|
+
if (!editorEl) {
|
|
1236
|
+
return { destroy: () => {
|
|
1237
|
+
} };
|
|
1238
|
+
}
|
|
1239
|
+
editorEl.classList.add("dm-editor--has-block-handle");
|
|
1240
|
+
editorEl.appendChild(root);
|
|
1241
|
+
if (dropIndicator) {
|
|
1242
|
+
editorEl.appendChild(indicator);
|
|
1243
|
+
}
|
|
1244
|
+
hide();
|
|
1245
|
+
hideDropIndicator();
|
|
1246
|
+
hoverEl = editorEl.parentElement ?? editorEl;
|
|
1247
|
+
hoverEl.addEventListener("mousemove", onMouseMove);
|
|
1248
|
+
hoverEl.addEventListener("mouseleave", onMouseLeave);
|
|
1249
|
+
hoverEl.addEventListener("mouseenter", onMouseEnter);
|
|
1250
|
+
editorEl.addEventListener("dm:dismiss-overlays", onDismissOverlays);
|
|
1251
|
+
plusBtn.addEventListener("mousedown", onPlusBtnMouseDown);
|
|
1252
|
+
plusBtn.addEventListener("click", onPlusClick);
|
|
1253
|
+
dragBtn.addEventListener("mousedown", onDragBtnMouseDown);
|
|
1254
|
+
dragBtn.addEventListener("click", onDragBtnClick);
|
|
1255
|
+
dragBtn.addEventListener("dragstart", onDragStart);
|
|
1256
|
+
dragBtn.addEventListener("dragend", onDragEnd);
|
|
1257
|
+
return {
|
|
1258
|
+
destroy: () => {
|
|
1259
|
+
clearHideTimer();
|
|
1260
|
+
stopAutoScroll();
|
|
1261
|
+
stopDragListeners();
|
|
1262
|
+
hideDropIndicator();
|
|
1263
|
+
indicator.remove();
|
|
1264
|
+
teardownDragPreview();
|
|
1265
|
+
if (hoverRaf !== null) {
|
|
1266
|
+
cancelAnimationFrame(hoverRaf);
|
|
1267
|
+
hoverRaf = null;
|
|
1268
|
+
}
|
|
1269
|
+
hoverEl?.removeEventListener("mousemove", onMouseMove);
|
|
1270
|
+
hoverEl?.removeEventListener("mouseleave", onMouseLeave);
|
|
1271
|
+
hoverEl?.removeEventListener("mouseenter", onMouseEnter);
|
|
1272
|
+
editorEl?.removeEventListener("dm:dismiss-overlays", onDismissOverlays);
|
|
1273
|
+
plusBtn.removeEventListener("mousedown", onPlusBtnMouseDown);
|
|
1274
|
+
plusBtn.removeEventListener("click", onPlusClick);
|
|
1275
|
+
dragBtn.removeEventListener("mousedown", onDragBtnMouseDown);
|
|
1276
|
+
document.removeEventListener("mouseup", releaseDragPress);
|
|
1277
|
+
dragBtn.removeEventListener("click", onDragBtnClick);
|
|
1278
|
+
dragBtn.removeEventListener("dragstart", onDragStart);
|
|
1279
|
+
dragBtn.removeEventListener("dragend", onDragEnd);
|
|
1280
|
+
editorEl?.classList.remove("dm-editor--has-block-handle");
|
|
1281
|
+
root.remove();
|
|
1282
|
+
editorEl = null;
|
|
1283
|
+
hoverEl = null;
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
var BlockHandle = core.Extension.create({
|
|
1290
|
+
name: "blockHandle",
|
|
1291
|
+
addOptions() {
|
|
1292
|
+
return {
|
|
1293
|
+
hideDelay: 200,
|
|
1294
|
+
disableDrag: false,
|
|
1295
|
+
autoScroll: true,
|
|
1296
|
+
autoScrollThreshold: 48,
|
|
1297
|
+
autoScrollMaxSpeed: 18,
|
|
1298
|
+
nested: false,
|
|
1299
|
+
dropIndicator: true,
|
|
1300
|
+
nestThreshold: DEFAULT_NEST_THRESHOLD
|
|
1301
|
+
};
|
|
1302
|
+
},
|
|
1303
|
+
addProseMirrorPlugins() {
|
|
1304
|
+
const editor = this.editor;
|
|
1305
|
+
if (!editor) return [];
|
|
1306
|
+
return [
|
|
1307
|
+
createBlockHandlePlugin({
|
|
1308
|
+
pluginKey: blockHandlePluginKey,
|
|
1309
|
+
editor,
|
|
1310
|
+
hideDelay: this.options.hideDelay ?? 200,
|
|
1311
|
+
disableDrag: this.options.disableDrag ?? false,
|
|
1312
|
+
autoScroll: this.options.autoScroll ?? true,
|
|
1313
|
+
autoScrollThreshold: this.options.autoScrollThreshold ?? 48,
|
|
1314
|
+
autoScrollMaxSpeed: this.options.autoScrollMaxSpeed ?? 18,
|
|
1315
|
+
nested: resolveNestedConfig(this.options.nested),
|
|
1316
|
+
dropIndicator: this.options.dropIndicator ?? true,
|
|
1317
|
+
nestThreshold: this.options.nestThreshold ?? DEFAULT_NEST_THRESHOLD
|
|
1318
|
+
})
|
|
1319
|
+
];
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
function resolveNestedConfig(nested) {
|
|
1323
|
+
if (nested === true) {
|
|
1324
|
+
return {
|
|
1325
|
+
allowedNodes: [...DEFAULT_NESTED_NODES],
|
|
1326
|
+
allowedContainers: [],
|
|
1327
|
+
gutterBias: null,
|
|
1328
|
+
matchers: [...DEFAULT_BLOCK_MATCHERS]
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
if (nested && typeof nested === "object") {
|
|
1332
|
+
const allowedNodes = nested.allowedNodes ?? DEFAULT_NESTED_NODES;
|
|
1333
|
+
if (allowedNodes.length === 0) {
|
|
1334
|
+
return { allowedNodes: [], allowedContainers: [], gutterBias: null, matchers: [] };
|
|
1335
|
+
}
|
|
1336
|
+
const useDefaults = nested.defaultMatchers ?? true;
|
|
1337
|
+
const matchers = [];
|
|
1338
|
+
if (useDefaults) matchers.push(...DEFAULT_BLOCK_MATCHERS);
|
|
1339
|
+
if (nested.matchers) matchers.push(...nested.matchers);
|
|
1340
|
+
return {
|
|
1341
|
+
allowedNodes: [...allowedNodes],
|
|
1342
|
+
allowedContainers: nested.allowedContainers ? [...nested.allowedContainers] : [],
|
|
1343
|
+
gutterBias: resolveGutterBias(nested.promoteOnEdge),
|
|
1344
|
+
matchers
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
return { allowedNodes: [], allowedContainers: [], gutterBias: null, matchers: [] };
|
|
1348
|
+
}
|
|
1349
|
+
var KeyboardReorder = core.Extension.create({
|
|
1350
|
+
name: "keyboardReorder",
|
|
1351
|
+
addKeyboardShortcuts() {
|
|
1352
|
+
const { editor } = this;
|
|
1353
|
+
return {
|
|
1354
|
+
"Mod-Shift-ArrowUp": () => moveCurrentBlock(editor, "up"),
|
|
1355
|
+
"Mod-Shift-ArrowDown": () => moveCurrentBlock(editor, "down")
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
function moveCurrentBlock(editor, direction) {
|
|
1360
|
+
if (!editor || editor.isDestroyed) return false;
|
|
1361
|
+
if (!editor.isEditable) return false;
|
|
1362
|
+
const { state: state$1, view } = editor;
|
|
1363
|
+
const { $from } = state$1.selection;
|
|
1364
|
+
const topLevel = findTopLevelBlock(state$1.doc, $from.pos);
|
|
1365
|
+
if (!topLevel) return false;
|
|
1366
|
+
if (direction === "up" && topLevel.index === 0) return false;
|
|
1367
|
+
if (direction === "down" && topLevel.index >= state$1.doc.childCount - 1) return false;
|
|
1368
|
+
const selectionOffsetInBlock = Math.max(
|
|
1369
|
+
0,
|
|
1370
|
+
Math.min($from.pos - topLevel.pos, topLevel.node.nodeSize - 1)
|
|
1371
|
+
);
|
|
1372
|
+
let targetPos;
|
|
1373
|
+
if (direction === "up") {
|
|
1374
|
+
targetPos = state$1.doc.resolve(topLevel.pos).posAtIndex(topLevel.index - 1, 0);
|
|
1375
|
+
} else {
|
|
1376
|
+
const nextSibling = state$1.doc.child(topLevel.index + 1);
|
|
1377
|
+
const nextStart = state$1.doc.resolve(topLevel.pos).posAtIndex(topLevel.index + 1, 0);
|
|
1378
|
+
targetPos = nextStart + nextSibling.nodeSize;
|
|
1379
|
+
}
|
|
1380
|
+
const tr = state$1.tr;
|
|
1381
|
+
moveBlock(tr, topLevel.pos, targetPos);
|
|
1382
|
+
const newBlockPos = direction === "up" ? targetPos : targetPos - topLevel.node.nodeSize;
|
|
1383
|
+
const selectionPos = Math.min(
|
|
1384
|
+
newBlockPos + selectionOffsetInBlock,
|
|
1385
|
+
Math.max(0, tr.doc.content.size - 1)
|
|
1386
|
+
);
|
|
1387
|
+
tr.setSelection(state.TextSelection.near(tr.doc.resolve(selectionPos)));
|
|
1388
|
+
view.dispatch(tr.scrollIntoView());
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
function deleteBlock(tr, blockPos) {
|
|
1392
|
+
if (blockPos < 0 || blockPos >= tr.doc.content.size) return tr;
|
|
1393
|
+
const node = tr.doc.nodeAt(blockPos);
|
|
1394
|
+
if (!node) return tr;
|
|
1395
|
+
const blockEnd = blockPos + node.nodeSize;
|
|
1396
|
+
const { from, to } = expandToEmptyWrappers(tr.doc, blockPos, blockEnd);
|
|
1397
|
+
const wouldEmptyDoc = from === 0 && to === tr.doc.content.size;
|
|
1398
|
+
if (wouldEmptyDoc) {
|
|
1399
|
+
const paragraphType = tr.doc.type.schema.nodes["paragraph"];
|
|
1400
|
+
if (!paragraphType) {
|
|
1401
|
+
return tr;
|
|
1402
|
+
}
|
|
1403
|
+
const replacement = paragraphType.createAndFill();
|
|
1404
|
+
if (!replacement) return tr;
|
|
1405
|
+
tr.replaceWith(from, to, replacement);
|
|
1406
|
+
tr.setSelection(state.TextSelection.near(tr.doc.resolve(from + 1)));
|
|
1407
|
+
return tr;
|
|
1408
|
+
}
|
|
1409
|
+
tr.delete(from, to);
|
|
1410
|
+
return tr;
|
|
1411
|
+
}
|
|
1412
|
+
function duplicateBlock(tr, blockPos, transformAttrs) {
|
|
1413
|
+
if (blockPos < 0 || blockPos >= tr.doc.content.size) return tr;
|
|
1414
|
+
const node = tr.doc.nodeAt(blockPos);
|
|
1415
|
+
if (!node) return tr;
|
|
1416
|
+
const blockEnd = blockPos + node.nodeSize;
|
|
1417
|
+
const attrs = transformAttrs ? transformAttrs(node.attrs) : node.attrs;
|
|
1418
|
+
const copy = node.type.create(attrs, node.content, node.marks);
|
|
1419
|
+
tr.insert(blockEnd, copy);
|
|
1420
|
+
return tr;
|
|
1421
|
+
}
|
|
1422
|
+
function turnIntoBlock(tr, blockPos, targetType, attrs) {
|
|
1423
|
+
if (blockPos < 0 || blockPos >= tr.doc.content.size) return tr;
|
|
1424
|
+
const node = tr.doc.nodeAt(blockPos);
|
|
1425
|
+
if (!node) return tr;
|
|
1426
|
+
if (!targetType.isTextblock) return tr;
|
|
1427
|
+
const blockEnd = blockPos + node.nodeSize;
|
|
1428
|
+
const validKeys = Object.keys(targetType.spec.attrs ?? {});
|
|
1429
|
+
const preserved = {};
|
|
1430
|
+
const sourceAttrs = node.attrs;
|
|
1431
|
+
for (const key of validKeys) {
|
|
1432
|
+
if (key in sourceAttrs) preserved[key] = sourceAttrs[key];
|
|
1433
|
+
}
|
|
1434
|
+
const mergedAttrs = attrs ? { ...preserved, ...attrs } : preserved;
|
|
1435
|
+
tr.setBlockType(blockPos, blockEnd, targetType, mergedAttrs);
|
|
1436
|
+
return tr;
|
|
1437
|
+
}
|
|
1438
|
+
function turnIntoWrapper(editor, blockPos, command) {
|
|
1439
|
+
const { state: state$1 } = editor.view;
|
|
1440
|
+
if (blockPos < 0 || blockPos >= state$1.doc.content.size) return false;
|
|
1441
|
+
const node = state$1.doc.nodeAt(blockPos);
|
|
1442
|
+
if (!node?.type.isTextblock) return false;
|
|
1443
|
+
const isListTarget = command !== "toggleBlockquote";
|
|
1444
|
+
const needsStepDown = isListTarget && node.type.name !== "paragraph";
|
|
1445
|
+
const tr = state$1.tr;
|
|
1446
|
+
if (needsStepDown) {
|
|
1447
|
+
const paragraphType = state$1.schema.nodes["paragraph"];
|
|
1448
|
+
if (!paragraphType) return false;
|
|
1449
|
+
tr.setBlockType(blockPos, blockPos + node.nodeSize, paragraphType);
|
|
1450
|
+
}
|
|
1451
|
+
tr.setSelection(state.TextSelection.create(tr.doc, blockPos + 1));
|
|
1452
|
+
editor.view.dispatch(tr);
|
|
1453
|
+
switch (command) {
|
|
1454
|
+
case "toggleBulletList":
|
|
1455
|
+
return editor.commands.toggleBulletList();
|
|
1456
|
+
case "toggleOrderedList":
|
|
1457
|
+
return editor.commands.toggleOrderedList();
|
|
1458
|
+
case "toggleTaskList":
|
|
1459
|
+
return editor.commands.toggleTaskList();
|
|
1460
|
+
case "toggleBlockquote":
|
|
1461
|
+
return editor.commands.toggleBlockquote();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// src/BlockContextMenu.ts
|
|
1466
|
+
var blockContextMenuPluginKey = new state.PluginKey("blockContextMenu");
|
|
1467
|
+
var DEFAULT_TURN_INTO = [
|
|
1468
|
+
{ label: "Paragraph", icon: "textT", nodeType: "paragraph" },
|
|
1469
|
+
{ label: "Heading 1", icon: "textHOne", nodeType: "heading", attrs: { level: 1 } },
|
|
1470
|
+
{ label: "Heading 2", icon: "textHTwo", nodeType: "heading", attrs: { level: 2 } },
|
|
1471
|
+
{ label: "Heading 3", icon: "textHThree", nodeType: "heading", attrs: { level: 3 } },
|
|
1472
|
+
{ label: "Bullet list", icon: "listBullets", nodeType: "bulletList", command: "toggleBulletList" },
|
|
1473
|
+
{ label: "Ordered list", icon: "listNumbers", nodeType: "orderedList", command: "toggleOrderedList" },
|
|
1474
|
+
{ label: "To-do list", icon: "listChecks", nodeType: "taskList", command: "toggleTaskList" },
|
|
1475
|
+
{ label: "Quote", icon: "quotes", nodeType: "blockquote", command: "toggleBlockquote" },
|
|
1476
|
+
{ label: "Code block", icon: "codeBlock", nodeType: "codeBlock" }
|
|
1477
|
+
];
|
|
1478
|
+
function defaultCopyLinkUrl(blockId) {
|
|
1479
|
+
if (typeof window === "undefined") return `#${blockId}`;
|
|
1480
|
+
const { pathname, search } = window.location;
|
|
1481
|
+
return `${pathname}${search}#${blockId}`;
|
|
1482
|
+
}
|
|
1483
|
+
function matchingSelectorFor(anchor) {
|
|
1484
|
+
const ariaLabel = anchor.getAttribute("aria-label");
|
|
1485
|
+
if (ariaLabel) {
|
|
1486
|
+
const escaped = ariaLabel.replace(/"/g, '\\"');
|
|
1487
|
+
return `button[aria-label="${escaped}"]`;
|
|
1488
|
+
}
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
function createBlockContextMenuPlugin(options) {
|
|
1492
|
+
const { pluginKey, editor, turnIntoEnabled, turnIntoTargets, copyLinkEnabled, onCopyLink, blockColorEnabled } = options;
|
|
1493
|
+
const uniqueIDExt = editor.extensionManager.extensions.find((ext) => ext.name === "uniqueID");
|
|
1494
|
+
const uniqueIDAttrName = uniqueIDExt ? uniqueIDExt.options.attributeName ?? "id" : null;
|
|
1495
|
+
const uniqueIDGenerate = uniqueIDExt ? uniqueIDExt.options.generateID ?? null : null;
|
|
1496
|
+
const blockColorExt = editor.extensionManager.extensions.find((ext) => ext.name === "blockColor");
|
|
1497
|
+
const blockColorOpts = blockColorExt ? blockColorExt.options : null;
|
|
1498
|
+
const blockColorTypes = blockColorOpts?.types ?? null;
|
|
1499
|
+
const blockBgPalette = blockColorOpts?.bgColors ?? [];
|
|
1500
|
+
const blockTextPalette = blockColorOpts?.textColors ?? [];
|
|
1501
|
+
const root = document.createElement("div");
|
|
1502
|
+
root.className = "dm-block-context-menu";
|
|
1503
|
+
root.setAttribute("role", "menu");
|
|
1504
|
+
root.setAttribute("aria-label", "Block options");
|
|
1505
|
+
root.setAttribute("data-dm-editor-ui", "");
|
|
1506
|
+
let editorEl = null;
|
|
1507
|
+
let cleanupFloating = null;
|
|
1508
|
+
let currentBlockPos = null;
|
|
1509
|
+
let focusedIndex = 0;
|
|
1510
|
+
let menuItemButtons = [];
|
|
1511
|
+
let initialFocusRaf = null;
|
|
1512
|
+
let scrollLocked = false;
|
|
1513
|
+
const isOpen = () => root.hasAttribute("data-show");
|
|
1514
|
+
const onBlockScroll = (event) => {
|
|
1515
|
+
const target = event.target;
|
|
1516
|
+
if (target instanceof Node && root.contains(target)) {
|
|
1517
|
+
const hasOverflow = root.scrollHeight > root.clientHeight;
|
|
1518
|
+
if (hasOverflow) return;
|
|
1519
|
+
}
|
|
1520
|
+
event.preventDefault();
|
|
1521
|
+
};
|
|
1522
|
+
const lockScroll = () => {
|
|
1523
|
+
if (scrollLocked) return;
|
|
1524
|
+
scrollLocked = true;
|
|
1525
|
+
document.addEventListener("wheel", onBlockScroll, { passive: false });
|
|
1526
|
+
document.addEventListener("touchmove", onBlockScroll, { passive: false });
|
|
1527
|
+
};
|
|
1528
|
+
const unlockScroll = () => {
|
|
1529
|
+
if (!scrollLocked) return;
|
|
1530
|
+
scrollLocked = false;
|
|
1531
|
+
document.removeEventListener("wheel", onBlockScroll);
|
|
1532
|
+
document.removeEventListener("touchmove", onBlockScroll);
|
|
1533
|
+
};
|
|
1534
|
+
const hide = () => {
|
|
1535
|
+
if (initialFocusRaf !== null) {
|
|
1536
|
+
cancelAnimationFrame(initialFocusRaf);
|
|
1537
|
+
initialFocusRaf = null;
|
|
1538
|
+
}
|
|
1539
|
+
cleanupFloating?.();
|
|
1540
|
+
cleanupFloating = null;
|
|
1541
|
+
unlockScroll();
|
|
1542
|
+
editorEl?.removeAttribute("data-block-context-menu-open");
|
|
1543
|
+
const view = editor.view;
|
|
1544
|
+
if (view) {
|
|
1545
|
+
const tr = view.state.tr.setMeta(pluginKey, { activeBlockPos: null });
|
|
1546
|
+
tr.setMeta("addToHistory", false);
|
|
1547
|
+
view.dispatch(tr);
|
|
1548
|
+
}
|
|
1549
|
+
root.removeAttribute("data-show");
|
|
1550
|
+
currentBlockPos = null;
|
|
1551
|
+
menuItemButtons = [];
|
|
1552
|
+
focusedIndex = 0;
|
|
1553
|
+
};
|
|
1554
|
+
const focusItem = (index) => {
|
|
1555
|
+
if (menuItemButtons.length === 0) return;
|
|
1556
|
+
const clamped = Math.max(0, Math.min(index, menuItemButtons.length - 1));
|
|
1557
|
+
for (let i = 0; i < menuItemButtons.length; i++) {
|
|
1558
|
+
const btn = menuItemButtons[i];
|
|
1559
|
+
if (btn) btn.tabIndex = i === clamped ? 0 : -1;
|
|
1560
|
+
}
|
|
1561
|
+
focusedIndex = clamped;
|
|
1562
|
+
menuItemButtons[clamped]?.focus();
|
|
1563
|
+
};
|
|
1564
|
+
const runAndClose = (apply) => {
|
|
1565
|
+
const tr = editor.view.state.tr;
|
|
1566
|
+
try {
|
|
1567
|
+
apply(tr);
|
|
1568
|
+
editor.view.dispatch(tr.scrollIntoView());
|
|
1569
|
+
} finally {
|
|
1570
|
+
hide();
|
|
1571
|
+
editor.view.focus();
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1574
|
+
const runDelete = () => {
|
|
1575
|
+
if (currentBlockPos === null) return;
|
|
1576
|
+
const pos = currentBlockPos;
|
|
1577
|
+
runAndClose((tr) => {
|
|
1578
|
+
deleteBlock(tr, pos);
|
|
1579
|
+
});
|
|
1580
|
+
};
|
|
1581
|
+
const runDuplicate = () => {
|
|
1582
|
+
if (currentBlockPos === null) return;
|
|
1583
|
+
const pos = currentBlockPos;
|
|
1584
|
+
const transformAttrs = uniqueIDAttrName && uniqueIDGenerate ? (attrs) => ({ ...attrs, [uniqueIDAttrName]: uniqueIDGenerate() }) : void 0;
|
|
1585
|
+
runAndClose((tr) => {
|
|
1586
|
+
duplicateBlock(tr, pos, transformAttrs);
|
|
1587
|
+
});
|
|
1588
|
+
};
|
|
1589
|
+
const runCopyLink = (blockId) => {
|
|
1590
|
+
const url = onCopyLink(blockId, editor);
|
|
1591
|
+
void core.writeToClipboard(url).then((ok) => {
|
|
1592
|
+
const name = ok ? "dm:copy-link-success" : "dm:copy-link-error";
|
|
1593
|
+
editorEl?.dispatchEvent(new CustomEvent(name, {
|
|
1594
|
+
bubbles: false,
|
|
1595
|
+
detail: { url, blockId }
|
|
1596
|
+
}));
|
|
1597
|
+
});
|
|
1598
|
+
hide();
|
|
1599
|
+
editor.view.focus();
|
|
1600
|
+
};
|
|
1601
|
+
const runTurnInto = (target) => {
|
|
1602
|
+
if (currentBlockPos === null) return;
|
|
1603
|
+
const sourceNode = editor.view.state.doc.nodeAt(currentBlockPos);
|
|
1604
|
+
if (!sourceNode) return;
|
|
1605
|
+
const pos = sourceNode.type.isTextblock ? currentBlockPos : currentBlockPos + 1;
|
|
1606
|
+
if (target.command) {
|
|
1607
|
+
turnIntoWrapper(editor, pos, target.command);
|
|
1608
|
+
hide();
|
|
1609
|
+
editor.view.focus();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const targetType = editor.view.state.schema.nodes[target.nodeType];
|
|
1613
|
+
if (!targetType) return;
|
|
1614
|
+
runAndClose((tr) => {
|
|
1615
|
+
turnIntoBlock(tr, pos, targetType, target.attrs);
|
|
1616
|
+
});
|
|
1617
|
+
};
|
|
1618
|
+
const runSetColor = (attr, color) => {
|
|
1619
|
+
if (currentBlockPos === null) return;
|
|
1620
|
+
const pos = currentBlockPos;
|
|
1621
|
+
runAndClose((tr) => {
|
|
1622
|
+
const n = tr.doc.nodeAt(pos);
|
|
1623
|
+
if (!n) return;
|
|
1624
|
+
tr.setNodeMarkup(pos, void 0, { ...n.attrs, [attr]: color });
|
|
1625
|
+
core.stripInlineColorConflicts(
|
|
1626
|
+
tr,
|
|
1627
|
+
editor.view.state,
|
|
1628
|
+
pos,
|
|
1629
|
+
pos + n.nodeSize,
|
|
1630
|
+
attr === "textColor" ? "text" : "bg"
|
|
1631
|
+
);
|
|
1632
|
+
});
|
|
1633
|
+
};
|
|
1634
|
+
const renderItems = (blockPos) => {
|
|
1635
|
+
root.innerHTML = "";
|
|
1636
|
+
menuItemButtons = [];
|
|
1637
|
+
const node = editor.view.state.doc.nodeAt(blockPos);
|
|
1638
|
+
if (!node) return;
|
|
1639
|
+
const primaryGroup = document.createElement("div");
|
|
1640
|
+
primaryGroup.className = "dm-block-context-menu-group";
|
|
1641
|
+
primaryGroup.setAttribute("role", "group");
|
|
1642
|
+
primaryGroup.appendChild(
|
|
1643
|
+
makeItem("Delete", "trash", runDelete)
|
|
1644
|
+
);
|
|
1645
|
+
if (node.type.name !== "horizontalRule") {
|
|
1646
|
+
primaryGroup.appendChild(
|
|
1647
|
+
makeItem("Duplicate", "copy", runDuplicate)
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
if (copyLinkEnabled && uniqueIDAttrName) {
|
|
1651
|
+
const id = node.attrs[uniqueIDAttrName];
|
|
1652
|
+
if (typeof id === "string" && id.length > 0) {
|
|
1653
|
+
primaryGroup.appendChild(
|
|
1654
|
+
makeItem("Copy link", "link", () => {
|
|
1655
|
+
runCopyLink(id);
|
|
1656
|
+
})
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
root.appendChild(primaryGroup);
|
|
1661
|
+
if (blockColorEnabled && blockColorTypes?.includes(node.type.name)) {
|
|
1662
|
+
const label = document.createElement("div");
|
|
1663
|
+
label.className = "dm-block-context-menu-group-label";
|
|
1664
|
+
label.textContent = "Colors";
|
|
1665
|
+
root.appendChild(label);
|
|
1666
|
+
const currentBg = node.attrs["bgColor"];
|
|
1667
|
+
const currentText = node.attrs["textColor"];
|
|
1668
|
+
root.appendChild(
|
|
1669
|
+
buildSwatchRow(
|
|
1670
|
+
"Text color",
|
|
1671
|
+
"text",
|
|
1672
|
+
blockTextPalette,
|
|
1673
|
+
currentText,
|
|
1674
|
+
(color) => {
|
|
1675
|
+
runSetColor("textColor", color);
|
|
1676
|
+
}
|
|
1677
|
+
)
|
|
1678
|
+
);
|
|
1679
|
+
root.appendChild(
|
|
1680
|
+
buildSwatchRow(
|
|
1681
|
+
"Background",
|
|
1682
|
+
"bg",
|
|
1683
|
+
blockBgPalette,
|
|
1684
|
+
currentBg,
|
|
1685
|
+
(color) => {
|
|
1686
|
+
runSetColor("bgColor", color);
|
|
1687
|
+
}
|
|
1688
|
+
)
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
const sourceIsTextblock = node.type.isTextblock;
|
|
1692
|
+
const sourceIsWrapper = !sourceIsTextblock && node.firstChild?.isTextblock === true;
|
|
1693
|
+
if (turnIntoEnabled && (sourceIsTextblock || sourceIsWrapper)) {
|
|
1694
|
+
const ancestorPos = sourceIsTextblock ? blockPos : blockPos + 1;
|
|
1695
|
+
const $pos = editor.view.state.doc.resolve(ancestorPos);
|
|
1696
|
+
const ancestorTypeNames = /* @__PURE__ */ new Set();
|
|
1697
|
+
for (let d = 0; d <= $pos.depth; d++) {
|
|
1698
|
+
ancestorTypeNames.add($pos.node(d).type.name);
|
|
1699
|
+
}
|
|
1700
|
+
const isListItemFamilySource = node.type.name === "listItem" || node.type.name === "taskItem";
|
|
1701
|
+
const eligible = turnIntoTargets.filter((target) => {
|
|
1702
|
+
const type = editor.view.state.schema.nodes[target.nodeType];
|
|
1703
|
+
if (!type) return false;
|
|
1704
|
+
if (target.command) {
|
|
1705
|
+
if (ancestorTypeNames.has(target.nodeType)) return false;
|
|
1706
|
+
if (target.command === "toggleBlockquote" && isListItemFamilySource) return false;
|
|
1707
|
+
return true;
|
|
1708
|
+
}
|
|
1709
|
+
if (!sourceIsTextblock) return false;
|
|
1710
|
+
if (!type.isTextblock) return false;
|
|
1711
|
+
if (type.name !== node.type.name) return true;
|
|
1712
|
+
const targetAttrs = target.attrs;
|
|
1713
|
+
if (!targetAttrs) return false;
|
|
1714
|
+
const nodeAttrs = node.attrs;
|
|
1715
|
+
for (const k of Object.keys(targetAttrs)) {
|
|
1716
|
+
if (nodeAttrs[k] !== targetAttrs[k]) return true;
|
|
1717
|
+
}
|
|
1718
|
+
return false;
|
|
1719
|
+
});
|
|
1720
|
+
if (eligible.length > 0) {
|
|
1721
|
+
const label = document.createElement("div");
|
|
1722
|
+
label.className = "dm-block-context-menu-group-label";
|
|
1723
|
+
label.textContent = "Turn into";
|
|
1724
|
+
root.appendChild(label);
|
|
1725
|
+
const group = document.createElement("div");
|
|
1726
|
+
group.className = "dm-block-context-menu-group";
|
|
1727
|
+
group.setAttribute("role", "group");
|
|
1728
|
+
group.setAttribute("aria-label", "Turn into");
|
|
1729
|
+
for (const target of eligible) {
|
|
1730
|
+
group.appendChild(
|
|
1731
|
+
makeItem(target.label, target.icon, () => {
|
|
1732
|
+
runTurnInto(target);
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
root.appendChild(group);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
const createMenuButton = (config) => {
|
|
1741
|
+
const btn = document.createElement("button");
|
|
1742
|
+
btn.type = "button";
|
|
1743
|
+
btn.className = config.className;
|
|
1744
|
+
btn.setAttribute("role", "menuitem");
|
|
1745
|
+
btn.setAttribute("aria-label", config.ariaLabel);
|
|
1746
|
+
if (config.attributes) {
|
|
1747
|
+
for (const [k, v] of Object.entries(config.attributes)) {
|
|
1748
|
+
btn.setAttribute(k, v);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
btn.tabIndex = menuItemButtons.length === 0 ? 0 : -1;
|
|
1752
|
+
btn.addEventListener("mousedown", (e) => {
|
|
1753
|
+
e.preventDefault();
|
|
1754
|
+
});
|
|
1755
|
+
btn.addEventListener("click", (e) => {
|
|
1756
|
+
e.preventDefault();
|
|
1757
|
+
config.onClick();
|
|
1758
|
+
});
|
|
1759
|
+
menuItemButtons.push(btn);
|
|
1760
|
+
return btn;
|
|
1761
|
+
};
|
|
1762
|
+
const makeItem = (label, iconKey, onClick) => {
|
|
1763
|
+
const btn = createMenuButton({
|
|
1764
|
+
className: "dm-block-context-menu-item",
|
|
1765
|
+
ariaLabel: label,
|
|
1766
|
+
onClick
|
|
1767
|
+
});
|
|
1768
|
+
const iconHTML = core.defaultIcons[iconKey] ?? "";
|
|
1769
|
+
const iconSpan = document.createElement("span");
|
|
1770
|
+
iconSpan.className = "dm-block-context-menu-item-icon";
|
|
1771
|
+
iconSpan.setAttribute("aria-hidden", "true");
|
|
1772
|
+
iconSpan.innerHTML = iconHTML;
|
|
1773
|
+
const labelSpan = document.createElement("span");
|
|
1774
|
+
labelSpan.className = "dm-block-context-menu-item-label";
|
|
1775
|
+
labelSpan.textContent = label;
|
|
1776
|
+
btn.appendChild(iconSpan);
|
|
1777
|
+
btn.appendChild(labelSpan);
|
|
1778
|
+
return btn;
|
|
1779
|
+
};
|
|
1780
|
+
const makeSwatch = (variant, color, current, onClick) => {
|
|
1781
|
+
const ariaLabel = color === null ? variant === "bg" ? "No background" : "Default text color" : `${variant === "bg" ? "Background" : "Text color"}: ${color}`;
|
|
1782
|
+
const isPressed = current === color || color === null && !current;
|
|
1783
|
+
return createMenuButton({
|
|
1784
|
+
className: `dm-block-color-swatch dm-block-color-swatch--${variant}`,
|
|
1785
|
+
ariaLabel,
|
|
1786
|
+
onClick: () => {
|
|
1787
|
+
onClick(color);
|
|
1788
|
+
},
|
|
1789
|
+
attributes: {
|
|
1790
|
+
"data-color": color ?? "null",
|
|
1791
|
+
"aria-pressed": isPressed ? "true" : "false"
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
};
|
|
1795
|
+
const buildSwatchRow = (rowLabel, variant, palette, current, onClick) => {
|
|
1796
|
+
const row = document.createElement("div");
|
|
1797
|
+
row.className = "dm-block-color-row";
|
|
1798
|
+
row.setAttribute("role", "group");
|
|
1799
|
+
row.setAttribute("aria-label", rowLabel);
|
|
1800
|
+
const visuallyHidden = document.createElement("span");
|
|
1801
|
+
visuallyHidden.className = "dm-block-color-row-label";
|
|
1802
|
+
visuallyHidden.textContent = rowLabel;
|
|
1803
|
+
row.appendChild(visuallyHidden);
|
|
1804
|
+
row.appendChild(makeSwatch(variant, null, current, onClick));
|
|
1805
|
+
for (const c of palette) {
|
|
1806
|
+
row.appendChild(makeSwatch(variant, c, current, onClick));
|
|
1807
|
+
}
|
|
1808
|
+
return row;
|
|
1809
|
+
};
|
|
1810
|
+
const open = (detail) => {
|
|
1811
|
+
if (!editorEl) return;
|
|
1812
|
+
const node = editor.view.state.doc.nodeAt(detail.blockPos);
|
|
1813
|
+
if (!node) return;
|
|
1814
|
+
currentBlockPos = detail.blockPos;
|
|
1815
|
+
renderItems(detail.blockPos);
|
|
1816
|
+
if (menuItemButtons.length === 0) return;
|
|
1817
|
+
editorEl.setAttribute("data-block-context-menu-open", "");
|
|
1818
|
+
editorEl.dispatchEvent(new Event("dm:dismiss-overlays", { bubbles: false }));
|
|
1819
|
+
root.setAttribute("data-show", "");
|
|
1820
|
+
lockScroll();
|
|
1821
|
+
{
|
|
1822
|
+
const tr = editor.view.state.tr.setMeta(pluginKey, { activeBlockPos: detail.blockPos });
|
|
1823
|
+
tr.setMeta("addToHistory", false);
|
|
1824
|
+
editor.view.dispatch(tr);
|
|
1825
|
+
}
|
|
1826
|
+
cleanupFloating?.();
|
|
1827
|
+
let anchorEl = detail.anchorElement;
|
|
1828
|
+
const bubbleMenuRef = anchorEl.closest(".dm-bubble-menu");
|
|
1829
|
+
const matchingSelector = matchingSelectorFor(anchorEl);
|
|
1830
|
+
let lastRect = anchorEl.getBoundingClientRect();
|
|
1831
|
+
const virtualRef = {
|
|
1832
|
+
getBoundingClientRect: () => {
|
|
1833
|
+
if (anchorEl.isConnected) {
|
|
1834
|
+
lastRect = anchorEl.getBoundingClientRect();
|
|
1835
|
+
return lastRect;
|
|
1836
|
+
}
|
|
1837
|
+
if (matchingSelector && bubbleMenuRef?.isConnected) {
|
|
1838
|
+
const fresh = bubbleMenuRef.querySelector(matchingSelector);
|
|
1839
|
+
if (fresh) {
|
|
1840
|
+
anchorEl = fresh;
|
|
1841
|
+
lastRect = fresh.getBoundingClientRect();
|
|
1842
|
+
return lastRect;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return lastRect;
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
cleanupFloating = core.positionFloatingOnce(virtualRef, root, {
|
|
1849
|
+
placement: "right-start",
|
|
1850
|
+
offsetValue: 4
|
|
1851
|
+
});
|
|
1852
|
+
focusedIndex = 0;
|
|
1853
|
+
initialFocusRaf = requestAnimationFrame(() => {
|
|
1854
|
+
initialFocusRaf = null;
|
|
1855
|
+
menuItemButtons[0]?.focus();
|
|
1856
|
+
});
|
|
1857
|
+
};
|
|
1858
|
+
const onOpen = (event) => {
|
|
1859
|
+
const detail = event.detail;
|
|
1860
|
+
if (!detail) return;
|
|
1861
|
+
open(detail);
|
|
1862
|
+
};
|
|
1863
|
+
const onDismiss = () => {
|
|
1864
|
+
if (!isOpen()) return;
|
|
1865
|
+
hide();
|
|
1866
|
+
};
|
|
1867
|
+
const onClickOutside = (event) => {
|
|
1868
|
+
if (!isOpen()) return;
|
|
1869
|
+
const target = event.target;
|
|
1870
|
+
if (!(target instanceof Node)) return;
|
|
1871
|
+
if (root.contains(target)) return;
|
|
1872
|
+
hide();
|
|
1873
|
+
};
|
|
1874
|
+
const onKeyDown = (event) => {
|
|
1875
|
+
if (!isOpen()) return;
|
|
1876
|
+
switch (event.key) {
|
|
1877
|
+
case "ArrowDown":
|
|
1878
|
+
event.preventDefault();
|
|
1879
|
+
focusItem((focusedIndex + 1) % menuItemButtons.length);
|
|
1880
|
+
return;
|
|
1881
|
+
case "ArrowUp":
|
|
1882
|
+
event.preventDefault();
|
|
1883
|
+
focusItem((focusedIndex - 1 + menuItemButtons.length) % menuItemButtons.length);
|
|
1884
|
+
return;
|
|
1885
|
+
case "Home":
|
|
1886
|
+
event.preventDefault();
|
|
1887
|
+
focusItem(0);
|
|
1888
|
+
return;
|
|
1889
|
+
case "End":
|
|
1890
|
+
event.preventDefault();
|
|
1891
|
+
focusItem(menuItemButtons.length - 1);
|
|
1892
|
+
return;
|
|
1893
|
+
case "Escape":
|
|
1894
|
+
event.preventDefault();
|
|
1895
|
+
event.stopPropagation();
|
|
1896
|
+
hide();
|
|
1897
|
+
editor.view.focus();
|
|
1898
|
+
return;
|
|
1899
|
+
default:
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
return new state.Plugin({
|
|
1904
|
+
key: pluginKey,
|
|
1905
|
+
state: {
|
|
1906
|
+
init: () => ({ activeBlockPos: null }),
|
|
1907
|
+
apply(tr, value) {
|
|
1908
|
+
const meta = tr.getMeta(pluginKey);
|
|
1909
|
+
if (meta && "activeBlockPos" in meta) {
|
|
1910
|
+
return { activeBlockPos: meta.activeBlockPos ?? null };
|
|
1911
|
+
}
|
|
1912
|
+
if (value.activeBlockPos !== null && tr.docChanged) {
|
|
1913
|
+
const mapped = tr.mapping.mapResult(value.activeBlockPos, 1);
|
|
1914
|
+
return { activeBlockPos: mapped.deleted ? null : mapped.pos };
|
|
1915
|
+
}
|
|
1916
|
+
return value;
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
props: {
|
|
1920
|
+
decorations(state) {
|
|
1921
|
+
const plugin = pluginKey.getState(state);
|
|
1922
|
+
const activePos = plugin?.activeBlockPos ?? null;
|
|
1923
|
+
if (activePos === null) return null;
|
|
1924
|
+
const node = state.doc.nodeAt(activePos);
|
|
1925
|
+
if (!node) return null;
|
|
1926
|
+
return view.DecorationSet.create(state.doc, [
|
|
1927
|
+
view.Decoration.node(activePos, activePos + node.nodeSize, { class: "dm-block-context-active" })
|
|
1928
|
+
]);
|
|
1929
|
+
}
|
|
1930
|
+
},
|
|
1931
|
+
view: (editorView) => {
|
|
1932
|
+
editorEl = editorView.dom.closest(".dm-editor");
|
|
1933
|
+
if (!editorEl) return { destroy: () => {
|
|
1934
|
+
} };
|
|
1935
|
+
editorEl.appendChild(root);
|
|
1936
|
+
hide();
|
|
1937
|
+
editorEl.addEventListener("dm:block-context-menu-open", onOpen);
|
|
1938
|
+
editorEl.addEventListener("dm:dismiss-overlays", onDismiss);
|
|
1939
|
+
document.addEventListener("mousedown", onClickOutside, true);
|
|
1940
|
+
root.addEventListener("keydown", onKeyDown);
|
|
1941
|
+
return {
|
|
1942
|
+
destroy: () => {
|
|
1943
|
+
hide();
|
|
1944
|
+
editorEl?.removeEventListener("dm:block-context-menu-open", onOpen);
|
|
1945
|
+
editorEl?.removeEventListener("dm:dismiss-overlays", onDismiss);
|
|
1946
|
+
document.removeEventListener("mousedown", onClickOutside, true);
|
|
1947
|
+
root.removeEventListener("keydown", onKeyDown);
|
|
1948
|
+
root.remove();
|
|
1949
|
+
editorEl = null;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
var BlockContextMenu = core.Extension.create({
|
|
1956
|
+
name: "blockContextMenu",
|
|
1957
|
+
addOptions() {
|
|
1958
|
+
return {
|
|
1959
|
+
turnIntoEnabled: true,
|
|
1960
|
+
turnIntoTargets: DEFAULT_TURN_INTO,
|
|
1961
|
+
copyLinkEnabled: true,
|
|
1962
|
+
onCopyLink: defaultCopyLinkUrl,
|
|
1963
|
+
blockColorEnabled: true
|
|
1964
|
+
};
|
|
1965
|
+
},
|
|
1966
|
+
addProseMirrorPlugins() {
|
|
1967
|
+
const editor = this.editor;
|
|
1968
|
+
if (!editor) return [];
|
|
1969
|
+
const opts = this.options;
|
|
1970
|
+
return [
|
|
1971
|
+
createBlockContextMenuPlugin({
|
|
1972
|
+
pluginKey: blockContextMenuPluginKey,
|
|
1973
|
+
editor,
|
|
1974
|
+
turnIntoEnabled: opts.turnIntoEnabled ?? true,
|
|
1975
|
+
turnIntoTargets: opts.turnIntoTargets ?? DEFAULT_TURN_INTO,
|
|
1976
|
+
copyLinkEnabled: opts.copyLinkEnabled ?? true,
|
|
1977
|
+
onCopyLink: opts.onCopyLink ?? defaultCopyLinkUrl,
|
|
1978
|
+
blockColorEnabled: opts.blockColorEnabled ?? true
|
|
1979
|
+
})
|
|
1980
|
+
];
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
var idCounter = 0;
|
|
1984
|
+
function createSlashSuggestionRenderer() {
|
|
1985
|
+
let root = null;
|
|
1986
|
+
let cleanupFloating = null;
|
|
1987
|
+
let itemButtons = [];
|
|
1988
|
+
let flatItems = [];
|
|
1989
|
+
let selectedIndex = 0;
|
|
1990
|
+
let currentCommand = null;
|
|
1991
|
+
let destroyed = false;
|
|
1992
|
+
const rendererId = `dm-slash-${String(++idCounter)}`;
|
|
1993
|
+
const renderPopup = (props) => {
|
|
1994
|
+
if (!root) return;
|
|
1995
|
+
root.innerHTML = "";
|
|
1996
|
+
itemButtons = [];
|
|
1997
|
+
flatItems = [];
|
|
1998
|
+
if (props.items.length === 0) {
|
|
1999
|
+
const empty = document.createElement("div");
|
|
2000
|
+
empty.className = "dm-slash-command-empty";
|
|
2001
|
+
empty.setAttribute("role", "status");
|
|
2002
|
+
empty.setAttribute("aria-live", "polite");
|
|
2003
|
+
empty.textContent = "No matches";
|
|
2004
|
+
root.appendChild(empty);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
const groups = core.groupFloatingMenuItems(props.items);
|
|
2008
|
+
for (const group of groups) {
|
|
2009
|
+
if (group.name) {
|
|
2010
|
+
const label = document.createElement("div");
|
|
2011
|
+
label.className = "dm-slash-command-group-label";
|
|
2012
|
+
label.textContent = group.name;
|
|
2013
|
+
root.appendChild(label);
|
|
2014
|
+
}
|
|
2015
|
+
const groupEl = document.createElement("div");
|
|
2016
|
+
groupEl.className = "dm-slash-command-group";
|
|
2017
|
+
groupEl.setAttribute("role", "group");
|
|
2018
|
+
if (group.name) groupEl.setAttribute("aria-label", group.name);
|
|
2019
|
+
for (const item of group.items) {
|
|
2020
|
+
const btn = document.createElement("button");
|
|
2021
|
+
btn.type = "button";
|
|
2022
|
+
btn.className = "dm-slash-command-item";
|
|
2023
|
+
btn.setAttribute("role", "menuitem");
|
|
2024
|
+
btn.setAttribute("aria-label", item.label);
|
|
2025
|
+
btn.tabIndex = -1;
|
|
2026
|
+
btn.id = `${rendererId}-item-${String(flatItems.length)}`;
|
|
2027
|
+
const iconHTML = item.icon ? core.defaultIcons[item.icon] ?? "" : "";
|
|
2028
|
+
if (iconHTML) {
|
|
2029
|
+
const iconSpan = document.createElement("span");
|
|
2030
|
+
iconSpan.className = "dm-slash-command-item-icon";
|
|
2031
|
+
iconSpan.setAttribute("aria-hidden", "true");
|
|
2032
|
+
iconSpan.innerHTML = iconHTML;
|
|
2033
|
+
btn.appendChild(iconSpan);
|
|
2034
|
+
}
|
|
2035
|
+
const textSpan = document.createElement("span");
|
|
2036
|
+
textSpan.className = "dm-slash-command-item-text";
|
|
2037
|
+
const labelSpan = document.createElement("span");
|
|
2038
|
+
labelSpan.className = "dm-slash-command-item-label";
|
|
2039
|
+
labelSpan.textContent = item.label;
|
|
2040
|
+
textSpan.appendChild(labelSpan);
|
|
2041
|
+
if (item.description) {
|
|
2042
|
+
const descSpan = document.createElement("span");
|
|
2043
|
+
descSpan.className = "dm-slash-command-item-description";
|
|
2044
|
+
descSpan.textContent = item.description;
|
|
2045
|
+
textSpan.appendChild(descSpan);
|
|
2046
|
+
}
|
|
2047
|
+
btn.appendChild(textSpan);
|
|
2048
|
+
if (item.shortcut) {
|
|
2049
|
+
const shortcutSpan = document.createElement("span");
|
|
2050
|
+
shortcutSpan.className = "dm-slash-command-item-shortcut";
|
|
2051
|
+
shortcutSpan.setAttribute("aria-hidden", "true");
|
|
2052
|
+
shortcutSpan.textContent = item.shortcut;
|
|
2053
|
+
btn.appendChild(shortcutSpan);
|
|
2054
|
+
}
|
|
2055
|
+
const indexForItem = flatItems.length;
|
|
2056
|
+
btn.addEventListener("mousedown", (e) => {
|
|
2057
|
+
e.preventDefault();
|
|
2058
|
+
});
|
|
2059
|
+
btn.addEventListener("mouseenter", () => {
|
|
2060
|
+
selectItem(indexForItem);
|
|
2061
|
+
});
|
|
2062
|
+
btn.addEventListener("click", (e) => {
|
|
2063
|
+
e.preventDefault();
|
|
2064
|
+
if (destroyed) return;
|
|
2065
|
+
currentCommand?.(item);
|
|
2066
|
+
});
|
|
2067
|
+
itemButtons.push(btn);
|
|
2068
|
+
flatItems.push(item);
|
|
2069
|
+
groupEl.appendChild(btn);
|
|
2070
|
+
}
|
|
2071
|
+
root.appendChild(groupEl);
|
|
2072
|
+
}
|
|
2073
|
+
if (selectedIndex >= flatItems.length) selectedIndex = 0;
|
|
2074
|
+
highlight(selectedIndex);
|
|
2075
|
+
};
|
|
2076
|
+
const highlight = (index) => {
|
|
2077
|
+
for (const [i, btn] of itemButtons.entries()) {
|
|
2078
|
+
if (i === index) {
|
|
2079
|
+
btn.setAttribute("data-selected", "");
|
|
2080
|
+
} else {
|
|
2081
|
+
btn.removeAttribute("data-selected");
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
const selected = itemButtons[index];
|
|
2085
|
+
if (root && selected) {
|
|
2086
|
+
const btnTop = selected.offsetTop;
|
|
2087
|
+
const btnBottom = btnTop + selected.offsetHeight;
|
|
2088
|
+
const viewTop = root.scrollTop;
|
|
2089
|
+
const viewBottom = viewTop + root.clientHeight;
|
|
2090
|
+
if (btnTop < viewTop) root.scrollTop = btnTop;
|
|
2091
|
+
else if (btnBottom > viewBottom) root.scrollTop = btnBottom - root.clientHeight;
|
|
2092
|
+
root.setAttribute("aria-activedescendant", selected.id);
|
|
2093
|
+
} else {
|
|
2094
|
+
root?.removeAttribute("aria-activedescendant");
|
|
2095
|
+
}
|
|
2096
|
+
};
|
|
2097
|
+
const selectItem = (index) => {
|
|
2098
|
+
if (index < 0 || index >= itemButtons.length) return;
|
|
2099
|
+
selectedIndex = index;
|
|
2100
|
+
highlight(index);
|
|
2101
|
+
};
|
|
2102
|
+
const reposition = (props) => {
|
|
2103
|
+
if (!root) return;
|
|
2104
|
+
cleanupFloating?.();
|
|
2105
|
+
const virtualRef = {
|
|
2106
|
+
getBoundingClientRect: () => props.clientRect() ?? new DOMRect()
|
|
2107
|
+
};
|
|
2108
|
+
cleanupFloating = core.positionFloatingOnce(
|
|
2109
|
+
virtualRef,
|
|
2110
|
+
root,
|
|
2111
|
+
{ placement: "bottom-start", offsetValue: 4 }
|
|
2112
|
+
);
|
|
2113
|
+
};
|
|
2114
|
+
return {
|
|
2115
|
+
onStart(props) {
|
|
2116
|
+
currentCommand = props.command;
|
|
2117
|
+
selectedIndex = 0;
|
|
2118
|
+
destroyed = false;
|
|
2119
|
+
root = document.createElement("div");
|
|
2120
|
+
root.className = "dm-slash-command-menu";
|
|
2121
|
+
root.setAttribute("role", "menu");
|
|
2122
|
+
root.setAttribute("aria-label", "Insert block");
|
|
2123
|
+
root.setAttribute("data-dm-editor-ui", "");
|
|
2124
|
+
const editorEl = props.element.closest(".dm-editor");
|
|
2125
|
+
(editorEl ?? document.body).appendChild(root);
|
|
2126
|
+
root.setAttribute("data-show", "");
|
|
2127
|
+
renderPopup(props);
|
|
2128
|
+
reposition(props);
|
|
2129
|
+
},
|
|
2130
|
+
onUpdate(props) {
|
|
2131
|
+
currentCommand = props.command;
|
|
2132
|
+
if (!root) return;
|
|
2133
|
+
renderPopup(props);
|
|
2134
|
+
reposition(props);
|
|
2135
|
+
},
|
|
2136
|
+
onExit() {
|
|
2137
|
+
destroyed = true;
|
|
2138
|
+
cleanupFloating?.();
|
|
2139
|
+
cleanupFloating = null;
|
|
2140
|
+
root?.remove();
|
|
2141
|
+
root = null;
|
|
2142
|
+
itemButtons = [];
|
|
2143
|
+
flatItems = [];
|
|
2144
|
+
selectedIndex = 0;
|
|
2145
|
+
currentCommand = null;
|
|
2146
|
+
},
|
|
2147
|
+
onKeyDown(event) {
|
|
2148
|
+
if (!root || flatItems.length === 0) return false;
|
|
2149
|
+
switch (event.key) {
|
|
2150
|
+
case "ArrowDown":
|
|
2151
|
+
selectItem((selectedIndex + 1) % flatItems.length);
|
|
2152
|
+
return true;
|
|
2153
|
+
case "ArrowUp":
|
|
2154
|
+
selectItem((selectedIndex - 1 + flatItems.length) % flatItems.length);
|
|
2155
|
+
return true;
|
|
2156
|
+
case "Home":
|
|
2157
|
+
selectItem(0);
|
|
2158
|
+
return true;
|
|
2159
|
+
case "End":
|
|
2160
|
+
selectItem(flatItems.length - 1);
|
|
2161
|
+
return true;
|
|
2162
|
+
case "Enter":
|
|
2163
|
+
case "Tab": {
|
|
2164
|
+
const item = flatItems[selectedIndex];
|
|
2165
|
+
if (item && currentCommand) currentCommand(item);
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
default:
|
|
2169
|
+
return false;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
// src/SlashCommand.ts
|
|
2176
|
+
var slashCommandPluginKey = new state.PluginKey("slashCommand");
|
|
2177
|
+
var PM_LEAF_PLACEHOLDER = "\uFFFC";
|
|
2178
|
+
var INITIAL_STATE = {
|
|
2179
|
+
active: false,
|
|
2180
|
+
query: "",
|
|
2181
|
+
range: null
|
|
2182
|
+
};
|
|
2183
|
+
function justTypedTrigger(tr, char) {
|
|
2184
|
+
if (tr.steps.length !== 1) return false;
|
|
2185
|
+
const step = tr.steps[0];
|
|
2186
|
+
if (!(step instanceof transform.ReplaceStep)) return false;
|
|
2187
|
+
const slice = step.slice;
|
|
2188
|
+
if (slice.size !== 1 || slice.openStart !== 0 || slice.openEnd !== 0) return false;
|
|
2189
|
+
const node = slice.content.firstChild;
|
|
2190
|
+
return !!(node && node.isText && node.text === char);
|
|
2191
|
+
}
|
|
2192
|
+
function findSlashQuery(state, triggerChar, invalidNodes) {
|
|
2193
|
+
const { selection } = state;
|
|
2194
|
+
if (!selection.empty) return null;
|
|
2195
|
+
const { $from } = selection;
|
|
2196
|
+
if ($from.parent.type.spec.code) return null;
|
|
2197
|
+
if (invalidNodes.includes($from.parent.type.name)) return null;
|
|
2198
|
+
const textBefore = $from.parent.textBetween(
|
|
2199
|
+
0,
|
|
2200
|
+
$from.parentOffset,
|
|
2201
|
+
void 0,
|
|
2202
|
+
PM_LEAF_PLACEHOLDER
|
|
2203
|
+
);
|
|
2204
|
+
const triggerIndex = textBefore.lastIndexOf(triggerChar);
|
|
2205
|
+
if (triggerIndex === -1) return null;
|
|
2206
|
+
if (triggerIndex > 0 && !/\s/.test(textBefore[triggerIndex - 1] ?? "")) return null;
|
|
2207
|
+
const queryText = textBefore.slice(triggerIndex + triggerChar.length);
|
|
2208
|
+
if (/[\n\t]/.test(queryText)) return null;
|
|
2209
|
+
const from = $from.start() + triggerIndex;
|
|
2210
|
+
const to = $from.pos;
|
|
2211
|
+
return { query: queryText, range: { from, to } };
|
|
2212
|
+
}
|
|
2213
|
+
function filterByCursorAncestors(items, editor) {
|
|
2214
|
+
const { $from } = editor.view.state.selection;
|
|
2215
|
+
let wrappingListType = null;
|
|
2216
|
+
for (let d = $from.depth; d >= 1; d--) {
|
|
2217
|
+
const t = $from.node(d).type.name;
|
|
2218
|
+
if (t === "listItem" || t === "taskItem") {
|
|
2219
|
+
if ($from.index(d) === 0 && d >= 1) {
|
|
2220
|
+
wrappingListType = $from.node(d - 1).type.name;
|
|
2221
|
+
}
|
|
2222
|
+
break;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
return items.filter((item) => {
|
|
2226
|
+
if (!item.hideWhenInside || item.hideWhenInside.length === 0) return true;
|
|
2227
|
+
if (!wrappingListType) return true;
|
|
2228
|
+
return !item.hideWhenInside.includes(wrappingListType);
|
|
2229
|
+
});
|
|
2230
|
+
}
|
|
2231
|
+
function filterSlashItems(items, query) {
|
|
2232
|
+
if (query.length === 0) return items;
|
|
2233
|
+
const q = query.toLowerCase();
|
|
2234
|
+
const ranked = [];
|
|
2235
|
+
for (const item of items) {
|
|
2236
|
+
const label = item.label.toLowerCase();
|
|
2237
|
+
if (label.startsWith(q)) {
|
|
2238
|
+
ranked.push({ item, score: 0 });
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
if (label.includes(q)) {
|
|
2242
|
+
ranked.push({ item, score: 1 });
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
const keywords = item.keywords ?? [];
|
|
2246
|
+
const kwIndex = keywords.findIndex((k) => k.toLowerCase().includes(q));
|
|
2247
|
+
if (kwIndex !== -1) {
|
|
2248
|
+
ranked.push({ item, score: 2 + kwIndex * 0.01 });
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
ranked.sort((a, b) => a.score - b.score);
|
|
2252
|
+
return ranked.map((r) => r.item);
|
|
2253
|
+
}
|
|
2254
|
+
function createSlashCommandPlugin(options) {
|
|
2255
|
+
const { pluginKey, editor, char, items: itemsOverride, render, invalidNodes } = options;
|
|
2256
|
+
let renderer = null;
|
|
2257
|
+
return new state.Plugin({
|
|
2258
|
+
key: pluginKey,
|
|
2259
|
+
state: {
|
|
2260
|
+
init: () => ({ ...INITIAL_STATE }),
|
|
2261
|
+
apply(tr, prev, _oldState, newState) {
|
|
2262
|
+
if (tr.getMeta(pluginKey) === "dismiss") return { ...INITIAL_STATE };
|
|
2263
|
+
if (!tr.docChanged && !tr.selectionSet) return prev;
|
|
2264
|
+
if (prev.active && prev.range) {
|
|
2265
|
+
let { from, to } = prev.range;
|
|
2266
|
+
if (tr.docChanged) {
|
|
2267
|
+
const fromResult = tr.mapping.mapResult(from);
|
|
2268
|
+
const toResult = tr.mapping.mapResult(to);
|
|
2269
|
+
if (fromResult.deleted) return { ...INITIAL_STATE };
|
|
2270
|
+
from = fromResult.pos;
|
|
2271
|
+
to = toResult.pos;
|
|
2272
|
+
if (newState.doc.textBetween(from, from + 1, void 0, PM_LEAF_PLACEHOLDER) !== char) {
|
|
2273
|
+
return { ...INITIAL_STATE };
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (!newState.selection.empty) return { ...INITIAL_STATE };
|
|
2277
|
+
const cursor = newState.selection.from;
|
|
2278
|
+
if (cursor < from + 1) return { ...INITIAL_STATE };
|
|
2279
|
+
if (cursor > to) {
|
|
2280
|
+
if (!tr.docChanged) return { ...INITIAL_STATE };
|
|
2281
|
+
to = cursor;
|
|
2282
|
+
}
|
|
2283
|
+
const $cursor = newState.doc.resolve(cursor);
|
|
2284
|
+
if ($cursor.parent.type.spec.code) return { ...INITIAL_STATE };
|
|
2285
|
+
if (invalidNodes.includes($cursor.parent.type.name)) {
|
|
2286
|
+
return { ...INITIAL_STATE };
|
|
2287
|
+
}
|
|
2288
|
+
const query = newState.doc.textBetween(
|
|
2289
|
+
from + 1,
|
|
2290
|
+
to,
|
|
2291
|
+
void 0,
|
|
2292
|
+
PM_LEAF_PLACEHOLDER
|
|
2293
|
+
);
|
|
2294
|
+
if (/[\n\t]/.test(query)) return { ...INITIAL_STATE };
|
|
2295
|
+
return { active: true, query, range: { from, to } };
|
|
2296
|
+
}
|
|
2297
|
+
if (!tr.docChanged) return prev;
|
|
2298
|
+
if (!justTypedTrigger(tr, char)) return prev;
|
|
2299
|
+
const result = findSlashQuery(newState, char, invalidNodes);
|
|
2300
|
+
if (result) return { active: true, query: result.query, range: result.range };
|
|
2301
|
+
return prev;
|
|
2302
|
+
}
|
|
2303
|
+
},
|
|
2304
|
+
view(editorView) {
|
|
2305
|
+
const editorEl = editorView.dom.closest(".dm-editor");
|
|
2306
|
+
let suppressDismissHandler = false;
|
|
2307
|
+
const dismissHandler = () => {
|
|
2308
|
+
if (suppressDismissHandler) return;
|
|
2309
|
+
const state = pluginKey.getState(editor.view.state);
|
|
2310
|
+
if (state?.active) dismissSlashCommand(editor.view);
|
|
2311
|
+
};
|
|
2312
|
+
editorEl?.addEventListener("dm:dismiss-overlays", dismissHandler);
|
|
2313
|
+
let wasActive = false;
|
|
2314
|
+
return {
|
|
2315
|
+
update(view) {
|
|
2316
|
+
const state = pluginKey.getState(view.state);
|
|
2317
|
+
if (!state) return;
|
|
2318
|
+
const becameActive = state.active && !wasActive;
|
|
2319
|
+
wasActive = state.active;
|
|
2320
|
+
if (becameActive) {
|
|
2321
|
+
suppressDismissHandler = true;
|
|
2322
|
+
try {
|
|
2323
|
+
editorEl?.dispatchEvent(new Event("dm:dismiss-overlays", { bubbles: false }));
|
|
2324
|
+
} finally {
|
|
2325
|
+
suppressDismissHandler = false;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
if (state.active && state.range) {
|
|
2329
|
+
const items = core.FloatingMenuController.resolveItems(editor, itemsOverride);
|
|
2330
|
+
const contextual = filterByCursorAncestors(items, editor);
|
|
2331
|
+
const filtered = filterSlashItems(contextual, state.query);
|
|
2332
|
+
const command = (item) => {
|
|
2333
|
+
const current = pluginKey.getState(view.state);
|
|
2334
|
+
if (!current?.range) return;
|
|
2335
|
+
const tr = view.state.tr;
|
|
2336
|
+
tr.delete(current.range.from, current.range.to);
|
|
2337
|
+
tr.setMeta(pluginKey, "dismiss");
|
|
2338
|
+
view.dispatch(tr);
|
|
2339
|
+
core.FloatingMenuController.executeItem(editor, item);
|
|
2340
|
+
};
|
|
2341
|
+
const clientRect = () => {
|
|
2342
|
+
const current = pluginKey.getState(view.state);
|
|
2343
|
+
if (!current?.range) return null;
|
|
2344
|
+
try {
|
|
2345
|
+
const coords = view.coordsAtPos(current.range.from);
|
|
2346
|
+
return new DOMRect(
|
|
2347
|
+
coords.left,
|
|
2348
|
+
coords.top,
|
|
2349
|
+
0,
|
|
2350
|
+
coords.bottom - coords.top
|
|
2351
|
+
);
|
|
2352
|
+
} catch {
|
|
2353
|
+
return null;
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
const props = {
|
|
2357
|
+
editor,
|
|
2358
|
+
query: state.query,
|
|
2359
|
+
range: state.range,
|
|
2360
|
+
items: filtered,
|
|
2361
|
+
command,
|
|
2362
|
+
clientRect,
|
|
2363
|
+
element: view.dom
|
|
2364
|
+
};
|
|
2365
|
+
if (!renderer) {
|
|
2366
|
+
renderer = render();
|
|
2367
|
+
renderer.onStart(props);
|
|
2368
|
+
} else {
|
|
2369
|
+
renderer.onUpdate(props);
|
|
2370
|
+
}
|
|
2371
|
+
} else if (renderer) {
|
|
2372
|
+
renderer.onExit();
|
|
2373
|
+
renderer = null;
|
|
2374
|
+
}
|
|
2375
|
+
},
|
|
2376
|
+
destroy() {
|
|
2377
|
+
editorEl?.removeEventListener("dm:dismiss-overlays", dismissHandler);
|
|
2378
|
+
if (renderer) {
|
|
2379
|
+
try {
|
|
2380
|
+
renderer.onExit();
|
|
2381
|
+
} finally {
|
|
2382
|
+
renderer = null;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
};
|
|
2387
|
+
},
|
|
2388
|
+
props: {
|
|
2389
|
+
// Use handleDOMEvents.keydown (not handleKeyDown) so we intercept
|
|
2390
|
+
// keys BEFORE other keymap plugins claim them (same trick as Mention).
|
|
2391
|
+
handleDOMEvents: {
|
|
2392
|
+
keydown(view, event) {
|
|
2393
|
+
const state = pluginKey.getState(view.state);
|
|
2394
|
+
if (!state?.active) return false;
|
|
2395
|
+
if (event.key === "Escape") {
|
|
2396
|
+
event.preventDefault();
|
|
2397
|
+
const tr = view.state.tr;
|
|
2398
|
+
tr.setMeta(pluginKey, "dismiss");
|
|
2399
|
+
view.dispatch(tr);
|
|
2400
|
+
return true;
|
|
2401
|
+
}
|
|
2402
|
+
if (renderer) {
|
|
2403
|
+
const handled = renderer.onKeyDown(event);
|
|
2404
|
+
if (handled) event.preventDefault();
|
|
2405
|
+
return handled;
|
|
2406
|
+
}
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
},
|
|
2410
|
+
decorations(state) {
|
|
2411
|
+
const pluginState = pluginKey.getState(state);
|
|
2412
|
+
if (!pluginState?.active || !pluginState.range) return view.DecorationSet.empty;
|
|
2413
|
+
return view.DecorationSet.create(state.doc, [
|
|
2414
|
+
view.Decoration.inline(pluginState.range.from, pluginState.range.to, {
|
|
2415
|
+
class: "dm-slash-command-query",
|
|
2416
|
+
nodeName: "span"
|
|
2417
|
+
})
|
|
2418
|
+
]);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
var SlashCommand = core.Extension.create({
|
|
2424
|
+
name: "slashCommand",
|
|
2425
|
+
addOptions() {
|
|
2426
|
+
return {
|
|
2427
|
+
char: "/",
|
|
2428
|
+
invalidNodes: ["codeBlock"]
|
|
2429
|
+
};
|
|
2430
|
+
},
|
|
2431
|
+
addProseMirrorPlugins() {
|
|
2432
|
+
const editor = this.editor;
|
|
2433
|
+
if (!editor) return [];
|
|
2434
|
+
return [
|
|
2435
|
+
createSlashCommandPlugin({
|
|
2436
|
+
pluginKey: slashCommandPluginKey,
|
|
2437
|
+
editor,
|
|
2438
|
+
char: this.options.char ?? "/",
|
|
2439
|
+
...this.options.items !== void 0 && { items: this.options.items },
|
|
2440
|
+
render: this.options.render ?? createSlashSuggestionRenderer,
|
|
2441
|
+
invalidNodes: this.options.invalidNodes ?? ["codeBlock"]
|
|
2442
|
+
})
|
|
2443
|
+
];
|
|
2444
|
+
}
|
|
2445
|
+
});
|
|
2446
|
+
function dismissSlashCommand(view) {
|
|
2447
|
+
view.dispatch(view.state.tr.setMeta(slashCommandPluginKey, "dismiss"));
|
|
2448
|
+
}
|
|
2449
|
+
var LIST_TYPES = /* @__PURE__ */ new Set(["bulletList", "orderedList", "taskList"]);
|
|
2450
|
+
var LIST_ITEM_TYPES3 = /* @__PURE__ */ new Set(["listItem", "taskItem"]);
|
|
2451
|
+
var SmartPaste = core.Extension.create({
|
|
2452
|
+
name: "smartPaste",
|
|
2453
|
+
addOptions() {
|
|
2454
|
+
return {
|
|
2455
|
+
enabled: true
|
|
2456
|
+
};
|
|
2457
|
+
},
|
|
2458
|
+
addProseMirrorPlugins() {
|
|
2459
|
+
if (this.options.enabled === false) return [];
|
|
2460
|
+
return [
|
|
2461
|
+
new state.Plugin({
|
|
2462
|
+
props: {
|
|
2463
|
+
handlePaste: (view, _event, slice) => handleSmartPaste(view, slice)
|
|
2464
|
+
}
|
|
2465
|
+
})
|
|
2466
|
+
];
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
function handleSmartPaste(view, slice) {
|
|
2470
|
+
const { state } = view;
|
|
2471
|
+
const { selection } = state;
|
|
2472
|
+
const $from = selection.$from;
|
|
2473
|
+
if (!$from.parent.isTextblock) return false;
|
|
2474
|
+
if (!sliceHasNonParagraphBlock(slice)) return false;
|
|
2475
|
+
if (sliceIsSingleSameTypeAsParent(slice, $from.parent.type.name)) return false;
|
|
2476
|
+
if (tryPasteListSliceIntoList(view, slice)) return true;
|
|
2477
|
+
const tr = state.tr;
|
|
2478
|
+
if (!selection.empty) tr.deleteSelection();
|
|
2479
|
+
const $pos = tr.selection.$from;
|
|
2480
|
+
const parent = $pos.parent;
|
|
2481
|
+
const parentStart = $pos.before($pos.depth);
|
|
2482
|
+
const parentEnd = $pos.after($pos.depth);
|
|
2483
|
+
const offset = $pos.parentOffset;
|
|
2484
|
+
const parentSize = parent.content.size;
|
|
2485
|
+
if (hasTrailingHardBreakAtCursor(parent, offset, parentSize)) {
|
|
2486
|
+
const hbStart = parentEnd - 2;
|
|
2487
|
+
const hbEnd = parentEnd - 1;
|
|
2488
|
+
tr.delete(hbStart, hbEnd);
|
|
2489
|
+
const adjustedParentEnd = parentEnd - 1;
|
|
2490
|
+
tr.insert(adjustedParentEnd, slice.content);
|
|
2491
|
+
setCaretAtEndOfInserted(tr, adjustedParentEnd, slice.content);
|
|
2492
|
+
} else if (parentSize === 0) {
|
|
2493
|
+
const isListItemLabel = $pos.depth >= 2 && LIST_ITEM_TYPES3.has($pos.node($pos.depth - 1).type.name) && $pos.index($pos.depth - 1) === 0;
|
|
2494
|
+
if (isListItemLabel) {
|
|
2495
|
+
tr.insert(parentEnd, slice.content);
|
|
2496
|
+
setCaretAtEndOfInserted(tr, parentEnd, slice.content);
|
|
2497
|
+
} else {
|
|
2498
|
+
tr.replaceWith(parentStart, parentEnd, slice.content);
|
|
2499
|
+
setCaretAtEndOfInserted(tr, parentStart, slice.content);
|
|
2500
|
+
}
|
|
2501
|
+
} else if (offset === 0) {
|
|
2502
|
+
tr.insert(parentStart, slice.content);
|
|
2503
|
+
setCaretAtEndOfInserted(tr, parentStart, slice.content);
|
|
2504
|
+
} else if (offset === parentSize) {
|
|
2505
|
+
tr.insert(parentEnd, slice.content);
|
|
2506
|
+
setCaretAtEndOfInserted(tr, parentEnd, slice.content);
|
|
2507
|
+
} else {
|
|
2508
|
+
const cursorPos = $pos.pos;
|
|
2509
|
+
tr.split(cursorPos);
|
|
2510
|
+
const insertAt = cursorPos + 1;
|
|
2511
|
+
tr.insert(insertAt, slice.content);
|
|
2512
|
+
setCaretAtEndOfInserted(tr, insertAt, slice.content);
|
|
2513
|
+
}
|
|
2514
|
+
view.dispatch(tr.scrollIntoView());
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
function sliceHasNonParagraphBlock(slice) {
|
|
2518
|
+
let result = false;
|
|
2519
|
+
slice.content.forEach((child) => {
|
|
2520
|
+
if (child.isBlock && child.type.name !== "paragraph") result = true;
|
|
2521
|
+
});
|
|
2522
|
+
return result;
|
|
2523
|
+
}
|
|
2524
|
+
function sliceIsSingleSameTypeAsParent(slice, parentTypeName) {
|
|
2525
|
+
if (slice.content.childCount !== 1) return false;
|
|
2526
|
+
return slice.content.firstChild?.type.name === parentTypeName;
|
|
2527
|
+
}
|
|
2528
|
+
function hasTrailingHardBreakAtCursor(parent, offset, parentSize) {
|
|
2529
|
+
if (offset !== parentSize) return false;
|
|
2530
|
+
return parent.lastChild?.type.name === "hardBreak";
|
|
2531
|
+
}
|
|
2532
|
+
function tryPasteListSliceIntoList(view, slice) {
|
|
2533
|
+
const { state } = view;
|
|
2534
|
+
const { selection, schema } = state;
|
|
2535
|
+
if (slice.content.childCount !== 1) return false;
|
|
2536
|
+
const sliceTop = slice.content.firstChild;
|
|
2537
|
+
if (!sliceTop || !LIST_TYPES.has(sliceTop.type.name)) return false;
|
|
2538
|
+
const $from = selection.$from;
|
|
2539
|
+
let listDepth = -1;
|
|
2540
|
+
for (let d = $from.depth; d > 0; d--) {
|
|
2541
|
+
if (LIST_TYPES.has($from.node(d).type.name)) {
|
|
2542
|
+
listDepth = d;
|
|
2543
|
+
break;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
if (listDepth === -1) return false;
|
|
2547
|
+
const listItemDepth = listDepth + 1;
|
|
2548
|
+
if ($from.depth < listItemDepth) return false;
|
|
2549
|
+
const listParent = $from.node(listDepth);
|
|
2550
|
+
const adapted = convertListItemForParent(schema, sliceTop.content, listParent.type);
|
|
2551
|
+
if (adapted.childCount === 0) return false;
|
|
2552
|
+
const tr = state.tr;
|
|
2553
|
+
if (!selection.empty) tr.deleteSelection();
|
|
2554
|
+
const $pos = tr.selection.$from;
|
|
2555
|
+
if ($pos.depth < listItemDepth) return false;
|
|
2556
|
+
if (!LIST_TYPES.has($pos.node(listDepth).type.name)) return false;
|
|
2557
|
+
const parent = $pos.parent;
|
|
2558
|
+
const parentEnd = $pos.after($pos.depth);
|
|
2559
|
+
const offset = $pos.parentOffset;
|
|
2560
|
+
const parentSize = parent.content.size;
|
|
2561
|
+
const liStart = $pos.before(listItemDepth);
|
|
2562
|
+
const liEnd = $pos.after(listItemDepth);
|
|
2563
|
+
const itemHasOnlyOneChild = $pos.node(listItemDepth).childCount === 1;
|
|
2564
|
+
let insertAt;
|
|
2565
|
+
if (hasTrailingHardBreakAtCursor(parent, offset, parentSize)) {
|
|
2566
|
+
const hbStart = parentEnd - 2;
|
|
2567
|
+
const hbEnd = parentEnd - 1;
|
|
2568
|
+
tr.delete(hbStart, hbEnd);
|
|
2569
|
+
const adjustedLiEnd = liEnd - 1;
|
|
2570
|
+
tr.insert(adjustedLiEnd, adapted);
|
|
2571
|
+
insertAt = adjustedLiEnd;
|
|
2572
|
+
} else if (parentSize === 0 && itemHasOnlyOneChild) {
|
|
2573
|
+
tr.replaceWith(liStart, liEnd, adapted);
|
|
2574
|
+
insertAt = liStart;
|
|
2575
|
+
} else if (offset === 0) {
|
|
2576
|
+
tr.insert(liStart, adapted);
|
|
2577
|
+
insertAt = liStart;
|
|
2578
|
+
} else if (offset === parentSize) {
|
|
2579
|
+
tr.insert(liEnd, adapted);
|
|
2580
|
+
insertAt = liEnd;
|
|
2581
|
+
} else {
|
|
2582
|
+
const cursorPos = $pos.pos;
|
|
2583
|
+
tr.split(cursorPos, 2);
|
|
2584
|
+
insertAt = cursorPos + 2;
|
|
2585
|
+
tr.insert(insertAt, adapted);
|
|
2586
|
+
}
|
|
2587
|
+
setCaretAtEndOfInserted(tr, insertAt, adapted);
|
|
2588
|
+
view.dispatch(tr.scrollIntoView());
|
|
2589
|
+
return true;
|
|
2590
|
+
}
|
|
2591
|
+
function setCaretAtEndOfInserted(tr, insertAt, content) {
|
|
2592
|
+
if (content.childCount === 0) return;
|
|
2593
|
+
const target = Math.max(0, Math.min(tr.doc.content.size, insertAt + content.size - 1));
|
|
2594
|
+
const $target = tr.doc.resolve(target);
|
|
2595
|
+
if ($target.parent.isTextblock) {
|
|
2596
|
+
tr.setSelection(state.TextSelection.create(tr.doc, target));
|
|
2597
|
+
} else {
|
|
2598
|
+
tr.setSelection(state.Selection.near($target, -1));
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
exports.BlockContextMenu = BlockContextMenu;
|
|
2603
|
+
exports.BlockHandle = BlockHandle;
|
|
2604
|
+
exports.DEFAULT_BLOCK_MATCHERS = DEFAULT_BLOCK_MATCHERS;
|
|
2605
|
+
exports.DEFAULT_NESTED_NODES = DEFAULT_NESTED_NODES;
|
|
2606
|
+
exports.FloatingMenu = FloatingMenu;
|
|
2607
|
+
exports.KeyboardReorder = KeyboardReorder;
|
|
2608
|
+
exports.SlashCommand = SlashCommand;
|
|
2609
|
+
exports.SmartPaste = SmartPaste;
|
|
2610
|
+
exports.blockContextMenuPluginKey = blockContextMenuPluginKey;
|
|
2611
|
+
exports.blockHandlePluginKey = blockHandlePluginKey;
|
|
2612
|
+
exports.createBlockContextMenuPlugin = createBlockContextMenuPlugin;
|
|
2613
|
+
exports.createBlockHandlePlugin = createBlockHandlePlugin;
|
|
2614
|
+
exports.createFloatingMenuPlugin = createFloatingMenuPlugin;
|
|
2615
|
+
exports.createSlashCommandPlugin = createSlashCommandPlugin;
|
|
2616
|
+
exports.createSlashSuggestionRenderer = createSlashSuggestionRenderer;
|
|
2617
|
+
exports.dismissSlashCommand = dismissSlashCommand;
|
|
2618
|
+
exports.filterSlashItems = filterSlashItems;
|
|
2619
|
+
exports.floatingMenuPluginKey = floatingMenuPluginKey;
|
|
2620
|
+
exports.slashCommandPluginKey = slashCommandPluginKey;
|
|
2621
|
+
//# sourceMappingURL=index.cjs.map
|
|
2622
|
+
//# sourceMappingURL=index.cjs.map
|