@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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* factory owns the imperative inline-DOM construction and carries a live caret across
|
|
5
5
|
* a decoration-driven rebuild the edit path's pending restore doesn't cover.
|
|
6
6
|
*/
|
|
7
|
+
import { hidesMarkers } from '../../../presentation-mode';
|
|
7
8
|
import { buildAmbientSpan } from '../../../ambient/ambient-dom';
|
|
8
9
|
import { computeInlineContent, contentLengthOf, getContentRange, isProseKind } from '../../../core/inline';
|
|
9
10
|
import { renderInlineNodes } from '../../../core/inline-render';
|
|
@@ -90,7 +91,8 @@ export function createTextRender(deps) {
|
|
|
90
91
|
buildPortalWidget,
|
|
91
92
|
// Attribute-only stamps for the construct-reveal trigger, mode-gated so the
|
|
92
93
|
// other modes' DOM stays byte-identical.
|
|
93
|
-
tagConstructMarkers: deps.presentationMode === 'preview-inline'
|
|
94
|
+
tagConstructMarkers: deps.presentationMode === 'preview-inline',
|
|
95
|
+
pendingBreakSeat: hidesMarkers(deps.presentationMode)
|
|
94
96
|
}));
|
|
95
97
|
return frag;
|
|
96
98
|
}
|
|
@@ -69,6 +69,9 @@ export interface WidgetInteraction {
|
|
|
69
69
|
/** Snap a click that landed outside any text node to the nearest widget edge, or open a
|
|
70
70
|
* reveal-source widget it hit. */
|
|
71
71
|
snapClickToWidgetEdge(clickX: number | null, clickY: number | null, press?: WidgetPress): void;
|
|
72
|
+
/** A caret restored strictly inside a reveal-source widget — a formula that just closed
|
|
73
|
+
* around it — opens that source there instead of being pushed past the island. */
|
|
74
|
+
revealInterior(offset: number): boolean;
|
|
72
75
|
/** A reveal-source widget currently shows its editable `$…$` source. */
|
|
73
76
|
isRevealing(): boolean;
|
|
74
77
|
/** Escape (cancel to rendered) while source is shown. Enter is deliberately NOT
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { tick } from 'svelte';
|
|
7
7
|
import { resolvedInlineContent } from '../../../core/inline/inline-cache';
|
|
8
|
-
import {
|
|
8
|
+
import { flattenInlineWidgets, getInlineWidgetEditing, 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';
|
|
@@ -15,6 +15,42 @@ import { traceRevealOpen, traceRevealFold } from '../../../debug/interaction-tra
|
|
|
15
15
|
import { assertInvariant } from '../../../assert';
|
|
16
16
|
import { caretIsInTextContent, hasModifier, isPlainTypingKey } from './click-snap-guard';
|
|
17
17
|
import { findWidgetNodeByStart, findFirstEdgeWidget, findLastEdgeWidget, rawHasNoTextBefore, rawHasNoTextAfter, widgetElByStart } from './widget-adjacency';
|
|
18
|
+
/**
|
|
19
|
+
* The inline-code wash over a revealed source, painted through the CSS Custom Highlight API
|
|
20
|
+
* (`::highlight(md-inline-reveal)` in editor.css) rather than a wrapper span: the reveal's
|
|
21
|
+
* contract, and every offset read on it, is that the source is a bare text node in the block.
|
|
22
|
+
* The range selects the NODE, so it keeps covering the text as typing grows it. Absent the API
|
|
23
|
+
* (jsdom, an old engine) the source simply shows unwashed.
|
|
24
|
+
*/
|
|
25
|
+
const REVEAL_HIGHLIGHT = 'md-inline-reveal';
|
|
26
|
+
const washRanges = new WeakMap();
|
|
27
|
+
function revealHighlight() {
|
|
28
|
+
if (typeof CSS === 'undefined' || !('highlights' in CSS) || typeof Highlight !== 'function') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
let highlight = CSS.highlights.get(REVEAL_HIGHLIGHT);
|
|
32
|
+
if (!highlight) {
|
|
33
|
+
highlight = new Highlight();
|
|
34
|
+
CSS.highlights.set(REVEAL_HIGHLIGHT, highlight);
|
|
35
|
+
}
|
|
36
|
+
return highlight;
|
|
37
|
+
}
|
|
38
|
+
function washRevealedSource(node) {
|
|
39
|
+
const highlight = revealHighlight();
|
|
40
|
+
if (!highlight)
|
|
41
|
+
return;
|
|
42
|
+
const range = document.createRange();
|
|
43
|
+
range.selectNode(node);
|
|
44
|
+
washRanges.set(node, range);
|
|
45
|
+
highlight.add(range);
|
|
46
|
+
}
|
|
47
|
+
function unwashRevealedSource(node) {
|
|
48
|
+
const range = washRanges.get(node);
|
|
49
|
+
if (!range)
|
|
50
|
+
return;
|
|
51
|
+
washRanges.delete(node);
|
|
52
|
+
revealHighlight()?.delete(range);
|
|
53
|
+
}
|
|
18
54
|
export function createWidgetInteraction(deps) {
|
|
19
55
|
const isReading = () => deps.getPresentationMode?.() === 'reading';
|
|
20
56
|
// Resolver-aware so widget detection matches the render path's view; a mismatch
|
|
@@ -22,12 +58,26 @@ export function createWidgetInteraction(deps) {
|
|
|
22
58
|
function inlinesOf(node) {
|
|
23
59
|
return resolvedInlineContent(node, deps.linkRef);
|
|
24
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Every widget in this block, DESCENDING into non-widget parents. The flat inline list is
|
|
63
|
+
* the wrong shape to scan for widgets: a construct wrapping one — emphasis around a math
|
|
64
|
+
* span, a link around an image — hides it, and every pointer and arrow-key path that
|
|
65
|
+
* scanned the flat list simply did not see it. `> *… $L/9 \times 10^{20}$ …*` is the case
|
|
66
|
+
* that surfaced it: the quote's whole text is one emphasis node, so nothing inside it was
|
|
67
|
+
* ever clickable.
|
|
68
|
+
*/
|
|
69
|
+
function widgetsOf() {
|
|
70
|
+
return flattenInlineWidgets(inlinesOf(deps.node), deps.node.raw);
|
|
71
|
+
}
|
|
25
72
|
let revealState = null;
|
|
26
73
|
// Outside the record because they must survive `resetReveal` for the kernel's async
|
|
27
74
|
// restore. Identity is load-bearing: two byte-identical widgets share a reuse-pool key,
|
|
28
75
|
// so a lookup can return the OTHER instance and `replaceWith` would MOVE it.
|
|
29
76
|
let activeSourceNode = null;
|
|
30
77
|
let revealedWidget = null;
|
|
78
|
+
// The offset the last fold parked, which lies inside the construct it just folded: the
|
|
79
|
+
// restore must not read it as a formula closing around the caret and reopen the reveal.
|
|
80
|
+
let foldParkedCaret = null;
|
|
31
81
|
// Every fold entry is module-private and pre-guarded by all of its callers, so a fold
|
|
32
82
|
// with no active reveal means a new caller skipped the guard or a flag leaked (G1.26).
|
|
33
83
|
function assertFoldTargetsActiveReveal(entry) {
|
|
@@ -38,6 +88,7 @@ export function createWidgetInteraction(deps) {
|
|
|
38
88
|
function restoreRenderedWidget() {
|
|
39
89
|
if (activeSourceNode === null || revealedWidget === null)
|
|
40
90
|
return;
|
|
91
|
+
unwashRevealedSource(activeSourceNode);
|
|
41
92
|
activeSourceNode.replaceWith(revealedWidget);
|
|
42
93
|
activeSourceNode = null;
|
|
43
94
|
revealedWidget = null;
|
|
@@ -93,6 +144,7 @@ export function createWidgetInteraction(deps) {
|
|
|
93
144
|
revealedWidget = widget;
|
|
94
145
|
activeSourceNode = document.createTextNode(source);
|
|
95
146
|
widget.replaceWith(activeSourceNode);
|
|
147
|
+
washRevealedSource(activeSourceNode);
|
|
96
148
|
},
|
|
97
149
|
// Re-inserts the exact element the swap detached, still current because the edit
|
|
98
150
|
// was discarded. The persist path re-renders reactively instead.
|
|
@@ -138,9 +190,12 @@ export function createWidgetInteraction(deps) {
|
|
|
138
190
|
const { caretBefore, originalDisplay } = active;
|
|
139
191
|
// The reactive re-render rebuilds the island, so drop the swap handles without a
|
|
140
192
|
// DOM restore, then run the canonical teardown.
|
|
193
|
+
if (sourceNode)
|
|
194
|
+
unwashRevealedSource(sourceNode);
|
|
141
195
|
activeSourceNode = null;
|
|
142
196
|
revealedWidget = null;
|
|
143
197
|
resetReveal();
|
|
198
|
+
foldParkedCaret = caretAfter;
|
|
144
199
|
// No edit: fold without touching the CST. A zero-diff write still pushes a dead
|
|
145
200
|
// undo entry, so the user's next Ctrl+Z would revert nothing instead of their
|
|
146
201
|
// prior action. The pending-cursor set alone re-renders from the untouched CST.
|
|
@@ -243,10 +298,49 @@ export function createWidgetInteraction(deps) {
|
|
|
243
298
|
}
|
|
244
299
|
// The ONE hit test the pointerdown probe, the click dispatch, and the post-fold
|
|
245
300
|
// re-resolve share.
|
|
301
|
+
/**
|
|
302
|
+
* What the pointer is actually over, asked of the DOM rather than of a rectangle. An
|
|
303
|
+
* island's border box is not what the reader sees: a KaTeX render (a superscript, a
|
|
304
|
+
* fraction) paints ABOVE and BELOW its own box, so a click on the visible glyphs missed
|
|
305
|
+
* the rect by a pixel or two and the widget looked inert to the mouse while the keyboard
|
|
306
|
+
* reached it fine. `elementFromPoint` has no such gap — it answers with whatever is
|
|
307
|
+
* painted at the point, overflow included.
|
|
308
|
+
*/
|
|
309
|
+
function revealWidgetFromDom(el, x, y) {
|
|
310
|
+
// Feature-detected: jsdom implements no hit testing, and the rect scan below is the
|
|
311
|
+
// answer there — the same shape as `getTargetRanges`' detection on the code surface.
|
|
312
|
+
if (typeof document.elementFromPoint !== 'function')
|
|
313
|
+
return null;
|
|
314
|
+
const at = document.elementFromPoint(x, y);
|
|
315
|
+
const island = at instanceof Element ? at.closest('[data-inline-widget][data-source-start]') : null;
|
|
316
|
+
// Containment guard: the point may sit over another block's widget entirely.
|
|
317
|
+
if (!island || !el.contains(island))
|
|
318
|
+
return null;
|
|
319
|
+
const start = Number(island.getAttribute('data-source-start'));
|
|
320
|
+
if (!Number.isInteger(start))
|
|
321
|
+
return null;
|
|
322
|
+
const inline = widgetsOf().find((n) => n.start === start && getInlineWidgetEditing(n.kind)?.revealSource);
|
|
323
|
+
return inline ? { inline } : null;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The caret a click on a rendered widget wants: the END of the content a kind names, INSIDE
|
|
327
|
+
* its delimiters, so typing continues the construct rather than escaping it. A kind naming no
|
|
328
|
+
* span keeps the leading edge: guessing an end from its parsed text put a footnote's caret
|
|
329
|
+
* past its bracket.
|
|
330
|
+
*/
|
|
331
|
+
function insideEndOffset(inline) {
|
|
332
|
+
const source = deps.node.raw.slice(inline.start, inline.end);
|
|
333
|
+
const span = getInlineWidgetEditing(inline.kind)?.revealContentSpan?.(source);
|
|
334
|
+
return span && span.end >= 0 && span.end <= source.length ? span.end : 0;
|
|
335
|
+
}
|
|
246
336
|
function hitTestRevealWidget(el, x, y) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
337
|
+
const painted = revealWidgetFromDom(el, x, y);
|
|
338
|
+
if (painted)
|
|
339
|
+
return painted;
|
|
340
|
+
// Fallback for a point the DOM cannot answer for (a synthetic call with no element
|
|
341
|
+
// under it): the original box test, which still covers a widget that paints inside
|
|
342
|
+
// its own bounds.
|
|
343
|
+
for (const inline of widgetsOf()) {
|
|
250
344
|
if (!getInlineWidgetEditing(inline.kind)?.revealSource)
|
|
251
345
|
continue;
|
|
252
346
|
const widget = widgetElByStart(el, inline.start);
|
|
@@ -292,26 +386,30 @@ export function createWidgetInteraction(deps) {
|
|
|
292
386
|
targetStart += deps.node.raw.length - rawBefore;
|
|
293
387
|
}
|
|
294
388
|
}
|
|
295
|
-
const target =
|
|
296
|
-
isInlineWidget(n, deps.node.raw) &&
|
|
297
|
-
getInlineWidgetEditing(n.kind)?.revealSource);
|
|
389
|
+
const target = widgetsOf().find((n) => n.start === targetStart && getInlineWidgetEditing(n.kind)?.revealSource);
|
|
298
390
|
if (!target)
|
|
299
391
|
return;
|
|
300
392
|
el.focus();
|
|
301
393
|
revealOpenedByLastClick = true;
|
|
302
|
-
// A click can't map to a source glyph
|
|
303
|
-
|
|
394
|
+
// A click can't map to a source glyph, so it lands at the end of the content rather
|
|
395
|
+
// than guessing: the caret sits inside the delimiters, ready to type.
|
|
396
|
+
void startReveal(target, target.start, insideEndOffset(target));
|
|
304
397
|
}
|
|
305
|
-
/**
|
|
306
|
-
|
|
398
|
+
/**
|
|
399
|
+
* Take the whole revealed token; declines unless the double-click landed on it. Keyed off the
|
|
400
|
+
* point, not the selection: the root's second-press word select runs before this click and
|
|
401
|
+
* may have moved the selection off the source.
|
|
402
|
+
*/
|
|
403
|
+
function selectRevealedSource(x, y) {
|
|
307
404
|
const source = activeSourceNode;
|
|
308
|
-
if (!revealState || !source)
|
|
309
|
-
return false;
|
|
310
|
-
const selection = window.getSelection();
|
|
311
|
-
if (!selection || !selection.anchorNode || !source.contains(selection.anchorNode))
|
|
405
|
+
if (!revealState || !source || y === null)
|
|
312
406
|
return false;
|
|
313
407
|
const range = document.createRange();
|
|
314
408
|
range.selectNodeContents(source);
|
|
409
|
+
const onSource = Array.from(range.getClientRects()).some((r) => x >= r.left && x <= r.right && y >= r.top && y <= r.bottom);
|
|
410
|
+
const selection = window.getSelection();
|
|
411
|
+
if (!onSource || !selection)
|
|
412
|
+
return false;
|
|
315
413
|
selection.removeAllRanges();
|
|
316
414
|
selection.addRange(range);
|
|
317
415
|
return true;
|
|
@@ -411,6 +509,13 @@ export function createWidgetInteraction(deps) {
|
|
|
411
509
|
}
|
|
412
510
|
return true;
|
|
413
511
|
}
|
|
512
|
+
// A vertical arrow is the shared pipeline's line walk, which needs a real caret to read:
|
|
513
|
+
// seat one at the edge the arrow leaves from and decline, so the walk runs from there.
|
|
514
|
+
if (!e.shiftKey && (e.key === 'ArrowUp' || e.key === 'ArrowDown')) {
|
|
515
|
+
deps.cursor.setRaw(asRawOffset(e.key === 'ArrowUp' ? widget.start : widget.end));
|
|
516
|
+
deps.widgetSelection.clear();
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
414
519
|
// Reading mode still swallows — a selected widget owns its keys — but commits nothing.
|
|
415
520
|
// Undo anchored at the pre-select caret, so Ctrl+Z restores the caret where the user
|
|
416
521
|
// actually was when selection took over.
|
|
@@ -477,6 +582,26 @@ export function createWidgetInteraction(deps) {
|
|
|
477
582
|
});
|
|
478
583
|
}
|
|
479
584
|
}
|
|
585
|
+
// Only where the kind names a content span and the caret is inside it: a hard break is two
|
|
586
|
+
// bytes with a caret position between them and nothing to edit there.
|
|
587
|
+
function revealInterior(offset) {
|
|
588
|
+
const parkedByFold = foldParkedCaret === offset;
|
|
589
|
+
foldParkedCaret = null;
|
|
590
|
+
if (parkedByFold || revealState)
|
|
591
|
+
return false;
|
|
592
|
+
const widget = widgetsOf().find((w) => w.start < offset && offset < w.end);
|
|
593
|
+
if (!widget)
|
|
594
|
+
return false;
|
|
595
|
+
const editing = getInlineWidgetEditing(widget.kind);
|
|
596
|
+
if (!editing?.revealSource || !editing.revealContentSpan)
|
|
597
|
+
return false;
|
|
598
|
+
const at = offset - widget.start;
|
|
599
|
+
const span = editing.revealContentSpan(deps.node.raw.slice(widget.start, widget.end));
|
|
600
|
+
if (!span || at < span.start || at > span.end)
|
|
601
|
+
return false;
|
|
602
|
+
void startReveal(widget, offset, at);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
480
605
|
function enterEdgeWidget(side) {
|
|
481
606
|
const inlines = inlinesOf(deps.node);
|
|
482
607
|
if (inlines.length === 0)
|
|
@@ -518,14 +643,12 @@ export function createWidgetInteraction(deps) {
|
|
|
518
643
|
}
|
|
519
644
|
// The first click of a double-click already revealed, so the second lands in the source
|
|
520
645
|
// text and the browser's word rule takes `[` or `$` as a word of its own.
|
|
521
|
-
if (doubleClick && revealOpenedByLastClick && selectRevealedSource())
|
|
646
|
+
if (doubleClick && revealOpenedByLastClick && selectRevealedSource(clickX, clickY))
|
|
522
647
|
return;
|
|
523
648
|
// A click in a real text node keeps the native caret; a synthetic overlay would compete.
|
|
524
649
|
if (caretIsInTextContent(el, window.getSelection()))
|
|
525
650
|
return;
|
|
526
|
-
for (const inline of
|
|
527
|
-
if (!isInlineWidget(inline, deps.node.raw))
|
|
528
|
-
continue;
|
|
651
|
+
for (const inline of widgetsOf()) {
|
|
529
652
|
const widget = widgetElByStart(el, inline.start);
|
|
530
653
|
if (!widget)
|
|
531
654
|
continue;
|
|
@@ -549,9 +672,7 @@ export function createWidgetInteraction(deps) {
|
|
|
549
672
|
const focus = selectionFocusWalkOffset(el, deps.getAmbientLength());
|
|
550
673
|
if (focus === null)
|
|
551
674
|
return null;
|
|
552
|
-
for (const inline of
|
|
553
|
-
if (!isInlineWidget(inline, deps.node.raw))
|
|
554
|
-
continue;
|
|
675
|
+
for (const inline of widgetsOf()) {
|
|
555
676
|
if (key === 'ArrowRight' && focus >= inline.start && focus < inline.end) {
|
|
556
677
|
return inline.end;
|
|
557
678
|
}
|
|
@@ -579,6 +700,7 @@ export function createWidgetInteraction(deps) {
|
|
|
579
700
|
handleSelectedWidgetKeydown,
|
|
580
701
|
handleShiftArrowIntoWidget,
|
|
581
702
|
enterWidget,
|
|
703
|
+
revealInterior,
|
|
582
704
|
enterEdgeWidget,
|
|
583
705
|
snapClickToWidgetEdge,
|
|
584
706
|
isRevealing,
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
+
* Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
|
|
21
|
+
* first line-height of its OWN box — not its first line of text, since a card pads above that
|
|
22
|
+
* and a grip level with the first code line hangs below the card's shoulder. A declared anchor
|
|
23
|
+
* SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
|
|
24
|
+
* taller one is a card, and the band already covers it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function dragHandleAnchorY(host: HTMLElement): number | null;
|
|
27
|
+
/**
|
|
28
|
+
* Svelte attachment for the handle element: places the grip once the block has laid out, and
|
|
29
|
+
* again on every pointerover inside its host, so it follows edits made while hovered.
|
|
30
|
+
*
|
|
31
|
+
* The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
|
|
32
|
+
* target before any hover, so a target parked at the stylesheet's default while the grip would
|
|
33
|
+
* settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
|
|
34
|
+
*/
|
|
35
|
+
export declare function alignDragHandle(handle: HTMLElement): (() => void) | void;
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
/** A box within this many line-heights holds one row of content, margins included. */
|
|
45
|
+
const SINGLE_LINE = 1.5;
|
|
46
|
+
/**
|
|
47
|
+
* Vertical centre of the grip. A one-line block centres on its row. A taller one takes the
|
|
48
|
+
* first line-height of its OWN box — not its first line of text, since a card pads above that
|
|
49
|
+
* and a grip level with the first code line hangs below the card's shoulder. A declared anchor
|
|
50
|
+
* SHORTER than a line is a marker to centre on instead (a wrapped task item's checkbox); a
|
|
51
|
+
* taller one is a card, and the band already covers it.
|
|
52
|
+
*/
|
|
53
|
+
export function dragHandleAnchorY(host) {
|
|
54
|
+
const content = host.querySelector(BLOCK_CONTENT_SELECTOR);
|
|
55
|
+
if (!content)
|
|
56
|
+
return null;
|
|
57
|
+
const hostRect = host.getBoundingClientRect();
|
|
58
|
+
const line = lineHeightOf(content);
|
|
59
|
+
const rect = paintedRect(content);
|
|
60
|
+
// A one-line block IS its own band, and the eye puts the grip level with the row rather
|
|
61
|
+
// than a hair above its middle — a list item, a task row, a rule.
|
|
62
|
+
const box = rect.height <= line * SINGLE_LINE ? rect : (markerRect(content, line) ?? bandRect(rect, line));
|
|
63
|
+
if (!box || box.height === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const y = box.top + box.height / 2 - hostRect.top;
|
|
66
|
+
// A box painting outside the host (a scrolled or offscreen region) seats nothing.
|
|
67
|
+
return y >= 0 && y <= hostRect.height ? y : null;
|
|
68
|
+
}
|
|
69
|
+
function lineHeightOf(content) {
|
|
70
|
+
const line = parseFloat(getComputedStyle(content).lineHeight);
|
|
71
|
+
return Number.isFinite(line) && line > 0 ? line : 20;
|
|
72
|
+
}
|
|
73
|
+
/** The declared anchor, taken only while it is marker-sized and on the block's first line. */
|
|
74
|
+
function markerRect(content, line) {
|
|
75
|
+
const declared = content.matches(`[${DRAG_ANCHOR_ATTR}]`)
|
|
76
|
+
? content
|
|
77
|
+
: content.querySelector(`[${DRAG_ANCHOR_ATTR}]`);
|
|
78
|
+
const rect = declared?.getBoundingClientRect();
|
|
79
|
+
if (!rect || rect.height === 0 || rect.height > line)
|
|
80
|
+
return null;
|
|
81
|
+
return rect.top - content.getBoundingClientRect().top > line ? null : rect;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The box the band measures from: the content's own, unless it leads with an atomic widget
|
|
85
|
+
* (a picture in a paragraph), whose margin would otherwise seat the grip above the picture and
|
|
86
|
+
* flush with its corner.
|
|
87
|
+
*/
|
|
88
|
+
function paintedRect(content) {
|
|
89
|
+
const rect = content.getBoundingClientRect();
|
|
90
|
+
const widget = content.firstElementChild?.closest('[data-inline-widget]');
|
|
91
|
+
const widgetRect = widget?.getBoundingClientRect();
|
|
92
|
+
return widgetRect && widgetRect.height > 0 && widgetRect.top > rect.top ? widgetRect : rect;
|
|
93
|
+
}
|
|
94
|
+
/** The top line-height of a box; a box shorter than a line is its own band. */
|
|
95
|
+
function bandRect(rect, line) {
|
|
96
|
+
return new DOMRect(rect.x, rect.y, rect.width, Math.min(line, rect.height));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Svelte attachment for the handle element: places the grip once the block has laid out, and
|
|
100
|
+
* again on every pointerover inside its host, so it follows edits made while hovered.
|
|
101
|
+
*
|
|
102
|
+
* The mount placement is what makes the grip HITTABLE where it appears: it is its own hit
|
|
103
|
+
* target before any hover, so a target parked at the stylesheet's default while the grip would
|
|
104
|
+
* settle elsewhere is a target the pointer misses. One rAF-batched read per handle.
|
|
105
|
+
*/
|
|
106
|
+
export function alignDragHandle(handle) {
|
|
107
|
+
const host = handle.parentElement;
|
|
108
|
+
if (!host)
|
|
109
|
+
return;
|
|
110
|
+
const grip = handle.firstElementChild;
|
|
111
|
+
if (!grip)
|
|
112
|
+
return;
|
|
113
|
+
// Inline `top` on the grip (px from the host's top to its centre; the stylesheet's
|
|
114
|
+
// translateY(-50%) does the centring) overrides the stylesheet's half-line default.
|
|
115
|
+
const place = () => {
|
|
116
|
+
const y = dragHandleAnchorY(host);
|
|
117
|
+
if (y !== null)
|
|
118
|
+
grip.style.top = `${y}px`;
|
|
119
|
+
};
|
|
120
|
+
const settled = requestAnimationFrame(place);
|
|
121
|
+
host.addEventListener('pointerover', place);
|
|
122
|
+
return () => {
|
|
123
|
+
cancelAnimationFrame(settled);
|
|
124
|
+
host.removeEventListener('pointerover', place);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -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' | 'setScrollTop'>;
|
|
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;
|