@voithos-labs/aragonite 0.10.2 → 0.10.4

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 (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -55,6 +55,13 @@ export function createDeadSpaceCaret(deps) {
55
55
  const hit = blockAtPoint(root, probeX, probeY);
56
56
  if (!hit)
57
57
  return false;
58
+ // Prose lands the caret on the line the click is level with, as any editor does. A block
59
+ // with no character surface (an equation, a table, a rule) has no such line, and a revealed
60
+ // source is one only while it is being edited: a click that was not ON the block is a click
61
+ // on nothing, and focuses nothing (the click's owner then blurs what was being edited).
62
+ const transient = hit.charSurface?.classList.contains('md-source-surface') ?? false;
63
+ if ((!hit.charSurface || transient) && !pressedOnBlockContent(root, x, y))
64
+ return false;
58
65
  const landing = landingFor(hit, probeX, probeY);
59
66
  if (!landing)
60
67
  return false;
@@ -81,7 +88,43 @@ export function createDeadSpaceCaret(deps) {
81
88
  leafOf(component, landing.path)?.snapCaretToPoint?.(probeX, probeY);
82
89
  return true;
83
90
  }
91
+ /** The nearest block's hit for a press, with the probe point clamped into its band. */
92
+ function hitNearPoint(root, x, y) {
93
+ const blocks = measureBlocks(root);
94
+ const band = nearestBand(blocks.map((b) => b.rect), y);
95
+ if (!band)
96
+ return null;
97
+ if (band.belowAll && lastMountedTopLevel(blocks) !== deps.lastBlockIndex())
98
+ return null;
99
+ const { x: probeX, y: probeY } = probePointIn(blocks[band.index].rect, x, y, band.belowAll);
100
+ const hit = blockAtPoint(root, probeX, probeY);
101
+ return hit && { hit, probeX, probeY };
102
+ }
103
+ function anchorAtPoint(root, x, y) {
104
+ const near = hitNearPoint(root, x, y);
105
+ if (!near)
106
+ return null;
107
+ const { hit, probeX, probeY } = near;
108
+ // No character surface to measure against — a rendered equation, a rule, a table — so the
109
+ // block is the unit, and the funnel picks the side by the drag's direction. An offset would
110
+ // put a range END at the block's start and leave it out; a cell would make the drag a caret
111
+ // placement in the nearest column. A rule names no landing at all, and is a unit all the same.
112
+ if (!hit.charSurface)
113
+ return { path: hit.path.slice(), wholeBlock: true };
114
+ const landing = landingFor(hit, probeX, probeY);
115
+ if (!landing)
116
+ return null;
117
+ if (landing.path.length > 0)
118
+ return { path: hit.path.slice(), wholeBlock: true };
119
+ return { path: hit.path.slice(), offset: landing.offset };
120
+ }
121
+ function blockPathNearPoint(root, x, y) {
122
+ return hitNearPoint(root, x, y)?.hit.path.slice() ?? null;
123
+ }
84
124
  return {
125
+ isDeadSpaceTarget: isDeadSpace,
126
+ anchorAtPoint,
127
+ blockPathNearPoint,
85
128
  notePress(root, event) {
86
129
  pressedOnDeadSpace =
87
130
  isDeadSpace(root, event.target) &&
@@ -155,6 +198,15 @@ function isDeadSpace(root, target) {
155
198
  return true;
156
199
  return target instanceof Element && target.classList.contains('block-list');
157
200
  }
201
+ /** Whether the ORIGINAL point (not the clamped probe) sits inside some block's content: on a
202
+ * descendant of a host, not on the host's own box or the dead space around it. */
203
+ function pressedOnBlockContent(root, x, y) {
204
+ const direct = document.elementFromPoint(x, y);
205
+ if (!(direct instanceof Element) || isDeadSpace(root, direct))
206
+ return false;
207
+ const host = direct.closest('[data-block-path]');
208
+ return host !== null && host !== direct;
209
+ }
158
210
  /**
159
211
  * Where the caret goes for a resolved hit: an internal child path (empty for a
160
212
  * character-addressed surface) plus the offset within that leaf. Null declines the click.
@@ -165,8 +217,12 @@ function landingFor(hit, probeX, probeY) {
165
217
  // A kind with only the drag hook addresses cells and named no caret landing.
166
218
  if (hit.foreignDragHitTest)
167
219
  return null;
220
+ // A leaf with no character surface (a rule, a folded equation) reached here was pressed on
221
+ // its own box: the block itself is the landing, and its `focus` ignores the offset.
222
+ if (!hit.charSurface)
223
+ return { path: [], offset: 0 };
168
224
  // Reading mode flips contenteditable off, and a non-editable leaf has no character position.
169
- if (!hit.charSurface?.matches('[contenteditable="true"]'))
225
+ if (!hit.charSurface.matches('[contenteditable="true"]'))
170
226
  return null;
171
227
  const offset = offsetFromViewportPoint(hit.charSurface, probeX, probeY);
172
228
  return offset === null ? null : { path: [], offset };
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import type { UserScrollport } from '../cursor/scroll-ancestors';
7
7
  import type { SelectionState } from './selection-state.svelte';
8
- import type { SelectionPoint } from './primitives';
8
+ import type { SelectionEndpoint } from './primitives';
9
9
  import type { BlockElLookup } from '../editor-keys';
10
+ import { type SelectionPoint } from './primitives';
10
11
  export interface DragContext {
11
12
  editorRoot: HTMLElement;
12
13
  /** What autoscrolls this drag: an element, or the window (`cursor/scroll-ancestors`). */
@@ -15,8 +16,31 @@ export interface DragContext {
15
16
  getBlockElByPath: BlockElLookup;
16
17
  /** Aborted on editor unmount; forwarded to the session's teardown. */
17
18
  lifetimeSignal?: AbortSignal;
19
+ /**
20
+ * The press began outside every editable surface (the editor's margin), so no native drag
21
+ * is extending a selection underneath: the session paints the same-block range itself.
22
+ */
23
+ paintSameBlock?: boolean;
24
+ /** A drag continuing a multi-click: the range grows by that rung's unit. */
25
+ granularity?: DragGranularity;
26
+ }
27
+ /** The unit a multi-click drag grows by. Spans are raw offsets; the press's own span is the
28
+ * floor the range never shrinks below. */
29
+ export interface DragGranularity {
30
+ /** Where the press selected; a pointer that names no other block paints here. */
31
+ surface: HTMLElement;
32
+ anchorSpan: {
33
+ start: number;
34
+ end: number;
35
+ };
36
+ spanAround(offset: number): {
37
+ start: number;
38
+ end: number;
39
+ };
40
+ /** A focus in another block, pushed to the unit's boundary on the side away from the anchor. */
41
+ expandFocus(point: SelectionPoint, side: 'before' | 'after'): SelectionEndpoint;
18
42
  }
19
43
  /** Document-level pointer listeners for a cross-block drag started at `down`, plus a disposer. */
20
- export declare function installDragListener(ctx: DragContext, anchorPoint: SelectionPoint, down: PointerEvent): {
44
+ export declare function installDragListener(ctx: DragContext, anchorPoint: SelectionEndpoint, down: PointerEvent): {
21
45
  dispose(): void;
22
46
  };
@@ -3,7 +3,9 @@
3
3
  * `createPointerDragSession`, whose document-level listeners deliver events even after the
4
4
  * pointer leaves the originating block.
5
5
  */
6
- import { applyCollapsedCaret } from './native-bridge';
6
+ import { caretOffsetAtPoint } from '../cursor/point-offset';
7
+ import { applyCollapsedCaret, applySingleBlockRange, clearNativeSelection } from './native-bridge';
8
+ import { isWholeBlockEndpoint } from './primitives';
7
9
  import { comparePaths } from './path-math';
8
10
  import { createPointerDragSession } from './pointer-session';
9
11
  import { blockNearPoint } from './nearest-block';
@@ -17,25 +19,116 @@ export function installDragListener(ctx, anchorPoint, down) {
17
19
  const near = blockNearPoint(ctx.editorRoot, clientX, clientY);
18
20
  if (!near)
19
21
  return;
22
+ if (ctx.granularity) {
23
+ processGranularMove(ctx.granularity, near, clientX, clientY);
24
+ return;
25
+ }
20
26
  if (comparePaths(near.path, anchorPoint.path) === 0) {
27
+ if (!('offset' in anchorPoint)) {
28
+ if (ctx.paintSameBlock)
29
+ takeAnchorBlockWhole();
30
+ return;
31
+ }
21
32
  if (ctx.selection.isCrossBlock) {
22
33
  // Pointer returned to the anchor block: collapse so the overlay stops painting a
23
34
  // stale remote range. The browser's drag has been extending the native selection
24
35
  // underneath all along, so handing back gives the right single-block highlight.
25
36
  ctx.selection.collapse();
26
37
  }
38
+ if (ctx.paintSameBlock)
39
+ paintSameBlockRange(near.endpointHere());
27
40
  return;
28
41
  }
29
42
  const focusPoint = near.endpointHere();
30
43
  if (!focusPoint)
31
44
  return;
32
- if (!ctx.selection.isCrossBlock) {
45
+ if (isWholeBlockEndpoint(focusPoint) && !reachedCentreLine(near.path, clientY))
46
+ return;
47
+ // A whole unit re-enters rather than extends: its anchor's side is resolved against the
48
+ // new focus by the entry, where an extend would keep the unit's start as the anchor.
49
+ if (!ctx.selection.isCrossBlock || ctx.selection.wholeUnitPath) {
33
50
  ctx.selection.enterCrossBlock(anchorPoint, focusPoint);
34
51
  }
35
52
  else {
36
53
  ctx.selection.extendFocus(focusPoint);
37
54
  }
38
55
  }
56
+ // The anchor's side flips with the drag's direction (the press's span ends the range going up,
57
+ // starts it going down), so a flip re-enters; a same-side move only extends.
58
+ let anchorAfter = null;
59
+ function processGranularMove(unit, near, clientX, clientY) {
60
+ const anchorIsChar = 'offset' in anchorPoint && !anchorPoint.cellCoordinate;
61
+ if (!anchorIsChar || comparePaths(near.path, anchorPoint.path) === 0) {
62
+ if (ctx.selection.isCrossBlock)
63
+ ctx.selection.collapse();
64
+ anchorAfter = null;
65
+ const offset = caretOffsetAtPoint(unit.surface, clientX, clientY);
66
+ if (offset === null)
67
+ return;
68
+ const span = unit.spanAround(offset);
69
+ unit.surface.focus({ preventScroll: true });
70
+ applySingleBlockRange(unit.surface, Math.min(unit.anchorSpan.start, span.start), Math.max(unit.anchorSpan.end, span.end));
71
+ return;
72
+ }
73
+ const focusPoint = near.endpointHere();
74
+ if (!focusPoint)
75
+ return;
76
+ if (isWholeBlockEndpoint(focusPoint) && !reachedCentreLine(near.path, clientY))
77
+ return;
78
+ const after = comparePaths(near.path, anchorPoint.path) > 0;
79
+ const focus = isWholeBlockEndpoint(focusPoint)
80
+ ? focusPoint
81
+ : unit.expandFocus(focusPoint, after ? 'after' : 'before');
82
+ if (!ctx.selection.isCrossBlock || anchorAfter !== after) {
83
+ anchorAfter = after;
84
+ ctx.selection.enterCrossBlock({
85
+ path: anchorPoint.path.slice(),
86
+ offset: after ? unit.anchorSpan.start : unit.anchorSpan.end
87
+ }, focus);
88
+ }
89
+ else {
90
+ ctx.selection.extendFocus(focus);
91
+ }
92
+ }
93
+ // A block with no positions inside it is in or out as a unit, and the unit joins the range
94
+ // once the pointer has crossed its centre line coming from the anchor's side: a sweep that
95
+ // merely touched its edge has not asked for it. Not reached, the last focus stands, so the
96
+ // range grows to the block only when the pointer commits to it.
97
+ function reachedCentreLine(path, clientY) {
98
+ const box = ctx.getBlockElByPath(path)?.getBoundingClientRect();
99
+ if (!box)
100
+ return true;
101
+ const middle = box.top + box.height / 2;
102
+ return comparePaths(anchorPoint.path, path) < 0 ? clientY >= middle : clientY <= middle;
103
+ }
104
+ // A whole-block anchor (a table, an equation) has nothing to paint natively either, and a range
105
+ // that only appears once the pointer reaches ANOTHER block reads as a drag that does nothing.
106
+ // The block is taken whole the moment the pointer moves, as the unit the state stores and the
107
+ // overlay paints (`wholeUnitPath`); a pointer returning from outside takes it whole again.
108
+ function takeAnchorBlockWhole() {
109
+ if (ctx.selection.wholeUnitPath)
110
+ return;
111
+ // Both ends whole: the pointer's position INSIDE the block is not a rectangle to grow (a
112
+ // press beside a table dragged up over it would otherwise select the rows below the
113
+ // pointer, the opposite of the sweep). The block is the unit until the drag leaves it.
114
+ ctx.selection.enterCrossBlock(anchorPoint, {
115
+ path: anchorPoint.path.slice(),
116
+ wholeBlock: true
117
+ });
118
+ }
119
+ // A margin-started drag has no native selection under it, so the range inside the anchor
120
+ // block is written here, and the block takes focus so the range is live for the next key.
121
+ function paintSameBlockRange(focusPoint) {
122
+ if (!focusPoint || !('offset' in focusPoint) || !('offset' in anchorPoint))
123
+ return;
124
+ const blockEl = ctx.getBlockElByPath(anchorPoint.path);
125
+ if (!blockEl)
126
+ return;
127
+ // A no-op once it holds focus; the range is a DOM Range, so it is written low-to-high (a
128
+ // drag from the right margin leftward would otherwise collapse it).
129
+ blockEl.focus({ preventScroll: true });
130
+ applySingleBlockRange(blockEl, Math.min(anchorPoint.offset, focusPoint.offset), Math.max(anchorPoint.offset, focusPoint.offset));
131
+ }
39
132
  // Pointer may land on a scrollable element directly (the table's `.table-block` edge), so
40
133
  // search from `target` itself, not its parent.
41
134
  function scrollableSelfOrAncestor(target) {
@@ -79,6 +172,14 @@ function parkCaretInFocusBlock(ctx) {
79
172
  const focus = ctx.selection.focus;
80
173
  if (!focus)
81
174
  return;
175
+ // A whole unit has no text node for a caret: focus parks on the editor root, whose keydown
176
+ // and clipboard arms serve a custom range (as they do for a windowed-out block), with no
177
+ // native range left behind for the browser to act on.
178
+ if (ctx.selection.wholeUnitPath) {
179
+ clearNativeSelection();
180
+ ctx.editorRoot.focus({ preventScroll: true });
181
+ return;
182
+ }
82
183
  // A drag ending inside a table leaves a focus addressing the table block by cell index; the
83
184
  // landing is the cell that actually holds a caret.
84
185
  const landing = ctx.selection.cellLandingFor(focus);
@@ -4,7 +4,7 @@
4
4
  * in, boolean out; `tryGapStop` is the one seam that turns it into an arrival.
5
5
  * The gap is deliberately not a `SelectionPoint`: it is never a cross-block endpoint.
6
6
  */
7
- import { isBlockNode, nodeAt } from '../tree-operations/node-ops';
7
+ import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
8
8
  import { tryGetBlockKindDescriptor } from '../schema/block-kind-descriptor';
9
9
  import { isReadingMode } from '../presentation-mode';
10
10
  import { placeGapCaret } from './caret-doors';
@@ -18,8 +18,9 @@ export declare function scrollFocusBlockIntoView(selection: SelectionState, getB
18
18
  /**
19
19
  * Extend focus to the next leaf in document order (Shift+ArrowDown / Shift+ArrowRight leaving
20
20
  * the block), entering cross-block mode if needed. Returns true if focus moved. `axis` =
21
- * 'vertical' skips vertically-transparent leaves, mirroring single-block focus dispatch;
22
- * 'horizontal' lands unconditionally so an image-only paragraph is selectable in one step.
21
+ * 'vertical' skips vertically-transparent leaves — a range covers one either way, so unlike a
22
+ * collapsed caret it needs no object stop; 'horizontal' lands unconditionally so an image-only
23
+ * paragraph is selectable in one step.
23
24
  */
24
25
  export declare function extendFocusToNextBlock(selection: SelectionState, doc: Document, currentBlockEl: HTMLElement, currentBlockPath: number[], axis?: 'horizontal' | 'vertical', getBlockElByPath?: BlockElLookup): boolean;
25
26
  /**
@@ -3,7 +3,7 @@ import { isVerticallyTransparentNode } from '../core/inline/transparency';
3
3
  import { readNativeCaretInBlock, applyCollapsedCaret, focusCollapsedCaret, applySingleBlockRange, clearNativeSelection } from './native-bridge';
4
4
  import { offsetFromViewportPoint } from '../cursor/point-offset';
5
5
  import { nextPath, previousPath, firstPath, lastPath, firstLeafAtOrAfter, lastLeafAtOrBefore, findCellPathForElement } from './path-lookup';
6
- import { nodeAt } from '../tree-operations/node-ops';
6
+ import { nodeAt } from '../tree-operations/node-primitives';
7
7
  import { comparePaths, isStrictAncestorOf, pathsEqual } from './path-math';
8
8
  import { displayLength } from '../core/lines';
9
9
  // ── Enter / Collapse / Scroll ──────────────────────────────────────────────
@@ -79,8 +79,9 @@ function extendFocusOrRestore(selection, target, offset, getBlockElByPath) {
79
79
  /**
80
80
  * Extend focus to the next leaf in document order (Shift+ArrowDown / Shift+ArrowRight leaving
81
81
  * the block), entering cross-block mode if needed. Returns true if focus moved. `axis` =
82
- * 'vertical' skips vertically-transparent leaves, mirroring single-block focus dispatch;
83
- * 'horizontal' lands unconditionally so an image-only paragraph is selectable in one step.
82
+ * 'vertical' skips vertically-transparent leaves — a range covers one either way, so unlike a
83
+ * collapsed caret it needs no object stop; 'horizontal' lands unconditionally so an image-only
84
+ * paragraph is selectable in one step.
84
85
  */
85
86
  export function extendFocusToNextBlock(selection, doc, currentBlockEl, currentBlockPath, axis = 'horizontal', getBlockElByPath) {
86
87
  const leafTarget = axis === 'vertical'
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The click ladder past the caret: the second press of a run selects the word under it, the
3
+ * third the surface's content, and a drag from either extends at that granularity. Owned from
4
+ * the second press, so the browser paints nothing of its own: its word rule is per platform,
5
+ * its walk enters atomic islands, and a marker here never joins a word.
6
+ */
7
+ import type { UserScrollport } from '../cursor/scroll-ancestors';
8
+ import type { BlockElLookup } from '../editor-keys';
9
+ import type { SelectionState } from './selection-state.svelte';
10
+ export type ClickGranularity = 'word' | 'block';
11
+ export interface Span {
12
+ start: number;
13
+ end: number;
14
+ }
15
+ /**
16
+ * [start, end) of the segment at `offset` in `text`: the word ending there when it is one, else
17
+ * the segment starting there (whitespace or punctuation, as the engine would take). Null on
18
+ * empty text, or on a platform with no segmenter.
19
+ */
20
+ export declare function wordSpanAt(text: string, offset: number): Span | null;
21
+ /** The raw span `granularity` takes around a raw offset in `surface`; null where the word rule
22
+ * has nothing to read. */
23
+ export declare function spanAround(surface: HTMLElement, granularity: ClickGranularity, rawOffset: number): Span | null;
24
+ /** The rung a press's click count reaches; the run stays at the block rung past three. */
25
+ export declare function granularityForClickCount(clickCount: number): ClickGranularity | null;
26
+ export interface MultiClickDeps {
27
+ editorRoot: HTMLElement;
28
+ selection: SelectionState;
29
+ getBlockElByPath: BlockElLookup;
30
+ getScrollContainer(): UserScrollport;
31
+ lifetimeSignal?: AbortSignal;
32
+ /** A press outside every editable surface (the margin, a container's own box) names the
33
+ * nearest block, as a single click there does; null where the press is chrome's. */
34
+ marginBlockAt(target: EventTarget | null, clientX: number, clientY: number): number[] | null;
35
+ }
36
+ /**
37
+ * Root listeners for the ladder. The count rides the mousedown (a pointer event carries none),
38
+ * and the drag rides the pointerdown of the same press, so both are heard. The mousedown is
39
+ * cancelled, never the pointerdown (that would silence the click), and so is the mouseup: the
40
+ * browser's release seats a caret at a press that landed off a glyph, over the range just painted.
41
+ */
42
+ export declare function installMultiClickSelect(deps: MultiClickDeps): () => void;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * The click ladder past the caret: the second press of a run selects the word under it, the
3
+ * third the surface's content, and a drag from either extends at that granularity. Owned from
4
+ * the second press, so the browser paints nothing of its own: its word rule is per platform,
5
+ * its walk enters atomic islands, and a marker here never joins a word.
6
+ */
7
+ import { ambientLengthOf } from '../ambient/ambient-dom';
8
+ import { asDomTextOffset, asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
9
+ import { caretOffsetAtPoint } from '../cursor/point-offset';
10
+ import { containerDomTextLength, maskedWalkText } from '../cursor/widget-offset';
11
+ import { isWholeBlockInputProxy } from '../editor-actions/whole-block-focus-surface';
12
+ import { installDragListener } from './drag-pointer';
13
+ import { applySingleBlockRange, applySurfaceContentRange } from './native-bridge';
14
+ import { blockNearPoint } from './nearest-block';
15
+ // ── The word rule ────────────────────────────────────────────────────────────
16
+ let segmenter;
17
+ function wordSegmenter() {
18
+ if (segmenter === undefined) {
19
+ segmenter =
20
+ typeof Intl.Segmenter === 'function'
21
+ ? new Intl.Segmenter(undefined, { granularity: 'word' })
22
+ : null;
23
+ }
24
+ return segmenter;
25
+ }
26
+ /**
27
+ * [start, end) of the segment at `offset` in `text`: the word ending there when it is one, else
28
+ * the segment starting there (whitespace or punctuation, as the engine would take). Null on
29
+ * empty text, or on a platform with no segmenter.
30
+ */
31
+ export function wordSpanAt(text, offset) {
32
+ const seg = wordSegmenter();
33
+ if (!seg || text.length === 0)
34
+ return null;
35
+ const segments = seg.segment(text);
36
+ const at = Math.max(0, Math.min(offset, text.length));
37
+ const left = at > 0 ? segments.containing(at - 1) : undefined;
38
+ const right = at < text.length ? segments.containing(at) : undefined;
39
+ if (left && right && left.index === right.index)
40
+ return spanOf(left);
41
+ if (left?.isWordLike)
42
+ return spanOf(left);
43
+ const pick = right ?? left;
44
+ return pick ? spanOf(pick) : null;
45
+ }
46
+ function spanOf(segment) {
47
+ return { start: segment.index, end: segment.index + segment.segment.length };
48
+ }
49
+ /** The raw span `granularity` takes around a raw offset in `surface`; null where the word rule
50
+ * has nothing to read. */
51
+ export function spanAround(surface, granularity, rawOffset) {
52
+ const ambient = ambientLengthOf(surface);
53
+ if (granularity === 'block') {
54
+ return { start: 0, end: toClampedRawOffset(containerDomTextLength(surface), ambient) };
55
+ }
56
+ const walk = toDomTextOffset(asRawOffset(rawOffset), ambient);
57
+ const span = wordSpanAt(maskedWalkText(surface), walk);
58
+ if (!span)
59
+ return null;
60
+ return {
61
+ start: toClampedRawOffset(asDomTextOffset(span.start), ambient),
62
+ end: toClampedRawOffset(asDomTextOffset(span.end), ambient)
63
+ };
64
+ }
65
+ /** The rung a press's click count reaches; the run stays at the block rung past three. */
66
+ export function granularityForClickCount(clickCount) {
67
+ if (clickCount === 2)
68
+ return 'word';
69
+ return clickCount >= 3 ? 'block' : null;
70
+ }
71
+ /**
72
+ * Root listeners for the ladder. The count rides the mousedown (a pointer event carries none),
73
+ * and the drag rides the pointerdown of the same press, so both are heard. The mousedown is
74
+ * cancelled, never the pointerdown (that would silence the click), and so is the mouseup: the
75
+ * browser's release seats a caret at a press that landed off a glyph, over the range just painted.
76
+ */
77
+ export function installMultiClickSelect(deps) {
78
+ let lastPointerDown = null;
79
+ let claimed = false;
80
+ const onPointerDown = (e) => {
81
+ lastPointerDown = e;
82
+ };
83
+ const onMouseUp = (e) => {
84
+ if (claimed)
85
+ e.preventDefault();
86
+ };
87
+ const onMouseDown = (e) => {
88
+ claimed = false;
89
+ const granularity = granularityForClickCount(e.detail);
90
+ if (!granularity || e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
91
+ return;
92
+ if (pressesInlineWidget(e.target))
93
+ return;
94
+ const press = pressedSurface(deps, e);
95
+ if (!press)
96
+ return;
97
+ const anchorSpan = selectAtPoint(press.surface, granularity, e.clientX, e.clientY);
98
+ if (!anchorSpan)
99
+ return;
100
+ e.preventDefault();
101
+ claimed = true;
102
+ const anchor = press.anchor ?? blockNearPoint(deps.editorRoot, e.clientX, e.clientY)?.endpointHere();
103
+ if (!anchor || !lastPointerDown)
104
+ return;
105
+ installDragListener({
106
+ editorRoot: deps.editorRoot,
107
+ scrollContainer: deps.getScrollContainer(),
108
+ selection: deps.selection,
109
+ getBlockElByPath: deps.getBlockElByPath,
110
+ lifetimeSignal: deps.lifetimeSignal,
111
+ granularity: createGranularity(deps.getBlockElByPath, press.surface, granularity, anchorSpan)
112
+ }, anchor, lastPointerDown);
113
+ };
114
+ deps.editorRoot.addEventListener('pointerdown', onPointerDown);
115
+ deps.editorRoot.addEventListener('mousedown', onMouseDown);
116
+ deps.editorRoot.addEventListener('mouseup', onMouseUp);
117
+ return () => {
118
+ deps.editorRoot.removeEventListener('pointerdown', onPointerDown);
119
+ deps.editorRoot.removeEventListener('mousedown', onMouseDown);
120
+ deps.editorRoot.removeEventListener('mouseup', onMouseUp);
121
+ };
122
+ }
123
+ /** The surface a press selects in: the editable under it, else the nearest block's, the way a
124
+ * single click in the margin lands there. A block with no text surface is nobody's word. */
125
+ function pressedSurface(deps, e) {
126
+ // Up through an island inside the editable (a list's `- ` prefix) to the surface itself.
127
+ const own = surfaceOf(e.target);
128
+ if (own)
129
+ return isWholeBlockInputProxy(own) ? null : { surface: own, anchor: null };
130
+ const path = deps.marginBlockAt(e.target, e.clientX, e.clientY);
131
+ const block = path && deps.getBlockElByPath(path);
132
+ if (!block)
133
+ return null;
134
+ // A container's own box (a quote's gutter) selects in the leaf nearest the press, the way a
135
+ // click beside a line lands in that line.
136
+ const surface = block.contentEditable === 'true' ? block : nearestSurfaceIn(block, e.clientY);
137
+ const surfacePath = surface && pathOfSurface(surface);
138
+ const offset = surface && caretOffsetAtPoint(surface, e.clientX, e.clientY);
139
+ if (!surface)
140
+ return null;
141
+ return { surface, anchor: surfacePath && offset !== null ? { path: surfacePath, offset } : null };
142
+ }
143
+ function nearestSurfaceIn(block, clientY) {
144
+ let best = null;
145
+ let bestDistance = Infinity;
146
+ for (const el of block.querySelectorAll('[contenteditable="true"]')) {
147
+ const rect = el.getBoundingClientRect();
148
+ const distance = clientY < rect.top ? rect.top - clientY : Math.max(0, clientY - rect.bottom);
149
+ if (distance < bestDistance) {
150
+ best = el;
151
+ bestDistance = distance;
152
+ }
153
+ }
154
+ return best;
155
+ }
156
+ /** The path of the block a surface is the content of; null for a surface addressed some other
157
+ * way (a table's cell), whose drag is not a char-point drag. */
158
+ function pathOfSurface(surface) {
159
+ const host = surface.closest('[data-block-path]');
160
+ const raw = host?.getAttribute('data-block-path');
161
+ if (!raw || !host?.contains(surface))
162
+ return null;
163
+ const path = JSON.parse(raw);
164
+ return host.querySelector('[contenteditable="true"]') === surface ? path : null;
165
+ }
166
+ /** Paint the rung's span at the press and answer it, in raw offsets. */
167
+ function selectAtPoint(surface, granularity, clientX, clientY) {
168
+ if (granularity === 'block') {
169
+ applySurfaceContentRange(surface);
170
+ return spanAround(surface, 'block', 0);
171
+ }
172
+ const offset = caretOffsetAtPoint(surface, clientX, clientY);
173
+ const span = offset === null ? null : spanAround(surface, 'word', offset);
174
+ if (!span || span.start === span.end)
175
+ return null;
176
+ applySingleBlockRange(surface, span.start, span.end);
177
+ return span;
178
+ }
179
+ function createGranularity(getBlockElByPath, surface, granularity, anchorSpan) {
180
+ return {
181
+ surface,
182
+ anchorSpan,
183
+ spanAround: (offset) => spanAround(surface, granularity, offset) ?? { start: offset, end: offset },
184
+ expandFocus: (point, side) => {
185
+ const el = point.cellCoordinate ? null : getBlockElByPath(point.path);
186
+ const span = el && spanAround(el, granularity, point.offset);
187
+ if (!span)
188
+ return point;
189
+ return { path: point.path.slice(), offset: side === 'after' ? span.end : span.start };
190
+ }
191
+ };
192
+ }
193
+ /** A press on an inline widget is that widget's own gesture (a footnote's double-click takes
194
+ * its whole token), so the ladder leaves it alone. */
195
+ function pressesInlineWidget(target) {
196
+ return target instanceof Element && target.closest('[data-inline-widget]') !== null;
197
+ }
198
+ function surfaceOf(target) {
199
+ let el = target instanceof HTMLElement ? target : null;
200
+ while (el && el.contentEditable !== 'true')
201
+ el = el.parentElement;
202
+ return el;
203
+ }
@@ -25,6 +25,9 @@ export declare function applyCollapsedCaret(blockEl: HTMLElement, point: Selecti
25
25
  * Returns whether an element was found; a missing target is a no-op.
26
26
  */
27
27
  export declare function focusCollapsedCaret(getBlockElByPath: (path: number[]) => HTMLElement | null, point: SelectionPoint): boolean;
28
+ /** Select a surface's content whole, past its ambient prefix: the first-press Ctrl+A range and
29
+ * the triple-click one. */
30
+ export declare function applySurfaceContentRange(el: HTMLElement): void;
28
31
  export declare function applySingleBlockRange(blockEl: HTMLElement, startOffset: number, endOffset: number): void;
29
32
  export declare function clearNativeSelection(): void;
30
33
  /**
@@ -4,9 +4,10 @@
4
4
  * DOM→raw conversion subtracts the length of a leading ambient marker span, whose textContent
5
5
  * counts toward DOM offsets but not raw.
6
6
  */
7
- import { asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
7
+ import { asDomTextOffset, asRawOffset, toClampedRawOffset, toDomTextOffset } from '../cursor/coordinate-spaces';
8
+ import { createRangeFromOffsets } from '../cursor/content-offsets';
8
9
  import { clampToLandableRaw, createRangeAtDomTextOffsets, domTextOffsetAtNode } from '../cursor/widget-offset';
9
- import { ambientLengthOf, placeCaretAfterAmbientSpan } from '../ambient/ambient-dom';
10
+ import { ambientLengthOf, ambientSpanOf, placeCaretAfterAmbientSpan } from '../ambient/ambient-dom';
10
11
  // ── Read native → SelectionPoint ────────────────────────────────────────────
11
12
  /**
12
13
  * Read the collapsed caret inside `blockEl` into a raw-semantic SelectionPoint.
@@ -64,12 +65,40 @@ export function focusCollapsedCaret(getBlockElByPath, point) {
64
65
  blockEl.focus();
65
66
  return true;
66
67
  }
68
+ /** Select a surface's content whole, past its ambient prefix: the first-press Ctrl+A range and
69
+ * the triple-click one. */
70
+ export function applySurfaceContentRange(el) {
71
+ const ambient = ambientSpanOf(el);
72
+ const ambientLen = ambient?.textContent?.length ?? 0;
73
+ const textLen = el.textContent?.length ?? 0;
74
+ if (ambient && textLen > ambientLen) {
75
+ if (!placeCaretAfterAmbientSpan(el))
76
+ return;
77
+ // textLen counts the full textContent (marker included) — a DomTextOffset by construction.
78
+ const endRange = createRangeFromOffsets(el, asDomTextOffset(textLen), asDomTextOffset(textLen));
79
+ if (endRange) {
80
+ window.getSelection()?.extend(endRange.endContainer, endRange.endOffset);
81
+ }
82
+ return;
83
+ }
84
+ const range = document.createRange();
85
+ range.selectNodeContents(el);
86
+ const sel = window.getSelection();
87
+ sel?.removeAllRanges();
88
+ sel?.addRange(range);
89
+ }
67
90
  export function applySingleBlockRange(blockEl, startOffset, endOffset) {
68
91
  const ambient = ambientLengthOf(blockEl);
69
92
  const range = createRangeAtDomTextOffsets(blockEl, toDomTextOffset(asRawOffset(startOffset), ambient), toDomTextOffset(asRawOffset(endOffset), ambient));
70
93
  if (!range)
71
94
  return;
72
95
  const sel = window.getSelection();
96
+ // A start at raw 0 under an ambient marker goes AFTER the island, as a caret does: Chromium
97
+ // drops a range that opens inside its contenteditable="false".
98
+ if (ambient > 0 && startOffset <= 0 && placeCaretAfterAmbientSpan(blockEl)) {
99
+ sel?.extend(range.endContainer, range.endOffset);
100
+ return;
101
+ }
73
102
  sel?.removeAllRanges();
74
103
  sel?.addRange(range);
75
104
  }
@@ -27,6 +27,14 @@ export declare function findBlockPathForElement(el: Element | null): number[] |
27
27
  * cell grid; addresses the grid by selector contract (`data-table-row-idx`, `role="cell"`).
28
28
  */
29
29
  export declare function findCellPathForElement(el: Element | null): number[] | null;
30
- /** The path of the editing SURFACE `el` sits in — the enclosing cell where there is one, else
31
- * the enclosing block. The pairing {@link findCellPathForElement} says every producer owes. */
30
+ /** The editing SURFACE `el` sits in: the enclosing cell where there is one, else the enclosing
31
+ * block, and which of the two answered — a caller whose bytes live at a different scope for a
32
+ * cell reads the flag rather than re-deriving the pairing. */
33
+ export interface EditingSurface {
34
+ path: number[];
35
+ inCell: boolean;
36
+ }
37
+ /** The pairing {@link findCellPathForElement} says every producer owes, in one place. */
38
+ export declare function findSurfaceForElement(el: Element | null): EditingSurface | null;
39
+ /** {@link findSurfaceForElement} for a caller that needs only the path. */
32
40
  export declare function findSurfacePathForElement(el: Element | null): number[] | null;