@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Document-tree path navigation. Pure functions over a Document + path. */
|
|
2
|
-
import { nodeAt } from '../tree-operations/node-
|
|
2
|
+
import { nodeAt } from '../tree-operations/node-primitives';
|
|
3
3
|
/** Block immediately after `path` in doc order (children before siblings), else null. */
|
|
4
4
|
export function nextPath(doc, path) {
|
|
5
5
|
const node = nodeAt(doc, path);
|
|
@@ -125,8 +125,15 @@ export function findCellPathForElement(el) {
|
|
|
125
125
|
return null;
|
|
126
126
|
return [...tablePath, rowIdx, colIdx];
|
|
127
127
|
}
|
|
128
|
-
/** The
|
|
129
|
-
|
|
128
|
+
/** The pairing {@link findCellPathForElement} says every producer owes, in one place. */
|
|
129
|
+
export function findSurfaceForElement(el) {
|
|
130
|
+
const cellPath = findCellPathForElement(el);
|
|
131
|
+
if (cellPath)
|
|
132
|
+
return { path: cellPath, inCell: true };
|
|
133
|
+
const blockPath = findBlockPathForElement(el);
|
|
134
|
+
return blockPath ? { path: blockPath, inCell: false } : null;
|
|
135
|
+
}
|
|
136
|
+
/** {@link findSurfaceForElement} for a caller that needs only the path. */
|
|
130
137
|
export function findSurfacePathForElement(el) {
|
|
131
|
-
return
|
|
138
|
+
return findSurfaceForElement(el)?.path ?? null;
|
|
132
139
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door a widget declares over an element whose pointer drags belong to its own gesture (a
|
|
3
|
+
* diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
|
|
4
|
+
* gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
|
|
5
|
+
* are delegated at the app root, so a component's own handler runs after the editor's.
|
|
6
|
+
*/
|
|
7
|
+
/** Declared on the gesture surface itself; any descendant press counts. */
|
|
8
|
+
export declare const POINTER_GESTURE_ATTR = "data-pointer-gesture";
|
|
9
|
+
export declare function claimsPointerGesture(target: EventTarget | null): boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door a widget declares over an element whose pointer drags belong to its own gesture (a
|
|
3
|
+
* diagram's pan, a canvas's brush): the editor's pointer arms decline a press inside it, so the
|
|
4
|
+
* gesture never doubles as a block range. A declaration, not a `stopPropagation`: pointer events
|
|
5
|
+
* are delegated at the app root, so a component's own handler runs after the editor's.
|
|
6
|
+
*/
|
|
7
|
+
/** Declared on the gesture surface itself; any descendant press counts. */
|
|
8
|
+
export const POINTER_GESTURE_ATTR = 'data-pointer-gesture';
|
|
9
|
+
export function claimsPointerGesture(target) {
|
|
10
|
+
return target instanceof Element && target.closest(`[${POINTER_GESTURE_ATTR}]`) !== null;
|
|
11
|
+
}
|
|
@@ -73,3 +73,10 @@ export type BlockSelectionClass = 'outside' | 'start' | 'middle' | 'end' | 'sing
|
|
|
73
73
|
* tells the caller to delegate to the browser instead of painting.
|
|
74
74
|
*/
|
|
75
75
|
export declare function classifyBlockForSelection(path: number[], selection: EditorSelection): BlockSelectionClass;
|
|
76
|
+
/**
|
|
77
|
+
* Whether this block paints the range over its whole box: the range holds its entire subtree and
|
|
78
|
+
* no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
|
|
79
|
+
* a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
|
|
80
|
+
* the covering block takes the box in one piece and its children paint nothing.
|
|
81
|
+
*/
|
|
82
|
+
export declare function blockPaintsWholeBox(path: readonly number[], selection: EditorSelection, wholeUnitPath: readonly number[] | null): boolean;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* classification, the delete-commit snapshot rule. No DOM, no state. Path-level predicates
|
|
4
4
|
* live in `./path-math`.
|
|
5
5
|
*/
|
|
6
|
-
import { comparePaths, isPathBetween, pathHasPrefix } from './path-math';
|
|
6
|
+
import { comparePaths, isPathBetween, isStrictAncestorOf, pathHasPrefix, pathsEqual } from './path-math';
|
|
7
7
|
import { asCellIndex, asRawOffset, docPathFrom } from '../cursor/coordinate-spaces';
|
|
8
8
|
import { devWarn } from '../dev-warn';
|
|
9
9
|
export function isWholeBlockEndpoint(endpoint) {
|
|
@@ -89,3 +89,21 @@ export function classifyBlockForSelection(path, selection) {
|
|
|
89
89
|
return 'middle';
|
|
90
90
|
return 'outside';
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether this block paints the range over its whole box: the range holds its entire subtree and
|
|
94
|
+
* no ancestor's box already covers it, or it is the whole unit a single-block range holds. Chrome
|
|
95
|
+
* a container derives from its markers (a GitHub alert's badge) has no child host to paint it, so
|
|
96
|
+
* the covering block takes the box in one piece and its children paint nothing.
|
|
97
|
+
*/
|
|
98
|
+
export function blockPaintsWholeBox(path, selection, wholeUnitPath) {
|
|
99
|
+
if (wholeUnitPath)
|
|
100
|
+
return pathsEqual(path, wholeUnitPath);
|
|
101
|
+
const { start, end } = normalize(selection);
|
|
102
|
+
return (holdsSubtree(path, start.path, end.path) &&
|
|
103
|
+
!holdsSubtree(path.slice(0, -1), start.path, end.path));
|
|
104
|
+
}
|
|
105
|
+
/** The range holds this block's whole subtree: inside it in document order, and not an ancestor
|
|
106
|
+
* of the end endpoint, whose own descendants the range cuts through. */
|
|
107
|
+
function holdsSubtree(path, start, end) {
|
|
108
|
+
return isPathBetween(path, start, end) && !isStrictAncestorOf(path, end);
|
|
109
|
+
}
|
|
@@ -10,8 +10,10 @@ import { charOffsetOf, walkBetween } from './primitives';
|
|
|
10
10
|
import { comparePaths, isStrictAncestorOf, isPathSubtreeBetween, lowestCommonAncestor, pathHasPrefix, pathsEqual } from './path-math';
|
|
11
11
|
import { cascadeCleanupEmptyAncestors } from '../tree-operations/cleanup';
|
|
12
12
|
import { deleteAtPath, replaceAtPath } from '../tree-operations/path-mutate';
|
|
13
|
-
import { blockNodeAt,
|
|
14
|
-
import {
|
|
13
|
+
import { blockNodeAt, emptyParagraph, nodeAt, normalizeOwnRaw } from '../tree-operations/node-primitives';
|
|
14
|
+
import { cleanJoinedRaw } from '../tree-operations/node-ops';
|
|
15
|
+
import { ensureUnsharedPath } from '../tree-operations/unshare';
|
|
16
|
+
import { rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
|
|
15
17
|
// Wall primitives live with the chrome branch, which imports the atoms below back. The
|
|
16
18
|
// resulting cycle is function-body-only (resolved at call time, never at module load), so it
|
|
17
19
|
// is safe.
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { displayLength } from '../core/lines';
|
|
8
8
|
import { comparePaths, pathsEqual } from './path-math';
|
|
9
9
|
import { resolveEndWall, planCrossBlockDeletion, applyPlannedDeletion, truncateEndInPlace, truncateStartInPlace } from './range-delete-ceremony';
|
|
10
|
-
import { ensureUnsharedPath
|
|
10
|
+
import { ensureUnsharedPath } from '../tree-operations/unshare';
|
|
11
|
+
import { rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
|
|
11
12
|
import { reservedChromeKindOf, isReservedChromeChild } from '../schema/reserved-chrome';
|
|
12
13
|
// ── Public API ──────────────────────────────────────────────────────────────
|
|
13
14
|
/**
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { deleteSnapshot } from './primitives';
|
|
7
7
|
import { metadataOf } from '../core/nodes';
|
|
8
|
-
import {
|
|
8
|
+
import { emptyParagraph } from '../tree-operations/node-primitives';
|
|
9
|
+
import { deleteNode } from '../tree-operations/settle';
|
|
9
10
|
import { trailingLineEnding } from '../core/lines';
|
|
10
11
|
import { expectStateForNode, getStateForNode } from '../reactivity/state-registry';
|
|
11
12
|
import { deleteRow as mutDeleteRow, deleteColumn as mutDeleteColumn, canDeleteRow, canDeleteColumn } from '../tree-operations/table-mutations';
|
|
@@ -11,8 +11,9 @@ import { cellRowCol } from '../cursor/coordinate-spaces';
|
|
|
11
11
|
import { cellIndexOf } from './primitives';
|
|
12
12
|
import { resolveEndWall, planCrossBlockDeletion, applyPlannedDeletion, rebuildSharedAncestries, truncateEndInPlace, truncateStartInPlace } from './range-delete-ceremony';
|
|
13
13
|
import { comparePaths } from './path-math';
|
|
14
|
-
import { blockNodeAt, emptyParagraph } from '../tree-operations/node-
|
|
15
|
-
import { ensureUnsharedNode, ensureUnsharedPath, ensureUnsharedSubtree, rebuildOwnedContainer
|
|
14
|
+
import { blockNodeAt, emptyParagraph } from '../tree-operations/node-primitives';
|
|
15
|
+
import { ensureUnsharedNode, ensureUnsharedPath, ensureUnsharedSubtree, rebuildOwnedContainer } from '../tree-operations/unshare';
|
|
16
|
+
import { rebuildUnsharedAncestry } from '../tree-operations/chain-rebuild';
|
|
16
17
|
import { rebuildTableRowRaw } from '../schema/container-rebuilders';
|
|
17
18
|
import { isCollapsedContainer } from '../schema/reserved-chrome';
|
|
18
19
|
import { nearestChromeContainer, isChromeChild } from './range-delete-chrome';
|
|
@@ -31,3 +31,7 @@ export interface RangeDeleteResult {
|
|
|
31
31
|
* post-unshare references. Caller pre-normalizes and keeps endpoints on focusable blocks.
|
|
32
32
|
*/
|
|
33
33
|
export declare function rangeDelete(doc: Document, start: SelectionPoint, end: SelectionPoint, sharing: SharingState, grammar: GrammarView | undefined, presentationMode: PresentationMode | undefined, linkRef: InlineResolverRef | undefined): RangeDeleteResult;
|
|
34
|
+
/** The container prefix the survivor renders under, so the join seam can read its candidate back
|
|
35
|
+
* through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
|
|
36
|
+
* way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
|
|
37
|
+
export declare function containerAmbientPrefix(doc: Document, path: readonly number[]): string;
|
|
@@ -7,11 +7,30 @@ import { metadataOf } from '../core/nodes';
|
|
|
7
7
|
import { walkBetween, charOffsetOf } from './primitives';
|
|
8
8
|
import { comparePaths, lowestCommonAncestor, isPathSubtreeBetween } from './path-math';
|
|
9
9
|
import { firstLeafAtOrAfter } from './path-lookup';
|
|
10
|
-
import { blockNodeAt,
|
|
10
|
+
import { blockNodeAt, nodeAt, normalizeBodyWrite, normalizeOwnRaw, writeOwnRaw } from '../tree-operations/node-primitives';
|
|
11
|
+
import { settleSeparatorOnBlank } from '../tree-operations/settle';
|
|
12
|
+
import { isBlankParagraph } from '../core/parser';
|
|
13
|
+
import { displayLength } from '../core/lines';
|
|
14
|
+
import { deleteAtPath } from '../tree-operations/path-mutate';
|
|
15
|
+
import { cleanJoinedRaw } from '../tree-operations/node-ops';
|
|
11
16
|
import { deleteSubtreesIdentityGated, installTruncatedEndpoint, reparseTruncatedEndpoint } from './range-delete-ceremony';
|
|
12
|
-
import { ensureUnsharedNode, ensureUnsharedPath
|
|
17
|
+
import { ensureUnsharedNode, ensureUnsharedPath } from '../tree-operations/unshare';
|
|
18
|
+
import { rebuildUnsharedAncestry, rebuildUnsharedChain } from '../tree-operations/chain-rebuild';
|
|
13
19
|
import { involvesTable, tableAwareRangeDelete } from './range-delete-table';
|
|
14
20
|
import { involvesReservedChrome, chromeAwareRangeDelete } from './range-delete-chrome';
|
|
21
|
+
/** Whoever takes the slot gets the caret, at its start; the block above when nothing does. */
|
|
22
|
+
function deleteWholeUnit(doc, path, sharing, grammar) {
|
|
23
|
+
const parentPath = path.slice(0, -1);
|
|
24
|
+
const index = path[path.length - 1];
|
|
25
|
+
// The path-addressed door, so the commit's id ledger sees the slot go.
|
|
26
|
+
const chain = ensureUnsharedPath(doc, parentPath, sharing);
|
|
27
|
+
deleteAtPath(doc, path, sharing);
|
|
28
|
+
if (chain.length > 0)
|
|
29
|
+
rebuildUnsharedChain(doc, chain, sharing, null, grammar);
|
|
30
|
+
const survivors = (chain.length > 0 ? chain[chain.length - 1] : doc).children ?? [];
|
|
31
|
+
const landing = Math.max(0, Math.min(index, survivors.length - 1));
|
|
32
|
+
return { newDoc: doc, collapsedCaret: { path: [...parentPath, landing], offset: 0 } };
|
|
33
|
+
}
|
|
15
34
|
/**
|
|
16
35
|
* Delete [start, end] in place, merge at start's position with its container context preserved,
|
|
17
36
|
* cascade-clean empty ancestors, rebuild container raws. Copy-path-on-write: every spliced or
|
|
@@ -38,6 +57,15 @@ export function rangeDelete(doc, start, end, sharing, grammar, presentationMode,
|
|
|
38
57
|
const endRaw = endBlock.raw;
|
|
39
58
|
const startOffset = charOffsetOf(start, 'rangeDelete:prose-merge-start');
|
|
40
59
|
const endOffset = charOffsetOf(end, 'rangeDelete:prose-merge-end');
|
|
60
|
+
// The range holds one block whole, so none of its own bytes survive: the byte arm below would
|
|
61
|
+
// keep a husk holding a bare line ending, which no reload reads as that kind. A paragraph is
|
|
62
|
+
// the one kind that survives empty, because a blank one IS the separating line below it.
|
|
63
|
+
if (sameBlock &&
|
|
64
|
+
startOffset === 0 &&
|
|
65
|
+
endOffset >= displayLength(startRaw) &&
|
|
66
|
+
!isBlankParagraph({ kind: startBlock.kind, raw: '' })) {
|
|
67
|
+
return deleteWholeUnit(doc, start.path, sharing, grammar);
|
|
68
|
+
}
|
|
41
69
|
// The end slice answers to the END block's rule before the join: start's rule below speaks
|
|
42
70
|
// only for start's bytes, so a truncation from the end block's head strands its closer. A
|
|
43
71
|
// same-block merge is one block's bytes and takes that rule once, whole, on the arm below.
|
|
@@ -110,7 +138,7 @@ export function rangeDelete(doc, start, end, sharing, grammar, presentationMode,
|
|
|
110
138
|
/** The container prefix the survivor renders under, so the join seam can read its candidate back
|
|
111
139
|
* through it (live-mode.md § 4.5). An ambient marker rides the container's FIRST child only, the
|
|
112
140
|
* way `BlockList` forwards it, and a list item is the one built-in container that paints one. */
|
|
113
|
-
function containerAmbientPrefix(doc, path) {
|
|
141
|
+
export function containerAmbientPrefix(doc, path) {
|
|
114
142
|
if (path.length < 2 || path[path.length - 1] !== 0)
|
|
115
143
|
return '';
|
|
116
144
|
const parent = blockNodeAt(doc, path.slice(0, -1));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dragging a selection and dropping it. The browser's own version is two native edits committed
|
|
3
|
+
* apart — `deleteByDrag` on the source, `insertFromDrop` on the target — so undo takes two presses
|
|
4
|
+
* over a document that lost bytes in between, and inside one block the source commit rebuilds the
|
|
5
|
+
* surface under the drop. Owned here instead: one snapshot over two raw writes, which splice
|
|
6
|
+
* nothing, so no path moves under the second.
|
|
7
|
+
*/
|
|
8
|
+
import type { Document } from '../core/nodes';
|
|
9
|
+
import type { GrammarView } from '../schema/block-openers';
|
|
10
|
+
import type { LinkReferenceResolverRef, PresentationModeGetter } from '../editor-keys';
|
|
11
|
+
import type { PluginActivation } from '../schema/plugin-activation';
|
|
12
|
+
import type { CommitController } from '../action-contracts';
|
|
13
|
+
import type { PasteCommitCoordinator } from '../tree-operations/paste/paste-deps';
|
|
14
|
+
import { type EditorEvents } from '../editor-events';
|
|
15
|
+
export interface SelectionDropDeps {
|
|
16
|
+
editorRoot: HTMLElement;
|
|
17
|
+
getDoc(): Document;
|
|
18
|
+
controller: CommitController;
|
|
19
|
+
coordinator: PasteCommitCoordinator;
|
|
20
|
+
getPresentationMode: PresentationModeGetter | undefined;
|
|
21
|
+
linkRef: LinkReferenceResolverRef | undefined;
|
|
22
|
+
grammar: GrammarView | undefined;
|
|
23
|
+
activePlugins: PluginActivation | undefined;
|
|
24
|
+
/** The instance event surface: a ceremony that throws mid-move has nowhere else to report. */
|
|
25
|
+
events: EditorEvents;
|
|
26
|
+
isReadOnly(): boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function installSelectionDrop(deps: SelectionDropDeps): () => void;
|
|
29
|
+
/** Where a splice of `delta` blocks at `at` inside `parent` leaves `target`: what the drop's
|
|
30
|
+
* second write addresses once the first one's reparse changed its slot's block count. */
|
|
31
|
+
export declare function shiftPathAfterSplice(target: number[], parent: number[], at: number, delta: number): number[];
|
|
32
|
+
/** Where a drop at `offset` lands once `[start, end)` left the same block, `shrunkBy` bytes
|
|
33
|
+
* shorter than the range was wide once the join seam cleaned up. Null inside the range. */
|
|
34
|
+
export declare function dropOffsetAfterCut(offset: number, start: number, end: number, shrunkBy: number): number | null;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dragging a selection and dropping it. The browser's own version is two native edits committed
|
|
3
|
+
* apart — `deleteByDrag` on the source, `insertFromDrop` on the target — so undo takes two presses
|
|
4
|
+
* over a document that lost bytes in between, and inside one block the source commit rebuilds the
|
|
5
|
+
* surface under the drop. Owned here instead: one snapshot over two raw writes, which splice
|
|
6
|
+
* nothing, so no path moves under the second.
|
|
7
|
+
*/
|
|
8
|
+
import { emitClipboardError } from '../editor-events';
|
|
9
|
+
import { ambientLengthOf } from '../ambient/ambient-dom';
|
|
10
|
+
import { toClampedRawOffset } from '../cursor/coordinate-spaces';
|
|
11
|
+
import { domTextOffsetAtNode } from '../cursor/widget-offset';
|
|
12
|
+
import { trailingLineEnding, trimTrailingLineEnding } from '../core/lines';
|
|
13
|
+
import { replaceRangeRaw } from '../components/blocks/text/live-selection-edit';
|
|
14
|
+
import { blockNodeAt, emptyParagraph, writeOwnRaw } from '../tree-operations/node-primitives';
|
|
15
|
+
import { cloneNode } from '../tree-operations/clone';
|
|
16
|
+
import { cutRangeFromDisplay } from '../tree-operations/node-ops';
|
|
17
|
+
import { rebuildAncestryRaw } from '../schema/container-raw';
|
|
18
|
+
import { applyPasteTransforms } from '../tree-operations/paste/paste-transforms';
|
|
19
|
+
import { replaceBlockAtParent } from '../tree-operations/paste/replace-block-at-parent';
|
|
20
|
+
import { parseReplacement } from '../tree-operations/paste/replacement-parse';
|
|
21
|
+
import { blockNearPoint } from './nearest-block';
|
|
22
|
+
import { findSurfaceForElement } from './path-lookup';
|
|
23
|
+
import { containerAmbientPrefix } from './range-delete';
|
|
24
|
+
/** A drag that IS this gesture but whose shape the seam does not move — a range leaving its
|
|
25
|
+
* surface, an empty one. The drop cancels it rather than handing it back. */
|
|
26
|
+
const DECLINED = 'declined';
|
|
27
|
+
// ── Public entry ───────────────────────────────────────────────────────────
|
|
28
|
+
export function installSelectionDrop(deps) {
|
|
29
|
+
let source = null;
|
|
30
|
+
const onDragStart = (e) => {
|
|
31
|
+
source = readSelectionSource(deps.editorRoot, e.target);
|
|
32
|
+
};
|
|
33
|
+
const onDragEnd = () => {
|
|
34
|
+
source = null;
|
|
35
|
+
};
|
|
36
|
+
// The editor is the drop target for every drag this seam recognized, declined ones included:
|
|
37
|
+
// the drop below must RUN to cancel the browser's pair of native edits.
|
|
38
|
+
const onDragOver = (e) => {
|
|
39
|
+
if (source)
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
};
|
|
42
|
+
const onDrop = (e) => {
|
|
43
|
+
const from = source;
|
|
44
|
+
source = null;
|
|
45
|
+
if (!from)
|
|
46
|
+
return;
|
|
47
|
+
// Before every decline below, not after: the browser's two edits land apart and its undo
|
|
48
|
+
// is not ours, so a shape this seam declines mutates nothing rather than half of it.
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
if (from === DECLINED || deps.isReadOnly())
|
|
51
|
+
return;
|
|
52
|
+
const target = dropTarget(deps, e.clientX, e.clientY);
|
|
53
|
+
if (!target)
|
|
54
|
+
return;
|
|
55
|
+
const text = movedText(deps, from);
|
|
56
|
+
if (text === null)
|
|
57
|
+
return;
|
|
58
|
+
void runDrop(deps, from, target, text, e.ctrlKey || e.altKey).catch((error) => {
|
|
59
|
+
// A throw between the two writes leaves a snapshot pushed and half the move applied;
|
|
60
|
+
// the host hears it on the channel the paste route reports through.
|
|
61
|
+
emitClipboardError(deps.events, { error, path: from.path });
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
deps.editorRoot.addEventListener('dragstart', onDragStart);
|
|
65
|
+
deps.editorRoot.addEventListener('dragend', onDragEnd);
|
|
66
|
+
deps.editorRoot.addEventListener('dragover', onDragOver);
|
|
67
|
+
deps.editorRoot.addEventListener('drop', onDrop);
|
|
68
|
+
return () => {
|
|
69
|
+
deps.editorRoot.removeEventListener('dragstart', onDragStart);
|
|
70
|
+
deps.editorRoot.removeEventListener('dragend', onDragEnd);
|
|
71
|
+
deps.editorRoot.removeEventListener('dragover', onDragOver);
|
|
72
|
+
deps.editorRoot.removeEventListener('drop', onDrop);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Where a splice of `delta` blocks at `at` inside `parent` leaves `target`: what the drop's
|
|
76
|
+
* second write addresses once the first one's reparse changed its slot's block count. */
|
|
77
|
+
export function shiftPathAfterSplice(target, parent, at, delta) {
|
|
78
|
+
if (delta === 0 || target.length <= parent.length)
|
|
79
|
+
return target;
|
|
80
|
+
for (let i = 0; i < parent.length; i++)
|
|
81
|
+
if (target[i] !== parent[i])
|
|
82
|
+
return target;
|
|
83
|
+
if (target[parent.length] <= at)
|
|
84
|
+
return target;
|
|
85
|
+
const shifted = target.slice();
|
|
86
|
+
shifted[parent.length] += delta;
|
|
87
|
+
return shifted;
|
|
88
|
+
}
|
|
89
|
+
/** Where a drop at `offset` lands once `[start, end)` left the same block, `shrunkBy` bytes
|
|
90
|
+
* shorter than the range was wide once the join seam cleaned up. Null inside the range. */
|
|
91
|
+
export function dropOffsetAfterCut(offset, start, end, shrunkBy) {
|
|
92
|
+
if (offset > start && offset < end)
|
|
93
|
+
return null;
|
|
94
|
+
return offset <= start ? offset : Math.max(0, offset - shrunkBy);
|
|
95
|
+
}
|
|
96
|
+
// ── Reading the gesture ────────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* The native range the drag carries, in its surface's raw offsets. `null` is "not this gesture" and
|
|
99
|
+
* leaves the drag to the browser; {@link DECLINED} is this gesture in a shape the seam does not
|
|
100
|
+
* move, which the drop cancels. A cross-block selection reaches neither: it paints through the
|
|
101
|
+
* overlay and leaves no native range for the browser to drag.
|
|
102
|
+
*/
|
|
103
|
+
function readSelectionSource(editorRoot, dragged) {
|
|
104
|
+
const sel = window.getSelection();
|
|
105
|
+
if (!sel || sel.isCollapsed || sel.rangeCount === 0)
|
|
106
|
+
return null;
|
|
107
|
+
const range = sel.getRangeAt(0);
|
|
108
|
+
// A draggable of its own (a rendered link, an image) is not this gesture, even with a range
|
|
109
|
+
// painted elsewhere: a selection drag grips a node the range covers.
|
|
110
|
+
if (!(dragged instanceof Node) || !range.intersectsNode(dragged))
|
|
111
|
+
return null;
|
|
112
|
+
const surface = surfaceOf(range.startContainer);
|
|
113
|
+
if (!surface || !editorRoot.contains(surface))
|
|
114
|
+
return null;
|
|
115
|
+
// Past this point the drag IS the editor's selection, so every remaining shape declines.
|
|
116
|
+
if (!surface.contains(range.endContainer))
|
|
117
|
+
return DECLINED;
|
|
118
|
+
const found = findSurfaceForElement(surface);
|
|
119
|
+
if (!found)
|
|
120
|
+
return DECLINED;
|
|
121
|
+
const ambient = ambientLengthOf(surface);
|
|
122
|
+
const start = toClampedRawOffset(domTextOffsetAtNode(surface, range.startContainer, range.startOffset), ambient);
|
|
123
|
+
const end = toClampedRawOffset(domTextOffsetAtNode(surface, range.endContainer, range.endOffset), ambient);
|
|
124
|
+
return start < end ? { ...found, start, end } : DECLINED;
|
|
125
|
+
}
|
|
126
|
+
/** The block and offset a drop point addresses, as a single-click caret would land. Null where
|
|
127
|
+
* the point names no character position (a whole-block unit, a table cell). */
|
|
128
|
+
function dropTarget(deps, clientX, clientY) {
|
|
129
|
+
const near = blockNearPoint(deps.editorRoot, clientX, clientY);
|
|
130
|
+
const point = near?.endpointHere();
|
|
131
|
+
if (!point || !('offset' in point) || point.cellCoordinate)
|
|
132
|
+
return null;
|
|
133
|
+
return blockNodeAt(deps.getDoc(), point.path) ? { path: point.path, offset: point.offset } : null;
|
|
134
|
+
}
|
|
135
|
+
/** The source surface's own bytes for the range, past the paste transforms. Null for a payload
|
|
136
|
+
* this seam does not own: a line break needs the structural paste route. */
|
|
137
|
+
function movedText(deps, from) {
|
|
138
|
+
const node = blockNodeAt(deps.getDoc(), from.path);
|
|
139
|
+
if (!node)
|
|
140
|
+
return null;
|
|
141
|
+
const raw = trimTrailingLineEnding(node.raw).slice(from.start, from.end);
|
|
142
|
+
const text = applyPasteTransforms(raw, deps.activePlugins);
|
|
143
|
+
return text && !/[\r\n]/.test(text) ? text : null;
|
|
144
|
+
}
|
|
145
|
+
// ── The commit ceremony ────────────────────────────────────────────────────
|
|
146
|
+
async function runDrop(deps, from, to, text, copy) {
|
|
147
|
+
if (copy) {
|
|
148
|
+
await writeBlockRaw(deps, to.path, insert(to.offset, text), to.offset + text.length, true);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const cut = cutFrom(deps, from);
|
|
152
|
+
if (!cut)
|
|
153
|
+
return;
|
|
154
|
+
if (pathsEqual(cut.path, to.path)) {
|
|
155
|
+
const offset = dropOffsetAfterCut(to.offset, from.start, from.end, cut.shrunkBy);
|
|
156
|
+
if (offset === null)
|
|
157
|
+
return;
|
|
158
|
+
const merged = spliceAt(cut.raw, offset, text);
|
|
159
|
+
await writeBlockRaw(deps, cut.path, () => merged, offset + text.length, true);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
deps.controller.pushUndoSnapshotPath(from.path, from.start);
|
|
163
|
+
// A table's caret door reads a cell landing, never a character offset (G1.29), so the
|
|
164
|
+
// transient caret the second write moves off is its first cell.
|
|
165
|
+
const sourceCaret = from.inCell ? 0 : from.start;
|
|
166
|
+
const spliced = await writeBlockRaw(deps, cut.path, () => cut.raw, sourceCaret, false);
|
|
167
|
+
const at = cut.path[cut.path.length - 1];
|
|
168
|
+
const target = shiftPathAfterSplice(to.path, cut.path.slice(0, -1), at, spliced);
|
|
169
|
+
await writeBlockRaw(deps, target, insert(to.offset, text), to.offset + text.length, false);
|
|
170
|
+
}
|
|
171
|
+
function insert(offset, text) {
|
|
172
|
+
return (raw) => spliceAt(raw, offset, text);
|
|
173
|
+
}
|
|
174
|
+
/** The source surface's display bytes with the dragged range gone, through the delete seam so a
|
|
175
|
+
* live-mode join cleans up after itself. */
|
|
176
|
+
function cutFrom(deps, from) {
|
|
177
|
+
const node = blockNodeAt(deps.getDoc(), from.path);
|
|
178
|
+
if (!node)
|
|
179
|
+
return null;
|
|
180
|
+
if (from.inCell)
|
|
181
|
+
return cutFromCell(deps, from, node);
|
|
182
|
+
const before = trimTrailingLineEnding(node.raw);
|
|
183
|
+
const edit = replaceRangeRaw(node, { start: from.start, end: from.end }, '', deps.getPresentationMode?.(), deps.linkRef, containerAmbientPrefix(deps.getDoc(), from.path));
|
|
184
|
+
const raw = trimTrailingLineEnding(edit.raw);
|
|
185
|
+
return { path: from.path, raw, shrunkBy: before.length - raw.length };
|
|
186
|
+
}
|
|
187
|
+
/** A cell's bytes are joined into its row, so the TABLE is the block the cut rewrites: the cell's
|
|
188
|
+
* own range-delete on a copy, then the kind's escape and the ancestry rebuild around it. */
|
|
189
|
+
function cutFromCell(deps, from, cell) {
|
|
190
|
+
const tablePath = from.path.slice(0, -2);
|
|
191
|
+
// The cell path is resolved from a DOM selector contract, so the kind is read, not assumed.
|
|
192
|
+
const table = blockNodeAt(deps.getDoc(), tablePath);
|
|
193
|
+
if (!table || table.kind !== 'table')
|
|
194
|
+
return null;
|
|
195
|
+
const cut = cutRangeFromDisplay(cell, cell.raw, { start: from.start, end: from.end }, deps.getPresentationMode?.(), deps.linkRef);
|
|
196
|
+
const rebuilt = cloneNode(table);
|
|
197
|
+
const inner = from.path.slice(-2);
|
|
198
|
+
const [rowIdx, colIdx] = inner;
|
|
199
|
+
const written = rebuilt.children?.[rowIdx]?.children?.[colIdx];
|
|
200
|
+
if (!written)
|
|
201
|
+
return null;
|
|
202
|
+
writeOwnRaw(written, cut.display, deps.grammar);
|
|
203
|
+
rebuildAncestryRaw(rebuilt, inner);
|
|
204
|
+
const raw = trimTrailingLineEnding(rebuilt.raw);
|
|
205
|
+
return { path: tablePath, raw, shrunkBy: trimTrailingLineEnding(table.raw).length - raw.length };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Replace the block at `path` with the reparse of the bytes `rewrite` returns, at its parent
|
|
209
|
+
* scope. Answers how many blocks the slot grew or shrank by, which is what keeps a second
|
|
210
|
+
* write's path honest. `own` pushes this write's own undo entry.
|
|
211
|
+
*/
|
|
212
|
+
async function writeBlockRaw(deps, path, rewrite, caret, own) {
|
|
213
|
+
const doc = deps.getDoc();
|
|
214
|
+
const node = blockNodeAt(doc, path);
|
|
215
|
+
if (!node)
|
|
216
|
+
return 0;
|
|
217
|
+
const written = rewrite(trimTrailingLineEnding(node.raw));
|
|
218
|
+
// A block emptied by the cut keeps its slot as a blank paragraph: no splice, so the second
|
|
219
|
+
// write's path is still the one this gesture resolved.
|
|
220
|
+
const parsed = parseReplacement(node, written, deps.grammar, () => [
|
|
221
|
+
emptyParagraph(node.leadingTrivia ?? '', trailingLineEnding(node.raw))
|
|
222
|
+
]);
|
|
223
|
+
if (!parsed)
|
|
224
|
+
return 0;
|
|
225
|
+
const landed = await replaceBlockAtParent({
|
|
226
|
+
doc,
|
|
227
|
+
blockPath: path,
|
|
228
|
+
replacement: parsed.replacement,
|
|
229
|
+
controller: deps.coordinator,
|
|
230
|
+
undoEntry: own ? 'own' : 'join',
|
|
231
|
+
focusReplacementIndex: parsed.replacement.length - 1,
|
|
232
|
+
focusOffset: caret,
|
|
233
|
+
source: 'selection-drop',
|
|
234
|
+
...(deps.grammar ? { grammar: deps.grammar } : {})
|
|
235
|
+
});
|
|
236
|
+
// The LANDED count, not the parse's: a body rule inside the scope can rewrite the list. Zero
|
|
237
|
+
// is the unmounted-scope decline, which wrote nothing and so moved nothing.
|
|
238
|
+
return Math.max(0, landed - 1);
|
|
239
|
+
}
|
|
240
|
+
// ── Small helpers ──────────────────────────────────────────────────────────
|
|
241
|
+
function spliceAt(raw, offset, text) {
|
|
242
|
+
const at = Math.max(0, Math.min(offset, raw.length));
|
|
243
|
+
return raw.slice(0, at) + text + raw.slice(at);
|
|
244
|
+
}
|
|
245
|
+
function pathsEqual(a, b) {
|
|
246
|
+
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
247
|
+
}
|
|
248
|
+
function surfaceOf(node) {
|
|
249
|
+
let el = node instanceof HTMLElement ? node : node.parentElement;
|
|
250
|
+
while (el && el.contentEditable !== 'true')
|
|
251
|
+
el = el.parentElement;
|
|
252
|
+
return el;
|
|
253
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* can't be carried by only one. A gap caret takes {@link restoreGapCaret}, the same road minus
|
|
6
6
|
* the endpoint pair.
|
|
7
7
|
*/
|
|
8
|
-
import { isBlockNode, nodeAt } from '../tree-operations/node-
|
|
8
|
+
import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
9
9
|
import { applySelectionToDom } from './native-bridge';
|
|
10
10
|
import { placeGapCaret } from './caret-doors';
|
|
11
11
|
import { gapScopeChildren } from './gap-caret';
|
|
@@ -34,6 +34,12 @@ export interface SelectionState {
|
|
|
34
34
|
readonly selectAllCount: number;
|
|
35
35
|
/** The third mode: a collapsed caret in a between-blocks boundary (`gap-caret.ts`). */
|
|
36
36
|
readonly gapCaret: GapCaretPosition | null;
|
|
37
|
+
/**
|
|
38
|
+
* One block taken whole as the entire range — a press on a surface-less leaf (an equation)
|
|
39
|
+
* dragged inside it. The stored pair spans the block's bytes on a shared path, which the
|
|
40
|
+
* same-path guard would otherwise refuse, so the overlay reads this to paint it as a unit.
|
|
41
|
+
*/
|
|
42
|
+
readonly wholeUnitPath: number[] | null;
|
|
37
43
|
enterCrossBlock(anchor: SelectionEndpoint, focus: SelectionEndpoint): void;
|
|
38
44
|
extendFocus(point: SelectionEndpoint): void;
|
|
39
45
|
collapse(): void;
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* native browser selection rules, and the two are mutually exclusive here rather than at any
|
|
5
5
|
* call site. Transitions: `docs/design/editor.md` § Cross-block selection.
|
|
6
6
|
*/
|
|
7
|
-
import { nodeAt } from '../tree-operations/node-
|
|
7
|
+
import { nodeAt } from '../tree-operations/node-primitives';
|
|
8
8
|
import { isWholeBlockEndpoint, normalize } from './primitives';
|
|
9
9
|
import { cellEndpointDeepPath, normalizeTableEndpoint, snapCrossBlockTableEndpoints } from './table-endpoint-snap';
|
|
10
10
|
import { normalizeCharEndpoint } from './char-endpoint-snap';
|
|
11
11
|
import { pathsEqual } from './path-math';
|
|
12
|
+
import { displayLength } from '../core/lines';
|
|
12
13
|
import { assertInvariant } from '../assert';
|
|
13
14
|
import { checkCrossBlockEndpointCoordinates } from '../invariants/selection-endpoints';
|
|
14
15
|
export function createSelectionState(options) {
|
|
@@ -19,6 +20,7 @@ class SelectionStateImpl {
|
|
|
19
20
|
#anchor = $state(null);
|
|
20
21
|
#focus = $state(null);
|
|
21
22
|
#gapCaret = $state(null);
|
|
23
|
+
#wholeUnit = $state(null);
|
|
22
24
|
#selectAllCount = $state(0);
|
|
23
25
|
#onChange;
|
|
24
26
|
#getDoc;
|
|
@@ -63,6 +65,9 @@ class SelectionStateImpl {
|
|
|
63
65
|
get isCrossBlock() {
|
|
64
66
|
return this.#anchor !== null && this.#focus !== null;
|
|
65
67
|
}
|
|
68
|
+
get wholeUnitPath() {
|
|
69
|
+
return this.#wholeUnit?.slice() ?? null;
|
|
70
|
+
}
|
|
66
71
|
get isCustomRendered() {
|
|
67
72
|
const getDoc = this.#getDoc;
|
|
68
73
|
if (!getDoc)
|
|
@@ -71,6 +76,8 @@ class SelectionStateImpl {
|
|
|
71
76
|
const focus = this.#focus;
|
|
72
77
|
if (!anchor || !focus)
|
|
73
78
|
return false;
|
|
79
|
+
if (this.#wholeUnit)
|
|
80
|
+
return true;
|
|
74
81
|
if (!pathsEqual(anchor.path, focus.path))
|
|
75
82
|
return true;
|
|
76
83
|
if (anchor.offset === focus.offset)
|
|
@@ -102,6 +109,19 @@ class SelectionStateImpl {
|
|
|
102
109
|
}
|
|
103
110
|
enterCrossBlock(anchor, focus) {
|
|
104
111
|
this.#gapCaret = null;
|
|
112
|
+
// Both ends the same surface-less block: the unit itself is the range, stored as its full
|
|
113
|
+
// byte span and flagged, since below a same-path pair is refused as prose.
|
|
114
|
+
if (isWholeBlockEndpoint(anchor) &&
|
|
115
|
+
isWholeBlockEndpoint(focus) &&
|
|
116
|
+
pathsEqual(anchor.path, focus.path)) {
|
|
117
|
+
const path = anchor.path.slice();
|
|
118
|
+
this.#anchor = { path, offset: 0 };
|
|
119
|
+
this.#focus = { path: path.slice(), offset: this.#byteLengthAt(path) };
|
|
120
|
+
this.#wholeUnit = path.slice();
|
|
121
|
+
this.#notify();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.#wholeUnit = null;
|
|
105
125
|
const a = this.#normalizePoint(anchor, focus.path);
|
|
106
126
|
const f = this.#normalizePoint(focus, anchor.path);
|
|
107
127
|
// A same-path prose pair is a single-block range the browser owns; storing it mints an
|
|
@@ -124,6 +144,13 @@ class SelectionStateImpl {
|
|
|
124
144
|
throw new Error('SelectionState.extendFocus called without an anchor');
|
|
125
145
|
}
|
|
126
146
|
this.#gapCaret = null;
|
|
147
|
+
// Leaving a whole unit: its anchor is the block as a whole again, so the side it means
|
|
148
|
+
// (start below the new focus, end above it) is resolved afresh rather than kept at 0.
|
|
149
|
+
if (this.#wholeUnit) {
|
|
150
|
+
const unit = { path: this.#wholeUnit, wholeBlock: true };
|
|
151
|
+
this.#anchor = this.#normalizePoint(unit, point.path);
|
|
152
|
+
this.#wholeUnit = null;
|
|
153
|
+
}
|
|
127
154
|
const f = this.#normalizePoint(point, this.#anchor.path);
|
|
128
155
|
// A focus back on the anchor's prose leaf contracts to a single-block range. No
|
|
129
156
|
// `offset !== offset` guard, unlike #isSamePathProseRange: extendFocus never seeds, so
|
|
@@ -142,6 +169,11 @@ class SelectionStateImpl {
|
|
|
142
169
|
}
|
|
143
170
|
// G1.29 at the storing seam, the belt behind #normalizePoint: both entries carry it because
|
|
144
171
|
// both store an endpoint pair.
|
|
172
|
+
#byteLengthAt(path) {
|
|
173
|
+
const doc = this.#getDoc?.();
|
|
174
|
+
const node = doc ? nodeAt(doc, path) : null;
|
|
175
|
+
return node && 'raw' in node ? displayLength(node.raw) : 0;
|
|
176
|
+
}
|
|
145
177
|
#assertEndpointCoordinates(anchor, focus) {
|
|
146
178
|
const getDoc = this.#getDoc;
|
|
147
179
|
if (!getDoc)
|
|
@@ -177,6 +209,7 @@ class SelectionStateImpl {
|
|
|
177
209
|
this.#anchor = null;
|
|
178
210
|
this.#focus = null;
|
|
179
211
|
this.#gapCaret = null;
|
|
212
|
+
this.#wholeUnit = null;
|
|
180
213
|
this.#notify();
|
|
181
214
|
}
|
|
182
215
|
clear() {
|
|
@@ -185,6 +218,7 @@ class SelectionStateImpl {
|
|
|
185
218
|
this.#anchor = null;
|
|
186
219
|
this.#focus = null;
|
|
187
220
|
this.#gapCaret = null;
|
|
221
|
+
this.#wholeUnit = null;
|
|
188
222
|
this.#selectAllCount = 0;
|
|
189
223
|
this.#notify();
|
|
190
224
|
}
|
|
@@ -198,6 +232,7 @@ class SelectionStateImpl {
|
|
|
198
232
|
setGapCaret(pos) {
|
|
199
233
|
this.#anchor = null;
|
|
200
234
|
this.#focus = null;
|
|
235
|
+
this.#wholeUnit = null;
|
|
201
236
|
this.#gapCaret = { parentPath: pos.parentPath.slice(), index: pos.index };
|
|
202
237
|
this.#notify();
|
|
203
238
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* An intra-table pair is deliberately left unsnapped, so rectangular sub-cell selection survives.
|
|
6
6
|
*/
|
|
7
7
|
import { metadataOf } from '../core/nodes';
|
|
8
|
-
import { isBlockNode, nodeAt } from '../tree-operations/node-
|
|
8
|
+
import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
9
9
|
import { cellIndexOf } from './primitives';
|
|
10
10
|
import { asCellIndex, cellRowCol } from '../cursor/coordinate-spaces';
|
|
11
11
|
import { comparePaths, pathHasPrefix } from './path-math';
|