@agent-native/toolkit 0.4.9 → 0.5.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.
Files changed (150) hide show
  1. package/README.md +17 -6
  2. package/dist/collab-ui/agent-identity.d.ts +9 -0
  3. package/dist/collab-ui/agent-identity.d.ts.map +1 -0
  4. package/dist/collab-ui/agent-identity.js +9 -0
  5. package/dist/collab-ui/agent-identity.js.map +1 -0
  6. package/dist/collab-ui/index.d.ts +2 -0
  7. package/dist/collab-ui/index.d.ts.map +1 -1
  8. package/dist/collab-ui/index.js +2 -0
  9. package/dist/collab-ui/index.js.map +1 -1
  10. package/dist/collab-ui/lead-client.d.ts +3 -0
  11. package/dist/collab-ui/lead-client.d.ts.map +1 -0
  12. package/dist/collab-ui/lead-client.js +29 -0
  13. package/dist/collab-ui/lead-client.js.map +1 -0
  14. package/dist/collab-ui/types.d.ts.map +1 -1
  15. package/dist/collab-ui/types.js +11 -2
  16. package/dist/collab-ui/types.js.map +1 -1
  17. package/dist/collab-ui/types.spec.d.ts +2 -0
  18. package/dist/collab-ui/types.spec.d.ts.map +1 -0
  19. package/dist/collab-ui/types.spec.js +15 -0
  20. package/dist/collab-ui/types.spec.js.map +1 -0
  21. package/dist/context-ui/ContextMeter.d.ts +10 -0
  22. package/dist/context-ui/ContextMeter.d.ts.map +1 -0
  23. package/dist/context-ui/ContextMeter.js +20 -0
  24. package/dist/context-ui/ContextMeter.js.map +1 -0
  25. package/dist/context-ui/ContextSegmentRow.d.ts +14 -0
  26. package/dist/context-ui/ContextSegmentRow.d.ts.map +1 -0
  27. package/dist/context-ui/ContextSegmentRow.js +25 -0
  28. package/dist/context-ui/ContextSegmentRow.js.map +1 -0
  29. package/dist/context-ui/ContextTreemap.d.ts +7 -0
  30. package/dist/context-ui/ContextTreemap.d.ts.map +1 -0
  31. package/dist/context-ui/ContextTreemap.js +55 -0
  32. package/dist/context-ui/ContextTreemap.js.map +1 -0
  33. package/dist/context-ui/ContextXRayPanel.d.ts +13 -0
  34. package/dist/context-ui/ContextXRayPanel.d.ts.map +1 -0
  35. package/dist/context-ui/ContextXRayPanel.js +120 -0
  36. package/dist/context-ui/ContextXRayPanel.js.map +1 -0
  37. package/dist/context-ui/format.d.ts +6 -0
  38. package/dist/context-ui/format.d.ts.map +1 -0
  39. package/dist/context-ui/format.js +58 -0
  40. package/dist/context-ui/format.js.map +1 -0
  41. package/dist/context-ui/index.d.ts +7 -0
  42. package/dist/context-ui/index.d.ts.map +1 -0
  43. package/dist/context-ui/index.js +6 -0
  44. package/dist/context-ui/index.js.map +1 -0
  45. package/dist/context-ui/types.d.ts +39 -0
  46. package/dist/context-ui/types.d.ts.map +1 -0
  47. package/dist/context-ui/types.js +2 -0
  48. package/dist/context-ui/types.js.map +1 -0
  49. package/dist/design-tweaks/index.d.ts +2 -0
  50. package/dist/design-tweaks/index.d.ts.map +1 -0
  51. package/dist/design-tweaks/index.js +2 -0
  52. package/dist/design-tweaks/index.js.map +1 -0
  53. package/dist/design-tweaks/visual-style-controls.d.ts +90 -0
  54. package/dist/design-tweaks/visual-style-controls.d.ts.map +1 -0
  55. package/dist/design-tweaks/visual-style-controls.js +499 -0
  56. package/dist/design-tweaks/visual-style-controls.js.map +1 -0
  57. package/dist/editor/BubbleToolbar.d.ts +37 -0
  58. package/dist/editor/BubbleToolbar.d.ts.map +1 -0
  59. package/dist/editor/BubbleToolbar.js +171 -0
  60. package/dist/editor/BubbleToolbar.js.map +1 -0
  61. package/dist/editor/CodeBlockNode.d.ts +49 -0
  62. package/dist/editor/CodeBlockNode.d.ts.map +1 -0
  63. package/dist/editor/CodeBlockNode.js +127 -0
  64. package/dist/editor/CodeBlockNode.js.map +1 -0
  65. package/dist/editor/DragHandle.d.ts +97 -0
  66. package/dist/editor/DragHandle.d.ts.map +1 -0
  67. package/dist/editor/DragHandle.js +1066 -0
  68. package/dist/editor/DragHandle.js.map +1 -0
  69. package/dist/editor/DragHandle.spec.d.ts +2 -0
  70. package/dist/editor/DragHandle.spec.d.ts.map +1 -0
  71. package/dist/editor/DragHandle.spec.js +407 -0
  72. package/dist/editor/DragHandle.spec.js.map +1 -0
  73. package/dist/editor/ImageExtension.d.ts +63 -0
  74. package/dist/editor/ImageExtension.d.ts.map +1 -0
  75. package/dist/editor/ImageExtension.js +243 -0
  76. package/dist/editor/ImageExtension.js.map +1 -0
  77. package/dist/editor/ImageExtension.roundtrip.spec.d.ts +2 -0
  78. package/dist/editor/ImageExtension.roundtrip.spec.d.ts.map +1 -0
  79. package/dist/editor/ImageExtension.roundtrip.spec.js +103 -0
  80. package/dist/editor/ImageExtension.roundtrip.spec.js.map +1 -0
  81. package/dist/editor/RichMarkdownEditor.collab.spec.d.ts +2 -0
  82. package/dist/editor/RichMarkdownEditor.collab.spec.d.ts.map +1 -0
  83. package/dist/editor/RichMarkdownEditor.collab.spec.js +65 -0
  84. package/dist/editor/RichMarkdownEditor.collab.spec.js.map +1 -0
  85. package/dist/editor/RichMarkdownEditor.d.ts +52 -0
  86. package/dist/editor/RichMarkdownEditor.d.ts.map +1 -0
  87. package/dist/editor/RichMarkdownEditor.js +38 -0
  88. package/dist/editor/RichMarkdownEditor.js.map +1 -0
  89. package/dist/editor/RichMarkdownEditor.roundtrip.spec.d.ts +2 -0
  90. package/dist/editor/RichMarkdownEditor.roundtrip.spec.d.ts.map +1 -0
  91. package/dist/editor/RichMarkdownEditor.roundtrip.spec.js +186 -0
  92. package/dist/editor/RichMarkdownEditor.roundtrip.spec.js.map +1 -0
  93. package/dist/editor/RunId.d.ts +28 -0
  94. package/dist/editor/RunId.d.ts.map +1 -0
  95. package/dist/editor/RunId.js +60 -0
  96. package/dist/editor/RunId.js.map +1 -0
  97. package/dist/editor/SharedRichEditor.d.ts +110 -0
  98. package/dist/editor/SharedRichEditor.d.ts.map +1 -0
  99. package/dist/editor/SharedRichEditor.js +144 -0
  100. package/dist/editor/SharedRichEditor.js.map +1 -0
  101. package/dist/editor/SlashCommandMenu.d.ts +41 -0
  102. package/dist/editor/SlashCommandMenu.d.ts.map +1 -0
  103. package/dist/editor/SlashCommandMenu.js +221 -0
  104. package/dist/editor/SlashCommandMenu.js.map +1 -0
  105. package/dist/editor/disableHistory.spec.d.ts +2 -0
  106. package/dist/editor/disableHistory.spec.d.ts.map +1 -0
  107. package/dist/editor/disableHistory.spec.js +73 -0
  108. package/dist/editor/disableHistory.spec.js.map +1 -0
  109. package/dist/editor/extensions.d.ts +178 -0
  110. package/dist/editor/extensions.d.ts.map +1 -0
  111. package/dist/editor/extensions.js +224 -0
  112. package/dist/editor/extensions.js.map +1 -0
  113. package/dist/editor/gfmDoc.d.ts +24 -0
  114. package/dist/editor/gfmDoc.d.ts.map +1 -0
  115. package/dist/editor/gfmDoc.js +83 -0
  116. package/dist/editor/gfmDoc.js.map +1 -0
  117. package/dist/editor/gfmDoc.spec.d.ts +2 -0
  118. package/dist/editor/gfmDoc.spec.d.ts.map +1 -0
  119. package/dist/editor/gfmDoc.spec.js +194 -0
  120. package/dist/editor/gfmDoc.spec.js.map +1 -0
  121. package/dist/editor/index.d.ts +13 -0
  122. package/dist/editor/index.d.ts.map +1 -0
  123. package/dist/editor/index.js +13 -0
  124. package/dist/editor/index.js.map +1 -0
  125. package/dist/editor/structuralReorderUndo.spec.d.ts +2 -0
  126. package/dist/editor/structuralReorderUndo.spec.d.ts.map +1 -0
  127. package/dist/editor/structuralReorderUndo.spec.js +120 -0
  128. package/dist/editor/structuralReorderUndo.spec.js.map +1 -0
  129. package/dist/editor/surgical-apply.d.ts +66 -0
  130. package/dist/editor/surgical-apply.d.ts.map +1 -0
  131. package/dist/editor/surgical-apply.js +147 -0
  132. package/dist/editor/surgical-apply.js.map +1 -0
  133. package/dist/editor/surgical-apply.spec.d.ts +2 -0
  134. package/dist/editor/surgical-apply.spec.d.ts.map +1 -0
  135. package/dist/editor/surgical-apply.spec.js +257 -0
  136. package/dist/editor/surgical-apply.spec.js.map +1 -0
  137. package/dist/editor/useCollabReconcile.concurrent.spec.d.ts +2 -0
  138. package/dist/editor/useCollabReconcile.concurrent.spec.d.ts.map +1 -0
  139. package/dist/editor/useCollabReconcile.concurrent.spec.js +329 -0
  140. package/dist/editor/useCollabReconcile.concurrent.spec.js.map +1 -0
  141. package/dist/editor/useCollabReconcile.d.ts +110 -0
  142. package/dist/editor/useCollabReconcile.d.ts.map +1 -0
  143. package/dist/editor/useCollabReconcile.idempotent.spec.d.ts +2 -0
  144. package/dist/editor/useCollabReconcile.idempotent.spec.d.ts.map +1 -0
  145. package/dist/editor/useCollabReconcile.idempotent.spec.js +248 -0
  146. package/dist/editor/useCollabReconcile.idempotent.spec.js.map +1 -0
  147. package/dist/editor/useCollabReconcile.js +440 -0
  148. package/dist/editor/useCollabReconcile.js.map +1 -0
  149. package/dist/editor.css +510 -0
  150. package/package.json +63 -9
