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