@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
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { createPendingMarksState } from '../cursor/pending-marks';
|
|
29
29
|
import { createRevealAnchorState } from '../cursor/reveal-anchor';
|
|
30
30
|
import { createHeightOracle } from '../cursor/height-oracle';
|
|
31
|
-
import {
|
|
31
|
+
import { HEIGHT_ESTIMATES } from '../cursor/typography-estimates';
|
|
32
32
|
import {
|
|
33
33
|
clippingAncestors,
|
|
34
34
|
userScrollportFor,
|
|
@@ -37,18 +37,34 @@
|
|
|
37
37
|
import { createScrollport, type Scrollport } from '../cursor/scrollport';
|
|
38
38
|
import { createDeadSpaceCaret } from '../selection/dead-space-caret';
|
|
39
39
|
import { resetForPointerDown } from '../selection/cross-block/pointer';
|
|
40
|
+
import { installDragListener } from '../selection/drag-pointer';
|
|
40
41
|
import { createContentVersion } from '../reactivity/content-version.svelte';
|
|
41
42
|
import { useContainerWindowing } from '../reactivity/use-container-windowing.svelte';
|
|
42
43
|
import { refSlotsOver, replaceRefs, revealChildOrWait } from '../reactivity/publish-ref.svelte';
|
|
43
44
|
import { createSelectionState } from '../selection/selection-state.svelte';
|
|
44
45
|
import { createSelectionDescription } from '../selection/selection-description';
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
BLOCK_ACTIONS_LABEL,
|
|
48
|
+
BLOCK_MENU_LABEL,
|
|
49
|
+
EDITOR_LABEL,
|
|
50
|
+
movedBlockToPosition
|
|
51
|
+
} from '../a11y-strings';
|
|
52
|
+
import TailInsert from './TailInsert.svelte';
|
|
53
|
+
import BlockMenu, {
|
|
54
|
+
insertFlyoutEntries,
|
|
55
|
+
insertMenuEntries,
|
|
56
|
+
insertSnippets,
|
|
57
|
+
type MenuEntry
|
|
58
|
+
} from './menu/BlockMenu.svelte';
|
|
59
|
+
import { runClipboardAction, type ClipboardAction } from './menu/clipboard-actions';
|
|
60
|
+
import { blockContextActionsFor, type BlockContextAction } from '../schema/context-actions';
|
|
61
|
+
import { isProseBackground, registerDefaultContextActions } from './menu/default-context-actions';
|
|
46
62
|
import type { EditorSelection } from '../selection/primitives';
|
|
47
63
|
import { createWidgetSelectionState } from './image/widget-selection-state.svelte';
|
|
48
64
|
import { bootstrapCodeLanguages } from './blocks/code/code-bootstrap';
|
|
49
65
|
import { assignIds } from '../block-id';
|
|
50
66
|
import { ensureEditableContainers, emptyParagraph } from '../tree-operations';
|
|
51
|
-
import { blockNodeAt, isBlockNode, nodeAt } from '../tree-operations/node-
|
|
67
|
+
import { blockNodeAt, isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
52
68
|
import { serialize } from '../core/serializer';
|
|
53
69
|
import { parse } from '../core/parser';
|
|
54
70
|
import { defaultLinkActivation } from '../core/url-policy';
|
|
@@ -80,19 +96,26 @@
|
|
|
80
96
|
} from '../debug/interaction-trace';
|
|
81
97
|
import { readCurrentSelection } from '../selection/native-bridge';
|
|
82
98
|
import { restoreSelection, type SelectionRestoreOutcome } from '../selection/selection-restore';
|
|
83
|
-
import { findSurfacePathForElement
|
|
99
|
+
import { findSurfacePathForElement } from '../selection/path-lookup';
|
|
84
100
|
import { createCaretRestore } from '../selection/caret-restore';
|
|
85
101
|
import { createCrossBlockHandlers } from '../selection/cross-block/dispatch';
|
|
86
102
|
import { createCrossBlockCommands } from '../selection/cross-block/format-toggle';
|
|
87
|
-
import { isPreviewMode } from '../presentation-mode';
|
|
88
103
|
import { normalizeKeybindingOverrides } from '../schema/keybinding-overrides';
|
|
89
104
|
import { createEditorRootKeydown } from './editor-root-keydown';
|
|
90
105
|
import { createEditorRootClipboard } from './editor-root-clipboard';
|
|
106
|
+
import { createModeFlip } from './editor-root-mode-flip';
|
|
107
|
+
import { createFocusAttribution } from './editor-root-focus';
|
|
108
|
+
import {
|
|
109
|
+
installHeaderSlotCompensation,
|
|
110
|
+
installTypeScaleProbe,
|
|
111
|
+
installViewportHeightWatcher,
|
|
112
|
+
installWidthWatcher
|
|
113
|
+
} from './editor-root-geometry';
|
|
91
114
|
import {
|
|
115
|
+
installDoubleClickWordSelect,
|
|
92
116
|
installEditorBlurAnnouncer,
|
|
93
117
|
installModActiveTracker,
|
|
94
118
|
installSelectionChangeBridge,
|
|
95
|
-
installViewportHeightWatcher,
|
|
96
119
|
onRoot,
|
|
97
120
|
removeAll
|
|
98
121
|
} from './editor-root-listeners';
|
|
@@ -102,12 +125,8 @@
|
|
|
102
125
|
registerEditor,
|
|
103
126
|
unregisterEditor,
|
|
104
127
|
markEditorInteracted,
|
|
105
|
-
releaseInteractedEditor
|
|
106
|
-
isTextEntrySurface
|
|
128
|
+
releaseInteractedEditor
|
|
107
129
|
} from '../active-editor';
|
|
108
|
-
import { ambientLengthOf } from '../ambient/ambient-dom';
|
|
109
|
-
import { toClampedRawOffset } from '../cursor/coordinate-spaces';
|
|
110
|
-
import { domTextOffsetAtNode } from '../cursor/widget-offset';
|
|
111
130
|
import {
|
|
112
131
|
canRunCommandById,
|
|
113
132
|
isCommandActiveById,
|
|
@@ -134,12 +153,12 @@
|
|
|
134
153
|
import { runStartupInvariantChecks } from '../invariants/install';
|
|
135
154
|
import { assertInvariant } from '../assert';
|
|
136
155
|
import { checkMarkerCssParity } from '../invariants/marker-css-parity';
|
|
137
|
-
import { checkLandableCaret } from '../invariants/landable-caret';
|
|
138
156
|
import { registerBuiltInBlocks } from './built-in-blocks';
|
|
139
157
|
import { BLOCK_CONTENT_SELECTOR } from './block-content-selector';
|
|
140
158
|
|
|
141
159
|
registerBuiltInBlocks();
|
|
142
160
|
bootstrapCodeLanguages();
|
|
161
|
+
registerDefaultContextActions();
|
|
143
162
|
runStartupInvariantChecks();
|
|
144
163
|
|
|
145
164
|
// `__registryEnablement` is a harness-only door; the intersection keeps it off the
|
|
@@ -151,8 +170,10 @@
|
|
|
151
170
|
imageLoadPolicy = 'auto',
|
|
152
171
|
onLinkActivate,
|
|
153
172
|
onPasteImage,
|
|
173
|
+
onRunCode,
|
|
174
|
+
codeMenuItems,
|
|
154
175
|
header,
|
|
155
|
-
blockDragHandles =
|
|
176
|
+
blockDragHandles = true,
|
|
156
177
|
searchBar = true,
|
|
157
178
|
searchBarAnchor,
|
|
158
179
|
keybindings,
|
|
@@ -447,6 +468,189 @@
|
|
|
447
468
|
revealBlock: (index) => revealPath([index])
|
|
448
469
|
});
|
|
449
470
|
|
|
471
|
+
// ── Block menu ──────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
// Opened by the bottom `+` and by a right-click on prose with nothing selected; a right-click
|
|
474
|
+
// over a selection leaves the formatting popover (the host's) in charge and only suppresses
|
|
475
|
+
// the native menu. Tables run their own cell menu and have prevented the default first.
|
|
476
|
+
let blockMenu = $state<{
|
|
477
|
+
x: number;
|
|
478
|
+
y: number;
|
|
479
|
+
anchor: () => { x: number; y: number };
|
|
480
|
+
items: MenuEntry[];
|
|
481
|
+
label: string;
|
|
482
|
+
pick: (id: string) => void;
|
|
483
|
+
} | null>(null);
|
|
484
|
+
|
|
485
|
+
// Open/close transitions only, never the mount, so a subscriber's first news is a real menu.
|
|
486
|
+
let menuWasOpen = false;
|
|
487
|
+
$effect(() => {
|
|
488
|
+
const open = blockMenu !== null;
|
|
489
|
+
if (open === menuWasOpen) return;
|
|
490
|
+
menuWasOpen = open;
|
|
491
|
+
events.emit('menuChange', open);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
function anchorOn(el: Element, point: { x: number; y: number }): () => { x: number; y: number } {
|
|
495
|
+
const rect = el.getBoundingClientRect();
|
|
496
|
+
const dx = point.x - rect.left;
|
|
497
|
+
const dy = point.y - rect.top;
|
|
498
|
+
return () => {
|
|
499
|
+
const now = el.getBoundingClientRect();
|
|
500
|
+
return { x: now.left + dx, y: now.top + dy };
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// A right-click on a BLOCK — a fence, an equation, an image — is that block's context menu:
|
|
505
|
+
// its kind's registered actions, then the defaults. Prose is the page's background and keeps
|
|
506
|
+
// the browser's own menu (spelling, the platform's clipboard), as does a selection, whose
|
|
507
|
+
// affordance is the host's formatting popover. The margin shows nothing at all.
|
|
508
|
+
function onRootContextMenu(e: MouseEvent): void {
|
|
509
|
+
if (e.defaultPrevented || effectiveMode === 'reading' || !editorEl) return;
|
|
510
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
511
|
+
if (!target || isHostChrome(target)) return;
|
|
512
|
+
e.preventDefault();
|
|
513
|
+
// The keyboard's contextmenu event (Windows fires it on the ContextMenu key's release)
|
|
514
|
+
// lands on the item the keydown's own menu already focused: that menu is the answer.
|
|
515
|
+
if (target.closest('.md-menu')) return;
|
|
516
|
+
const host = target.closest<HTMLElement>('.block-host[data-block-path]');
|
|
517
|
+
const path = pathOf(host);
|
|
518
|
+
if (!host || !path) return;
|
|
519
|
+
const point = { x: e.clientX, y: e.clientY };
|
|
520
|
+
const native = window.getSelection();
|
|
521
|
+
const selected = !!native && !native.isCollapsed && editorEl.contains(native.anchorNode);
|
|
522
|
+
// A selection's menu is the clipboard's, over the selection as it stands. So is prose's
|
|
523
|
+
// (the page's background), at a caret placed where the press landed; a nested block (a
|
|
524
|
+
// fence inside a list item) takes the same for now.
|
|
525
|
+
const node = path.length === 1 ? doc.children[path[0]] : undefined;
|
|
526
|
+
if (selected || !node || isProseBackground(node)) {
|
|
527
|
+
if (!selected) placeCaretAtPoint(e.clientX, e.clientY);
|
|
528
|
+
// Top-level prose is where a sibling block makes sense; a nested block or a selection
|
|
529
|
+
// gets the clipboard alone.
|
|
530
|
+
const insertAfter = !selected && node && isProseBackground(node) ? path[0] : null;
|
|
531
|
+
openClipboardMenu(point, host, insertAfter);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const index = path[0];
|
|
535
|
+
const actions = blockContextActionsFor(node, path);
|
|
536
|
+
if (actions.length === 0) return;
|
|
537
|
+
const ctx = {
|
|
538
|
+
node,
|
|
539
|
+
path,
|
|
540
|
+
deleteBlock: async () => {
|
|
541
|
+
await blockEdit.deleteBlock(index);
|
|
542
|
+
},
|
|
543
|
+
replaceRaw: async (raw: string) => {
|
|
544
|
+
await blockEdit.updateBlockContent(index, raw);
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
blockMenu = {
|
|
548
|
+
...point,
|
|
549
|
+
anchor: anchorOn(host, point),
|
|
550
|
+
items: actions.map(({ id, label, icon, danger }) => ({
|
|
551
|
+
id,
|
|
552
|
+
label,
|
|
553
|
+
icon: icon as MenuEntry['icon'],
|
|
554
|
+
danger
|
|
555
|
+
})),
|
|
556
|
+
label: BLOCK_ACTIONS_LABEL,
|
|
557
|
+
pick: (id) => {
|
|
558
|
+
blockMenu = null;
|
|
559
|
+
const action = actions.find((a: BlockContextAction) => a.id === id);
|
|
560
|
+
if (action) void action.run(ctx);
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// The editing surface a paste goes to: whatever editable holds focus inside the root.
|
|
566
|
+
function focusedEditable(): HTMLElement | null {
|
|
567
|
+
const active = document.activeElement;
|
|
568
|
+
return active instanceof HTMLElement && editorEl?.contains(active) && active.isContentEditable
|
|
569
|
+
? active
|
|
570
|
+
: null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function clipboardRows(): MenuEntry[] {
|
|
574
|
+
const selected = !!focusedEditable() && !(window.getSelection()?.isCollapsed ?? true);
|
|
575
|
+
return [
|
|
576
|
+
{ id: 'clip.cut', label: 'Cut', icon: 'scissors', disabled: !selected },
|
|
577
|
+
{ id: 'clip.copy', label: 'Copy', icon: 'copy', disabled: !selected },
|
|
578
|
+
{ id: 'clip.paste', label: 'Paste', icon: 'clipboard' },
|
|
579
|
+
{ id: 'clip.paste-plain', label: 'Paste as plain text', icon: 'type' }
|
|
580
|
+
];
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function runClipboardRow(id: string): boolean {
|
|
584
|
+
if (!id.startsWith('clip.')) return false;
|
|
585
|
+
void runClipboardAction(id.slice('clip.'.length) as ClipboardAction, focusedEditable());
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** `insertAfter` names the top-level block an "Insert block" flyout mints an empty sibling
|
|
590
|
+
* after; null leaves the menu to the clipboard rows alone. */
|
|
591
|
+
function openClipboardMenu(
|
|
592
|
+
point: { x: number; y: number },
|
|
593
|
+
anchorEl: Element,
|
|
594
|
+
insertAfter: number | null = null
|
|
595
|
+
): void {
|
|
596
|
+
const insert: MenuEntry[] =
|
|
597
|
+
insertAfter === null
|
|
598
|
+
? []
|
|
599
|
+
: [
|
|
600
|
+
{ id: 'sep', label: '', divider: true },
|
|
601
|
+
{ id: 'insert', label: 'Insert block', icon: 'plus', children: insertFlyoutEntries() }
|
|
602
|
+
];
|
|
603
|
+
blockMenu = {
|
|
604
|
+
...point,
|
|
605
|
+
anchor: anchorOn(anchorEl, point),
|
|
606
|
+
items: [...clipboardRows(), ...insert],
|
|
607
|
+
label: BLOCK_ACTIONS_LABEL,
|
|
608
|
+
pick: (id) => {
|
|
609
|
+
blockMenu = null;
|
|
610
|
+
if (runClipboardRow(id)) return;
|
|
611
|
+
const md = insertSnippets().get(id);
|
|
612
|
+
if (md && insertAfter !== null) void insertBlockAfter(insertAfter, md);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// The same two steps the tail's `+` takes: mint the empty paragraph, which lands the caret in
|
|
618
|
+
// it, then hand the snippet to the surface that now holds focus.
|
|
619
|
+
async function insertBlockAfter(index: number, md: string): Promise<void> {
|
|
620
|
+
await blockEdit.insertParagraph(index + 1, '');
|
|
621
|
+
insertMarkdown(md);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function pathOf(host: HTMLElement | null): number[] | null {
|
|
625
|
+
const raw = host?.dataset.blockPath;
|
|
626
|
+
if (!raw) return null;
|
|
627
|
+
try {
|
|
628
|
+
const parsed: unknown = JSON.parse(raw);
|
|
629
|
+
return Array.isArray(parsed) && parsed.every((n) => typeof n === 'number') ? parsed : null;
|
|
630
|
+
} catch {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
async function onTailPlus(button: HTMLElement): Promise<void> {
|
|
636
|
+
await blockEdit.insertParagraph(doc.children.length, '');
|
|
637
|
+
const rect = button.getBoundingClientRect();
|
|
638
|
+
const point = { x: rect.left, y: rect.bottom + 4 };
|
|
639
|
+
const snippets = insertSnippets();
|
|
640
|
+
blockMenu = {
|
|
641
|
+
...point,
|
|
642
|
+
anchor: anchorOn(button, point),
|
|
643
|
+
items: [...insertMenuEntries(), { id: 'sep', label: '', divider: true }, ...clipboardRows()],
|
|
644
|
+
label: BLOCK_MENU_LABEL,
|
|
645
|
+
pick: (id) => {
|
|
646
|
+
blockMenu = null;
|
|
647
|
+
if (runClipboardRow(id)) return;
|
|
648
|
+
const md = snippets.get(id);
|
|
649
|
+
if (md) insertMarkdown(md);
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
450
654
|
// ── Link card door ──────────────────────────────────────────────────
|
|
451
655
|
|
|
452
656
|
// The caret snapshot and the entry guards ride the STATE, not the callers, so entry path N+1
|
|
@@ -509,9 +713,27 @@
|
|
|
509
713
|
}
|
|
510
714
|
const anchor = target?.closest('a[href]') as HTMLAnchorElement | null;
|
|
511
715
|
// The helper claims only clicks whose target IS the root, so nothing the
|
|
512
|
-
// editor renders is touched.
|
|
716
|
+
// editor renders is touched. A margin drag's release arrives as a root click too (the
|
|
717
|
+
// press and release targets meet at the root); with a range just painted, it is no
|
|
718
|
+
// click to answer.
|
|
513
719
|
if (!anchor) {
|
|
514
|
-
|
|
720
|
+
const pressed = marginDrag;
|
|
721
|
+
const dragged =
|
|
722
|
+
pressed &&
|
|
723
|
+
(Math.abs(e.clientX - marginDown.x) > 3 || Math.abs(e.clientY - marginDown.y) > 3);
|
|
724
|
+
marginDrag = false;
|
|
725
|
+
if (dragged) return;
|
|
726
|
+
if (deadSpaceCaret.handleClick(root, e)) return;
|
|
727
|
+
// A press the editor took on a block (a host's own padding beside a table, a rule, a
|
|
728
|
+
// folded equation's face) that did not move is a click on it: the click helper
|
|
729
|
+
// claims only dead space, so the same landing is resolved here.
|
|
730
|
+
if (pressed && !deadSpaceCaret.isDeadSpaceTarget(root, e.target)) {
|
|
731
|
+
if (placeCaretAtPoint(e.clientX, e.clientY)) return;
|
|
732
|
+
}
|
|
733
|
+
// Declined everywhere: a click on nothing still LEAVES what was being edited (a
|
|
734
|
+
// revealed equation folds on blur), since the margin press suppressed the blur the
|
|
735
|
+
// browser would have done.
|
|
736
|
+
if (pressed) blurEditingSurface(root);
|
|
515
737
|
return;
|
|
516
738
|
}
|
|
517
739
|
// Host chrome follows the page's link behaviour, not plain-click-edits.
|
|
@@ -529,9 +751,62 @@
|
|
|
529
751
|
e.preventDefault();
|
|
530
752
|
}
|
|
531
753
|
};
|
|
754
|
+
// Dead space, and the parts of a block that are neither editable nor controls: a rendered
|
|
755
|
+
// equation, a diagram, a card's rendered face. A press on any of them cannot grow a native
|
|
756
|
+
// selection, so the editor's drag runs from it.
|
|
757
|
+
// A whole-block input proxy is an editable in name only (it catches IME for a block with no
|
|
758
|
+
// text), so a press on it starts the editor's drag like a press on the block itself.
|
|
759
|
+
const NOT_A_DRAG_START =
|
|
760
|
+
'[contenteditable="true"]:not([data-whole-block-input]), button, input, textarea, select, ' +
|
|
761
|
+
'a, summary, [role="checkbox"], ' +
|
|
762
|
+
'.code-rail, .table-add-zone, .editor-tail, .md-menu, .block-drag-handle';
|
|
763
|
+
const dragStartsHere = (rootEl: HTMLElement, target: EventTarget | null): boolean => {
|
|
764
|
+
if (deadSpaceCaret.isDeadSpaceTarget(rootEl, target)) return true;
|
|
765
|
+
if (!(target instanceof Element) || !rootEl.contains(target)) return false;
|
|
766
|
+
return target.closest(NOT_A_DRAG_START) === null;
|
|
767
|
+
};
|
|
768
|
+
// A drag that STARTS in the margin: the browser cannot grow a selection from a
|
|
769
|
+
// non-editable press into an editable block, so the editor runs the drag itself, anchored
|
|
770
|
+
// where a click there would land. The mousedown's default is suppressed so the browser
|
|
771
|
+
// starts no selection of its own to fight it; `click` still fires for the caret placement.
|
|
772
|
+
let marginDrag = false;
|
|
773
|
+
let marginDown = { x: 0, y: 0 };
|
|
774
|
+
const startMarginDrag = (e: PointerEvent) => {
|
|
775
|
+
marginDrag = false;
|
|
776
|
+
marginDown = { x: e.clientX, y: e.clientY };
|
|
777
|
+
if (e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
778
|
+
if (effectiveMode === 'reading' || !dragStartsHere(root, e.target)) return;
|
|
779
|
+
const anchor = deadSpaceCaret.anchorAtPoint(root, e.clientX, e.clientY);
|
|
780
|
+
if (!anchor) return;
|
|
781
|
+
marginDrag = true;
|
|
782
|
+
resetForPointerDown(selectionState, stickyColumn, edgeAffinity, false);
|
|
783
|
+
// A block that runs its own drag from a nearby press (a table's cell rectangle) takes
|
|
784
|
+
// it; the generic drag is for blocks that have none.
|
|
785
|
+
if (!('offset' in anchor)) {
|
|
786
|
+
const component = getBlockComponent(anchor.path);
|
|
787
|
+
if (component?.startDragAtPoint?.(e.clientX, e.clientY, e)) return;
|
|
788
|
+
}
|
|
789
|
+
installDragListener(
|
|
790
|
+
{
|
|
791
|
+
editorRoot: root,
|
|
792
|
+
scrollContainer: getScrollHost() ?? root,
|
|
793
|
+
selection: selectionState,
|
|
794
|
+
getBlockElByPath,
|
|
795
|
+
lifetimeSignal: lifetimeController.signal,
|
|
796
|
+
paintSameBlock: true
|
|
797
|
+
},
|
|
798
|
+
anchor,
|
|
799
|
+
e
|
|
800
|
+
);
|
|
801
|
+
};
|
|
532
802
|
return removeAll(
|
|
533
803
|
onRoot(root, 'click', handleClick),
|
|
534
|
-
|
|
804
|
+
installDoubleClickWordSelect(root),
|
|
805
|
+
onRoot(root, 'pointerdown', startMarginDrag),
|
|
806
|
+
onRoot(root, 'mousedown', (e: MouseEvent) => {
|
|
807
|
+
deadSpaceCaret.notePress(root, e);
|
|
808
|
+
if (marginDrag) e.preventDefault();
|
|
809
|
+
})
|
|
535
810
|
);
|
|
536
811
|
});
|
|
537
812
|
|
|
@@ -862,8 +1137,7 @@
|
|
|
862
1137
|
resolveImageUrl: resolveImageUrlImpl,
|
|
863
1138
|
resolveLinkUrl: resolveLinkUrlImpl,
|
|
864
1139
|
imageLoadPolicy: () => imageLoadPolicy,
|
|
865
|
-
// Reading mode forces the
|
|
866
|
-
// and the table grips all read this one getter.
|
|
1140
|
+
// Reading mode forces the affordance off through the prop's own funnel.
|
|
867
1141
|
blockDragHandles: () => blockDragHandles && effectiveMode !== 'reading',
|
|
868
1142
|
presentationMode: () => effectiveMode,
|
|
869
1143
|
theme: () => theme,
|
|
@@ -874,89 +1148,45 @@
|
|
|
874
1148
|
get onPasteImage() {
|
|
875
1149
|
return onPasteImage;
|
|
876
1150
|
},
|
|
1151
|
+
// Accessors for the same reason as onPasteImage above.
|
|
1152
|
+
get onRunCode() {
|
|
1153
|
+
return onRunCode;
|
|
1154
|
+
},
|
|
1155
|
+
get codeMenuItems() {
|
|
1156
|
+
return codeMenuItems;
|
|
1157
|
+
},
|
|
877
1158
|
brokenImageUrls
|
|
878
1159
|
} satisfies EditorPolicies);
|
|
879
1160
|
|
|
880
1161
|
// ── Root DOM effects ────────────────────────────────────────────────
|
|
881
1162
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// The flip's PRE phase, the last moment the outgoing mode owns the DOM: past it the mode's
|
|
905
|
-
// render key has rebuilt every block from its own CST bytes, and the reveal's ephemeral edit
|
|
906
|
-
// is gone. Reading keeps its entry snapshot — no caret of its own to recapture on the way out.
|
|
907
|
-
let flipCaret: { path: number[]; offset: number } | null = null;
|
|
908
|
-
// svelte-ignore state_referenced_locally
|
|
909
|
-
let preFlipSeenMode = effectiveMode;
|
|
1163
|
+
// The pre half runs while the outgoing mode still owns the DOM, the post half after the
|
|
1164
|
+
// mode's render key has rebuilt every block; the factory carries the caret across the gap.
|
|
1165
|
+
const modeFlip = createModeFlip({
|
|
1166
|
+
get editorEl() {
|
|
1167
|
+
return editorEl;
|
|
1168
|
+
},
|
|
1169
|
+
get mode() {
|
|
1170
|
+
return effectiveMode;
|
|
1171
|
+
},
|
|
1172
|
+
selection: selectionState,
|
|
1173
|
+
getSelection,
|
|
1174
|
+
getBlockElByPath,
|
|
1175
|
+
isHostChrome,
|
|
1176
|
+
edgeAffinity,
|
|
1177
|
+
// Built below; a flip runs post-init, so the getter reads past the TDZ.
|
|
1178
|
+
get heightOracle() {
|
|
1179
|
+
return heightOracle;
|
|
1180
|
+
},
|
|
1181
|
+
events,
|
|
1182
|
+
restoreCaret: (path, offset) => restoreThroughRevealRoad(caretAt(path, offset), 'mount')
|
|
1183
|
+
});
|
|
910
1184
|
$effect.pre(() => {
|
|
911
1185
|
const mode = effectiveMode;
|
|
912
|
-
|
|
913
|
-
const from = preFlipSeenMode;
|
|
914
|
-
preFlipSeenMode = mode;
|
|
915
|
-
untrack(() => {
|
|
916
|
-
if (from !== 'reading') flipCaret = captureFlipCaret();
|
|
917
|
-
// A flip is a blur-class event: a live reveal or composition folds through the existing
|
|
918
|
-
// blur choke point. Host chrome is exempt, or a mode toggle blurs a title field mid-edit.
|
|
919
|
-
const active = document.activeElement;
|
|
920
|
-
if (active instanceof HTMLElement && editorEl?.contains(active) && !isHostChrome(active)) {
|
|
921
|
-
active.blur();
|
|
922
|
-
// A blur the editor performs announces the selection it drops: the document listener
|
|
923
|
-
// only reports a range the browser still anchors in the root, and this one is gone.
|
|
924
|
-
events.emit('selectionChange', getSelection());
|
|
925
|
-
}
|
|
926
|
-
});
|
|
1186
|
+
untrack(() => modeFlip.beforeFlip(mode));
|
|
927
1187
|
});
|
|
928
|
-
|
|
929
|
-
// svelte-ignore state_referenced_locally
|
|
930
|
-
let lastEffectiveMode = effectiveMode;
|
|
931
1188
|
$effect(() => {
|
|
932
|
-
|
|
933
|
-
if (mode === lastEffectiveMode) return;
|
|
934
|
-
lastEffectiveMode = mode;
|
|
935
|
-
// Which markers paint just changed, so a side recorded against the old geometry
|
|
936
|
-
// no longer names the offset the user meant.
|
|
937
|
-
edgeAffinity.reset();
|
|
938
|
-
// Hidden markers re-wrap prose, so every measured height is the other mode's. Not paired
|
|
939
|
-
// with `widthVersion++`: the flip has blurred, so a rebuild here loses the caret pin.
|
|
940
|
-
heightOracle.dropMeasured();
|
|
941
|
-
if (mode === 'reading') {
|
|
942
|
-
// The gap is an editor-owned caret no DOM blur can reach, so the flip clears it
|
|
943
|
-
// rather than each arrival path.
|
|
944
|
-
selectionState.clearGapCaret();
|
|
945
|
-
} else if (flipCaret) {
|
|
946
|
-
const caret = flipCaret;
|
|
947
|
-
flipCaret = null;
|
|
948
|
-
void (async () => {
|
|
949
|
-
// A post-tick focus like every structural op's; the road clamps the saved offset
|
|
950
|
-
// into the destination mode's landable range at the door. Yielding to a focused
|
|
951
|
-
// text-entry surface keeps the restore from stealing a host field mid-typing.
|
|
952
|
-
// The reveal is the bare MOUNT: a flip is a view operation, so it re-seats the
|
|
953
|
-
// caret without writing the scrollport the reader chose.
|
|
954
|
-
await tick();
|
|
955
|
-
if (effectiveMode !== mode || isTextEntrySurface(document.activeElement)) return;
|
|
956
|
-
await restoreThroughRevealRoad(caretAt(caret.path, caret.offset), 'mount');
|
|
957
|
-
})();
|
|
958
|
-
}
|
|
959
|
-
events.emit('presentationModeChange', mode);
|
|
1189
|
+
modeFlip.afterFlip(effectiveMode);
|
|
960
1190
|
});
|
|
961
1191
|
|
|
962
1192
|
// A theme flip invalidates no live edit, so it only has to be announced — for
|
|
@@ -1151,22 +1381,14 @@
|
|
|
1151
1381
|
// ── Resize invalidation ─────────────────────────────────────────────
|
|
1152
1382
|
|
|
1153
1383
|
// A WIDTH change re-wraps prose, staling every cached height, so the scopes rebuild
|
|
1154
|
-
// off this counter; a height-only resize spares the measured cache.
|
|
1155
|
-
// per-callback batching is the coalescing — no setTimeout/rAF debounce (G4.4).
|
|
1384
|
+
// off this counter; a height-only resize spares the measured cache.
|
|
1156
1385
|
let widthVersion = $state(0);
|
|
1157
1386
|
$effect(() => {
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
let lastWidth = el.clientWidth;
|
|
1161
|
-
const observer = new ResizeObserver(() => {
|
|
1162
|
-
const width = el.clientWidth;
|
|
1163
|
-
if (width === lastWidth) return;
|
|
1164
|
-
lastWidth = width;
|
|
1387
|
+
if (!editorEl) return;
|
|
1388
|
+
return installWidthWatcher(editorEl, () => {
|
|
1165
1389
|
heightOracle.dropMeasured();
|
|
1166
1390
|
widthVersion++;
|
|
1167
1391
|
});
|
|
1168
|
-
observer.observe(el);
|
|
1169
|
-
return () => observer.disconnect();
|
|
1170
1392
|
});
|
|
1171
1393
|
|
|
1172
1394
|
// The slice's own axis, watched on the RESOLVED port: the window's extent comes from the
|
|
@@ -1183,117 +1405,51 @@
|
|
|
1183
1405
|
|
|
1184
1406
|
// ── Type scale ──────────────────────────────────────────────────────
|
|
1185
1407
|
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
// all. Only the off-window set depends on the ESTIMATE moving. The width observer
|
|
1189
|
-
// can't see this (no other box in the root resizes), hence the `1em` probe.
|
|
1190
|
-
function applyTypeScale(fontSizePx: number): void {
|
|
1191
|
-
const next = fontSizePx / ESTIMATE_BASE_FONT_SIZE;
|
|
1192
|
-
// Sub-percent moves are sub-pixel on a line box — not worth a full rebuild.
|
|
1193
|
-
if (!(next > 0) || Math.abs(next - typeScale) < 0.01) return;
|
|
1194
|
-
typeScale = next;
|
|
1195
|
-
heightOracle.dropMeasured();
|
|
1196
|
-
widthVersion++;
|
|
1197
|
-
}
|
|
1408
|
+
// A font-size move puts the estimates off several-fold, which no other box in the root
|
|
1409
|
+
// reports; the probe's scale rides the width signal, since a rebuild is what it needs.
|
|
1198
1410
|
$effect(() => {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1411
|
+
if (!typeScaleProbeEl) return;
|
|
1412
|
+
return installTypeScaleProbe(typeScaleProbeEl, {
|
|
1413
|
+
getScale: () => typeScale,
|
|
1414
|
+
onScale: (next) => {
|
|
1415
|
+
typeScale = next;
|
|
1416
|
+
heightOracle.dropMeasured();
|
|
1417
|
+
widthVersion++;
|
|
1418
|
+
}
|
|
1205
1419
|
});
|
|
1206
|
-
observer.observe(el);
|
|
1207
|
-
return () => observer.disconnect();
|
|
1208
1420
|
});
|
|
1209
1421
|
|
|
1210
1422
|
// ── Header slot compensation ────────────────────────────────────────
|
|
1211
1423
|
|
|
1212
|
-
// The header slot's height lives outside the height model, so while the editor owns the
|
|
1213
|
-
// correction a growing header would slide the document under the reader. Compensating from
|
|
1214
|
-
// the SLOT's own resize is what composes with `correctAnchor` instead of double-correcting;
|
|
1215
|
-
// a reveal already holding the scroll outranks it.
|
|
1216
1424
|
$effect(() => {
|
|
1217
1425
|
const el = headerEl;
|
|
1218
1426
|
if (!el || !editorEl) return;
|
|
1219
1427
|
const port = getScrollport();
|
|
1220
1428
|
if (!port) return;
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const height = box ? box.blockSize : el.getBoundingClientRect().height;
|
|
1227
|
-
const delta = height - lastHeight;
|
|
1228
|
-
lastHeight = height;
|
|
1229
|
-
if (delta === 0 || !ownsScrollCorrection() || port.scrollTop() === 0) return;
|
|
1230
|
-
if (!topWindowing.revealHoldsScroll()) port.setScrollTop(port.scrollTop() + delta);
|
|
1429
|
+
return installHeaderSlotCompensation({
|
|
1430
|
+
el,
|
|
1431
|
+
port,
|
|
1432
|
+
ownsScrollCorrection,
|
|
1433
|
+
revealHoldsScroll: () => topWindowing.revealHoldsScroll()
|
|
1231
1434
|
});
|
|
1232
|
-
observer.observe(el);
|
|
1233
|
-
return () => observer.disconnect();
|
|
1234
1435
|
});
|
|
1235
1436
|
|
|
1236
1437
|
// ── Focus attribution ───────────────────────────────────────────────
|
|
1237
1438
|
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// state_unsafe_mutation.
|
|
1242
|
-
let focusedPath: number[] | null = null;
|
|
1243
|
-
|
|
1244
|
-
// Marks the block host whose leaf holds the caret; both preview modes' CSS key their
|
|
1245
|
-
// focused-block reveal off it. Imperative for focusedPath's teardown-safety reason,
|
|
1246
|
-
// and mode-gated so source/reading DOM stays byte-identical.
|
|
1247
|
-
let focusedHostEl: HTMLElement | null = null;
|
|
1248
|
-
function applyFocusedAttr(): void {
|
|
1249
|
-
if (focusedHostEl && isPreviewMode(effectiveMode)) {
|
|
1250
|
-
focusedHostEl.setAttribute('data-focused', '');
|
|
1251
|
-
} else {
|
|
1252
|
-
focusedHostEl?.removeAttribute('data-focused');
|
|
1439
|
+
const focusAttribution = createFocusAttribution({
|
|
1440
|
+
get mode() {
|
|
1441
|
+
return effectiveMode;
|
|
1253
1442
|
}
|
|
1254
|
-
}
|
|
1255
|
-
function setFocusedHost(host: HTMLElement | null): void {
|
|
1256
|
-
if (focusedHostEl === host) return;
|
|
1257
|
-
focusedHostEl?.removeAttribute('data-focused');
|
|
1258
|
-
focusedHostEl = host;
|
|
1259
|
-
applyFocusedAttr();
|
|
1260
|
-
}
|
|
1261
|
-
// Entering a preview mode marks the already-focused block, since no re-focus fires.
|
|
1443
|
+
});
|
|
1262
1444
|
$effect(() => {
|
|
1263
1445
|
void effectiveMode;
|
|
1264
|
-
|
|
1446
|
+
focusAttribution.applyForMode();
|
|
1265
1447
|
});
|
|
1266
|
-
|
|
1267
1448
|
$effect(() => {
|
|
1268
1449
|
if (!editorEl) return;
|
|
1269
|
-
|
|
1270
|
-
const onFocusIn = (e: FocusEvent) => {
|
|
1271
|
-
const host = (e.target as Element | null)?.closest('[data-block-path]');
|
|
1272
|
-
if (!host || !root.contains(host)) {
|
|
1273
|
-
focusedPath = null;
|
|
1274
|
-
setFocusedHost(null);
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
setFocusedHost(host as HTMLElement);
|
|
1278
|
-
const path = readBlockPath(host);
|
|
1279
|
-
focusedPath = path && path.length > 0 ? path : null;
|
|
1280
|
-
// G1.33 at the seam every caret door crosses: a door seats a caret by focusing the
|
|
1281
|
-
// surface, whoever minted it, so a consumer's own door inherits the guard here.
|
|
1282
|
-
const landed = e.target;
|
|
1283
|
-
if (landed instanceof HTMLElement) {
|
|
1284
|
-
assertInvariant('landable-caret', () =>
|
|
1285
|
-
checkLandableCaret(landed, effectiveMode, path ?? [])
|
|
1286
|
-
);
|
|
1287
|
-
}
|
|
1288
|
-
};
|
|
1289
|
-
const onFocusOut = (e: FocusEvent) => {
|
|
1290
|
-
const next = e.relatedTarget as Node | null;
|
|
1291
|
-
if (next && root.contains(next)) return; // moving between blocks — keep the pin
|
|
1292
|
-
focusedPath = null;
|
|
1293
|
-
setFocusedHost(null);
|
|
1294
|
-
};
|
|
1295
|
-
return removeAll(onRoot(root, 'focusin', onFocusIn), onRoot(root, 'focusout', onFocusOut));
|
|
1450
|
+
return focusAttribution.install(editorEl);
|
|
1296
1451
|
});
|
|
1452
|
+
|
|
1297
1453
|
// ── Top-level windowing ─────────────────────────────────────────────
|
|
1298
1454
|
|
|
1299
1455
|
// Assembled here, after the windowing signals it carries exist; the block
|
|
@@ -1308,7 +1464,7 @@
|
|
|
1308
1464
|
scrollHost: getScrollHost,
|
|
1309
1465
|
scrollport: getScrollport,
|
|
1310
1466
|
blockElLookup: getBlockElByPath,
|
|
1311
|
-
focusedPath:
|
|
1467
|
+
focusedPath: focusAttribution.getFocusedPath,
|
|
1312
1468
|
heightOracle,
|
|
1313
1469
|
correctsScroll: ownsScrollCorrection,
|
|
1314
1470
|
widthVersion: () => widthVersion,
|
|
@@ -1416,6 +1572,11 @@
|
|
|
1416
1572
|
* focused surface. A gap caret declines: its proxy is not a block, and a NESTED gap's proxy
|
|
1417
1573
|
* sits inside its container's host, which must not receive what was aimed at the gap.
|
|
1418
1574
|
*/
|
|
1575
|
+
function blurEditingSurface(root: HTMLElement): void {
|
|
1576
|
+
const active = document.activeElement;
|
|
1577
|
+
if (active instanceof HTMLElement && root.contains(active) && active !== root) active.blur();
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1419
1580
|
function focusedSurfacePath(): number[] | null {
|
|
1420
1581
|
if (selectionState.gapCaret) return null;
|
|
1421
1582
|
const active = document.activeElement;
|
|
@@ -1460,10 +1621,10 @@
|
|
|
1460
1621
|
|
|
1461
1622
|
// The door only resolves the focused surface; every rule (the reading gate, the cross-block
|
|
1462
1623
|
// range decline, the arms themselves) lives below the seam. See `editor-props.ts`.
|
|
1463
|
-
export function runCommand(commandId: string): boolean {
|
|
1624
|
+
export function runCommand(commandId: string, arg?: unknown): boolean {
|
|
1464
1625
|
return runCommandById(
|
|
1465
1626
|
commandId as AnyCommandId,
|
|
1466
|
-
|
|
1627
|
+
arg,
|
|
1467
1628
|
focusedCommandTarget(),
|
|
1468
1629
|
commandDispatchContext,
|
|
1469
1630
|
commandErrorSink
|
|
@@ -1632,6 +1793,7 @@
|
|
|
1632
1793
|
tabindex="-1"
|
|
1633
1794
|
role="group"
|
|
1634
1795
|
aria-label={EDITOR_LABEL}
|
|
1796
|
+
oncontextmenu={onRootContextMenu}
|
|
1635
1797
|
>
|
|
1636
1798
|
{#if searchBar}
|
|
1637
1799
|
<!-- Zero-height sticky anchor, so the bar doesn't scroll away with content. Portaled
|
|
@@ -1665,6 +1827,24 @@
|
|
|
1665
1827
|
window={topWindowing.window}
|
|
1666
1828
|
reorderable={true}
|
|
1667
1829
|
/>
|
|
1830
|
+
<!-- A sibling of the list like the header: the windowing scope wants the list bare. -->
|
|
1831
|
+
<TailInsert
|
|
1832
|
+
{blockEdit}
|
|
1833
|
+
childCount={doc.children.length}
|
|
1834
|
+
readOnly={effectiveMode === 'reading'}
|
|
1835
|
+
onPlus={(button) => void onTailPlus(button)}
|
|
1836
|
+
/>
|
|
1837
|
+
{#if blockMenu}
|
|
1838
|
+
<BlockMenu
|
|
1839
|
+
x={blockMenu.x}
|
|
1840
|
+
y={blockMenu.y}
|
|
1841
|
+
anchor={blockMenu.anchor}
|
|
1842
|
+
items={blockMenu.items}
|
|
1843
|
+
label={blockMenu.label}
|
|
1844
|
+
onPick={blockMenu.pick}
|
|
1845
|
+
onClose={() => (blockMenu = null)}
|
|
1846
|
+
/>
|
|
1847
|
+
{/if}
|
|
1668
1848
|
<ImageOverlayHost
|
|
1669
1849
|
{widgetSelection}
|
|
1670
1850
|
{controller}
|
|
@@ -1709,7 +1889,9 @@
|
|
|
1709
1889
|
.editor {
|
|
1710
1890
|
width: 100%;
|
|
1711
1891
|
flex: 1;
|
|
1712
|
-
|
|
1892
|
+
/* Wider on the left: the drag grip lives in that gutter, 1.25rem out from the content,
|
|
1893
|
+
and the rest of this padding is what keeps it off the scroll container's border. */
|
|
1894
|
+
padding: 1rem 1rem 1rem 1.5rem;
|
|
1713
1895
|
font-family: var(--font-editor, ui-monospace, monospace);
|
|
1714
1896
|
/* The type-scale root: every construct sizes in `em` off this, so one
|
|
1715
1897
|
declaration scales the whole surface. */
|
|
@@ -1724,9 +1906,10 @@
|
|
|
1724
1906
|
double-corrects. Do NOT restore `overflow-anchor` (VR-2). */
|
|
1725
1907
|
overflow-anchor: none;
|
|
1726
1908
|
scrollbar-width: thin;
|
|
1727
|
-
scrollbar-color: var(--color-
|
|
1728
|
-
|
|
1729
|
-
|
|
1909
|
+
scrollbar-color: var(--color-border, #3e3e3b) transparent;
|
|
1910
|
+
/* No box of its own. A document is the page's content, not a widget sitting on it, and
|
|
1911
|
+
an outline around the whole editor reads as a form field the moment the host gives it
|
|
1912
|
+
a column. A host that wants the frame draws it on its own container. */
|
|
1730
1913
|
/* Containing block for the image overlay portal. */
|
|
1731
1914
|
position: relative;
|
|
1732
1915
|
}
|