@@ -0,0 +1,1066 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Plugin, PluginKey, NodeSelection, TextSelection, } from "@tiptap/pm/state";
3
+ /**
4
+ * Default editor-wrapper CSS selector the drag handle scopes itself to.
5
+ *
6
+ * The handle, the drop indicator, and the `position: relative` anchor are all
7
+ * appended to / measured against the closest ancestor matching this selector.
8
+ * Content's editor wraps its ProseMirror DOM in a `.visual-editor-wrapper`
9
+ * element, so that is the historical default. Other apps (e.g. the plan editor)
10
+ * pass their own wrapper selector via {@link DragHandleOptions.wrapperSelector}.
11
+ */
12
+ export const DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR = ".visual-editor-wrapper";
13
+ const dragHandleKey = new PluginKey("dragHandle");
14
+ const HOVER_SIDE_OUTSET_REM = 8;
15
+ // Notion-style side drop: drag a block to a neighbour's LEFT/RIGHT region and it
16
+ // builds (or joins) a column layout instead of reordering. The activation region
17
+ // has to be GENEROUS or the gesture is dead for a real human — a natural drag
18
+ // releases somewhere over the block's body, nowhere near a thin edge sliver. The
19
+ // old values (28% of width, capped at 140px, AND only the vertical middle 60%)
20
+ // left a wide ~820px plan block with two ~17%-of-width edge slivers in a 35px-tall
21
+ // band as the ONLY column targets — ~66% of the block (the whole centre) plus the
22
+ // top/bottom only ever reordered, so "drag side by side" essentially never made
23
+ // columns. Now each side claims ~a third of the width across the FULL block
24
+ // height, with a middle band always preserved for before/after reorder.
25
+ const SIDE_DROP_ZONE_RATIO = 0.33;
26
+ const SIDE_DROP_ZONE_MIN_PX = 56;
27
+ const SIDE_DROP_ZONE_MAX_PX = 320;
28
+ // Never let the two side zones swallow the whole block: keep at least the middle
29
+ // ~10% of the width as the before/after reorder band so dropping over the centre
30
+ // still moves the block above/below the target (Notion keeps reorder reachable).
31
+ const SIDE_DROP_ZONE_MAX_WIDTH_FRACTION = 0.45;
32
+ const DRAG_HANDLE_MENU_STYLE_ID = "an-rich-md-drag-menu-styles";
33
+ const DRAG_HANDLE_MENU_WIDTH = 220;
34
+ const DRAG_HANDLE_MENU_GAP = 6;
35
+ const DRAG_HANDLE_MENU_VIEWPORT_PADDING = 8;
36
+ /**
37
+ * Wraps Tabler outline icon path data in the standard 24×24 stroke SVG so the
38
+ * DOM-based block menu renders the same icons the React UI uses (Tabler is the
39
+ * framework-wide icon set). The editor is plain DOM, not React, so we inline the
40
+ * markup instead of importing `@tabler/icons-react` components.
41
+ */
42
+ const tablerIconSvg = (paths) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
43
+ // Tabler `copy`, `trash`, and `plus` (outline). Path data copied verbatim from
44
+ // @tabler/icons so the glyphs stay pixel-identical to the React icon set.
45
+ const DRAG_HANDLE_MENU_ICON_DUPLICATE = tablerIconSvg('<path d="M7 9.667a2.667 2.667 0 0 1 2.667 -2.667h8.666a2.667 2.667 0 0 1 2.667 2.667v8.666a2.667 2.667 0 0 1 -2.667 2.667h-8.666a2.667 2.667 0 0 1 -2.667 -2.667l0 -8.666" /><path d="M4.012 16.737a2.005 2.005 0 0 1 -1.012 -1.737v-10c0 -1.1 .9 -2 2 -2h10c.75 0 1.158 .385 1.5 1" />');
46
+ const DRAG_HANDLE_MENU_ICON_DELETE = tablerIconSvg('<path d="M4 7l16 0" /><path d="M10 11l0 6" /><path d="M14 11l0 6" /><path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" /><path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />');
47
+ const DRAG_HANDLE_MENU_ICON_INSERT = tablerIconSvg('<path d="M12 5l0 14" /><path d="M5 12l14 0" />');
48
+ // Tabler `grip-vertical` (outline) for the left-margin drag grip.
49
+ const DRAG_HANDLE_GRIP_ICON = tablerIconSvg('<path d="M8 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M8 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M8 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M14 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M14 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M14 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />');
50
+ const dragHandleRegistrations = new Set();
51
+ let dragHandleGlobalHoverListeners = 0;
52
+ let activeDragRegistration = null;
53
+ // The registration whose grip is currently shown. Used to keep that grip alive
54
+ // while the cursor travels from a block's body to its grip, even when the grip
55
+ // sits in a contested gap (an inter-column gap or a tab body's left offset)
56
+ // where another editor's wide forgiving zone would otherwise re-win the hover
57
+ // and hide the grip out from under the approaching cursor.
58
+ let activeHoverRegistration = null;
59
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
60
+ const editorArea = (registration) => {
61
+ const rect = registration.view.dom.getBoundingClientRect();
62
+ return rect.width * rect.height;
63
+ };
64
+ const updateRegisteredHover = (clientX, clientY) => {
65
+ if (activeDragRegistration) {
66
+ for (const registration of dragHandleRegistrations) {
67
+ registration.hideHover?.();
68
+ }
69
+ activeHoverRegistration = null;
70
+ return;
71
+ }
72
+ const candidates = [];
73
+ for (const registration of dragHandleRegistrations) {
74
+ if (!registration.view.dom.isConnected || !registration.canHover?.()) {
75
+ registration.hideHover?.();
76
+ continue;
77
+ }
78
+ const block = registration.findHoverBlock?.(clientX, clientY);
79
+ if (block) {
80
+ candidates.push({ registration, block });
81
+ }
82
+ else {
83
+ registration.hideHover?.();
84
+ }
85
+ }
86
+ // Grip keepalive. Once a block's grip is showing, hold it while the cursor
87
+ // travels LEFT of that block's content toward its grip glyph — within the
88
+ // block's own vertical row and no further left than the glyph itself. This is
89
+ // what makes grips grabbable for blocks that are NOT flush with the page's
90
+ // left gutter (a right column, a tab body): their grip sits in a gap that the
91
+ // neighbour's wide forgiving zone also claims, so the normal picker would flip
92
+ // hover to the neighbour mid-approach and the grip would vanish before the
93
+ // cursor reaches it. The keepalive only bridges the body→grip gap — it does
94
+ // NOT fire while the cursor is over content (so the innermost/nested picking
95
+ // and gutter-grab rules below still decide there) and the row guard stops it
96
+ // from sticking the grip across vertical moves to another block's row.
97
+ if (activeHoverRegistration) {
98
+ const held = candidates.find((candidate) => candidate.registration === activeHoverRegistration);
99
+ const grip = activeHoverRegistration.gripRect?.();
100
+ if (held &&
101
+ grip &&
102
+ clientY >= held.block.rect.top &&
103
+ clientY < held.block.rect.bottom &&
104
+ clientX >= grip.left - 4 &&
105
+ clientX < held.block.rect.left) {
106
+ for (const registration of dragHandleRegistrations) {
107
+ if (registration !== held.registration)
108
+ registration.hideHover?.();
109
+ }
110
+ held.registration.showHoverBlock?.(held.block);
111
+ return;
112
+ }
113
+ }
114
+ // Pick which editor owns the grip when several register a hover block at this
115
+ // point. Nested region editors (e.g. each column inside a `columns` block) tile
116
+ // their container's whole footprint AND extend a wide forgiving zone
117
+ // (HOVER_SIDE_OUTSET_REM) into its left-margin gutter, so a pure
118
+ // "smallest editor wins" rule lets an inner block beat the container everywhere
119
+ // and leaves the container itself impossible to grab. Split candidates by where
120
+ // the cursor sits relative to each block:
121
+ // - Over a block's body (clientX at/after its left edge) the innermost
122
+ // (smallest) editor wins, so nested blocks stay grabbable from their content.
123
+ // - In the shared left-margin gutter (clientX left of every candidate's
124
+ // content, where the grip lives) the outermost (largest) editor wins, so the
125
+ // container block can be picked up and reordered.
126
+ // Prefer the candidate whose block actually sits UNDER the cursor
127
+ // horizontally. Without this, a left column's forgiving side zone reaches
128
+ // across the inter-column gap, ties the right column's editor on area, and
129
+ // wins — so hovering a right-column block shows the grip for the LEFT block
130
+ // (and right-column blocks appear to have no grip at all). `overContent`
131
+ // restricts to blocks the cursor is genuinely within; `rightOfLeftEdge` keeps
132
+ // the gutter-grab behaviour; fully left of every block → the container wins.
133
+ const overContent = candidates.filter((candidate) => clientX >= candidate.block.rect.left &&
134
+ clientX <= candidate.block.rect.right);
135
+ // The grip renders in a narrow band just LEFT of each block (≈24px). A block
136
+ // must OWN that band so moving the cursor onto its grip keeps showing (and
137
+ // lets you press) that block's grip — otherwise, for a column block whose grip
138
+ // sits in the gutter/inter-column gap, the "gutter → largest editor" rule
139
+ // below would flip the hover to the columns container and the grip would
140
+ // vanish out from under the cursor, making inner column blocks impossible to
141
+ // drag. The band is narrow, so it does not collide with the neighbouring
142
+ // column's content (the right column's grip lives in the inter-column gap,
143
+ // left of its own content but right of the left column's content).
144
+ const GRIP_HOVER_ZONE_PX = 28;
145
+ const overGrip = candidates.filter((candidate) => clientX >= candidate.block.rect.left - GRIP_HOVER_ZONE_PX &&
146
+ clientX < candidate.block.rect.left);
147
+ const rightOfLeftEdge = candidates.filter((candidate) => clientX >= candidate.block.rect.left);
148
+ let active;
149
+ const innerPool = overContent.length > 0
150
+ ? overContent
151
+ : overGrip.length > 0
152
+ ? overGrip
153
+ : rightOfLeftEdge;
154
+ if (innerPool.length > 0) {
155
+ innerPool.sort((a, b) => editorArea(a.registration) - editorArea(b.registration));
156
+ active = innerPool[0];
157
+ }
158
+ else {
159
+ candidates.sort((a, b) => editorArea(b.registration) - editorArea(a.registration));
160
+ active = candidates[0] ?? null;
161
+ }
162
+ for (const registration of dragHandleRegistrations) {
163
+ if (registration !== active?.registration)
164
+ registration.hideHover?.();
165
+ }
166
+ active?.registration.showHoverBlock?.(active.block);
167
+ activeHoverRegistration = active?.registration ?? null;
168
+ };
169
+ const handleGlobalHoverMove = (event) => {
170
+ updateRegisteredHover(event.clientX, event.clientY);
171
+ };
172
+ const retainGlobalHoverListener = () => {
173
+ dragHandleGlobalHoverListeners += 1;
174
+ if (dragHandleGlobalHoverListeners === 1) {
175
+ document.addEventListener("mousemove", handleGlobalHoverMove);
176
+ }
177
+ };
178
+ const releaseGlobalHoverListener = () => {
179
+ dragHandleGlobalHoverListeners = Math.max(0, dragHandleGlobalHoverListeners - 1);
180
+ if (dragHandleGlobalHoverListeners === 0) {
181
+ document.removeEventListener("mousemove", handleGlobalHoverMove);
182
+ }
183
+ };
184
+ const ensureDragHandleMenuStyles = () => {
185
+ if (document.getElementById(DRAG_HANDLE_MENU_STYLE_ID))
186
+ return;
187
+ const style = document.createElement("style");
188
+ style.id = DRAG_HANDLE_MENU_STYLE_ID;
189
+ style.textContent = `
190
+ .an-rich-md-drag-menu {
191
+ position: fixed;
192
+ z-index: 9999;
193
+ width: ${DRAG_HANDLE_MENU_WIDTH}px;
194
+ padding: 4px;
195
+ border: 1px solid hsl(var(--border, 214.3 31.8% 91.4%));
196
+ border-radius: 7px;
197
+ background: hsl(var(--popover, 0 0% 100%));
198
+ color: hsl(var(--popover-foreground, var(--foreground, 222.2 84% 4.9%)));
199
+ box-shadow:
200
+ 0 12px 32px rgb(15 23 42 / 0.16),
201
+ 0 2px 8px rgb(15 23 42 / 0.08);
202
+ font-family: inherit;
203
+ font-size: 13px;
204
+ line-height: 1.35;
205
+ }
206
+
207
+ .an-rich-md-drag-menu__item {
208
+ display: flex;
209
+ width: 100%;
210
+ align-items: center;
211
+ gap: 9px;
212
+ border: 0;
213
+ border-radius: 5px;
214
+ background: transparent;
215
+ color: inherit;
216
+ cursor: pointer;
217
+ font: inherit;
218
+ letter-spacing: 0;
219
+ padding: 7px 8px;
220
+ text-align: left;
221
+ }
222
+
223
+ .an-rich-md-drag-menu__item:hover,
224
+ .an-rich-md-drag-menu__item:focus-visible {
225
+ background: hsl(var(--accent, 210 40% 96.1%));
226
+ color: hsl(var(--accent-foreground, var(--foreground, 222.2 84% 4.9%)));
227
+ outline: none;
228
+ }
229
+
230
+ .an-rich-md-drag-menu__item[data-danger="true"] {
231
+ color: hsl(var(--destructive, 0 84.2% 60.2%));
232
+ }
233
+
234
+ .an-rich-md-drag-menu__item[data-danger="true"]:hover,
235
+ .an-rich-md-drag-menu__item[data-danger="true"]:focus-visible {
236
+ background: hsl(var(--destructive, 0 84.2% 60.2%) / 0.1);
237
+ }
238
+
239
+ .an-rich-md-drag-menu__icon {
240
+ display: flex;
241
+ flex: 0 0 auto;
242
+ align-items: center;
243
+ justify-content: center;
244
+ width: 18px;
245
+ height: 18px;
246
+ color: hsl(var(--muted-foreground, 215.4 16.3% 46.9%));
247
+ }
248
+
249
+ .an-rich-md-drag-menu__item[data-danger="true"] .an-rich-md-drag-menu__icon {
250
+ color: currentColor;
251
+ }
252
+
253
+ .an-rich-md-drag-menu__icon svg {
254
+ width: 17px;
255
+ height: 17px;
256
+ }
257
+
258
+ .an-rich-md-drag-menu__label {
259
+ min-width: 0;
260
+ overflow: hidden;
261
+ text-overflow: ellipsis;
262
+ white-space: nowrap;
263
+ }
264
+ `;
265
+ document.head.appendChild(style);
266
+ };
267
+ /**
268
+ * App-agnostic Tiptap extension providing a Notion-style left-margin drag grip
269
+ * (the `::` handle), block selection, and drag-to-reorder over top-level block
270
+ * nodes.
271
+ *
272
+ * Behavior:
273
+ * - On hover over any top-level block, a `.drag-handle` grip appears in the left
274
+ * margin (forgiving hit zone extends {@link HOVER_SIDE_OUTSET_REM}rem to the
275
+ * sides and into the gap above/between blocks).
276
+ * - Single-clicking the grip selects the block and opens a block action menu.
277
+ * Dragging past a small threshold starts a reorder, showing a floating clone
278
+ * preview (`.notion-drag-preview`) and a `.notion-drop-indicator` line.
279
+ * `Escape` cancels.
280
+ * - While dragging, the source block carries `.notion-block--dragging` and the
281
+ * document element carries `.notion-editor-is-dragging` so apps can style the
282
+ * in-flight state. Apps own all of these CSS class names.
283
+ * - Works for ANY top-level node ProseMirror renders as a direct child of the
284
+ * editor — including `group: "block"`, `draggable: true` atoms such as the
285
+ * plan editor's `planBlock`.
286
+ *
287
+ * The only app-specific coupling — the editor wrapper element the handle and
288
+ * drop indicator are anchored to — is configurable via
289
+ * {@link DragHandleOptions.wrapperSelector}, defaulting to
290
+ * {@link DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR} (`.visual-editor-wrapper`) so the
291
+ * Content editor keeps working byte-identically. The plan editor passes its own
292
+ * wrapper selector via `DragHandle.configure({ wrapperSelector })`.
293
+ */
294
+ export const DragHandle = Extension.create({
295
+ name: "dragHandle",
296
+ addOptions() {
297
+ return {
298
+ wrapperSelector: DEFAULT_DRAG_HANDLE_WRAPPER_SELECTOR,
299
+ getDragTransferData: undefined,
300
+ receiveDragTransferData: undefined,
301
+ };
302
+ },
303
+ addProseMirrorPlugins() {
304
+ const editor = this.editor;
305
+ const wrapperSelector = this.options.wrapperSelector;
306
+ const getDragTransferData = this.options.getDragTransferData;
307
+ const receiveDragTransferData = this.options.receiveDragTransferData;
308
+ const handleDrop = this.options.handleDrop;
309
+ let handle = null;
310
+ let menu = null;
311
+ let menuContext = null;
312
+ let currentBlock = null;
313
+ let dragStartPos = null;
314
+ let dragSession = null;
315
+ let currentRegistration = null;
316
+ const getHoverSideOutset = () => {
317
+ const rootFontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
318
+ return ((Number.isFinite(rootFontSize) ? rootFontSize : 16) *
319
+ HOVER_SIDE_OUTSET_REM);
320
+ };
321
+ const getTopLevelBlocks = (editorView) => {
322
+ const blocks = [];
323
+ editorView.state.doc.forEach((_node, offset) => {
324
+ const dom = editorView.nodeDOM(offset);
325
+ if (!(dom instanceof HTMLElement))
326
+ return;
327
+ blocks.push({
328
+ node: dom,
329
+ pmPos: offset,
330
+ rect: dom.getBoundingClientRect(),
331
+ });
332
+ });
333
+ return blocks;
334
+ };
335
+ const registrationForView = (editorView) => {
336
+ for (const registration of dragHandleRegistrations) {
337
+ if (registration.view === editorView)
338
+ return registration;
339
+ }
340
+ return null;
341
+ };
342
+ const findForgivingBlock = (editorView, clientX, clientY) => {
343
+ const blocks = getTopLevelBlocks(editorView);
344
+ if (blocks.length === 0)
345
+ return null;
346
+ const sideOutset = getHoverSideOutset();
347
+ const pageLeft = 0;
348
+ const pageRight = window.visualViewport?.width ?? window.innerWidth;
349
+ for (let index = 0; index < blocks.length; index++) {
350
+ const block = blocks[index];
351
+ const nextBlock = blocks[index + 1];
352
+ const blockBottomGap = nextBlock
353
+ ? Math.max(0, nextBlock.rect.top - block.rect.bottom)
354
+ : 0;
355
+ const zoneLeft = Math.max(pageLeft, block.rect.left - sideOutset);
356
+ const zoneRight = Math.min(pageRight, block.rect.right + sideOutset);
357
+ const zoneTop = index === 0
358
+ ? Math.max(0, block.rect.top - blockBottomGap)
359
+ : block.rect.top;
360
+ const zoneBottom = nextBlock ? nextBlock.rect.top : block.rect.bottom;
361
+ if (clientX >= zoneLeft &&
362
+ clientX <= zoneRight &&
363
+ clientY >= zoneTop &&
364
+ clientY < zoneBottom) {
365
+ return block;
366
+ }
367
+ }
368
+ return null;
369
+ };
370
+ const showHandleForBlock = (editorView, block) => {
371
+ if (!handle)
372
+ return;
373
+ currentBlock = block.node;
374
+ dragStartPos = block.pmPos;
375
+ const wrapper = editorView.dom.closest(wrapperSelector);
376
+ if (!wrapper)
377
+ return;
378
+ // Lazily (re)attach the grip the first time a wrapper is actually
379
+ // available. At plugin `view()` init the editor DOM may not yet be mounted
380
+ // inside the wrapper (React mounts `EditorContent` after the EditorView is
381
+ // constructed), so the init-time append can silently no-op and leave the
382
+ // grip orphaned. Re-home it here once the wrapper exists.
383
+ if (handle.parentElement !== wrapper) {
384
+ wrapper.style.position = "relative";
385
+ wrapper.appendChild(handle);
386
+ }
387
+ const wrapperRect = wrapper.getBoundingClientRect();
388
+ const handleLeft = block.rect.left - wrapperRect.left - 24;
389
+ handle.style.display = "flex";
390
+ handle.style.top = `${block.rect.top - wrapperRect.top + 2}px`;
391
+ handle.style.left = `${handleLeft}px`;
392
+ };
393
+ const selectBlockAt = (editorView, pos) => {
394
+ try {
395
+ const sel = NodeSelection.create(editorView.state.doc, pos);
396
+ editorView.dispatch(editorView.state.tr.setSelection(sel));
397
+ editorView.focus();
398
+ return sel;
399
+ }
400
+ catch {
401
+ return null;
402
+ }
403
+ };
404
+ const cleanupDragVisuals = () => {
405
+ dragSession?.preview?.remove();
406
+ dragSession?.dropLine?.remove();
407
+ dragSession?.sourceBlock.classList.remove("notion-block--dragging");
408
+ document.documentElement.classList.remove("notion-editor-is-dragging");
409
+ };
410
+ const createDragPreview = (block) => {
411
+ const blockRect = block.getBoundingClientRect();
412
+ const preview = document.createElement("div");
413
+ const clone = block.cloneNode(true);
414
+ clone.classList.remove("ProseMirror-selectednode", "notion-block--dragging");
415
+ clone.removeAttribute("contenteditable");
416
+ clone.style.background = "transparent";
417
+ clone.style.backgroundColor = "transparent";
418
+ clone.querySelectorAll("[contenteditable]").forEach((node) => {
419
+ node.removeAttribute("contenteditable");
420
+ });
421
+ clone.querySelectorAll("*").forEach((node) => {
422
+ node.style.background = "transparent";
423
+ node.style.backgroundColor = "transparent";
424
+ });
425
+ preview.className = "notion-drag-preview";
426
+ preview.style.width = `${blockRect.width}px`;
427
+ preview.appendChild(clone);
428
+ document.body.appendChild(preview);
429
+ return preview;
430
+ };
431
+ const createDropLine = (registration) => {
432
+ const wrapper = registration.view.dom.closest(registration.wrapperSelector);
433
+ if (!wrapper)
434
+ return null;
435
+ const line = document.createElement("div");
436
+ line.className = "notion-drop-indicator";
437
+ wrapper.appendChild(line);
438
+ return line;
439
+ };
440
+ const forceHideHandle = () => {
441
+ if (handle) {
442
+ handle.style.display = "none";
443
+ handle.setAttribute("aria-expanded", "false");
444
+ }
445
+ currentBlock = null;
446
+ dragStartPos = null;
447
+ };
448
+ const closeMenu = ({ hideGrip = false } = {}) => {
449
+ menu?.remove();
450
+ menu = null;
451
+ menuContext = null;
452
+ handle?.setAttribute("aria-expanded", "false");
453
+ document.removeEventListener("mousedown", handleMenuDocumentMouseDown, {
454
+ capture: true,
455
+ });
456
+ document.removeEventListener("keydown", handleMenuKeyDown, {
457
+ capture: true,
458
+ });
459
+ window.removeEventListener("resize", handleMenuViewportChange);
460
+ window.removeEventListener("scroll", handleMenuViewportChange, {
461
+ capture: true,
462
+ });
463
+ if (hideGrip)
464
+ forceHideHandle();
465
+ };
466
+ const resolveMenuContext = (context) => {
467
+ const latestBlock = getTopLevelBlocks(context.view).find((block) => block.node === context.sourceBlock);
468
+ const sourcePos = latestBlock?.pmPos ?? context.sourcePos;
469
+ const sourceNode = context.view.state.doc.nodeAt(sourcePos);
470
+ if (!sourceNode)
471
+ return null;
472
+ return {
473
+ ...context,
474
+ sourcePos,
475
+ sourceNode,
476
+ sourceNodeSize: sourceNode.nodeSize,
477
+ };
478
+ };
479
+ const focusSelectionNear = (view, tr, pos, bias) => {
480
+ tr.setSelection(TextSelection.near(tr.doc.resolve(clamp(pos, 0, tr.doc.content.size)), bias));
481
+ view.dispatch(tr.scrollIntoView());
482
+ view.focus();
483
+ };
484
+ const duplicateBlock = (context) => {
485
+ const resolved = resolveMenuContext(context);
486
+ if (!resolved)
487
+ return;
488
+ const insertPos = resolved.sourcePos + resolved.sourceNodeSize;
489
+ const tr = resolved.view.state.tr.insert(insertPos, resolved.sourceNode);
490
+ try {
491
+ tr.setSelection(NodeSelection.create(tr.doc, insertPos));
492
+ resolved.view.dispatch(tr.scrollIntoView());
493
+ resolved.view.focus();
494
+ }
495
+ catch {
496
+ focusSelectionNear(resolved.view, tr, insertPos, 1);
497
+ }
498
+ };
499
+ const deleteBlock = (context) => {
500
+ const resolved = resolveMenuContext(context);
501
+ if (!resolved)
502
+ return;
503
+ const { view, sourcePos, sourceNodeSize } = resolved;
504
+ const paragraph = view.state.schema.nodes.paragraph;
505
+ const sourceEnd = sourcePos + sourceNodeSize;
506
+ if (view.state.doc.childCount <= 1 && paragraph) {
507
+ const replacement = paragraph.createAndFill() ?? paragraph.create();
508
+ const tr = view.state.tr.replaceWith(sourcePos, sourceEnd, replacement);
509
+ focusSelectionNear(view, tr, sourcePos + 1, 1);
510
+ return;
511
+ }
512
+ const tr = view.state.tr.delete(sourcePos, sourceEnd);
513
+ const selectionBias = sourcePos >= tr.doc.content.size ? -1 : 1;
514
+ focusSelectionNear(view, tr, sourcePos, selectionBias);
515
+ };
516
+ const insertParagraphBelow = (context) => {
517
+ const resolved = resolveMenuContext(context);
518
+ const paragraph = resolved?.view.state.schema.nodes.paragraph;
519
+ if (!resolved || !paragraph)
520
+ return;
521
+ const insertPos = resolved.sourcePos + resolved.sourceNodeSize;
522
+ const paragraphNode = paragraph.createAndFill() ?? paragraph.create();
523
+ const tr = resolved.view.state.tr.insert(insertPos, paragraphNode);
524
+ tr.setSelection(TextSelection.create(tr.doc, insertPos + 1));
525
+ resolved.view.dispatch(tr.scrollIntoView());
526
+ resolved.view.focus();
527
+ };
528
+ const positionMenu = (anchorRect) => {
529
+ if (!menu)
530
+ return;
531
+ const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
532
+ const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
533
+ const menuHeight = menu.offsetHeight || 118;
534
+ const preferredLeft = anchorRect.right + DRAG_HANDLE_MENU_GAP;
535
+ const alternateLeft = anchorRect.left - DRAG_HANDLE_MENU_WIDTH - DRAG_HANDLE_MENU_GAP;
536
+ const left = preferredLeft +
537
+ DRAG_HANDLE_MENU_WIDTH +
538
+ DRAG_HANDLE_MENU_VIEWPORT_PADDING <=
539
+ viewportWidth
540
+ ? preferredLeft
541
+ : alternateLeft;
542
+ menu.style.left = `${clamp(left, DRAG_HANDLE_MENU_VIEWPORT_PADDING, viewportWidth -
543
+ DRAG_HANDLE_MENU_WIDTH -
544
+ DRAG_HANDLE_MENU_VIEWPORT_PADDING)}px`;
545
+ menu.style.top = `${clamp(anchorRect.top - 4, DRAG_HANDLE_MENU_VIEWPORT_PADDING, viewportHeight - menuHeight - DRAG_HANDLE_MENU_VIEWPORT_PADDING)}px`;
546
+ };
547
+ const createMenuItem = (label, iconSvg, action, options = {}) => {
548
+ const button = document.createElement("button");
549
+ button.type = "button";
550
+ button.className = "an-rich-md-drag-menu__item";
551
+ button.setAttribute("role", "menuitem");
552
+ button.setAttribute("data-plan-interactive", "true");
553
+ if (options.danger)
554
+ button.setAttribute("data-danger", "true");
555
+ const icon = document.createElement("span");
556
+ icon.className = "an-rich-md-drag-menu__icon";
557
+ icon.setAttribute("aria-hidden", "true");
558
+ icon.innerHTML = iconSvg;
559
+ const labelElement = document.createElement("span");
560
+ labelElement.className = "an-rich-md-drag-menu__label";
561
+ labelElement.textContent = label;
562
+ button.append(icon, labelElement);
563
+ button.addEventListener("mousedown", (event) => {
564
+ event.preventDefault();
565
+ });
566
+ button.addEventListener("click", (event) => {
567
+ event.preventDefault();
568
+ event.stopPropagation();
569
+ const context = menuContext;
570
+ if (!context)
571
+ return;
572
+ closeMenu({ hideGrip: true });
573
+ action(context);
574
+ });
575
+ return button;
576
+ };
577
+ const openMenu = (context, anchorRect) => {
578
+ const resolved = resolveMenuContext(context);
579
+ if (!resolved)
580
+ return;
581
+ closeMenu();
582
+ selectBlockAt(resolved.view, resolved.sourcePos);
583
+ ensureDragHandleMenuStyles();
584
+ const el = document.createElement("div");
585
+ el.className = "an-rich-md-drag-menu";
586
+ el.setAttribute("role", "menu");
587
+ el.setAttribute("aria-label", "Block actions");
588
+ el.setAttribute("data-plan-interactive", "true");
589
+ el.append(createMenuItem("Duplicate", DRAG_HANDLE_MENU_ICON_DUPLICATE, duplicateBlock), createMenuItem("Delete", DRAG_HANDLE_MENU_ICON_DELETE, deleteBlock, {
590
+ danger: true,
591
+ }), createMenuItem("Insert block below", DRAG_HANDLE_MENU_ICON_INSERT, insertParagraphBelow));
592
+ menu = el;
593
+ menuContext = {
594
+ view: resolved.view,
595
+ sourceBlock: resolved.sourceBlock,
596
+ sourcePos: resolved.sourcePos,
597
+ sourceNodeSize: resolved.sourceNodeSize,
598
+ };
599
+ document.body.appendChild(el);
600
+ positionMenu(anchorRect);
601
+ handle?.setAttribute("aria-expanded", "true");
602
+ document.addEventListener("mousedown", handleMenuDocumentMouseDown, {
603
+ capture: true,
604
+ });
605
+ document.addEventListener("keydown", handleMenuKeyDown, {
606
+ capture: true,
607
+ });
608
+ window.addEventListener("resize", handleMenuViewportChange);
609
+ window.addEventListener("scroll", handleMenuViewportChange, {
610
+ capture: true,
611
+ });
612
+ el.querySelector("button")?.focus({
613
+ preventScroll: true,
614
+ });
615
+ };
616
+ const findDropTarget = (registration, clientX, clientY) => {
617
+ const view = registration.view;
618
+ const block = findForgivingBlock(view, clientX, clientY);
619
+ if (!block)
620
+ return null;
621
+ const node = view.state.doc.nodeAt(block.pmPos);
622
+ if (!node)
623
+ return null;
624
+ let placement;
625
+ const withinBlockY = clientY >= block.rect.top && clientY <= block.rect.bottom;
626
+ // Side (column) zones span the FULL block height — only the horizontal
627
+ // position decides column-vs-reorder. Restricting to the vertical middle
628
+ // (the old 0.2 band) made the already-tiny edge slivers nearly unhittable.
629
+ const sideZoneWidth = Math.min(clamp(block.rect.width * SIDE_DROP_ZONE_RATIO, SIDE_DROP_ZONE_MIN_PX, SIDE_DROP_ZONE_MAX_PX), block.rect.width * SIDE_DROP_ZONE_MAX_WIDTH_FRACTION);
630
+ if (registration.handleDrop &&
631
+ withinBlockY &&
632
+ clientX <= block.rect.left + sideZoneWidth) {
633
+ placement = "left";
634
+ }
635
+ else if (registration.handleDrop &&
636
+ withinBlockY &&
637
+ clientX >= block.rect.right - sideZoneWidth) {
638
+ placement = "right";
639
+ }
640
+ else {
641
+ placement =
642
+ clientY < block.rect.top ||
643
+ (clientY <= block.rect.bottom &&
644
+ clientY < block.rect.top + block.rect.height / 2)
645
+ ? "before"
646
+ : "after";
647
+ }
648
+ const before = placement === "before" || placement === "left";
649
+ return {
650
+ registration,
651
+ view,
652
+ block: block.node,
653
+ placement,
654
+ pos: before ? block.pmPos : block.pmPos + node.nodeSize,
655
+ targetPos: block.pmPos,
656
+ targetNodeSize: node.nodeSize,
657
+ rect: block.rect,
658
+ };
659
+ };
660
+ const findAnyDropTarget = (session, clientX, clientY) => {
661
+ const candidates = [];
662
+ for (const registration of dragHandleRegistrations) {
663
+ if (!registration.view.dom.isConnected)
664
+ continue;
665
+ if (registration.view !== session.view &&
666
+ session.sourceBlock.contains(registration.view.dom)) {
667
+ continue;
668
+ }
669
+ const target = findDropTarget(registration, clientX, clientY);
670
+ if (target)
671
+ candidates.push(target);
672
+ }
673
+ candidates.sort((a, b) => {
674
+ const aRect = a.view.dom.getBoundingClientRect();
675
+ const bRect = b.view.dom.getBoundingClientRect();
676
+ return aRect.width * aRect.height - bRect.width * bRect.height;
677
+ });
678
+ return candidates[0] ?? null;
679
+ };
680
+ const positionDragPreview = (session, clientX, clientY) => {
681
+ if (!session.preview)
682
+ return;
683
+ session.preview.style.transform = `translate3d(${clientX + 12}px, ${clientY + 10}px, 0)`;
684
+ };
685
+ const updateDropLine = (session, target) => {
686
+ const sourceEnd = session.sourcePos + session.sourceNodeSize;
687
+ const isSideDrop = target?.placement === "left" || target?.placement === "right";
688
+ if (!target ||
689
+ (target.view === session.view &&
690
+ (isSideDrop
691
+ ? // A side drop only ever builds columns; the ProseMirror seam
692
+ // position is irrelevant. The only no-op is dropping a block on
693
+ // ITS OWN side — adjacent *different* blocks must still form
694
+ // columns (otherwise dropping onto an immediate neighbour's
695
+ // facing edge silently does nothing, which reads as "side drop
696
+ // works sometimes").
697
+ target.targetPos === session.sourcePos
698
+ : target.pos === session.sourcePos ||
699
+ target.pos === sourceEnd ||
700
+ (target.pos > session.sourcePos && target.pos < sourceEnd)))) {
701
+ session.dropTarget = null;
702
+ session.dropLine?.remove();
703
+ session.dropLine = null;
704
+ return;
705
+ }
706
+ const wrapper = target.view.dom.closest(target.registration.wrapperSelector);
707
+ if (!wrapper)
708
+ return;
709
+ if (!session.dropLine || session.dropLine.parentElement !== wrapper) {
710
+ session.dropLine?.remove();
711
+ session.dropLine = createDropLine(target.registration);
712
+ }
713
+ if (!session.dropLine)
714
+ return;
715
+ const wrapperRect = wrapper.getBoundingClientRect();
716
+ const editorRect = target.view.dom.getBoundingClientRect();
717
+ session.dropTarget = target;
718
+ // A column (side) drop and a reorder (before/after) drop both draw the
719
+ // `.notion-drop-indicator`, but they mean very different things, so the
720
+ // column case carries a modifier class apps style distinctly (a bolder,
721
+ // glowing vertical bar) — without a clear cue a human can't tell they've
722
+ // entered column-build mode before releasing.
723
+ const isColumnDrop = target.placement === "left" || target.placement === "right";
724
+ session.dropLine.classList.toggle("notion-drop-indicator--column", isColumnDrop);
725
+ if (isColumnDrop) {
726
+ // A vertical bar centred on the seam at the target's left/right edge,
727
+ // spanning the block's full height.
728
+ const SIDE_BAR_WIDTH = 4;
729
+ const seam = target.placement === "left" ? target.rect.left : target.rect.right;
730
+ session.dropLine.style.left = `${seam - wrapperRect.left - SIDE_BAR_WIDTH / 2}px`;
731
+ session.dropLine.style.top = `${target.rect.top - wrapperRect.top}px`;
732
+ session.dropLine.style.width = `${SIDE_BAR_WIDTH}px`;
733
+ session.dropLine.style.height = `${target.rect.height}px`;
734
+ return;
735
+ }
736
+ const top = target.placement === "before" ? target.rect.top : target.rect.bottom;
737
+ session.dropLine.style.left = `${editorRect.left - wrapperRect.left}px`;
738
+ session.dropLine.style.top = `${top - wrapperRect.top}px`;
739
+ session.dropLine.style.width = `${editorRect.width}px`;
740
+ session.dropLine.style.height = "3px";
741
+ };
742
+ const createHandle = () => {
743
+ const el = document.createElement("div");
744
+ el.className = "drag-handle";
745
+ el.contentEditable = "false";
746
+ el.draggable = false;
747
+ el.tabIndex = 0;
748
+ el.setAttribute("role", "button");
749
+ el.setAttribute("aria-label", "Open block menu or drag to reorder");
750
+ el.setAttribute("aria-haspopup", "menu");
751
+ el.setAttribute("aria-expanded", "false");
752
+ el.title = "Open block menu or drag to reorder";
753
+ // The icon must not be its own hit target: a real mouse-down inside a
754
+ // nested editor (a column) lands on the SVG, and a container block's
755
+ // capture-phase block-select handler (RegistryBlockNode) only spares the
756
+ // grip DIV — so a press on the icon gets swallowed and the block can't be
757
+ // dragged out of / between columns. `pointer-events:none` makes every
758
+ // press in the grip area resolve to the DIV instead.
759
+ // Tabler `grip-vertical` (the framework-wide icon set). `pointer-events:none`
760
+ // keeps every press in the grip area resolving to the DIV, not the SVG.
761
+ el.innerHTML = DRAG_HANDLE_GRIP_ICON;
762
+ const gripSvg = el.querySelector("svg");
763
+ if (gripSvg) {
764
+ gripSvg.setAttribute("width", "16");
765
+ gripSvg.setAttribute("height", "16");
766
+ gripSvg.style.pointerEvents = "none";
767
+ }
768
+ return el;
769
+ };
770
+ const hideHandle = () => {
771
+ if (menu)
772
+ return;
773
+ forceHideHandle();
774
+ };
775
+ const removeDragListeners = () => {
776
+ document.removeEventListener("mousemove", handleDocumentMouseMove);
777
+ document.removeEventListener("mouseup", handleDocumentMouseUp);
778
+ document.removeEventListener("keydown", handleDocumentKeyDown);
779
+ };
780
+ function handleMenuDocumentMouseDown(event) {
781
+ const target = event.target;
782
+ if (!(target instanceof Node))
783
+ return;
784
+ if (menu?.contains(target) || handle?.contains(target))
785
+ return;
786
+ closeMenu({ hideGrip: true });
787
+ }
788
+ function handleMenuKeyDown(event) {
789
+ if (!menu)
790
+ return;
791
+ if (event.key === "Escape") {
792
+ event.preventDefault();
793
+ closeMenu({ hideGrip: true });
794
+ return;
795
+ }
796
+ if (event.key !== "ArrowDown" &&
797
+ event.key !== "ArrowUp" &&
798
+ event.key !== "Home" &&
799
+ event.key !== "End") {
800
+ return;
801
+ }
802
+ const buttons = Array.from(menu.querySelectorAll("button"));
803
+ if (buttons.length === 0)
804
+ return;
805
+ event.preventDefault();
806
+ const activeIndex = buttons.indexOf(document.activeElement);
807
+ let nextIndex = activeIndex < 0 ? 0 : activeIndex;
808
+ if (event.key === "ArrowDown") {
809
+ nextIndex = (nextIndex + 1) % buttons.length;
810
+ }
811
+ else if (event.key === "ArrowUp") {
812
+ nextIndex = (nextIndex - 1 + buttons.length) % buttons.length;
813
+ }
814
+ else if (event.key === "Home") {
815
+ nextIndex = 0;
816
+ }
817
+ else if (event.key === "End") {
818
+ nextIndex = buttons.length - 1;
819
+ }
820
+ buttons[nextIndex]?.focus({ preventScroll: true });
821
+ }
822
+ function handleMenuViewportChange() {
823
+ closeMenu({ hideGrip: true });
824
+ }
825
+ const finishDragSession = (commit, event) => {
826
+ const session = dragSession;
827
+ if (!session)
828
+ return;
829
+ removeDragListeners();
830
+ if (commit && session.dragging && session.dropTarget) {
831
+ const sourceStart = session.sourcePos;
832
+ const sourceEnd = session.sourcePos + session.sourceNodeSize;
833
+ const target = session.dropTarget;
834
+ const dropPos = target.pos;
835
+ const isSideDrop = target.placement === "left" || target.placement === "right";
836
+ if (target.view !== session.view ||
837
+ (isSideDrop
838
+ ? // Side drop (column build): proceed for any block that isn't the
839
+ // source itself, including the source's immediate neighbour.
840
+ target.targetPos !== sourceStart
841
+ : dropPos !== sourceStart &&
842
+ dropPos !== sourceEnd &&
843
+ !(dropPos > sourceStart && dropPos < sourceEnd))) {
844
+ const sourceNode = session.view.state.doc.nodeAt(sourceStart);
845
+ if (sourceNode) {
846
+ const sourceRegistration = registrationForView(session.view);
847
+ const transferData = sourceRegistration?.getDragTransferData?.({
848
+ view: session.view,
849
+ node: sourceNode,
850
+ pos: sourceStart,
851
+ });
852
+ const targetNode = target.view.state.doc.nodeAt(target.targetPos);
853
+ const handled = !!targetNode &&
854
+ (target.registration.handleDrop?.(transferData, {
855
+ view: target.view,
856
+ sourceView: session.view,
857
+ sourceNode,
858
+ sourcePos: sourceStart,
859
+ sourceNodeSize: sourceNode.nodeSize,
860
+ targetNode,
861
+ targetPos: target.targetPos,
862
+ targetNodeSize: target.targetNodeSize,
863
+ insertPos: dropPos,
864
+ placement: target.placement,
865
+ }) ??
866
+ false);
867
+ if (handled) {
868
+ target.view.focus();
869
+ }
870
+ else if (target.view === session.view) {
871
+ const insertPos = dropPos > sourceStart ? dropPos - sourceNode.nodeSize : dropPos;
872
+ const tr = session.view.state.tr
873
+ .delete(sourceStart, sourceEnd)
874
+ .insert(insertPos, sourceNode);
875
+ tr.setSelection(NodeSelection.create(tr.doc, insertPos));
876
+ session.view.dispatch(tr.scrollIntoView());
877
+ session.view.focus();
878
+ }
879
+ else {
880
+ try {
881
+ const targetNode = target.view.state.schema.nodeFromJSON(sourceNode.toJSON());
882
+ target.registration.receiveDragTransferData?.(transferData, {
883
+ view: target.view,
884
+ node: targetNode,
885
+ pos: dropPos,
886
+ sourceView: session.view,
887
+ });
888
+ const insertTr = target.view.state.tr.insert(dropPos, targetNode);
889
+ insertTr.setSelection(NodeSelection.create(insertTr.doc, dropPos));
890
+ target.view.dispatch(insertTr.scrollIntoView());
891
+ const deleteTr = session.view.state.tr.delete(sourceStart, sourceEnd);
892
+ session.view.dispatch(deleteTr);
893
+ target.view.focus();
894
+ }
895
+ catch {
896
+ // If the target schema cannot accept this node, leave the
897
+ // source document untouched.
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ else if (commit && !session.dragging && event) {
904
+ openMenu({
905
+ view: session.view,
906
+ sourceBlock: session.sourceBlock,
907
+ sourcePos: session.sourcePos,
908
+ sourceNodeSize: session.sourceNodeSize,
909
+ }, handle?.getBoundingClientRect() ??
910
+ session.sourceBlock.getBoundingClientRect());
911
+ }
912
+ cleanupDragVisuals();
913
+ dragSession = null;
914
+ if (activeDragRegistration === currentRegistration) {
915
+ activeDragRegistration = null;
916
+ }
917
+ if (session.dragging || !commit)
918
+ hideHandle();
919
+ };
920
+ const beginDragSession = (session, event) => {
921
+ session.dragging = true;
922
+ session.preview = createDragPreview(session.sourceBlock);
923
+ session.sourceBlock.classList.add("notion-block--dragging");
924
+ document.documentElement.classList.add("notion-editor-is-dragging");
925
+ positionDragPreview(session, event.clientX, event.clientY);
926
+ updateDropLine(session, findAnyDropTarget(session, event.clientX, event.clientY));
927
+ };
928
+ function handleDocumentMouseMove(event) {
929
+ if (!dragSession)
930
+ return;
931
+ event.preventDefault();
932
+ const movedEnough = Math.hypot(event.clientX - dragSession.startX, event.clientY - dragSession.startY) > 4;
933
+ if (!dragSession.dragging && movedEnough) {
934
+ beginDragSession(dragSession, event);
935
+ }
936
+ if (!dragSession.dragging)
937
+ return;
938
+ positionDragPreview(dragSession, event.clientX, event.clientY);
939
+ updateDropLine(dragSession, findAnyDropTarget(dragSession, event.clientX, event.clientY));
940
+ }
941
+ function handleDocumentMouseUp(event) {
942
+ event.preventDefault();
943
+ finishDragSession(true, event);
944
+ }
945
+ function handleDocumentKeyDown(event) {
946
+ if (event.key !== "Escape")
947
+ return;
948
+ event.preventDefault();
949
+ finishDragSession(false);
950
+ }
951
+ return [
952
+ new Plugin({
953
+ key: dragHandleKey,
954
+ view(editorView) {
955
+ const registration = {
956
+ view: editorView,
957
+ wrapperSelector,
958
+ getDragTransferData,
959
+ receiveDragTransferData,
960
+ handleDrop,
961
+ canHover: () => !!handle && !menu && !dragSession && editor.isEditable,
962
+ findHoverBlock: (clientX, clientY) => findForgivingBlock(editorView, clientX, clientY),
963
+ showHoverBlock: (block) => showHandleForBlock(editorView, block),
964
+ hideHover: () => hideHandle(),
965
+ gripRect: () => handle && handle.style.display !== "none"
966
+ ? handle.getBoundingClientRect()
967
+ : null,
968
+ };
969
+ currentRegistration = registration;
970
+ dragHandleRegistrations.add(registration);
971
+ retainGlobalHoverListener();
972
+ handle = createHandle();
973
+ const wrapper = editorView.dom.closest(wrapperSelector);
974
+ if (wrapper) {
975
+ wrapper.style.position = "relative";
976
+ wrapper.appendChild(handle);
977
+ }
978
+ handle.addEventListener("mousedown", (e) => {
979
+ e.stopPropagation();
980
+ if (e.button !== 0)
981
+ return;
982
+ closeMenu();
983
+ if (!editor.isEditable) {
984
+ e.preventDefault();
985
+ return;
986
+ }
987
+ if (!currentBlock || dragStartPos === null)
988
+ return;
989
+ const sourceNode = editorView.state.doc.nodeAt(dragStartPos);
990
+ if (!sourceNode)
991
+ return;
992
+ e.preventDefault();
993
+ dragSession = {
994
+ view: editorView,
995
+ sourceBlock: currentBlock,
996
+ sourcePos: dragStartPos,
997
+ sourceNodeSize: sourceNode.nodeSize,
998
+ startX: e.clientX,
999
+ startY: e.clientY,
1000
+ dragging: false,
1001
+ preview: null,
1002
+ dropLine: null,
1003
+ dropTarget: null,
1004
+ };
1005
+ activeDragRegistration = registration;
1006
+ document.addEventListener("mousemove", handleDocumentMouseMove);
1007
+ document.addEventListener("mouseup", handleDocumentMouseUp);
1008
+ document.addEventListener("keydown", handleDocumentKeyDown);
1009
+ });
1010
+ handle.addEventListener("keydown", (e) => {
1011
+ if (e.key !== "Enter" && e.key !== " ")
1012
+ return;
1013
+ e.preventDefault();
1014
+ e.stopPropagation();
1015
+ closeMenu();
1016
+ if (!editor.isEditable || !currentBlock || dragStartPos === null) {
1017
+ return;
1018
+ }
1019
+ const sourceNode = editorView.state.doc.nodeAt(dragStartPos);
1020
+ if (!sourceNode)
1021
+ return;
1022
+ openMenu({
1023
+ view: editorView,
1024
+ sourceBlock: currentBlock,
1025
+ sourcePos: dragStartPos,
1026
+ sourceNodeSize: sourceNode.nodeSize,
1027
+ }, handle?.getBoundingClientRect() ??
1028
+ currentBlock.getBoundingClientRect());
1029
+ });
1030
+ return {
1031
+ destroy() {
1032
+ closeMenu({ hideGrip: true });
1033
+ finishDragSession(false);
1034
+ releaseGlobalHoverListener();
1035
+ dragHandleRegistrations.delete(registration);
1036
+ if (activeDragRegistration === registration) {
1037
+ activeDragRegistration = null;
1038
+ }
1039
+ if (activeHoverRegistration === registration) {
1040
+ activeHoverRegistration = null;
1041
+ }
1042
+ handle?.remove();
1043
+ handle = null;
1044
+ currentRegistration = null;
1045
+ },
1046
+ };
1047
+ },
1048
+ props: {
1049
+ handleDOMEvents: {
1050
+ mousemove(_view, event) {
1051
+ updateRegisteredHover(event.clientX, event.clientY);
1052
+ return false;
1053
+ },
1054
+ drop() {
1055
+ closeMenu({ hideGrip: true });
1056
+ finishDragSession(false);
1057
+ hideHandle();
1058
+ return false;
1059
+ },
1060
+ },
1061
+ },
1062
+ }),
1063
+ ];
1064
+ },
1065
+ });
1066
+ //# sourceMappingURL=DragHandle.js.map