@voithos-labs/aragonite 0.10.2 → 0.10.3
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 +18 -0
- package/dist/a11y-strings.js +18 -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 +35 -24
- package/dist/components/BlockHost.svelte +16 -9
- package/dist/components/Editor.svelte +381 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +17 -3
- 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 +686 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +4 -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 +11 -0
- 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 +243 -29
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
- 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/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 +56 -2
- package/dist/components/blocks/text/live-selection-edit.js +27 -0
- 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 +144 -22
- package/dist/components/drag-handle.d.ts +35 -0
- package/dist/components/drag-handle.js +126 -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 +10 -6
- package/dist/components/editor-root-listeners.js +23 -22
- 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 +59 -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/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 +11 -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-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/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/widget-offset.d.ts +6 -0
- package/dist/cursor/widget-offset.js +61 -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/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/inline-range-commit.js +1 -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 +21 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.js +11 -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 +36 -3
- 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 +97 -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/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +49 -24
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- 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 +25 -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 +1 -1
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +32 -30
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +62 -13
- package/dist/selection/dead-space-caret.d.ts +10 -0
- package/dist/selection/dead-space-caret.js +47 -1
- package/dist/selection/double-click-trim.d.ts +17 -0
- package/dist/selection/double-click-trim.js +57 -0
- package/dist/selection/drag-pointer.d.ts +7 -2
- package/dist/selection/drag-pointer.js +61 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.js +1 -1
- package/dist/selection/path-lookup.js +1 -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.js +30 -2
- 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 +57 -28
- package/dist/styles/editor.css +217 -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.js +1 -1
- 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 +126 -96
- package/docs/guide/plugin-api.md +31 -3
- package/docs/guide/plugin-guide.md +26 -3
- package/package.json +4 -2
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { makeBlockNode, metadataOf } from '../core/nodes';
|
|
5
5
|
import { cloneMetadata } from '../tree-operations/clone';
|
|
6
|
-
import { isBlockNode, nodeAt } from '../tree-operations/node-
|
|
6
|
+
import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
7
7
|
import { walkBetween, normalize, charOffsetOf, cellIndexOf } from './primitives';
|
|
8
8
|
import { snapCrossBlockTableEndpoints } from './table-endpoint-snap';
|
|
9
9
|
import { isStrictAncestorOf, pathHasPrefix, pathsEqual, sharedPrefixLength } from './path-math';
|
|
@@ -39,6 +39,11 @@ export function collectCrossBlockText(doc, anchor, focus) {
|
|
|
39
39
|
}
|
|
40
40
|
const startRaw = isBlockNode(startNode) ? startNode.raw : '';
|
|
41
41
|
const endRaw = isBlockNode(endNode) ? endNode.raw : '';
|
|
42
|
+
// One leaf taken whole (`SelectionState.wholeUnitPath`): the only same-path prose pair the
|
|
43
|
+
// state stores, and the head/tail split below would emit its bytes twice.
|
|
44
|
+
if (pathsEqual(start.path, end.path) && !start.cellCoordinate) {
|
|
45
|
+
return startRaw.slice(start.offset, end.offset);
|
|
46
|
+
}
|
|
42
47
|
let effectiveStartPath = start.path;
|
|
43
48
|
let chromeStart = null;
|
|
44
49
|
let startTail = '';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block a range holds whole, for the gestures that insert over it. Such a range has no seam
|
|
3
|
+
* inside it to merge at: the block leaves the tree entirely, so bytes that follow must land in
|
|
4
|
+
* its SLOT rather than at a caret in whatever took the slot.
|
|
5
|
+
*/
|
|
6
|
+
import type { DocumentView } from '../core/node-views';
|
|
7
|
+
import type { SelectionPoint } from './primitives';
|
|
8
|
+
/** The block both endpoints address and jointly span, or null. Cell coordinates index a grid
|
|
9
|
+
* rather than characters: the table's own coverage rule is `cross-block/paste.ts`'s. */
|
|
10
|
+
export declare function blockCoveredWhole(doc: DocumentView, anchor: SelectionPoint | null, focus: SelectionPoint | null): number[] | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block a range holds whole, for the gestures that insert over it. Such a range has no seam
|
|
3
|
+
* inside it to merge at: the block leaves the tree entirely, so bytes that follow must land in
|
|
4
|
+
* its SLOT rather than at a caret in whatever took the slot.
|
|
5
|
+
*/
|
|
6
|
+
import { displayLength } from '../core/lines';
|
|
7
|
+
import { isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
8
|
+
import { pathsEqual } from './path-math';
|
|
9
|
+
/** The block both endpoints address and jointly span, or null. Cell coordinates index a grid
|
|
10
|
+
* rather than characters: the table's own coverage rule is `cross-block/paste.ts`'s. */
|
|
11
|
+
export function blockCoveredWhole(doc, anchor, focus) {
|
|
12
|
+
if (!anchor || !focus)
|
|
13
|
+
return null;
|
|
14
|
+
if (!pathsEqual(anchor.path, focus.path))
|
|
15
|
+
return null;
|
|
16
|
+
if (anchor.cellCoordinate || focus.cellCoordinate)
|
|
17
|
+
return null;
|
|
18
|
+
const node = nodeAt(doc, anchor.path);
|
|
19
|
+
if (!node || !isBlockNode(node))
|
|
20
|
+
return null;
|
|
21
|
+
const low = Math.min(anchor.offset, focus.offset);
|
|
22
|
+
const high = Math.max(anchor.offset, focus.offset);
|
|
23
|
+
return low === 0 && high >= displayLength(node.raw) ? anchor.path.slice() : null;
|
|
24
|
+
}
|
|
@@ -71,6 +71,9 @@ export interface CrossBlockHandlers {
|
|
|
71
71
|
* programmatic insertion: no gesture to consume, so `replacement` carries the payload. */
|
|
72
72
|
handlePaste(e: ClipboardEvent | null, replacement?: string): Promise<boolean>;
|
|
73
73
|
handleBeforeInput(e: InputEvent): Promise<boolean>;
|
|
74
|
+
/** Type-replace from a door that carries no InputEvent: the editor root, where a range over a
|
|
75
|
+
* block with no character position leaves no editing surface for `beforeinput` to fire on. */
|
|
76
|
+
insertText(text: string): Promise<boolean>;
|
|
74
77
|
handleCompositionStart(): boolean;
|
|
75
78
|
/** Cross-block range delete for Cut handlers, after they synchronously wrote the clipboard. */
|
|
76
79
|
performCrossBlockDeleteFromEvent(): Promise<void>;
|
|
@@ -28,6 +28,14 @@ export function createCrossBlockHandlers(ctx) {
|
|
|
28
28
|
// (each editable surface, the editor root) inherits them. Keydown gates its own destructive
|
|
29
29
|
// branches, since it also carries navigation, which stays live.
|
|
30
30
|
const reading = () => isReadingMode(ctx.getPresentationMode);
|
|
31
|
+
const insertText = async (text) => {
|
|
32
|
+
if (reading())
|
|
33
|
+
return true;
|
|
34
|
+
if (!ctx.selection.isCrossBlock)
|
|
35
|
+
return false;
|
|
36
|
+
await handleCrossBlockTypeReplace(ctx, mutationCtx, text);
|
|
37
|
+
return true;
|
|
38
|
+
};
|
|
31
39
|
return {
|
|
32
40
|
handleKeyDown: keydown.handleKeyDown,
|
|
33
41
|
handleCompositionStart: keydown.handleCompositionStart,
|
|
@@ -44,8 +52,12 @@ export function createCrossBlockHandlers(ctx) {
|
|
|
44
52
|
e.preventDefault();
|
|
45
53
|
return true;
|
|
46
54
|
}
|
|
47
|
-
|
|
55
|
+
if (!ctx.selection.isCrossBlock || e.inputType !== 'insertText')
|
|
56
|
+
return false;
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
return insertText(e.data ?? '');
|
|
48
59
|
},
|
|
60
|
+
insertText,
|
|
49
61
|
performCrossBlockDeleteFromEvent: async () => {
|
|
50
62
|
// Reached from cut handlers after the clipboard write; declining the delete
|
|
51
63
|
// degrades a reading-mode cut to a copy.
|
|
@@ -10,7 +10,7 @@ import type { DocumentView } from '../../core/node-views';
|
|
|
10
10
|
import type { InlineMarkKind } from '../../schema/inline-construct-policy';
|
|
11
11
|
import type { PresentationMode } from '../../presentation-mode';
|
|
12
12
|
import type { GrammarView } from '../../schema/block-openers';
|
|
13
|
-
import { type NodeParent } from '../../tree-operations/node-
|
|
13
|
+
import { type NodeParent } from '../../tree-operations/node-primitives';
|
|
14
14
|
import type { SharingState } from '../../tree-operations/sharing';
|
|
15
15
|
import { type SelectionPoint } from '../primitives';
|
|
16
16
|
export interface CrossBlockFormatWrite {
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* through the single-block seam, so its arms, candidates and mode verification are the same ones.
|
|
7
7
|
* A grid joins by its cells, which the range covers whole.
|
|
8
8
|
*/
|
|
9
|
-
import { activeInlineFormats, inlineFormatsCovering, isInlineFormatActive, isInlineFormatActiveAfter, toggleInlineFormat } from '../../core/inline/format-toggle';
|
|
9
|
+
import { activeInlineFormats, inlineFormatsCovering, isInlineFormatActive, isInlineFormatActiveAfter, toggleInlineFormat, withoutBoundaryWhitespace } from '../../core/inline/format-toggle';
|
|
10
10
|
import { getContentRange } from '../../core/inline';
|
|
11
11
|
import { ownTrailingLineEnding, trimTrailingLineEnding } from '../../core/lines';
|
|
12
12
|
import { tryGetBlockKindDescriptor } from '../../schema/block-kind-descriptor';
|
|
13
|
-
import { normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-
|
|
14
|
-
import { ensureUnsharedPath
|
|
13
|
+
import { normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-primitives';
|
|
14
|
+
import { ensureUnsharedPath } from '../../tree-operations/unshare';
|
|
15
|
+
import { rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
|
|
15
16
|
import { comparePaths } from '../path-math';
|
|
16
17
|
import { charOffsetOf } from '../primitives';
|
|
17
18
|
import { coveredGridCells, gridEndpointCellIndex } from '../table-endpoint-snap';
|
|
@@ -170,18 +171,6 @@ function contentSpan(node, path, from, to) {
|
|
|
170
171
|
const selection = withoutBoundaryWhitespace(display, clampToContent(from ?? content.start, content), clampToContent(to ?? content.end, content));
|
|
171
172
|
return selection && { path, edit: { display, content, selection } };
|
|
172
173
|
}
|
|
173
|
-
/** The span minus its boundary whitespace, null once nothing is left. Markdown opens and closes a
|
|
174
|
-
* run against a word, never a space, so an untrimmed edge yields delimiters that form no
|
|
175
|
-
* construct — which the modes that paint them write anyway (`wrapCandidates` is unverified there). */
|
|
176
|
-
function withoutBoundaryWhitespace(display, from, to) {
|
|
177
|
-
let start = from;
|
|
178
|
-
let end = to;
|
|
179
|
-
while (start < end && /\s/.test(display[start]))
|
|
180
|
-
start++;
|
|
181
|
-
while (end > start && /\s/.test(display[end - 1]))
|
|
182
|
-
end--;
|
|
183
|
-
return start === end ? null : { start, end };
|
|
184
|
-
}
|
|
185
174
|
const clampToContent = (offset, content) => Math.min(Math.max(offset, content.start), content.end);
|
|
186
175
|
/**
|
|
187
176
|
* Whether the span actually changed sides. The single-block seam decides its own arm from the
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { ownTrailingLineEnding } from '../../core/lines';
|
|
8
8
|
import { docPathFrom } from '../../cursor/coordinate-spaces';
|
|
9
9
|
import { inlineMarkForCommand } from '../../schema/inline-construct-policy';
|
|
10
|
-
import { blockNodeAt } from '../../tree-operations/node-
|
|
10
|
+
import { blockNodeAt } from '../../tree-operations/node-primitives';
|
|
11
11
|
import { comparePaths } from '../path-math';
|
|
12
12
|
import { restoreSelection } from '../selection-restore';
|
|
13
13
|
import { applyCrossBlockFormat, crossBlockActiveFormats, planCrossBlockFormat } from './format-range';
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { CURSOR_START } from '../../block-component';
|
|
6
6
|
import { performCrossBlockDelete, performCrossBlockDeleteSync } from './ops';
|
|
7
|
-
import { isBlockNode } from '../../tree-operations/node-
|
|
7
|
+
import { isBlockNode } from '../../tree-operations/node-primitives';
|
|
8
8
|
import { isReadingMode } from '../../presentation-mode';
|
|
9
9
|
import { eventToChord } from '../../schema/keybindings';
|
|
10
10
|
import { dispatchKeyCommand } from '../../schema/block-commands';
|
|
@@ -9,7 +9,7 @@ import { deleteSnapshot } from '../primitives';
|
|
|
9
9
|
import { focusCollapsedCaret } from '../native-bridge';
|
|
10
10
|
import { rangeDelete } from '../range-delete';
|
|
11
11
|
import { trackChildIds } from '../../tree-operations/structural-change';
|
|
12
|
-
import { isBlockNode, nodeAt } from '../../tree-operations/node-
|
|
12
|
+
import { isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
|
|
13
13
|
import { pathsEqual } from '../path-math';
|
|
14
14
|
import { docPathFrom } from '../../cursor/coordinate-spaces';
|
|
15
15
|
import { getStateForNode } from '../../reactivity/state-registry';
|
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { tableCellCount } from '../table-endpoint-snap';
|
|
7
7
|
import { CURSOR_END } from '../../block-component';
|
|
8
|
-
import { normalizeLineEndings } from '../../core/lines';
|
|
8
|
+
import { normalizeLineEndings, terminateLine } from '../../core/lines';
|
|
9
9
|
import { performCrossBlockDelete } from './ops';
|
|
10
10
|
import { charOffsetOf } from '../primitives';
|
|
11
11
|
import { focusCollapsedCaret } from '../native-bridge';
|
|
12
|
+
import { blockCoveredWhole } from '../covered-block';
|
|
12
13
|
import { pasteDispatch } from '../../tree-operations/paste/dispatch';
|
|
13
14
|
import { applyPasteTransforms } from '../../tree-operations/paste/paste-transforms';
|
|
14
15
|
import { parse } from '../../core/parser';
|
|
15
|
-
import { blockNodeAt, isBlockNode, nodeAt } from '../../tree-operations/node-
|
|
16
|
+
import { blockNodeAt, isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
|
|
16
17
|
import { pathsEqual } from '../path-math';
|
|
17
18
|
import { replaceBlockAtParent } from '../../tree-operations/paste/replace-block-at-parent';
|
|
18
19
|
import { ensureEditableContainers, normalizeReplacementTrivia } from '../../tree-operations';
|
|
@@ -32,10 +33,12 @@ export async function handleCrossBlockPaste(ctx, mutCtx, e, replacement) {
|
|
|
32
33
|
if (!pasted)
|
|
33
34
|
return true;
|
|
34
35
|
const doc = ctx.getDoc();
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// The range holds one block whole, in either addressing: replace the block at its parent
|
|
37
|
+
// position, single undo. A sub-rectangle inside a table only clears cells, leaving the table.
|
|
38
|
+
const covered = wholeTablePath(ctx.selection, doc) ??
|
|
39
|
+
blockCoveredWhole(doc, ctx.selection.anchor, ctx.selection.focus);
|
|
40
|
+
if (covered) {
|
|
41
|
+
await replaceCoveredBlockWithPaste(ctx, mutCtx, pasted, covered);
|
|
39
42
|
return true;
|
|
40
43
|
}
|
|
41
44
|
// Read before the delete collapses the selection: the only coordinate an error report on the
|
|
@@ -96,60 +99,59 @@ async function landCaretAfterPaste(ctx, caretPath, inlineCaretOffset) {
|
|
|
96
99
|
ctx.getBlockElByPath(caretPath)?.focus();
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
// ──
|
|
100
|
-
|
|
102
|
+
// ── Covered-block paste ────────────────────────────────────────────────────
|
|
103
|
+
/** The table a cell rectangle covers whole (Ctrl+A's 2nd press inside a cell), or null. */
|
|
104
|
+
function wholeTablePath(selection, doc) {
|
|
101
105
|
const anchor = selection.anchor;
|
|
102
106
|
const focus = selection.focus;
|
|
103
107
|
if (!anchor || !focus)
|
|
104
|
-
return
|
|
108
|
+
return null;
|
|
105
109
|
if (!pathsEqual(anchor.path, focus.path))
|
|
106
|
-
return
|
|
110
|
+
return null;
|
|
107
111
|
const node = nodeAt(doc, anchor.path);
|
|
108
112
|
if (!node || !isBlockNode(node) || node.kind !== 'table')
|
|
109
|
-
return
|
|
113
|
+
return null;
|
|
110
114
|
const cellCount = tableCellCount(node);
|
|
111
115
|
if (cellCount === 0)
|
|
112
|
-
return
|
|
116
|
+
return null;
|
|
113
117
|
// Same-path intra-table selection: cell offsets are context-established, so read directly.
|
|
114
118
|
const lo = Math.min(anchor.offset, focus.offset);
|
|
115
119
|
const hi = Math.max(anchor.offset, focus.offset);
|
|
116
|
-
return lo === 0 && hi === cellCount - 1;
|
|
120
|
+
return lo === 0 && hi === cellCount - 1 ? anchor.path.slice() : null;
|
|
117
121
|
}
|
|
118
122
|
/**
|
|
119
|
-
* Replace the
|
|
120
|
-
*
|
|
121
|
-
*
|
|
123
|
+
* Replace the covered block with the pasted content at its parent position. Routes through
|
|
124
|
+
* replaceBlockAtParent so the splice lands at the doc/enclosing-container scope rather than the
|
|
125
|
+
* row-level blockEdit TableRowBlock propagates. One snapshot covers the replace.
|
|
122
126
|
*/
|
|
123
|
-
async function
|
|
124
|
-
const tablePath = ctx.selection.anchor.path;
|
|
127
|
+
async function replaceCoveredBlockWithPaste(ctx, mutCtx, pasted, blockPath) {
|
|
125
128
|
const doc = ctx.getDoc();
|
|
129
|
+
const covered = blockNodeAt(doc, blockPath);
|
|
130
|
+
if (!covered)
|
|
131
|
+
return;
|
|
126
132
|
// This route never reaches pasteDispatch, so the paste transforms and the instance grammar
|
|
127
|
-
// ride here too; both rules live in the helper, applied at both sites.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
133
|
+
// ride here too; both rules live in the helper, applied at both sites. Terminated in the
|
|
134
|
+
// covered block's OWN ending, or an unterminated clipboard line leaves the block below
|
|
135
|
+
// flowing into the last one pasted (G4.20).
|
|
136
|
+
const parsed = parse(terminateLine(applyPasteTransforms(pasted, ctx.activePlugins), covered.raw), { grammar: ctx.grammar, scope: 'fragment' });
|
|
132
137
|
if (parsed.children.length === 0)
|
|
133
138
|
return;
|
|
134
|
-
const
|
|
135
|
-
if (!tableNode)
|
|
136
|
-
return;
|
|
137
|
-
const replacement = normalizeReplacementTrivia(tableNode, parsed.children);
|
|
139
|
+
const replacement = normalizeReplacementTrivia(covered, parsed.children);
|
|
138
140
|
for (const node of replacement)
|
|
139
141
|
ensureEditableContainers(node);
|
|
140
142
|
mutCtx.pushUndoSnapshot();
|
|
141
143
|
ctx.selection.collapse();
|
|
142
144
|
await replaceBlockAtParent({
|
|
143
145
|
doc,
|
|
144
|
-
blockPath
|
|
146
|
+
blockPath,
|
|
145
147
|
replacement,
|
|
146
148
|
controller: ctx.pasteCoordinator,
|
|
147
149
|
undoEntry: 'join',
|
|
148
150
|
focusReplacementIndex: replacement.length - 1,
|
|
149
151
|
focusOffset: CURSOR_END,
|
|
150
|
-
source: 'cross-block-
|
|
152
|
+
source: 'cross-block-covered-block',
|
|
151
153
|
...(ctx.grammar ? { grammar: ctx.grammar } : {}),
|
|
152
|
-
// Nothing is reattached behind the clipboard here — the
|
|
154
|
+
// Nothing is reattached behind the clipboard here — the block's whole slot is the target —
|
|
153
155
|
// so the trailing blank rides in unfiltered (`paste/dispatch.ts` states the rule).
|
|
154
156
|
trailingSeparator: parsed.suffix
|
|
155
157
|
});
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* Delete the range, splice the character into the surviving leaf's raw, re-parse so a marker at
|
|
4
4
|
* offset 0 re-derives the kind (parity with the single-block type path). Routed through
|
|
5
5
|
* commitMultiScope so a kind change mints a fresh node, ids/refs stay synced, and the op is
|
|
6
|
-
* `updateContent`.
|
|
6
|
+
* `updateContent`. A range holding its block whole has no surviving leaf and takes the
|
|
7
|
+
* replace arm at the foot of the file instead.
|
|
7
8
|
*/
|
|
8
9
|
import type { CrossBlockDispatchContext } from './dispatch';
|
|
9
10
|
import type { CrossBlockMutationContext } from './ops';
|
|
10
|
-
export declare function handleCrossBlockTypeReplace(ctx: CrossBlockDispatchContext, mutCtx: CrossBlockMutationContext,
|
|
11
|
+
export declare function handleCrossBlockTypeReplace(ctx: CrossBlockDispatchContext, mutCtx: CrossBlockMutationContext, typed: string): Promise<void>;
|
|
@@ -3,37 +3,51 @@
|
|
|
3
3
|
* Delete the range, splice the character into the surviving leaf's raw, re-parse so a marker at
|
|
4
4
|
* offset 0 re-derives the kind (parity with the single-block type path). Routed through
|
|
5
5
|
* commitMultiScope so a kind change mints a fresh node, ids/refs stay synced, and the op is
|
|
6
|
-
* `updateContent`.
|
|
6
|
+
* `updateContent`. A range holding its block whole has no surviving leaf and takes the
|
|
7
|
+
* replace arm at the foot of the file instead.
|
|
7
8
|
*/
|
|
8
9
|
import { performCrossBlockDelete } from './ops';
|
|
9
10
|
import { charOffsetOf } from '../primitives';
|
|
10
|
-
import {
|
|
11
|
+
import { blockCoveredWhole } from '../covered-block';
|
|
12
|
+
import { CURSOR_END } from '../../block-component';
|
|
13
|
+
import { parse } from '../../core/parser';
|
|
14
|
+
import { terminateLine } from '../../core/lines';
|
|
15
|
+
import { replaceBlockAtParent } from '../../tree-operations/paste/replace-block-at-parent';
|
|
16
|
+
import { ensureEditableContainers, normalizeReplacementTrivia } from '../../tree-operations';
|
|
17
|
+
import { blockNodeAt, normalizeBodyWrite, writeOwnRaw } from '../../tree-operations/node-primitives';
|
|
18
|
+
import { updateNodeContent, settledCaretTarget } from '../../tree-operations/content-write';
|
|
11
19
|
import { focusCollapsedCaret } from '../native-bridge';
|
|
12
|
-
import { ensureUnsharedChild, ensureUnsharedNode, ensureUnsharedPath
|
|
20
|
+
import { ensureUnsharedChild, ensureUnsharedNode, ensureUnsharedPath } from '../../tree-operations/unshare';
|
|
21
|
+
import { rebuildUnsharedChain } from '../../tree-operations/chain-rebuild';
|
|
13
22
|
import { stampStructuralChange } from '../../tree-operations/structural-change';
|
|
14
23
|
import { getStateForNode } from '../../reactivity/state-registry';
|
|
15
24
|
import { docPathFrom } from '../../cursor/coordinate-spaces';
|
|
16
25
|
import { devWarn } from '../../dev-warn';
|
|
17
|
-
export async function handleCrossBlockTypeReplace(ctx, mutCtx,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
export async function handleCrossBlockTypeReplace(ctx, mutCtx, typed) {
|
|
27
|
+
// A range holding its block whole leaves no leaf to splice into, so the character replaces the
|
|
28
|
+
// block in its own slot. An empty insertion has nothing to stand there and takes the delete.
|
|
29
|
+
const covered = typed
|
|
30
|
+
? blockCoveredWhole(ctx.getDoc(), ctx.selection.anchor, ctx.selection.focus)
|
|
31
|
+
: null;
|
|
32
|
+
if (covered) {
|
|
33
|
+
await replaceCoveredBlockWithText(ctx, mutCtx, typed, covered);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
22
36
|
const caret = await performCrossBlockDelete(mutCtx, { skipCaretRestore: true });
|
|
23
37
|
if (!caret)
|
|
24
|
-
return
|
|
38
|
+
return;
|
|
25
39
|
// All caret placements below target caret.path's top-level block; mount it once here so each
|
|
26
40
|
// (the post-tick landing included) finds a live element.
|
|
27
41
|
await ctx.revealPath(caret.path);
|
|
28
42
|
if (!typed) {
|
|
29
43
|
focusCollapsedCaret(ctx.getBlockElByPath, caret);
|
|
30
|
-
return
|
|
44
|
+
return;
|
|
31
45
|
}
|
|
32
46
|
const doc = ctx.getDoc();
|
|
33
47
|
const targetNode = blockNodeAt(doc, caret.path);
|
|
34
48
|
if (!targetNode) {
|
|
35
49
|
focusCollapsedCaret(ctx.getBlockElByPath, caret);
|
|
36
|
-
return
|
|
50
|
+
return;
|
|
37
51
|
}
|
|
38
52
|
// `updateContent`, not `input`: symmetric with the single-block path's KIND-CHANGING branch
|
|
39
53
|
// (block-edit.ts updateBlockContent). Only that path's kind-stable branch emits the debounced
|
|
@@ -42,7 +56,7 @@ export async function handleCrossBlockTypeReplace(ctx, mutCtx, e) {
|
|
|
42
56
|
const scope = resolveTypedCharScope(ctx, caret.path);
|
|
43
57
|
if (!scope) {
|
|
44
58
|
focusCollapsedCaret(ctx.getBlockElByPath, caret);
|
|
45
|
-
return
|
|
59
|
+
return;
|
|
46
60
|
}
|
|
47
61
|
// resolveTypedCharScope returns the leaf's IMMEDIATE parent: every mounted container registers
|
|
48
62
|
// a BlockListState, and the document root stands in for a top-level leaf. The guard below is
|
|
@@ -97,7 +111,42 @@ export async function handleCrossBlockTypeReplace(ctx, mutCtx, e) {
|
|
|
97
111
|
focusCollapsedCaret(ctx.getBlockElByPath, { path, offset: target.offset });
|
|
98
112
|
}
|
|
99
113
|
});
|
|
100
|
-
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Replace the covered block with the parse of the typed character, at the block's parent
|
|
117
|
+
* position: the same door the covered-block paste takes, so both gestures splice at one scope
|
|
118
|
+
* and land one undo entry. Re-parsed rather than spliced, so a marker typed over the block
|
|
119
|
+
* derives its kind exactly as the single-block type path does.
|
|
120
|
+
*/
|
|
121
|
+
async function replaceCoveredBlockWithText(ctx, mutCtx, typed, blockPath) {
|
|
122
|
+
const doc = ctx.getDoc();
|
|
123
|
+
const covered = blockNodeAt(doc, blockPath);
|
|
124
|
+
if (!covered)
|
|
125
|
+
return;
|
|
126
|
+
// Terminated in the block's OWN ending: an unterminated line leaves the block below flowing
|
|
127
|
+
// into the one the character just minted (G4.20).
|
|
128
|
+
const parsed = parse(terminateLine(typed, covered.raw), {
|
|
129
|
+
grammar: ctx.grammar,
|
|
130
|
+
scope: 'fragment'
|
|
131
|
+
});
|
|
132
|
+
if (parsed.children.length === 0)
|
|
133
|
+
return;
|
|
134
|
+
const replacement = normalizeReplacementTrivia(covered, parsed.children);
|
|
135
|
+
for (const node of replacement)
|
|
136
|
+
ensureEditableContainers(node);
|
|
137
|
+
mutCtx.pushUndoSnapshot();
|
|
138
|
+
ctx.selection.collapse();
|
|
139
|
+
await replaceBlockAtParent({
|
|
140
|
+
doc,
|
|
141
|
+
blockPath,
|
|
142
|
+
replacement,
|
|
143
|
+
controller: ctx.pasteCoordinator,
|
|
144
|
+
undoEntry: 'join',
|
|
145
|
+
focusReplacementIndex: replacement.length - 1,
|
|
146
|
+
focusOffset: CURSOR_END,
|
|
147
|
+
source: 'cross-block-covered-block',
|
|
148
|
+
...(ctx.grammar ? { grammar: ctx.grammar } : {})
|
|
149
|
+
});
|
|
101
150
|
}
|
|
102
151
|
/** The scope's children, re-read after the commit: the ceremony replaces the node it published. */
|
|
103
152
|
function scopeChildrenOf(ctx, scopePath) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type BlockComponent } from '../block-component';
|
|
9
9
|
import { type GapStopScope } from './gap-caret';
|
|
10
|
+
import type { SelectionEndpoint } from './primitives';
|
|
10
11
|
export interface DeadSpaceCaretDeps {
|
|
11
12
|
getBlockComponent(path: number[]): BlockComponent | null;
|
|
12
13
|
/**
|
|
@@ -35,5 +36,14 @@ export interface DeadSpaceCaret {
|
|
|
35
36
|
* was claimed; a point past a windowed-out tail claims it and lands after the reveal.
|
|
36
37
|
*/
|
|
37
38
|
placeAtPoint(root: HTMLElement, x: number, y: number): boolean;
|
|
39
|
+
/** Whether a press target is the editor's dead space (the root, or a block list inside it). */
|
|
40
|
+
isDeadSpaceTarget(root: HTMLElement, target: EventTarget | null): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The endpoint a drag STARTING in dead space (or on a block's rendered face) anchors at: the
|
|
43
|
+
* landing a click there would take, without taking it. A character surface anchors at that
|
|
44
|
+
* offset; a rendered leaf (an equation) anchors as a WHOLE block, so the range takes it entire
|
|
45
|
+
* whichever way the drag goes. A coordinate-addressed kind (a table) anchors nothing.
|
|
46
|
+
*/
|
|
47
|
+
anchorAtPoint(root: HTMLElement, x: number, y: number): SelectionEndpoint | null;
|
|
38
48
|
}
|
|
39
49
|
export declare function createDeadSpaceCaret(deps: DeadSpaceCaretDeps): DeadSpaceCaret;
|
|
@@ -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,33 @@ export function createDeadSpaceCaret(deps) {
|
|
|
81
88
|
leafOf(component, landing.path)?.snapCaretToPoint?.(probeX, probeY);
|
|
82
89
|
return true;
|
|
83
90
|
}
|
|
91
|
+
function anchorAtPoint(root, x, y) {
|
|
92
|
+
const blocks = measureBlocks(root);
|
|
93
|
+
const band = nearestBand(blocks.map((b) => b.rect), y);
|
|
94
|
+
if (!band)
|
|
95
|
+
return null;
|
|
96
|
+
if (band.belowAll && lastMountedTopLevel(blocks) !== deps.lastBlockIndex())
|
|
97
|
+
return null;
|
|
98
|
+
const { x: probeX, y: probeY } = probePointIn(blocks[band.index].rect, x, y, band.belowAll);
|
|
99
|
+
const hit = blockAtPoint(root, probeX, probeY);
|
|
100
|
+
if (!hit)
|
|
101
|
+
return null;
|
|
102
|
+
// No character surface to measure against — a rendered equation, a rule, a table — so the
|
|
103
|
+
// block is the unit, and the funnel picks the side by the drag's direction. An offset would
|
|
104
|
+
// put a range END at the block's start and leave it out; a cell would make the drag a caret
|
|
105
|
+
// placement in the nearest column. A rule names no landing at all, and is a unit all the same.
|
|
106
|
+
if (!hit.charSurface)
|
|
107
|
+
return { path: hit.path.slice(), wholeBlock: true };
|
|
108
|
+
const landing = landingFor(hit, probeX, probeY);
|
|
109
|
+
if (!landing)
|
|
110
|
+
return null;
|
|
111
|
+
if (landing.path.length > 0)
|
|
112
|
+
return { path: hit.path.slice(), wholeBlock: true };
|
|
113
|
+
return { path: hit.path.slice(), offset: landing.offset };
|
|
114
|
+
}
|
|
84
115
|
return {
|
|
116
|
+
isDeadSpaceTarget: isDeadSpace,
|
|
117
|
+
anchorAtPoint,
|
|
85
118
|
notePress(root, event) {
|
|
86
119
|
pressedOnDeadSpace =
|
|
87
120
|
isDeadSpace(root, event.target) &&
|
|
@@ -155,6 +188,15 @@ function isDeadSpace(root, target) {
|
|
|
155
188
|
return true;
|
|
156
189
|
return target instanceof Element && target.classList.contains('block-list');
|
|
157
190
|
}
|
|
191
|
+
/** Whether the ORIGINAL point (not the clamped probe) sits inside some block's content: on a
|
|
192
|
+
* descendant of a host, not on the host's own box or the dead space around it. */
|
|
193
|
+
function pressedOnBlockContent(root, x, y) {
|
|
194
|
+
const direct = document.elementFromPoint(x, y);
|
|
195
|
+
if (!(direct instanceof Element) || isDeadSpace(root, direct))
|
|
196
|
+
return false;
|
|
197
|
+
const host = direct.closest('[data-block-path]');
|
|
198
|
+
return host !== null && host !== direct;
|
|
199
|
+
}
|
|
158
200
|
/**
|
|
159
201
|
* Where the caret goes for a resolved hit: an internal child path (empty for a
|
|
160
202
|
* character-addressed surface) plus the offset within that leaf. Null declines the click.
|
|
@@ -165,8 +207,12 @@ function landingFor(hit, probeX, probeY) {
|
|
|
165
207
|
// A kind with only the drag hook addresses cells and named no caret landing.
|
|
166
208
|
if (hit.foreignDragHitTest)
|
|
167
209
|
return null;
|
|
210
|
+
// A leaf with no character surface (a rule, a folded equation) reached here was pressed on
|
|
211
|
+
// its own box: the block itself is the landing, and its `focus` ignores the offset.
|
|
212
|
+
if (!hit.charSurface)
|
|
213
|
+
return { path: [], offset: 0 };
|
|
168
214
|
// Reading mode flips contenteditable off, and a non-editable leaf has no character position.
|
|
169
|
-
if (!hit.charSurface
|
|
215
|
+
if (!hit.charSurface.matches('[contenteditable="true"]'))
|
|
170
216
|
return null;
|
|
171
217
|
const offset = offsetFromViewportPoint(hit.charSurface, probeX, probeY);
|
|
172
218
|
return offset === null ? null : { path: [], offset };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
|
|
3
|
+
* after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
|
|
4
|
+
* space too, and every other platform selects the bare word. The browser paints its own range
|
|
5
|
+
* before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
|
|
6
|
+
* the native selection suppressed: the trimmed range is the first one ever painted.
|
|
7
|
+
*/
|
|
8
|
+
/** Trim trailing whitespace off the document's current selection when it ends in a text node.
|
|
9
|
+
* A selection that is only whitespace is left alone: collapsing it would undo the click. */
|
|
10
|
+
export declare function trimDoubleClickSelection(doc: Document): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Select the word under the point of a double-click's second press, trimmed, synchronously.
|
|
13
|
+
* Word edges come from the engine's own `modify` walk so they match what its double-click would
|
|
14
|
+
* have chosen. False when the point names no text position; the caller then leaves the press to
|
|
15
|
+
* the browser.
|
|
16
|
+
*/
|
|
17
|
+
export declare function selectWordAtPoint(doc: Document, x: number, y: number): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word selection without its trailing space. Windows Chromium's double-click takes the whitespace
|
|
3
|
+
* after a word (the desktop-Word convention); a formatting toggle over that range would wrap the
|
|
4
|
+
* space too, and every other platform selects the bare word. The browser paints its own range
|
|
5
|
+
* before any `dblclick` handler runs, so the word is selected HERE, on the second press, with
|
|
6
|
+
* the native selection suppressed: the trimmed range is the first one ever painted.
|
|
7
|
+
*/
|
|
8
|
+
/** Trim trailing whitespace off the document's current selection when it ends in a text node.
|
|
9
|
+
* A selection that is only whitespace is left alone: collapsing it would undo the click. */
|
|
10
|
+
export function trimDoubleClickSelection(doc) {
|
|
11
|
+
const sel = doc.getSelection();
|
|
12
|
+
if (!sel || sel.rangeCount === 0 || sel.isCollapsed)
|
|
13
|
+
return false;
|
|
14
|
+
const range = sel.getRangeAt(0);
|
|
15
|
+
const endNode = range.endContainer;
|
|
16
|
+
if (!(endNode instanceof Text))
|
|
17
|
+
return false;
|
|
18
|
+
const text = range.toString();
|
|
19
|
+
const trimmed = text.trimEnd().length;
|
|
20
|
+
if (trimmed === 0 || trimmed === text.length)
|
|
21
|
+
return false;
|
|
22
|
+
const cut = text.length - trimmed;
|
|
23
|
+
if (cut > range.endOffset)
|
|
24
|
+
return false;
|
|
25
|
+
sel.setBaseAndExtent(range.startContainer, range.startOffset, endNode, range.endOffset - cut);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Select the word under the point of a double-click's second press, trimmed, synchronously.
|
|
30
|
+
* Word edges come from the engine's own `modify` walk so they match what its double-click would
|
|
31
|
+
* have chosen. False when the point names no text position; the caller then leaves the press to
|
|
32
|
+
* the browser.
|
|
33
|
+
*/
|
|
34
|
+
export function selectWordAtPoint(doc, x, y) {
|
|
35
|
+
const sel = doc.getSelection();
|
|
36
|
+
const hit = textPositionAt(doc, x, y);
|
|
37
|
+
if (!sel || !hit || typeof sel.modify !== 'function')
|
|
38
|
+
return false;
|
|
39
|
+
sel.setBaseAndExtent(hit.node, hit.offset, hit.node, hit.offset);
|
|
40
|
+
sel.modify('move', 'backward', 'word');
|
|
41
|
+
sel.modify('extend', 'forward', 'word');
|
|
42
|
+
if (sel.isCollapsed)
|
|
43
|
+
return false;
|
|
44
|
+
trimDoubleClickSelection(doc);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
function textPositionAt(doc, x, y) {
|
|
48
|
+
const position = doc.caretPositionFromPoint?.(x, y);
|
|
49
|
+
if (position?.offsetNode instanceof Text) {
|
|
50
|
+
return { node: position.offsetNode, offset: position.offset };
|
|
51
|
+
}
|
|
52
|
+
const range = doc.caretRangeFromPoint?.(x, y);
|
|
53
|
+
if (range?.startContainer instanceof Text) {
|
|
54
|
+
return { node: range.startContainer, offset: range.startOffset };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
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
10
|
export interface DragContext {
|
|
11
11
|
editorRoot: HTMLElement;
|
|
@@ -15,8 +15,13 @@ export interface DragContext {
|
|
|
15
15
|
getBlockElByPath: BlockElLookup;
|
|
16
16
|
/** Aborted on editor unmount; forwarded to the session's teardown. */
|
|
17
17
|
lifetimeSignal?: AbortSignal;
|
|
18
|
+
/**
|
|
19
|
+
* The press began outside every editable surface (the editor's margin), so no native drag
|
|
20
|
+
* is extending a selection underneath: the session paints the same-block range itself.
|
|
21
|
+
*/
|
|
22
|
+
paintSameBlock?: boolean;
|
|
18
23
|
}
|
|
19
24
|
/** Document-level pointer listeners for a cross-block drag started at `down`, plus a disposer. */
|
|
20
|
-
export declare function installDragListener(ctx: DragContext, anchorPoint:
|
|
25
|
+
export declare function installDragListener(ctx: DragContext, anchorPoint: SelectionEndpoint, down: PointerEvent): {
|
|
21
26
|
dispose(): void;
|
|
22
27
|
};
|