@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.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +19 -0
- package/dist/a11y-strings.js +19 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +37 -24
- package/dist/components/BlockHost.svelte +18 -11
- package/dist/components/Editor.svelte +434 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +25 -17
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +16 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +22 -3
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +248 -30
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +37 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +181 -37
- package/dist/components/drag-handle.d.ts +41 -0
- package/dist/components/drag-handle.js +134 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +3 -6
- package/dist/components/editor-root-listeners.js +3 -24
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +67 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +19 -0
- package/dist/core/inline/inline-widgets.js +5 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline/transparency.js +3 -3
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +9 -0
- package/dist/cursor/widget-offset.js +71 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/decorations/reserved-attrs.js +1 -0
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +24 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +16 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +60 -10
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +103 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +75 -30
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +37 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +3 -29
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +31 -35
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +52 -13
- package/dist/selection/dead-space-caret.d.ts +13 -0
- package/dist/selection/dead-space-caret.js +57 -1
- package/dist/selection/drag-pointer.d.ts +26 -2
- package/dist/selection/drag-pointer.js +103 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +4 -3
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +11 -4
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.d.ts +4 -0
- package/dist/selection/range-delete.js +31 -3
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +75 -31
- package/dist/styles/editor.css +268 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +136 -104
- package/docs/guide/plugin-api.md +73 -29
- package/docs/guide/plugin-guide.md +55 -23
- package/package.json +9 -7
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- 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
|
|
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 {
|
|
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:
|
|
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 {
|
|
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 (!
|
|
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-
|
|
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,
|
|
22
|
-
* 'horizontal' lands unconditionally so an image-only
|
|
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-
|
|
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,
|
|
83
|
-
* 'horizontal' lands unconditionally so an image-only
|
|
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
|
|
31
|
-
* the
|
|
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;
|