@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
|
@@ -5,16 +5,53 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { tick } from 'svelte';
|
|
7
7
|
import { resolvedInlineContent } from '../../../core/inline/inline-cache';
|
|
8
|
-
import {
|
|
8
|
+
import { flattenInlineWidgets, getInlineWidgetEditing, isCharacterLikeWidget, isWidgetActivationClick } from '../../../core/inline/inline-widgets';
|
|
9
9
|
import { isVerticallyTransparentNode } from '../../../core/inline/transparency';
|
|
10
10
|
import { trimTrailingLineEnding, trailingLineEnding } from '../../../core/lines';
|
|
11
11
|
import { asRawOffset, toClampedRawOffset, toDomTextOffset } from '../../../cursor/coordinate-spaces';
|
|
12
12
|
import { domTextOffsetAtNode, createRangeAtDomTextOffsets, selectionFocusWalkOffset } from '../../../cursor/widget-offset';
|
|
13
13
|
import { createSourceReveal } from '../../../cursor/reveal-source';
|
|
14
|
+
import { nearestWidgetEdgeSeat } from '../../../cursor/widget-edge-snap';
|
|
14
15
|
import { traceRevealOpen, traceRevealFold } from '../../../debug/interaction-trace';
|
|
15
16
|
import { assertInvariant } from '../../../assert';
|
|
16
|
-
import { caretIsInTextContent, hasModifier, isPlainTypingKey } from './click-snap-guard';
|
|
17
|
+
import { caretIsInTextContent, hasModifier, isPlainTypingKey, surfaceHoldsRange } from './click-snap-guard';
|
|
17
18
|
import { findWidgetNodeByStart, findFirstEdgeWidget, findLastEdgeWidget, rawHasNoTextBefore, rawHasNoTextAfter, widgetElByStart } from './widget-adjacency';
|
|
19
|
+
/**
|
|
20
|
+
* The inline-code wash over a revealed source, painted through the CSS Custom Highlight API
|
|
21
|
+
* (`::highlight(md-inline-reveal)` in editor.css) rather than a wrapper span: the reveal's
|
|
22
|
+
* contract, and every offset read on it, is that the source is a bare text node in the block.
|
|
23
|
+
* The range selects the NODE, so it keeps covering the text as typing grows it. Absent the API
|
|
24
|
+
* (jsdom, an old engine) the source simply shows unwashed.
|
|
25
|
+
*/
|
|
26
|
+
const REVEAL_HIGHLIGHT = 'md-inline-reveal';
|
|
27
|
+
const washRanges = new WeakMap();
|
|
28
|
+
function revealHighlight() {
|
|
29
|
+
if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof Highlight !== 'function') {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
let highlight = CSS.highlights.get(REVEAL_HIGHLIGHT);
|
|
33
|
+
if (!highlight) {
|
|
34
|
+
highlight = new Highlight();
|
|
35
|
+
CSS.highlights.set(REVEAL_HIGHLIGHT, highlight);
|
|
36
|
+
}
|
|
37
|
+
return highlight;
|
|
38
|
+
}
|
|
39
|
+
function washRevealedSource(node) {
|
|
40
|
+
const highlight = revealHighlight();
|
|
41
|
+
if (!highlight)
|
|
42
|
+
return;
|
|
43
|
+
const range = document.createRange();
|
|
44
|
+
range.selectNode(node);
|
|
45
|
+
washRanges.set(node, range);
|
|
46
|
+
highlight.add(range);
|
|
47
|
+
}
|
|
48
|
+
function unwashRevealedSource(node) {
|
|
49
|
+
const range = washRanges.get(node);
|
|
50
|
+
if (!range)
|
|
51
|
+
return;
|
|
52
|
+
washRanges.delete(node);
|
|
53
|
+
revealHighlight()?.delete(range);
|
|
54
|
+
}
|
|
18
55
|
export function createWidgetInteraction(deps) {
|
|
19
56
|
const isReading = () => deps.getPresentationMode?.() === 'reading';
|
|
20
57
|
// Resolver-aware so widget detection matches the render path's view; a mismatch
|
|
@@ -22,12 +59,26 @@ export function createWidgetInteraction(deps) {
|
|
|
22
59
|
function inlinesOf(node) {
|
|
23
60
|
return resolvedInlineContent(node, deps.linkRef);
|
|
24
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Every widget in this block, DESCENDING into non-widget parents. The flat inline list is
|
|
64
|
+
* the wrong shape to scan for widgets: a construct wrapping one — emphasis around a math
|
|
65
|
+
* span, a link around an image — hides it, and every pointer and arrow-key path that
|
|
66
|
+
* scanned the flat list simply did not see it. `> *… $L/9 \times 10^{20}$ …*` is the case
|
|
67
|
+
* that surfaced it: the quote's whole text is one emphasis node, so nothing inside it was
|
|
68
|
+
* ever clickable.
|
|
69
|
+
*/
|
|
70
|
+
function widgetsOf() {
|
|
71
|
+
return flattenInlineWidgets(inlinesOf(deps.node), deps.node.raw);
|
|
72
|
+
}
|
|
25
73
|
let revealState = null;
|
|
26
74
|
// Outside the record because they must survive `resetReveal` for the kernel's async
|
|
27
75
|
// restore. Identity is load-bearing: two byte-identical widgets share a reuse-pool key,
|
|
28
76
|
// so a lookup can return the OTHER instance and `replaceWith` would MOVE it.
|
|
29
77
|
let activeSourceNode = null;
|
|
30
78
|
let revealedWidget = null;
|
|
79
|
+
// The offset the last fold parked, which lies inside the construct it just folded: the
|
|
80
|
+
// restore must not read it as a formula closing around the caret and reopen the reveal.
|
|
81
|
+
let foldParkedCaret = null;
|
|
31
82
|
// Every fold entry is module-private and pre-guarded by all of its callers, so a fold
|
|
32
83
|
// with no active reveal means a new caller skipped the guard or a flag leaked (G1.26).
|
|
33
84
|
function assertFoldTargetsActiveReveal(entry) {
|
|
@@ -38,6 +89,7 @@ export function createWidgetInteraction(deps) {
|
|
|
38
89
|
function restoreRenderedWidget() {
|
|
39
90
|
if (activeSourceNode === null || revealedWidget === null)
|
|
40
91
|
return;
|
|
92
|
+
unwashRevealedSource(activeSourceNode);
|
|
41
93
|
activeSourceNode.replaceWith(revealedWidget);
|
|
42
94
|
activeSourceNode = null;
|
|
43
95
|
revealedWidget = null;
|
|
@@ -93,6 +145,7 @@ export function createWidgetInteraction(deps) {
|
|
|
93
145
|
revealedWidget = widget;
|
|
94
146
|
activeSourceNode = document.createTextNode(source);
|
|
95
147
|
widget.replaceWith(activeSourceNode);
|
|
148
|
+
washRevealedSource(activeSourceNode);
|
|
96
149
|
},
|
|
97
150
|
// Re-inserts the exact element the swap detached, still current because the edit
|
|
98
151
|
// was discarded. The persist path re-renders reactively instead.
|
|
@@ -138,9 +191,12 @@ export function createWidgetInteraction(deps) {
|
|
|
138
191
|
const { caretBefore, originalDisplay } = active;
|
|
139
192
|
// The reactive re-render rebuilds the island, so drop the swap handles without a
|
|
140
193
|
// DOM restore, then run the canonical teardown.
|
|
194
|
+
if (sourceNode)
|
|
195
|
+
unwashRevealedSource(sourceNode);
|
|
141
196
|
activeSourceNode = null;
|
|
142
197
|
revealedWidget = null;
|
|
143
198
|
resetReveal();
|
|
199
|
+
foldParkedCaret = caretAfter;
|
|
144
200
|
// No edit: fold without touching the CST. A zero-diff write still pushes a dead
|
|
145
201
|
// undo entry, so the user's next Ctrl+Z would revert nothing instead of their
|
|
146
202
|
// prior action. The pending-cursor set alone re-renders from the untouched CST.
|
|
@@ -243,10 +299,60 @@ export function createWidgetInteraction(deps) {
|
|
|
243
299
|
}
|
|
244
300
|
// The ONE hit test the pointerdown probe, the click dispatch, and the post-fold
|
|
245
301
|
// re-resolve share.
|
|
302
|
+
/**
|
|
303
|
+
* What the pointer is actually over, asked of the DOM rather than of a rectangle. An
|
|
304
|
+
* island's border box is not what the reader sees: a KaTeX render (a superscript, a
|
|
305
|
+
* fraction) paints ABOVE and BELOW its own box, so a click on the visible glyphs missed
|
|
306
|
+
* the rect by a pixel or two and the widget looked inert to the mouse while the keyboard
|
|
307
|
+
* reached it fine. `elementFromPoint` has no such gap — it answers with whatever is
|
|
308
|
+
* painted at the point, overflow included.
|
|
309
|
+
*/
|
|
310
|
+
function revealWidgetFromDom(el, x, y) {
|
|
311
|
+
// Feature-detected: jsdom implements no hit testing, and the rect scan below is the
|
|
312
|
+
// answer there — the same shape as `getTargetRanges`' detection on the code surface.
|
|
313
|
+
if (typeof document.elementFromPoint !== 'function')
|
|
314
|
+
return null;
|
|
315
|
+
const at = document.elementFromPoint(x, y);
|
|
316
|
+
const island = at instanceof Element ? at.closest('[data-inline-widget][data-source-start]') : null;
|
|
317
|
+
// Containment guard: the point may sit over another block's widget entirely.
|
|
318
|
+
if (!island || !el.contains(island))
|
|
319
|
+
return null;
|
|
320
|
+
const start = Number(island.getAttribute('data-source-start'));
|
|
321
|
+
if (!Number.isInteger(start))
|
|
322
|
+
return null;
|
|
323
|
+
const inline = widgetsOf().find((n) => n.start === start && getInlineWidgetEditing(n.kind)?.revealSource);
|
|
324
|
+
return inline ? { inline } : null;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Where a click on a rendered widget seats the caret when its kind maps no point: the END of
|
|
328
|
+
* the content a kind names, INSIDE its delimiters, so typing continues the construct rather
|
|
329
|
+
* than escaping it. A kind naming no span keeps the leading edge — guessing an end from its
|
|
330
|
+
* parsed text put a footnote's caret past its bracket.
|
|
331
|
+
*/
|
|
332
|
+
function insideEndOffset(inline) {
|
|
333
|
+
const source = deps.node.raw.slice(inline.start, inline.end);
|
|
334
|
+
const span = getInlineWidgetEditing(inline.kind)?.revealContentSpan?.(source);
|
|
335
|
+
return span && span.end >= 0 && span.end <= source.length ? span.end : 0;
|
|
336
|
+
}
|
|
337
|
+
/** The offset a press names inside a widget's source. Read against the PRE-fold geometry:
|
|
338
|
+
* a fold reflows the line and the point stops meaning anything the moment it runs. */
|
|
339
|
+
function seatFromPoint(el, inline, x, y) {
|
|
340
|
+
const widget = widgetElByStart(el, inline.start);
|
|
341
|
+
const atPoint = getInlineWidgetEditing(inline.kind)?.revealOffsetAtPoint;
|
|
342
|
+
if (!widget || !atPoint)
|
|
343
|
+
return null;
|
|
344
|
+
const source = deps.node.raw.slice(inline.start, inline.end);
|
|
345
|
+
const seat = atPoint(widget, source, x, y);
|
|
346
|
+
return seat === null ? null : Math.max(0, Math.min(seat, source.length));
|
|
347
|
+
}
|
|
246
348
|
function hitTestRevealWidget(el, x, y) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
349
|
+
const painted = revealWidgetFromDom(el, x, y);
|
|
350
|
+
if (painted)
|
|
351
|
+
return painted;
|
|
352
|
+
// Fallback for a point the DOM cannot answer for (a synthetic call with no element
|
|
353
|
+
// under it): the original box test, which still covers a widget that paints inside
|
|
354
|
+
// its own bounds.
|
|
355
|
+
for (const inline of widgetsOf()) {
|
|
250
356
|
if (!getInlineWidgetEditing(inline.kind)?.revealSource)
|
|
251
357
|
continue;
|
|
252
358
|
const widget = widgetElByStart(el, inline.start);
|
|
@@ -276,6 +382,7 @@ export function createWidgetInteraction(deps) {
|
|
|
276
382
|
const hit = hitTestRevealWidget(el, clickX, clickY);
|
|
277
383
|
if (!hit)
|
|
278
384
|
return;
|
|
385
|
+
const seat = seatFromPoint(el, hit.inline, clickX, clickY);
|
|
279
386
|
let targetStart = hit.inline.start;
|
|
280
387
|
if (revealState) {
|
|
281
388
|
const active = revealState;
|
|
@@ -292,26 +399,28 @@ export function createWidgetInteraction(deps) {
|
|
|
292
399
|
targetStart += deps.node.raw.length - rawBefore;
|
|
293
400
|
}
|
|
294
401
|
}
|
|
295
|
-
const target =
|
|
296
|
-
isInlineWidget(n, deps.node.raw) &&
|
|
297
|
-
getInlineWidgetEditing(n.kind)?.revealSource);
|
|
402
|
+
const target = widgetsOf().find((n) => n.start === targetStart && getInlineWidgetEditing(n.kind)?.revealSource);
|
|
298
403
|
if (!target)
|
|
299
404
|
return;
|
|
300
405
|
el.focus();
|
|
301
406
|
revealOpenedByLastClick = true;
|
|
302
|
-
|
|
303
|
-
void startReveal(target, target.start, 0);
|
|
407
|
+
void startReveal(target, target.start, seat ?? insideEndOffset(target));
|
|
304
408
|
}
|
|
305
|
-
/**
|
|
306
|
-
|
|
409
|
+
/**
|
|
410
|
+
* Take the whole revealed token; declines unless the double-click landed on it. Keyed off the
|
|
411
|
+
* point, not the selection: the root's second-press word select runs before this click and
|
|
412
|
+
* may have moved the selection off the source.
|
|
413
|
+
*/
|
|
414
|
+
function selectRevealedSource(x, y) {
|
|
307
415
|
const source = activeSourceNode;
|
|
308
|
-
if (!revealState || !source)
|
|
309
|
-
return false;
|
|
310
|
-
const selection = window.getSelection();
|
|
311
|
-
if (!selection || !selection.anchorNode || !source.contains(selection.anchorNode))
|
|
416
|
+
if (!revealState || !source || y === null)
|
|
312
417
|
return false;
|
|
313
418
|
const range = document.createRange();
|
|
314
419
|
range.selectNodeContents(source);
|
|
420
|
+
const onSource = Array.from(range.getClientRects()).some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
|
|
421
|
+
const selection = window.getSelection();
|
|
422
|
+
if (!onSource || !selection)
|
|
423
|
+
return false;
|
|
315
424
|
selection.removeAllRanges();
|
|
316
425
|
selection.addRange(range);
|
|
317
426
|
return true;
|
|
@@ -411,6 +520,13 @@ export function createWidgetInteraction(deps) {
|
|
|
411
520
|
}
|
|
412
521
|
return true;
|
|
413
522
|
}
|
|
523
|
+
// A vertical arrow is the shared pipeline's line walk, which needs a real caret to read:
|
|
524
|
+
// seat one at the edge the arrow leaves from and decline, so the walk runs from there.
|
|
525
|
+
if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
526
|
+
deps.cursor.setRaw(asRawOffset(e.key === 'ArrowUp' ? widget.start : widget.end));
|
|
527
|
+
deps.widgetSelection.clear();
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
414
530
|
// Reading mode still swallows — a selected widget owns its keys — but commits nothing.
|
|
415
531
|
// Undo anchored at the pre-select caret, so Ctrl+Z restores the caret where the user
|
|
416
532
|
// actually was when selection took over.
|
|
@@ -477,6 +593,26 @@ export function createWidgetInteraction(deps) {
|
|
|
477
593
|
});
|
|
478
594
|
}
|
|
479
595
|
}
|
|
596
|
+
// Only where the kind names a content span and the caret is inside it: a hard break is two
|
|
597
|
+
// bytes with a caret position between them and nothing to edit there.
|
|
598
|
+
function revealInterior(offset) {
|
|
599
|
+
const parkedByFold = foldParkedCaret === offset;
|
|
600
|
+
foldParkedCaret = null;
|
|
601
|
+
if (parkedByFold || revealState)
|
|
602
|
+
return false;
|
|
603
|
+
const widget = widgetsOf().find((w) => w.start < offset && offset < w.end);
|
|
604
|
+
if (!widget)
|
|
605
|
+
return false;
|
|
606
|
+
const editing = getInlineWidgetEditing(widget.kind);
|
|
607
|
+
if (!editing?.revealSource || !editing.revealContentSpan)
|
|
608
|
+
return false;
|
|
609
|
+
const at = offset - widget.start;
|
|
610
|
+
const span = editing.revealContentSpan(deps.node.raw.slice(widget.start, widget.end));
|
|
611
|
+
if (!span || at < span.start || at > span.end)
|
|
612
|
+
return false;
|
|
613
|
+
void startReveal(widget, offset, at);
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
480
616
|
function enterEdgeWidget(side) {
|
|
481
617
|
const inlines = inlinesOf(deps.node);
|
|
482
618
|
if (inlines.length === 0)
|
|
@@ -518,28 +654,37 @@ export function createWidgetInteraction(deps) {
|
|
|
518
654
|
}
|
|
519
655
|
// The first click of a double-click already revealed, so the second lands in the source
|
|
520
656
|
// text and the browser's word rule takes `[` or `$` as a word of its own.
|
|
521
|
-
if (doubleClick && revealOpenedByLastClick && selectRevealedSource())
|
|
657
|
+
if (doubleClick && revealOpenedByLastClick && selectRevealedSource(clickX, clickY))
|
|
522
658
|
return;
|
|
523
|
-
//
|
|
524
|
-
|
|
659
|
+
// The snap below seats a CARET, so it stands down for a range this surface paints, which it
|
|
660
|
+
// would collapse; the rule `clampOutOfAmbient` already carries.
|
|
661
|
+
const live = window.getSelection();
|
|
662
|
+
if (surfaceHoldsRange(el, live))
|
|
525
663
|
return;
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
664
|
+
const seat = nearestWidgetEdgeSeat(measuredWidgets(el), clickX, clickY);
|
|
665
|
+
if (seat === null)
|
|
666
|
+
return;
|
|
667
|
+
// A press BESIDE an island stands down for a visible caret; a press ON one cannot, since the
|
|
668
|
+
// engine answers that hit test with a position in the neighbouring text.
|
|
669
|
+
if (!seat.inside && caretIsInTextContent(el, live))
|
|
670
|
+
return;
|
|
671
|
+
el.focus();
|
|
672
|
+
deps.cursor.setRaw(asRawOffset(seat.offset));
|
|
673
|
+
// `setRaw`'s walker may have landed in a trailing text node, where native renders.
|
|
674
|
+
if (!caretIsInTextContent(el, window.getSelection()))
|
|
675
|
+
deps.setSnapTarget(seat.offset);
|
|
676
|
+
}
|
|
677
|
+
function* measuredWidgets(el) {
|
|
678
|
+
for (const inline of widgetsOf()) {
|
|
529
679
|
const widget = widgetElByStart(el, inline.start);
|
|
530
|
-
if (
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
deps.cursor.setRaw(asRawOffset(snapTo));
|
|
538
|
-
// `setRaw`'s walker may have landed in a trailing text node, where native renders.
|
|
539
|
-
if (!caretIsInTextContent(el, window.getSelection())) {
|
|
540
|
-
deps.setSnapTarget(snapTo);
|
|
680
|
+
if (widget) {
|
|
681
|
+
yield {
|
|
682
|
+
start: inline.start,
|
|
683
|
+
end: inline.end,
|
|
684
|
+
rect: widget.getBoundingClientRect(),
|
|
685
|
+
seatsInside: isCharacterLikeWidget(inline.kind)
|
|
686
|
+
};
|
|
541
687
|
}
|
|
542
|
-
return;
|
|
543
688
|
}
|
|
544
689
|
}
|
|
545
690
|
function widgetExtensionTarget(key) {
|
|
@@ -549,9 +694,7 @@ export function createWidgetInteraction(deps) {
|
|
|
549
694
|
const focus = selectionFocusWalkOffset(el, deps.getAmbientLength());
|
|
550
695
|
if (focus === null)
|
|
551
696
|
return null;
|
|
552
|
-
for (const inline of
|
|
553
|
-
if (!isInlineWidget(inline, deps.node.raw))
|
|
554
|
-
continue;
|
|
697
|
+
for (const inline of widgetsOf()) {
|
|
555
698
|
if (key === 'ArrowRight' && focus >= inline.start && focus < inline.end) {
|
|
556
699
|
return inline.end;
|
|
557
700
|
}
|
|
@@ -579,6 +722,7 @@ export function createWidgetInteraction(deps) {
|
|
|
579
722
|
handleSelectedWidgetKeydown,
|
|
580
723
|
handleShiftArrowIntoWidget,
|
|
581
724
|
enterWidget,
|
|
725
|
+
revealInterior,
|
|
582
726
|
enterEdgeWidget,
|
|
583
727
|
snapClickToWidgetEdge,
|
|
584
728
|
isRevealing,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block drag handle's two policies, kept out of the components that render it: which
|
|
3
|
+
* blocks carry one, and where its grip sits.
|
|
4
|
+
*
|
|
5
|
+
* Presence: prose carries none. A paragraph, a heading, a quote and the note cards read as
|
|
6
|
+
* text on the page, and a grip beside every one of them is noise; the blocks that get one are
|
|
7
|
+
* the objects a reader picks up whole — pictures, code, tables, equations, diagrams, list
|
|
8
|
+
* items, dividers. A gripless block is still a reorder unit: keyboard reorder moves it and a
|
|
9
|
+
* dragged block still drops beside it.
|
|
10
|
+
*/
|
|
11
|
+
import type { NodeView } from '../core/node-views';
|
|
12
|
+
export declare function isImageOnlyParagraph(node: NodeView): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A picture's grip does NOT wait for `blockDragHandles`: dragging it is the only pointer road
|
|
15
|
+
* to move a picture, and unlike prose it is a discrete object a reader expects to pick up. The
|
|
16
|
+
* caller still gates reading mode, which shows no affordances at all.
|
|
17
|
+
*/
|
|
18
|
+
export declare function showsDragHandle(node: NodeView, handlesEnabled: boolean): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* A list item's grip reorders it among its SIBLINGS and nowhere else: the drag is scoped to
|
|
21
|
+
* the enclosing list, so a lone item has nothing to trade places with and its grip is a
|
|
22
|
+
* promise the drop can't keep. It reappears the moment a second item joins the list.
|
|
23
|
+
*/
|
|
24
|
+
export declare function showsListItemDragHandle(itemCount: number, handlesEnabled: boolean): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
|
|
27
|
+
* first line-height of its OWN box — not its first line of text, since a card pads above that
|
|
28
|
+
* and a grip level with the first code line hangs below the card's shoulder. A declared anchor
|
|
29
|
+
* SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
|
|
30
|
+
* taller one is a card, and the band already covers it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function dragHandleAnchorY(host: HTMLElement): number | null;
|
|
33
|
+
/**
|
|
34
|
+
* Svelte attachment for the handle element: places the grip once the block has laid out, and
|
|
35
|
+
* again on every pointerover inside its host, so it follows edits made while hovered.
|
|
36
|
+
*
|
|
37
|
+
* The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
|
|
38
|
+
* target before any hover, so a target parked at the stylesheet's default while the grip would
|
|
39
|
+
* settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
|
|
40
|
+
*/
|
|
41
|
+
export declare function alignDragHandle(handle: HTMLElement): (() => void) | void;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The block drag handle's two policies, kept out of the components that render it: which
|
|
3
|
+
* blocks carry one, and where its grip sits.
|
|
4
|
+
*
|
|
5
|
+
* Presence: prose carries none. A paragraph, a heading, a quote and the note cards read as
|
|
6
|
+
* text on the page, and a grip beside every one of them is noise; the blocks that get one are
|
|
7
|
+
* the objects a reader picks up whole — pictures, code, tables, equations, diagrams, list
|
|
8
|
+
* items, dividers. A gripless block is still a reorder unit: keyboard reorder moves it and a
|
|
9
|
+
* dragged block still drops beside it.
|
|
10
|
+
*/
|
|
11
|
+
import { BLOCK_CONTENT_SELECTOR, DRAG_ANCHOR_ATTR } from './block-content-selector';
|
|
12
|
+
/**
|
|
13
|
+
* Prose — the page's background and its asides — plus the list SHELL, whose own grip would
|
|
14
|
+
* land in the gutter on top of its first item's and take the hit test with it. A list moves
|
|
15
|
+
* an item at a time; the shell is a wrapper, not a thing to pick up.
|
|
16
|
+
*/
|
|
17
|
+
const GRIPLESS_KINDS = new Set([
|
|
18
|
+
'paragraph',
|
|
19
|
+
'heading',
|
|
20
|
+
'blockquote',
|
|
21
|
+
'admonition',
|
|
22
|
+
'githubAlert',
|
|
23
|
+
'list'
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* A paragraph holding nothing but images is a picture in the page, not prose: it reads as a
|
|
27
|
+
* block of its own and is exactly the thing a reader reaches to move. A false negative here
|
|
28
|
+
* (an alt with an escaped bracket, an image beside a word) is the plain no-grip paragraph.
|
|
29
|
+
*/
|
|
30
|
+
const IMAGE_ONLY_PARAGRAPH = /^\s*(?:!\[[^\]]*\](?:\([^)]*\)|\[[^\]]*\])\s*)+$/;
|
|
31
|
+
export function isImageOnlyParagraph(node) {
|
|
32
|
+
return node.kind === 'paragraph' && IMAGE_ONLY_PARAGRAPH.test(node.raw);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A picture's grip does NOT wait for `blockDragHandles`: dragging it is the only pointer road
|
|
36
|
+
* to move a picture, and unlike prose it is a discrete object a reader expects to pick up. The
|
|
37
|
+
* caller still gates reading mode, which shows no affordances at all.
|
|
38
|
+
*/
|
|
39
|
+
export function showsDragHandle(node, handlesEnabled) {
|
|
40
|
+
if (isImageOnlyParagraph(node))
|
|
41
|
+
return true;
|
|
42
|
+
return handlesEnabled && !GRIPLESS_KINDS.has(node.kind);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A list item's grip reorders it among its SIBLINGS and nowhere else: the drag is scoped to
|
|
46
|
+
* the enclosing list, so a lone item has nothing to trade places with and its grip is a
|
|
47
|
+
* promise the drop can't keep. It reappears the moment a second item joins the list.
|
|
48
|
+
*/
|
|
49
|
+
export function showsListItemDragHandle(itemCount, handlesEnabled) {
|
|
50
|
+
return handlesEnabled && itemCount > 1;
|
|
51
|
+
}
|
|
52
|
+
/** A box within this many line-heights holds one row of content, margins included. */
|
|
53
|
+
const SINGLE_LINE = 1.5;
|
|
54
|
+
/**
|
|
55
|
+
* Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
|
|
56
|
+
* first line-height of its OWN box — not its first line of text, since a card pads above that
|
|
57
|
+
* and a grip level with the first code line hangs below the card's shoulder. A declared anchor
|
|
58
|
+
* SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
|
|
59
|
+
* taller one is a card, and the band already covers it.
|
|
60
|
+
*/
|
|
61
|
+
export function dragHandleAnchorY(host) {
|
|
62
|
+
const content = host.querySelector(BLOCK_CONTENT_SELECTOR);
|
|
63
|
+
if (!content)
|
|
64
|
+
return null;
|
|
65
|
+
const hostRect = host.getBoundingClientRect();
|
|
66
|
+
const line = lineHeightOf(content);
|
|
67
|
+
const rect = paintedRect(content);
|
|
68
|
+
// A one-line block IS its own band, and the eye puts the grip level with the row rather
|
|
69
|
+
// than a hair above its middle — a list item, a task row, a rule.
|
|
70
|
+
const box = rect.height <= line * SINGLE_LINE ? rect : (markerRect(content, line) ?? bandRect(rect, line));
|
|
71
|
+
if (!box || box.height === 0)
|
|
72
|
+
return null;
|
|
73
|
+
const y = box.top + box.height / 2 - hostRect.top;
|
|
74
|
+
// A box painting outside the host (a scrolled or offscreen region) seats nothing.
|
|
75
|
+
return y >= 0 && y <= hostRect.height ? y : null;
|
|
76
|
+
}
|
|
77
|
+
function lineHeightOf(content) {
|
|
78
|
+
const line = parseFloat(getComputedStyle(content).lineHeight);
|
|
79
|
+
return Number.isFinite(line) && line > 0 ? line : 20;
|
|
80
|
+
}
|
|
81
|
+
/** The declared anchor, taken only while it is marker-sized and on the block's first line. */
|
|
82
|
+
function markerRect(content, line) {
|
|
83
|
+
const declared = content.matches(`[${DRAG_ANCHOR_ATTR}]`)
|
|
84
|
+
? content
|
|
85
|
+
: content.querySelector(`[${DRAG_ANCHOR_ATTR}]`);
|
|
86
|
+
const rect = declared?.getBoundingClientRect();
|
|
87
|
+
if (!rect || rect.height === 0 || rect.height > line)
|
|
88
|
+
return null;
|
|
89
|
+
return rect.top - content.getBoundingClientRect().top > line ? null : rect;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The box the band measures from: the content's own, unless it leads with an atomic widget
|
|
93
|
+
* (a picture in a paragraph), whose margin would otherwise seat the grip above the picture and
|
|
94
|
+
* flush with its corner.
|
|
95
|
+
*/
|
|
96
|
+
function paintedRect(content) {
|
|
97
|
+
const rect = content.getBoundingClientRect();
|
|
98
|
+
const widget = content.firstElementChild?.closest('[data-inline-widget]');
|
|
99
|
+
const widgetRect = widget?.getBoundingClientRect();
|
|
100
|
+
return widgetRect && widgetRect.height > 0 && widgetRect.top > rect.top ? widgetRect : rect;
|
|
101
|
+
}
|
|
102
|
+
/** The top line-height of a box; a box shorter than a line is its own band. */
|
|
103
|
+
function bandRect(rect, line) {
|
|
104
|
+
return new DOMRect(rect.x, rect.y, rect.width, Math.min(line, rect.height));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Svelte attachment for the handle element: places the grip once the block has laid out, and
|
|
108
|
+
* again on every pointerover inside its host, so it follows edits made while hovered.
|
|
109
|
+
*
|
|
110
|
+
* The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
|
|
111
|
+
* target before any hover, so a target parked at the stylesheet's default while the grip would
|
|
112
|
+
* settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
|
|
113
|
+
*/
|
|
114
|
+
export function alignDragHandle(handle) {
|
|
115
|
+
const host = handle.parentElement;
|
|
116
|
+
if (!host)
|
|
117
|
+
return;
|
|
118
|
+
const grip = handle.firstElementChild;
|
|
119
|
+
if (!grip)
|
|
120
|
+
return;
|
|
121
|
+
// Inline `top` on the grip (px from the host's top to its centre; the stylesheet's
|
|
122
|
+
// translateY(-50%) does the centring) overrides the stylesheet's half-line default.
|
|
123
|
+
const place = () => {
|
|
124
|
+
const y = dragHandleAnchorY(host);
|
|
125
|
+
if (y !== null)
|
|
126
|
+
grip.style.top = `${y}px`;
|
|
127
|
+
};
|
|
128
|
+
const settled = requestAnimationFrame(place);
|
|
129
|
+
host.addEventListener('pointerover', place);
|
|
130
|
+
return () => {
|
|
131
|
+
cancelAnimationFrame(settled);
|
|
132
|
+
host.removeEventListener('pointerover', place);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-root focus attribution: which block host holds the caret. The path drives each
|
|
3
|
+
* windowing scope's per-level pin, so a scroll that pushes the caret off-screen never tears
|
|
4
|
+
* down native focus/IME; the `data-focused` attribute is what both preview modes' CSS key
|
|
5
|
+
* their focused-block reveal off. The installing `$effect`s stay in `Editor.svelte`.
|
|
6
|
+
*/
|
|
7
|
+
import { type PresentationMode } from '../presentation-mode';
|
|
8
|
+
export interface FocusAttributionDeps {
|
|
9
|
+
/** A getter, never a value: every attribute write is gated on the mode in force. */
|
|
10
|
+
get mode(): PresentationMode;
|
|
11
|
+
}
|
|
12
|
+
export interface FocusAttribution {
|
|
13
|
+
/** `root` is the element the installing effect captured, not a live binding. */
|
|
14
|
+
install(root: HTMLElement): () => void;
|
|
15
|
+
/** Entering a preview mode marks the already-focused block, since no re-focus fires. */
|
|
16
|
+
applyForMode(): void;
|
|
17
|
+
getFocusedPath(): number[] | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function createFocusAttribution(deps: FocusAttributionDeps): FocusAttribution;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-root focus attribution: which block host holds the caret. The path drives each
|
|
3
|
+
* windowing scope's per-level pin, so a scroll that pushes the caret off-screen never tears
|
|
4
|
+
* down native focus/IME; the `data-focused` attribute is what both preview modes' CSS key
|
|
5
|
+
* their focused-block reveal off. The installing `$effect`s stay in `Editor.svelte`.
|
|
6
|
+
*/
|
|
7
|
+
import { assertInvariant } from '../assert';
|
|
8
|
+
import { checkLandableCaret } from '../invariants/landable-caret';
|
|
9
|
+
import { isPreviewMode } from '../presentation-mode';
|
|
10
|
+
import { readBlockPath } from '../selection/path-lookup';
|
|
11
|
+
import { onRoot, removeAll } from './editor-root-listeners';
|
|
12
|
+
export function createFocusAttribution(deps) {
|
|
13
|
+
// Plain fields, never reactive: focusout fires mid-teardown during a structural commit,
|
|
14
|
+
// where a reactive write would trip state_unsafe_mutation.
|
|
15
|
+
let focusedPath = null;
|
|
16
|
+
let focusedHostEl = null;
|
|
17
|
+
// Mode-gated so source/reading DOM stays byte-identical.
|
|
18
|
+
function applyFocusedAttr() {
|
|
19
|
+
if (focusedHostEl && isPreviewMode(deps.mode)) {
|
|
20
|
+
focusedHostEl.setAttribute('data-focused', '');
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
focusedHostEl?.removeAttribute('data-focused');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function setFocusedHost(host) {
|
|
27
|
+
if (focusedHostEl === host)
|
|
28
|
+
return;
|
|
29
|
+
focusedHostEl?.removeAttribute('data-focused');
|
|
30
|
+
focusedHostEl = host;
|
|
31
|
+
applyFocusedAttr();
|
|
32
|
+
}
|
|
33
|
+
function clear() {
|
|
34
|
+
focusedPath = null;
|
|
35
|
+
setFocusedHost(null);
|
|
36
|
+
}
|
|
37
|
+
function install(root) {
|
|
38
|
+
const onFocusIn = (e) => {
|
|
39
|
+
const host = e.target?.closest('[data-block-path]');
|
|
40
|
+
if (!host || !root.contains(host)) {
|
|
41
|
+
clear();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setFocusedHost(host);
|
|
45
|
+
const path = readBlockPath(host);
|
|
46
|
+
focusedPath = path && path.length > 0 ? path : null;
|
|
47
|
+
// G1.33 at the seam every caret door crosses: a door seats a caret by focusing the
|
|
48
|
+
// surface, whoever minted it, so a consumer's own door inherits the guard here.
|
|
49
|
+
const landed = e.target;
|
|
50
|
+
if (landed instanceof HTMLElement) {
|
|
51
|
+
assertInvariant('landable-caret', () => checkLandableCaret(landed, deps.mode, path ?? []));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const onFocusOut = (e) => {
|
|
55
|
+
const next = e.relatedTarget;
|
|
56
|
+
if (next && root.contains(next))
|
|
57
|
+
return; // moving between blocks — keep the pin
|
|
58
|
+
clear();
|
|
59
|
+
};
|
|
60
|
+
return removeAll(onRoot(root, 'focusin', onFocusIn), onRoot(root, 'focusout', onFocusOut));
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
install,
|
|
64
|
+
applyForMode: applyFocusedAttr,
|
|
65
|
+
getFocusedPath: () => focusedPath
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor-root geometry observers: the width and viewport-height watchers, the type-scale probe
|
|
3
|
+
* and the header-slot compensation. Each observes the element its installing `$effect`
|
|
4
|
+
* captured and returns the teardown; the reactive signals they feed (`widthVersion`,
|
|
5
|
+
* `typeScale`, `viewportHeightVersion`) stay in `Editor.svelte`, which is what the rest of the
|
|
6
|
+
* root reads. ResizeObserver's per-callback batching is the coalescing; no timer debounce (G4.4).
|
|
7
|
+
*/
|
|
8
|
+
import type { UserScrollport } from '../cursor/scroll-ancestors';
|
|
9
|
+
import type { Scrollport } from '../cursor/scrollport';
|
|
10
|
+
/** Calls `onWidthChange` when `el`'s width moves. A height-only resize is ignored: only a
|
|
11
|
+
* re-wrap stales the measured heights. */
|
|
12
|
+
export declare function installWidthWatcher(el: HTMLElement, onWidthChange: () => void): () => void;
|
|
13
|
+
/** Calls `bump` on a height change of the resolved scrollport `target`. */
|
|
14
|
+
export declare function installViewportHeightWatcher(target: UserScrollport, bump: () => void): () => void;
|
|
15
|
+
export interface TypeScaleProbeDeps {
|
|
16
|
+
/** Read at each report, so the sub-percent guard compares against the scale in force. */
|
|
17
|
+
getScale(): number;
|
|
18
|
+
onScale(next: number): void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The width watcher's sibling: a font-size move puts the height estimates off several-fold, so
|
|
22
|
+
* a document whose true height clears the activation watermark can fail to window at all. No
|
|
23
|
+
* other box in the root reports it, hence the `1em` probe; the scale is relative to the size
|
|
24
|
+
* `HEIGHT_ESTIMATES` were calibrated at.
|
|
25
|
+
*/
|
|
26
|
+
export declare function installTypeScaleProbe(el: HTMLElement, deps: TypeScaleProbeDeps): () => void;
|
|
27
|
+
export interface HeaderSlotCompensationDeps {
|
|
28
|
+
el: HTMLElement;
|
|
29
|
+
port: Pick<Scrollport, 'scrollTop' | 'scrollBy'>;
|
|
30
|
+
ownsScrollCorrection(): boolean;
|
|
31
|
+
revealHoldsScroll(): boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The header slot's height lives outside the height model, so while the editor owns the
|
|
35
|
+
* correction a growing header would slide the document under the reader. Compensating from
|
|
36
|
+
* the SLOT's own resize composes with `correctAnchor` instead of double-correcting; a reveal
|
|
37
|
+
* already holding the scroll outranks it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function installHeaderSlotCompensation(deps: HeaderSlotCompensationDeps): () => void;
|