@voithos-labs/aragonite 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -21
- package/THIRD-PARTY-NOTICES.md +25 -0
- package/dist/a11y-strings.d.ts +19 -0
- package/dist/a11y-strings.js +19 -0
- package/dist/action-contracts.d.ts +7 -1
- package/dist/ambient/ambient-dom.js +5 -1
- package/dist/block-component.d.ts +14 -0
- package/dist/components/BlockDragHandle.svelte +37 -24
- package/dist/components/BlockHost.svelte +18 -11
- package/dist/components/Editor.svelte +434 -198
- package/dist/components/Editor.svelte.d.ts +1 -1
- package/dist/components/SelectionOverlay.svelte +25 -17
- package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
- package/dist/components/TailInsert.svelte +107 -0
- package/dist/components/TailInsert.svelte.d.ts +17 -0
- package/dist/components/block-content-selector.d.ts +6 -2
- package/dist/components/block-content-selector.js +6 -2
- package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
- package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/code/CodeBlock.svelte +211 -30
- package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
- package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
- package/dist/components/blocks/code/code-bootstrap.js +16 -0
- package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
- package/dist/components/blocks/code/code-context-actions.js +24 -0
- package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
- package/dist/components/blocks/code/code-fence-exit.js +26 -0
- package/dist/components/blocks/code/code-languages.d.ts +6 -0
- package/dist/components/blocks/code/code-languages.js +22 -3
- package/dist/components/blocks/code/code-renderer.js +11 -0
- package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
- package/dist/components/blocks/editable-leaf.d.ts +37 -6
- package/dist/components/blocks/editable-leaf.js +248 -30
- package/dist/components/blocks/editable-surface.d.ts +9 -0
- package/dist/components/blocks/editable-surface.js +22 -3
- package/dist/components/blocks/list/ListBlock.svelte +1 -0
- package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
- package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
- package/dist/components/blocks/list/task-checkbox.js +11 -2
- package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
- package/dist/components/blocks/surface-wiring.svelte.js +8 -1
- package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
- package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
- package/dist/components/blocks/table/TableBlock.svelte +205 -186
- package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
- package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
- package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
- package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
- package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
- package/dist/components/blocks/table/cell-clipboard.js +34 -1
- package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
- package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
- package/dist/components/blocks/table/table-cell-paste.js +2 -1
- package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
- package/dist/components/blocks/table/table-menu-model.js +36 -17
- package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
- package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
- package/dist/components/blocks/text/click-snap-guard.js +8 -0
- package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
- package/dist/components/blocks/text/delimiter-autopair.js +216 -0
- package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
- package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
- package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
- package/dist/components/blocks/text/live-selection-edit.js +37 -7
- package/dist/components/blocks/text/text-clipboard.js +2 -2
- package/dist/components/blocks/text/text-keydown.d.ts +7 -1
- package/dist/components/blocks/text/text-keydown.js +11 -1
- package/dist/components/blocks/text/text-render.d.ts +1 -1
- package/dist/components/blocks/text/text-render.js +3 -1
- package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
- package/dist/components/blocks/text/widget-interaction.js +181 -37
- package/dist/components/drag-handle.d.ts +41 -0
- package/dist/components/drag-handle.js +134 -0
- package/dist/components/editor-root-focus.d.ts +19 -0
- package/dist/components/editor-root-focus.js +67 -0
- package/dist/components/editor-root-geometry.d.ts +39 -0
- package/dist/components/editor-root-geometry.js +91 -0
- package/dist/components/editor-root-keydown.d.ts +1 -1
- package/dist/components/editor-root-keydown.js +12 -3
- package/dist/components/editor-root-listeners.d.ts +3 -6
- package/dist/components/editor-root-listeners.js +3 -24
- package/dist/components/editor-root-mode-flip.d.ts +36 -0
- package/dist/components/editor-root-mode-flip.js +92 -0
- package/dist/components/image/ImageOverlayHost.svelte +14 -6
- package/dist/components/image/ImageProperties.svelte +512 -70
- package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
- package/dist/components/image/ImageResizeHandles.svelte +67 -34
- package/dist/components/image/image-crop.d.ts +39 -0
- package/dist/components/image/image-crop.js +74 -0
- package/dist/components/image/image-edit-commit.d.ts +1 -0
- package/dist/components/image/image-edit-commit.js +21 -5
- package/dist/components/image/image-source-bytes.js +10 -3
- package/dist/components/image/image-widget-editing.js +1 -0
- package/dist/components/image/widget-dom.js +5 -1
- package/dist/components/link-card/link-card-commit.js +1 -1
- package/dist/components/lrd-map-gate.js +1 -1
- package/dist/components/menu/BlockMenu.svelte +315 -0
- package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
- package/dist/components/menu/MenuIcon.svelte +153 -0
- package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
- package/dist/components/menu/SelectionToolbar.svelte +385 -0
- package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
- package/dist/components/menu/clipboard-actions.d.ts +12 -0
- package/dist/components/menu/clipboard-actions.js +42 -0
- package/dist/components/menu/default-context-actions.d.ts +15 -0
- package/dist/components/menu/default-context-actions.js +76 -0
- package/dist/components/menu/flyout-placement.d.ts +6 -0
- package/dist/components/menu/flyout-placement.js +25 -0
- package/dist/core/inline/format-toggle.d.ts +12 -4
- package/dist/core/inline/format-toggle.js +94 -40
- package/dist/core/inline/image-dimensions.d.ts +3 -0
- package/dist/core/inline/image-dimensions.js +46 -10
- package/dist/core/inline/inline-widgets.d.ts +19 -0
- package/dist/core/inline/inline-widgets.js +5 -0
- package/dist/core/inline/scan/brackets.js +1 -0
- package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
- package/dist/core/inline/scan/plugin-syntax.js +15 -1
- package/dist/core/inline/transparency.js +3 -3
- package/dist/core/inline-render.d.ts +6 -0
- package/dist/core/inline-render.js +32 -0
- package/dist/core/nodes.d.ts +14 -0
- package/dist/cursor/edge-affinity.js +2 -1
- package/dist/cursor/height-oracle.d.ts +2 -3
- package/dist/cursor/height-oracle.js +0 -1
- package/dist/cursor/overlay-remeasure.js +8 -0
- package/dist/cursor/reveal-source.js +7 -2
- package/dist/cursor/scroll-hold.d.ts +10 -0
- package/dist/cursor/scroll-hold.js +22 -0
- package/dist/cursor/scrollport.d.ts +9 -0
- package/dist/cursor/scrollport.js +30 -1
- package/dist/cursor/visual-lines.d.ts +5 -4
- package/dist/cursor/visual-lines.js +56 -11
- package/dist/cursor/widget-edge-snap.d.ts +28 -0
- package/dist/cursor/widget-edge-snap.js +38 -0
- package/dist/cursor/widget-offset.d.ts +9 -0
- package/dist/cursor/widget-offset.js +71 -4
- package/dist/debug/interaction-trace.d.ts +4 -0
- package/dist/debug/interaction-trace.js +15 -0
- package/dist/decorations/decoration-state.svelte.js +1 -1
- package/dist/decorations/reserved-attrs.js +1 -0
- package/dist/editor-actions/ancestry-folds.d.ts +2 -2
- package/dist/editor-actions/ancestry-folds.js +1 -1
- package/dist/editor-actions/block-edit-scope.js +1 -1
- package/dist/editor-actions/commit/text-batch.d.ts +3 -2
- package/dist/editor-actions/commit/text-batch.js +1 -1
- package/dist/editor-actions/commit/undo-controller.js +4 -2
- package/dist/editor-actions/container-edit.js +2 -1
- package/dist/editor-actions/enter-completion.d.ts +2 -0
- package/dist/editor-actions/enter-completion.js +23 -2
- package/dist/editor-actions/focus/focus-dispatch.js +7 -4
- package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
- package/dist/editor-actions/focus/focus-landing.js +20 -6
- package/dist/editor-actions/inline-range-commit.js +1 -1
- package/dist/editor-actions/plugin/container.d.ts +7 -0
- package/dist/editor-actions/plugin/container.js +3 -1
- package/dist/editor-actions/reorder-action.js +19 -10
- package/dist/editor-actions/reorder-drag.js +29 -1
- package/dist/editor-actions/replacement-focus.d.ts +1 -1
- package/dist/editor-actions/replacement-focus.js +1 -1
- package/dist/editor-actions/search-replace.js +1 -1
- package/dist/editor-actions/table-context.d.ts +4 -1
- package/dist/editor-actions/table-context.js +57 -1
- package/dist/editor-events.d.ts +3 -0
- package/dist/editor-keys.d.ts +33 -0
- package/dist/editor-props.d.ts +24 -12
- package/dist/index.d.ts +1 -1
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +16 -0
- package/dist/plugins/latex/BlockMath.svelte +264 -29
- package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
- package/dist/plugins/latex/index.d.ts +2 -1
- package/dist/plugins/latex/latex-kind.js +60 -10
- package/dist/plugins/latex/math-completion.js +4 -1
- package/dist/plugins/latex/math-layout.d.ts +15 -0
- package/dist/plugins/latex/math-layout.js +13 -0
- package/dist/plugins/latex/math-source.d.ts +19 -0
- package/dist/plugins/latex/math-source.js +103 -0
- package/dist/plugins/latex/register.d.ts +11 -2
- package/dist/plugins/latex/register.js +3 -1
- package/dist/plugins/latex/renderer.d.ts +3 -3
- package/dist/plugins/latex/renderer.js +13 -6
- package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
- package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
- package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
- package/dist/reactivity/list-windowing.svelte.js +75 -30
- package/dist/schema/block-completions.d.ts +8 -0
- package/dist/schema/block-completions.js +11 -0
- package/dist/schema/commands.d.ts +9 -5
- package/dist/schema/commands.js +12 -7
- package/dist/schema/context-actions.d.ts +31 -0
- package/dist/schema/context-actions.js +23 -0
- package/dist/schema/fenced-code-raw.js +31 -1
- package/dist/schema/operations.d.ts +8 -1
- package/dist/schema/reserved-chords.js +37 -4
- package/dist/schema/table-cell-raw.d.ts +1 -1
- package/dist/schema/table-cell-raw.js +1 -1
- package/dist/selection/block-hit-test.js +3 -2
- package/dist/selection/char-endpoint-snap.js +1 -1
- package/dist/selection/clipboard-text.js +6 -1
- package/dist/selection/covered-block.d.ts +10 -0
- package/dist/selection/covered-block.js +24 -0
- package/dist/selection/cross-block/dispatch.d.ts +3 -0
- package/dist/selection/cross-block/dispatch.js +13 -1
- package/dist/selection/cross-block/format-range.d.ts +1 -1
- package/dist/selection/cross-block/format-range.js +4 -15
- package/dist/selection/cross-block/format-toggle.js +1 -1
- package/dist/selection/cross-block/keydown.js +3 -29
- package/dist/selection/cross-block/ops.js +1 -1
- package/dist/selection/cross-block/paste.js +31 -35
- package/dist/selection/cross-block/type-replace.d.ts +3 -2
- package/dist/selection/cross-block/type-replace.js +52 -13
- package/dist/selection/dead-space-caret.d.ts +13 -0
- package/dist/selection/dead-space-caret.js +57 -1
- package/dist/selection/drag-pointer.d.ts +26 -2
- package/dist/selection/drag-pointer.js +103 -2
- package/dist/selection/gap-caret.js +1 -1
- package/dist/selection/keyboard-extend.d.ts +3 -2
- package/dist/selection/keyboard-extend.js +4 -3
- package/dist/selection/multi-click.d.ts +42 -0
- package/dist/selection/multi-click.js +203 -0
- package/dist/selection/native-bridge.d.ts +3 -0
- package/dist/selection/native-bridge.js +31 -2
- package/dist/selection/path-lookup.d.ts +10 -2
- package/dist/selection/path-lookup.js +11 -4
- package/dist/selection/pointer-gesture.d.ts +9 -0
- package/dist/selection/pointer-gesture.js +11 -0
- package/dist/selection/primitives.d.ts +7 -0
- package/dist/selection/primitives.js +19 -1
- package/dist/selection/range-delete-ceremony.js +4 -2
- package/dist/selection/range-delete-chrome.js +2 -1
- package/dist/selection/range-delete-table-coverage.js +2 -1
- package/dist/selection/range-delete-table.js +3 -2
- package/dist/selection/range-delete.d.ts +4 -0
- package/dist/selection/range-delete.js +31 -3
- package/dist/selection/selection-drop.d.ts +34 -0
- package/dist/selection/selection-drop.js +253 -0
- package/dist/selection/selection-restore.js +1 -1
- package/dist/selection/selection-state.svelte.d.ts +6 -0
- package/dist/selection/selection-state.svelte.js +36 -1
- package/dist/selection/table-endpoint-snap.js +1 -1
- package/dist/selection/table-rect-extend.js +1 -1
- package/dist/styles/editor-theme.css +75 -31
- package/dist/styles/editor.css +268 -18
- package/dist/testing/container-conformance.js +2 -2
- package/dist/testing/inline-conformance.js +2 -1
- package/dist/tree-operations/blockquote.js +1 -1
- package/dist/tree-operations/chain-rebuild.d.ts +63 -0
- package/dist/tree-operations/chain-rebuild.js +142 -0
- package/dist/tree-operations/children.d.ts +1 -1
- package/dist/tree-operations/children.js +1 -1
- package/dist/tree-operations/cleanup.js +1 -1
- package/dist/tree-operations/content-write.d.ts +50 -0
- package/dist/tree-operations/content-write.js +263 -0
- package/dist/tree-operations/index.d.ts +8 -3
- package/dist/tree-operations/index.js +6 -2
- package/dist/tree-operations/list/exit-replacement.js +1 -1
- package/dist/tree-operations/list/unwrap-merge.js +3 -3
- package/dist/tree-operations/node-ops.d.ts +17 -234
- package/dist/tree-operations/node-ops.js +47 -1113
- package/dist/tree-operations/node-primitives.d.ts +75 -0
- package/dist/tree-operations/node-primitives.js +117 -0
- package/dist/tree-operations/paste/apply.js +1 -1
- package/dist/tree-operations/paste/body-write.d.ts +1 -1
- package/dist/tree-operations/paste/body-write.js +2 -2
- package/dist/tree-operations/paste/container-match.js +4 -2
- package/dist/tree-operations/paste/dispatch.js +2 -1
- package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
- package/dist/tree-operations/paste/focus-target.d.ts +1 -1
- package/dist/tree-operations/paste/list-absorb.js +1 -1
- package/dist/tree-operations/paste/list-break-out.js +1 -1
- package/dist/tree-operations/paste/parent-scope.js +1 -1
- package/dist/tree-operations/paste/paste-replacement.js +1 -1
- package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
- package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
- package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
- package/dist/tree-operations/paste/replacement-parse.js +22 -0
- package/dist/tree-operations/path-mutate.d.ts +1 -1
- package/dist/tree-operations/path-mutate.js +2 -1
- package/dist/tree-operations/reorder-unit.js +1 -1
- package/dist/tree-operations/reorder.d.ts +5 -2
- package/dist/tree-operations/reorder.js +55 -2
- package/dist/tree-operations/settle.d.ts +105 -0
- package/dist/tree-operations/settle.js +660 -0
- package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
- package/dist/tree-operations/table-grid-clipboard.js +90 -0
- package/dist/tree-operations/unshare.d.ts +15 -77
- package/dist/tree-operations/unshare.js +15 -166
- package/docs/guide/consumer-guide.md +136 -104
- package/docs/guide/plugin-api.md +73 -29
- package/docs/guide/plugin-guide.md +55 -23
- package/package.json +9 -7
- package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
- package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
- package/dist/components/blocks/table/TableGrip.svelte +0 -91
- package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
- package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
- package/dist/components/blocks/table/table-drop-target.js +0 -16
- package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
- package/dist/components/blocks/table/table-reorder-drag.js +0 -97
|
@@ -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,37 @@
|
|
|
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';
|
|
41
|
+
import { installMultiClickSelect } from '../selection/multi-click';
|
|
42
|
+
import { claimsPointerGesture } from '../selection/pointer-gesture';
|
|
43
|
+
import { installSelectionDrop } from '../selection/selection-drop';
|
|
40
44
|
import { createContentVersion } from '../reactivity/content-version.svelte';
|
|
41
45
|
import { useContainerWindowing } from '../reactivity/use-container-windowing.svelte';
|
|
42
46
|
import { refSlotsOver, replaceRefs, revealChildOrWait } from '../reactivity/publish-ref.svelte';
|
|
43
47
|
import { createSelectionState } from '../selection/selection-state.svelte';
|
|
44
48
|
import { createSelectionDescription } from '../selection/selection-description';
|
|
45
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
BLOCK_ACTIONS_LABEL,
|
|
51
|
+
BLOCK_MENU_LABEL,
|
|
52
|
+
EDITOR_LABEL,
|
|
53
|
+
movedBlockToPosition
|
|
54
|
+
} from '../a11y-strings';
|
|
55
|
+
import TailInsert from './TailInsert.svelte';
|
|
56
|
+
import BlockMenu, {
|
|
57
|
+
insertFlyoutEntries,
|
|
58
|
+
insertMenuEntries,
|
|
59
|
+
insertSnippets,
|
|
60
|
+
type MenuEntry
|
|
61
|
+
} from './menu/BlockMenu.svelte';
|
|
62
|
+
import { runClipboardAction, type ClipboardAction } from './menu/clipboard-actions';
|
|
63
|
+
import { blockContextActionsFor, type BlockContextAction } from '../schema/context-actions';
|
|
64
|
+
import { isProseBackground, registerDefaultContextActions } from './menu/default-context-actions';
|
|
46
65
|
import type { EditorSelection } from '../selection/primitives';
|
|
47
66
|
import { createWidgetSelectionState } from './image/widget-selection-state.svelte';
|
|
48
67
|
import { bootstrapCodeLanguages } from './blocks/code/code-bootstrap';
|
|
49
68
|
import { assignIds } from '../block-id';
|
|
50
69
|
import { ensureEditableContainers, emptyParagraph } from '../tree-operations';
|
|
51
|
-
import { blockNodeAt, isBlockNode, nodeAt } from '../tree-operations/node-
|
|
70
|
+
import { blockNodeAt, isBlockNode, nodeAt } from '../tree-operations/node-primitives';
|
|
52
71
|
import { serialize } from '../core/serializer';
|
|
53
72
|
import { parse } from '../core/parser';
|
|
54
73
|
import { defaultLinkActivation } from '../core/url-policy';
|
|
@@ -80,19 +99,25 @@
|
|
|
80
99
|
} from '../debug/interaction-trace';
|
|
81
100
|
import { readCurrentSelection } from '../selection/native-bridge';
|
|
82
101
|
import { restoreSelection, type SelectionRestoreOutcome } from '../selection/selection-restore';
|
|
83
|
-
import { findSurfacePathForElement
|
|
102
|
+
import { findSurfacePathForElement } from '../selection/path-lookup';
|
|
84
103
|
import { createCaretRestore } from '../selection/caret-restore';
|
|
85
104
|
import { createCrossBlockHandlers } from '../selection/cross-block/dispatch';
|
|
86
105
|
import { createCrossBlockCommands } from '../selection/cross-block/format-toggle';
|
|
87
|
-
import { isPreviewMode } from '../presentation-mode';
|
|
88
106
|
import { normalizeKeybindingOverrides } from '../schema/keybinding-overrides';
|
|
89
107
|
import { createEditorRootKeydown } from './editor-root-keydown';
|
|
90
108
|
import { createEditorRootClipboard } from './editor-root-clipboard';
|
|
109
|
+
import { createModeFlip } from './editor-root-mode-flip';
|
|
110
|
+
import { createFocusAttribution } from './editor-root-focus';
|
|
111
|
+
import {
|
|
112
|
+
installHeaderSlotCompensation,
|
|
113
|
+
installTypeScaleProbe,
|
|
114
|
+
installViewportHeightWatcher,
|
|
115
|
+
installWidthWatcher
|
|
116
|
+
} from './editor-root-geometry';
|
|
91
117
|
import {
|
|
92
118
|
installEditorBlurAnnouncer,
|
|
93
119
|
installModActiveTracker,
|
|
94
120
|
installSelectionChangeBridge,
|
|
95
|
-
installViewportHeightWatcher,
|
|
96
121
|
onRoot,
|
|
97
122
|
removeAll
|
|
98
123
|
} from './editor-root-listeners';
|
|
@@ -102,12 +127,8 @@
|
|
|
102
127
|
registerEditor,
|
|
103
128
|
unregisterEditor,
|
|
104
129
|
markEditorInteracted,
|
|
105
|
-
releaseInteractedEditor
|
|
106
|
-
isTextEntrySurface
|
|
130
|
+
releaseInteractedEditor
|
|
107
131
|
} 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
132
|
import {
|
|
112
133
|
canRunCommandById,
|
|
113
134
|
isCommandActiveById,
|
|
@@ -127,6 +148,7 @@
|
|
|
127
148
|
import { bothEnable, createRegistryView, type KindEnablement } from '../schema/registry-view';
|
|
128
149
|
import BlockList from './BlockList.svelte';
|
|
129
150
|
import SearchBar from './SearchBar.svelte';
|
|
151
|
+
import SelectionToolbar from './menu/SelectionToolbar.svelte';
|
|
130
152
|
import ImageOverlayHost from './image/ImageOverlayHost.svelte';
|
|
131
153
|
import LinkCardHost from './link-card/LinkCardHost.svelte';
|
|
132
154
|
import { createLinkCardState } from './link-card/link-card-state.svelte';
|
|
@@ -134,12 +156,12 @@
|
|
|
134
156
|
import { runStartupInvariantChecks } from '../invariants/install';
|
|
135
157
|
import { assertInvariant } from '../assert';
|
|
136
158
|
import { checkMarkerCssParity } from '../invariants/marker-css-parity';
|
|
137
|
-
import { checkLandableCaret } from '../invariants/landable-caret';
|
|
138
159
|
import { registerBuiltInBlocks } from './built-in-blocks';
|
|
139
160
|
import { BLOCK_CONTENT_SELECTOR } from './block-content-selector';
|
|
140
161
|
|
|
141
162
|
registerBuiltInBlocks();
|
|
142
163
|
bootstrapCodeLanguages();
|
|
164
|
+
registerDefaultContextActions();
|
|
143
165
|
runStartupInvariantChecks();
|
|
144
166
|
|
|
145
167
|
// `__registryEnablement` is a harness-only door; the intersection keeps it off the
|
|
@@ -151,10 +173,13 @@
|
|
|
151
173
|
imageLoadPolicy = 'auto',
|
|
152
174
|
onLinkActivate,
|
|
153
175
|
onPasteImage,
|
|
176
|
+
onRunCode,
|
|
177
|
+
codeMenuItems,
|
|
154
178
|
header,
|
|
155
|
-
blockDragHandles =
|
|
179
|
+
blockDragHandles = true,
|
|
156
180
|
searchBar = true,
|
|
157
181
|
searchBarAnchor,
|
|
182
|
+
selectionToolbar = true,
|
|
158
183
|
keybindings,
|
|
159
184
|
theme = 'dark',
|
|
160
185
|
presentationMode = 'source',
|
|
@@ -447,6 +472,189 @@
|
|
|
447
472
|
revealBlock: (index) => revealPath([index])
|
|
448
473
|
});
|
|
449
474
|
|
|
475
|
+
// ── Block menu ──────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
// Opened by the bottom `+` and by a right-click on prose with nothing selected; a right-click
|
|
478
|
+
// over a selection leaves the formatting popover (the host's) in charge and only suppresses
|
|
479
|
+
// the native menu. Tables run their own cell menu and have prevented the default first.
|
|
480
|
+
let blockMenu = $state<{
|
|
481
|
+
x: number;
|
|
482
|
+
y: number;
|
|
483
|
+
anchor: () => { x: number; y: number };
|
|
484
|
+
items: MenuEntry[];
|
|
485
|
+
label: string;
|
|
486
|
+
pick: (id: string) => void;
|
|
487
|
+
} | null>(null);
|
|
488
|
+
|
|
489
|
+
// Open/close transitions only, never the mount, so a subscriber's first news is a real menu.
|
|
490
|
+
let menuWasOpen = false;
|
|
491
|
+
$effect(() => {
|
|
492
|
+
const open = blockMenu !== null;
|
|
493
|
+
if (open === menuWasOpen) return;
|
|
494
|
+
menuWasOpen = open;
|
|
495
|
+
events.emit('menuChange', open);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
function anchorOn(el: Element, point: { x: number; y: number }): () => { x: number; y: number } {
|
|
499
|
+
const rect = el.getBoundingClientRect();
|
|
500
|
+
const dx = point.x - rect.left;
|
|
501
|
+
const dy = point.y - rect.top;
|
|
502
|
+
return () => {
|
|
503
|
+
const now = el.getBoundingClientRect();
|
|
504
|
+
return { x: now.left + dx, y: now.top + dy };
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// A right-click on a BLOCK — a fence, an equation, an image — is that block's context menu:
|
|
509
|
+
// its kind's registered actions, then the defaults. Prose is the page's background and keeps
|
|
510
|
+
// the browser's own menu (spelling, the platform's clipboard), as does a selection, whose
|
|
511
|
+
// affordance is the host's formatting popover. The margin shows nothing at all.
|
|
512
|
+
function onRootContextMenu(e: MouseEvent): void {
|
|
513
|
+
if (e.defaultPrevented || effectiveMode === 'reading' || !editorEl) return;
|
|
514
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
515
|
+
if (!target || isHostChrome(target)) return;
|
|
516
|
+
e.preventDefault();
|
|
517
|
+
// The keyboard's contextmenu event (Windows fires it on the ContextMenu key's release)
|
|
518
|
+
// lands on the item the keydown's own menu already focused: that menu is the answer.
|
|
519
|
+
if (target.closest('.md-menu')) return;
|
|
520
|
+
const host = target.closest<HTMLElement>('.block-host[data-block-path]');
|
|
521
|
+
const path = pathOf(host);
|
|
522
|
+
if (!host || !path) return;
|
|
523
|
+
const point = { x: e.clientX, y: e.clientY };
|
|
524
|
+
const native = window.getSelection();
|
|
525
|
+
const selected = !!native && !native.isCollapsed && editorEl.contains(native.anchorNode);
|
|
526
|
+
// A selection's menu is the clipboard's, over the selection as it stands. So is prose's
|
|
527
|
+
// (the page's background), at a caret placed where the press landed; a nested block (a
|
|
528
|
+
// fence inside a list item) takes the same for now.
|
|
529
|
+
const node = path.length === 1 ? doc.children[path[0]] : undefined;
|
|
530
|
+
if (selected || !node || isProseBackground(node)) {
|
|
531
|
+
if (!selected) placeCaretAtPoint(e.clientX, e.clientY);
|
|
532
|
+
// Top-level prose is where a sibling block makes sense; a nested block or a selection
|
|
533
|
+
// gets the clipboard alone.
|
|
534
|
+
const insertAfter = !selected && node && isProseBackground(node) ? path[0] : null;
|
|
535
|
+
openClipboardMenu(point, host, insertAfter);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const index = path[0];
|
|
539
|
+
const actions = blockContextActionsFor(node, path);
|
|
540
|
+
if (actions.length === 0) return;
|
|
541
|
+
const ctx = {
|
|
542
|
+
node,
|
|
543
|
+
path,
|
|
544
|
+
deleteBlock: async () => {
|
|
545
|
+
await blockEdit.deleteBlock(index);
|
|
546
|
+
},
|
|
547
|
+
replaceRaw: async (raw: string) => {
|
|
548
|
+
await blockEdit.updateBlockContent(index, raw);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
blockMenu = {
|
|
552
|
+
...point,
|
|
553
|
+
anchor: anchorOn(host, point),
|
|
554
|
+
items: actions.map(({ id, label, icon, danger }) => ({
|
|
555
|
+
id,
|
|
556
|
+
label,
|
|
557
|
+
icon: icon as MenuEntry['icon'],
|
|
558
|
+
danger
|
|
559
|
+
})),
|
|
560
|
+
label: BLOCK_ACTIONS_LABEL,
|
|
561
|
+
pick: (id) => {
|
|
562
|
+
blockMenu = null;
|
|
563
|
+
const action = actions.find((a: BlockContextAction) => a.id === id);
|
|
564
|
+
if (action) void action.run(ctx);
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// The editing surface a paste goes to: whatever editable holds focus inside the root.
|
|
570
|
+
function focusedEditable(): HTMLElement | null {
|
|
571
|
+
const active = document.activeElement;
|
|
572
|
+
return active instanceof HTMLElement && editorEl?.contains(active) && active.isContentEditable
|
|
573
|
+
? active
|
|
574
|
+
: null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function clipboardRows(): MenuEntry[] {
|
|
578
|
+
const selected = !!focusedEditable() && !(window.getSelection()?.isCollapsed ?? true);
|
|
579
|
+
return [
|
|
580
|
+
{ id: 'clip.cut', label: 'Cut', icon: 'scissors', disabled: !selected },
|
|
581
|
+
{ id: 'clip.copy', label: 'Copy', icon: 'copy', disabled: !selected },
|
|
582
|
+
{ id: 'clip.paste', label: 'Paste', icon: 'clipboard' },
|
|
583
|
+
{ id: 'clip.paste-plain', label: 'Paste as plain text', icon: 'type' }
|
|
584
|
+
];
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function runClipboardRow(id: string): boolean {
|
|
588
|
+
if (!id.startsWith('clip.')) return false;
|
|
589
|
+
void runClipboardAction(id.slice('clip.'.length) as ClipboardAction, focusedEditable());
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** `insertAfter` names the top-level block an "Insert block" flyout mints an empty sibling
|
|
594
|
+
* after; null leaves the menu to the clipboard rows alone. */
|
|
595
|
+
function openClipboardMenu(
|
|
596
|
+
point: { x: number; y: number },
|
|
597
|
+
anchorEl: Element,
|
|
598
|
+
insertAfter: number | null = null
|
|
599
|
+
): void {
|
|
600
|
+
const insert: MenuEntry[] =
|
|
601
|
+
insertAfter === null
|
|
602
|
+
? []
|
|
603
|
+
: [
|
|
604
|
+
{ id: 'sep', label: '', divider: true },
|
|
605
|
+
{ id: 'insert', label: 'Insert block', icon: 'plus', children: insertFlyoutEntries() }
|
|
606
|
+
];
|
|
607
|
+
blockMenu = {
|
|
608
|
+
...point,
|
|
609
|
+
anchor: anchorOn(anchorEl, point),
|
|
610
|
+
items: [...clipboardRows(), ...insert],
|
|
611
|
+
label: BLOCK_ACTIONS_LABEL,
|
|
612
|
+
pick: (id) => {
|
|
613
|
+
blockMenu = null;
|
|
614
|
+
if (runClipboardRow(id)) return;
|
|
615
|
+
const md = insertSnippets().get(id);
|
|
616
|
+
if (md && insertAfter !== null) void insertBlockAfter(insertAfter, md);
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// The same two steps the tail's `+` takes: mint the empty paragraph, which lands the caret in
|
|
622
|
+
// it, then hand the snippet to the surface that now holds focus.
|
|
623
|
+
async function insertBlockAfter(index: number, md: string): Promise<void> {
|
|
624
|
+
await blockEdit.insertParagraph(index + 1, '');
|
|
625
|
+
insertMarkdown(md);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function pathOf(host: HTMLElement | null): number[] | null {
|
|
629
|
+
const raw = host?.dataset.blockPath;
|
|
630
|
+
if (!raw) return null;
|
|
631
|
+
try {
|
|
632
|
+
const parsed: unknown = JSON.parse(raw);
|
|
633
|
+
return Array.isArray(parsed) && parsed.every((n) => typeof n === 'number') ? parsed : null;
|
|
634
|
+
} catch {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function onTailPlus(button: HTMLElement): Promise<void> {
|
|
640
|
+
await blockEdit.insertParagraph(doc.children.length, '');
|
|
641
|
+
const rect = button.getBoundingClientRect();
|
|
642
|
+
const point = { x: rect.left, y: rect.bottom + 4 };
|
|
643
|
+
const snippets = insertSnippets();
|
|
644
|
+
blockMenu = {
|
|
645
|
+
...point,
|
|
646
|
+
anchor: anchorOn(button, point),
|
|
647
|
+
items: [...insertMenuEntries(), { id: 'sep', label: '', divider: true }, ...clipboardRows()],
|
|
648
|
+
label: BLOCK_MENU_LABEL,
|
|
649
|
+
pick: (id) => {
|
|
650
|
+
blockMenu = null;
|
|
651
|
+
if (runClipboardRow(id)) return;
|
|
652
|
+
const md = snippets.get(id);
|
|
653
|
+
if (md) insertMarkdown(md);
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
450
658
|
// ── Link card door ──────────────────────────────────────────────────
|
|
451
659
|
|
|
452
660
|
// The caret snapshot and the entry guards ride the STATE, not the callers, so entry path N+1
|
|
@@ -509,9 +717,32 @@
|
|
|
509
717
|
}
|
|
510
718
|
const anchor = target?.closest('a[href]') as HTMLAnchorElement | null;
|
|
511
719
|
// The helper claims only clicks whose target IS the root, so nothing the
|
|
512
|
-
// editor renders is touched.
|
|
720
|
+
// editor renders is touched. A margin drag's release arrives as a root click too (the
|
|
721
|
+
// press and release targets meet at the root); with a range just painted, it is no
|
|
722
|
+
// click to answer.
|
|
513
723
|
if (!anchor) {
|
|
514
|
-
|
|
724
|
+
// A multi-click places no caret: the ladder painted its range over this press.
|
|
725
|
+
if (e.detail >= 2) {
|
|
726
|
+
marginDrag = false;
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
const pressed = marginDrag;
|
|
730
|
+
const dragged =
|
|
731
|
+
pressed &&
|
|
732
|
+
(Math.abs(e.clientX - marginDown.x) > 3 || Math.abs(e.clientY - marginDown.y) > 3);
|
|
733
|
+
marginDrag = false;
|
|
734
|
+
if (dragged) return;
|
|
735
|
+
if (deadSpaceCaret.handleClick(root, e)) return;
|
|
736
|
+
// A press the editor took on a block (a host's own padding beside a table, a rule, a
|
|
737
|
+
// folded equation's face) that did not move is a click on it: the click helper
|
|
738
|
+
// claims only dead space, so the same landing is resolved here.
|
|
739
|
+
if (pressed && !deadSpaceCaret.isDeadSpaceTarget(root, e.target)) {
|
|
740
|
+
if (placeCaretAtPoint(e.clientX, e.clientY)) return;
|
|
741
|
+
}
|
|
742
|
+
// Declined everywhere: a click on nothing still LEAVES what was being edited (a
|
|
743
|
+
// revealed equation folds on blur), since the margin press suppressed the blur the
|
|
744
|
+
// browser would have done.
|
|
745
|
+
if (pressed) blurEditingSurface(root);
|
|
515
746
|
return;
|
|
516
747
|
}
|
|
517
748
|
// Host chrome follows the page's link behaviour, not plain-click-edits.
|
|
@@ -529,9 +760,92 @@
|
|
|
529
760
|
e.preventDefault();
|
|
530
761
|
}
|
|
531
762
|
};
|
|
763
|
+
// Dead space, and the parts of a block that are neither editable nor controls: a rendered
|
|
764
|
+
// equation, a diagram, a card's rendered face. A press on any of them cannot grow a native
|
|
765
|
+
// selection, so the editor's drag runs from it.
|
|
766
|
+
// A whole-block input proxy is an editable in name only (it catches IME for a block with no
|
|
767
|
+
// text), so a press on it starts the editor's drag like a press on the block itself.
|
|
768
|
+
const NOT_A_DRAG_START =
|
|
769
|
+
'[contenteditable="true"]:not([data-whole-block-input]), button, input, textarea, select, ' +
|
|
770
|
+
'a, summary, [role="checkbox"], ' +
|
|
771
|
+
'.code-rail, .table-add-zone, .editor-tail, .md-menu, .block-drag-handle';
|
|
772
|
+
const dragStartsHere = (rootEl: HTMLElement, target: EventTarget | null): boolean => {
|
|
773
|
+
if (claimsPointerGesture(target)) return false;
|
|
774
|
+
if (deadSpaceCaret.isDeadSpaceTarget(rootEl, target)) return true;
|
|
775
|
+
if (!(target instanceof Element) || !rootEl.contains(target)) return false;
|
|
776
|
+
return target.closest(NOT_A_DRAG_START) === null;
|
|
777
|
+
};
|
|
778
|
+
// A drag that STARTS in the margin: the browser cannot grow a selection from a
|
|
779
|
+
// non-editable press into an editable block, so the editor runs the drag itself, anchored
|
|
780
|
+
// where a click there would land. The mousedown's default is suppressed so the browser
|
|
781
|
+
// starts no selection of its own to fight it; `click` still fires for the caret placement.
|
|
782
|
+
let marginDrag = false;
|
|
783
|
+
let marginDown = { x: 0, y: 0 };
|
|
784
|
+
let marginSession: { dispose(): void } | null = null;
|
|
785
|
+
const startMarginDrag = (e: PointerEvent) => {
|
|
786
|
+
marginDrag = false;
|
|
787
|
+
marginDown = { x: e.clientX, y: e.clientY };
|
|
788
|
+
if (e.button !== 0 || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
789
|
+
if (effectiveMode === 'reading' || !dragStartsHere(root, e.target)) return;
|
|
790
|
+
const anchor = deadSpaceCaret.anchorAtPoint(root, e.clientX, e.clientY);
|
|
791
|
+
if (!anchor) return;
|
|
792
|
+
marginDrag = true;
|
|
793
|
+
resetForPointerDown(selectionState, stickyColumn, edgeAffinity, false);
|
|
794
|
+
// A block that runs its own drag from a nearby press (a table's cell rectangle) takes
|
|
795
|
+
// it; the generic drag is for blocks that have none.
|
|
796
|
+
if (!('offset' in anchor)) {
|
|
797
|
+
const component = getBlockComponent(anchor.path);
|
|
798
|
+
if (component?.startDragAtPoint?.(e.clientX, e.clientY, e)) return;
|
|
799
|
+
}
|
|
800
|
+
marginSession = installDragListener(
|
|
801
|
+
{
|
|
802
|
+
editorRoot: root,
|
|
803
|
+
scrollContainer: getScrollHost() ?? root,
|
|
804
|
+
selection: selectionState,
|
|
805
|
+
getBlockElByPath,
|
|
806
|
+
lifetimeSignal: lifetimeController.signal,
|
|
807
|
+
paintSameBlock: true
|
|
808
|
+
},
|
|
809
|
+
anchor,
|
|
810
|
+
e
|
|
811
|
+
);
|
|
812
|
+
};
|
|
532
813
|
return removeAll(
|
|
533
814
|
onRoot(root, 'click', handleClick),
|
|
534
|
-
|
|
815
|
+
installMultiClickSelect({
|
|
816
|
+
editorRoot: root,
|
|
817
|
+
selection: selectionState,
|
|
818
|
+
getBlockElByPath,
|
|
819
|
+
getScrollContainer: () => getScrollHost() ?? root,
|
|
820
|
+
lifetimeSignal: lifetimeController.signal,
|
|
821
|
+
marginBlockAt: (target, x, y) =>
|
|
822
|
+
effectiveMode !== 'reading' && dragStartsHere(root, target)
|
|
823
|
+
? deadSpaceCaret.blockPathNearPoint(root, x, y)
|
|
824
|
+
: null
|
|
825
|
+
}),
|
|
826
|
+
installSelectionDrop({
|
|
827
|
+
editorRoot: root,
|
|
828
|
+
getDoc: () => doc,
|
|
829
|
+
controller,
|
|
830
|
+
coordinator: pasteCoordinator,
|
|
831
|
+
getPresentationMode: () => effectiveMode,
|
|
832
|
+
linkRef: linkRefView,
|
|
833
|
+
grammar: registryView.grammar,
|
|
834
|
+
activePlugins,
|
|
835
|
+
events,
|
|
836
|
+
isReadOnly: () => effectiveMode === 'reading'
|
|
837
|
+
}),
|
|
838
|
+
onRoot(root, 'pointerdown', startMarginDrag),
|
|
839
|
+
onRoot(root, 'mousedown', (e: MouseEvent) => {
|
|
840
|
+
deadSpaceCaret.notePress(root, e);
|
|
841
|
+
// The second press of a click run is the ladder's, which runs its own drag.
|
|
842
|
+
if (marginDrag && e.detail >= 2) {
|
|
843
|
+
marginSession?.dispose();
|
|
844
|
+
marginSession = null;
|
|
845
|
+
marginDrag = false;
|
|
846
|
+
}
|
|
847
|
+
if (marginDrag) e.preventDefault();
|
|
848
|
+
})
|
|
535
849
|
);
|
|
536
850
|
});
|
|
537
851
|
|
|
@@ -862,8 +1176,7 @@
|
|
|
862
1176
|
resolveImageUrl: resolveImageUrlImpl,
|
|
863
1177
|
resolveLinkUrl: resolveLinkUrlImpl,
|
|
864
1178
|
imageLoadPolicy: () => imageLoadPolicy,
|
|
865
|
-
// Reading mode forces the
|
|
866
|
-
// and the table grips all read this one getter.
|
|
1179
|
+
// Reading mode forces the affordance off through the prop's own funnel.
|
|
867
1180
|
blockDragHandles: () => blockDragHandles && effectiveMode !== 'reading',
|
|
868
1181
|
presentationMode: () => effectiveMode,
|
|
869
1182
|
theme: () => theme,
|
|
@@ -874,89 +1187,45 @@
|
|
|
874
1187
|
get onPasteImage() {
|
|
875
1188
|
return onPasteImage;
|
|
876
1189
|
},
|
|
1190
|
+
// Accessors for the same reason as onPasteImage above.
|
|
1191
|
+
get onRunCode() {
|
|
1192
|
+
return onRunCode;
|
|
1193
|
+
},
|
|
1194
|
+
get codeMenuItems() {
|
|
1195
|
+
return codeMenuItems;
|
|
1196
|
+
},
|
|
877
1197
|
brokenImageUrls
|
|
878
1198
|
} satisfies EditorPolicies);
|
|
879
1199
|
|
|
880
1200
|
// ── Root DOM effects ────────────────────────────────────────────────
|
|
881
1201
|
|
|
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;
|
|
1202
|
+
// The pre half runs while the outgoing mode still owns the DOM, the post half after the
|
|
1203
|
+
// mode's render key has rebuilt every block; the factory carries the caret across the gap.
|
|
1204
|
+
const modeFlip = createModeFlip({
|
|
1205
|
+
get editorEl() {
|
|
1206
|
+
return editorEl;
|
|
1207
|
+
},
|
|
1208
|
+
get mode() {
|
|
1209
|
+
return effectiveMode;
|
|
1210
|
+
},
|
|
1211
|
+
selection: selectionState,
|
|
1212
|
+
getSelection,
|
|
1213
|
+
getBlockElByPath,
|
|
1214
|
+
isHostChrome,
|
|
1215
|
+
edgeAffinity,
|
|
1216
|
+
// Built below; a flip runs post-init, so the getter reads past the TDZ.
|
|
1217
|
+
get heightOracle() {
|
|
1218
|
+
return heightOracle;
|
|
1219
|
+
},
|
|
1220
|
+
events,
|
|
1221
|
+
restoreCaret: (path, offset) => restoreThroughRevealRoad(caretAt(path, offset), 'mount')
|
|
1222
|
+
});
|
|
910
1223
|
$effect.pre(() => {
|
|
911
1224
|
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
|
-
});
|
|
1225
|
+
untrack(() => modeFlip.beforeFlip(mode));
|
|
927
1226
|
});
|
|
928
|
-
|
|
929
|
-
// svelte-ignore state_referenced_locally
|
|
930
|
-
let lastEffectiveMode = effectiveMode;
|
|
931
1227
|
$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);
|
|
1228
|
+
modeFlip.afterFlip(effectiveMode);
|
|
960
1229
|
});
|
|
961
1230
|
|
|
962
1231
|
// A theme flip invalidates no live edit, so it only has to be announced — for
|
|
@@ -1151,22 +1420,14 @@
|
|
|
1151
1420
|
// ── Resize invalidation ─────────────────────────────────────────────
|
|
1152
1421
|
|
|
1153
1422
|
// 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).
|
|
1423
|
+
// off this counter; a height-only resize spares the measured cache.
|
|
1156
1424
|
let widthVersion = $state(0);
|
|
1157
1425
|
$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;
|
|
1426
|
+
if (!editorEl) return;
|
|
1427
|
+
return installWidthWatcher(editorEl, () => {
|
|
1165
1428
|
heightOracle.dropMeasured();
|
|
1166
1429
|
widthVersion++;
|
|
1167
1430
|
});
|
|
1168
|
-
observer.observe(el);
|
|
1169
|
-
return () => observer.disconnect();
|
|
1170
1431
|
});
|
|
1171
1432
|
|
|
1172
1433
|
// The slice's own axis, watched on the RESOLVED port: the window's extent comes from the
|
|
@@ -1183,117 +1444,51 @@
|
|
|
1183
1444
|
|
|
1184
1445
|
// ── Type scale ──────────────────────────────────────────────────────
|
|
1185
1446
|
|
|
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
|
-
}
|
|
1447
|
+
// A font-size move puts the estimates off several-fold, which no other box in the root
|
|
1448
|
+
// reports; the probe's scale rides the width signal, since a rebuild is what it needs.
|
|
1198
1449
|
$effect(() => {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1450
|
+
if (!typeScaleProbeEl) return;
|
|
1451
|
+
return installTypeScaleProbe(typeScaleProbeEl, {
|
|
1452
|
+
getScale: () => typeScale,
|
|
1453
|
+
onScale: (next) => {
|
|
1454
|
+
typeScale = next;
|
|
1455
|
+
heightOracle.dropMeasured();
|
|
1456
|
+
widthVersion++;
|
|
1457
|
+
}
|
|
1205
1458
|
});
|
|
1206
|
-
observer.observe(el);
|
|
1207
|
-
return () => observer.disconnect();
|
|
1208
1459
|
});
|
|
1209
1460
|
|
|
1210
1461
|
// ── Header slot compensation ────────────────────────────────────────
|
|
1211
1462
|
|
|
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
1463
|
$effect(() => {
|
|
1217
1464
|
const el = headerEl;
|
|
1218
1465
|
if (!el || !editorEl) return;
|
|
1219
1466
|
const port = getScrollport();
|
|
1220
1467
|
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);
|
|
1468
|
+
return installHeaderSlotCompensation({
|
|
1469
|
+
el,
|
|
1470
|
+
port,
|
|
1471
|
+
ownsScrollCorrection,
|
|
1472
|
+
revealHoldsScroll: () => topWindowing.revealHoldsScroll()
|
|
1231
1473
|
});
|
|
1232
|
-
observer.observe(el);
|
|
1233
|
-
return () => observer.disconnect();
|
|
1234
1474
|
});
|
|
1235
1475
|
|
|
1236
1476
|
// ── Focus attribution ───────────────────────────────────────────────
|
|
1237
1477
|
|
|
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');
|
|
1478
|
+
const focusAttribution = createFocusAttribution({
|
|
1479
|
+
get mode() {
|
|
1480
|
+
return effectiveMode;
|
|
1253
1481
|
}
|
|
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.
|
|
1482
|
+
});
|
|
1262
1483
|
$effect(() => {
|
|
1263
1484
|
void effectiveMode;
|
|
1264
|
-
|
|
1485
|
+
focusAttribution.applyForMode();
|
|
1265
1486
|
});
|
|
1266
|
-
|
|
1267
1487
|
$effect(() => {
|
|
1268
1488
|
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));
|
|
1489
|
+
return focusAttribution.install(editorEl);
|
|
1296
1490
|
});
|
|
1491
|
+
|
|
1297
1492
|
// ── Top-level windowing ─────────────────────────────────────────────
|
|
1298
1493
|
|
|
1299
1494
|
// Assembled here, after the windowing signals it carries exist; the block
|
|
@@ -1308,7 +1503,7 @@
|
|
|
1308
1503
|
scrollHost: getScrollHost,
|
|
1309
1504
|
scrollport: getScrollport,
|
|
1310
1505
|
blockElLookup: getBlockElByPath,
|
|
1311
|
-
focusedPath:
|
|
1506
|
+
focusedPath: focusAttribution.getFocusedPath,
|
|
1312
1507
|
heightOracle,
|
|
1313
1508
|
correctsScroll: ownsScrollCorrection,
|
|
1314
1509
|
widthVersion: () => widthVersion,
|
|
@@ -1416,6 +1611,11 @@
|
|
|
1416
1611
|
* focused surface. A gap caret declines: its proxy is not a block, and a NESTED gap's proxy
|
|
1417
1612
|
* sits inside its container's host, which must not receive what was aimed at the gap.
|
|
1418
1613
|
*/
|
|
1614
|
+
function blurEditingSurface(root: HTMLElement): void {
|
|
1615
|
+
const active = document.activeElement;
|
|
1616
|
+
if (active instanceof HTMLElement && root.contains(active) && active !== root) active.blur();
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1419
1619
|
function focusedSurfacePath(): number[] | null {
|
|
1420
1620
|
if (selectionState.gapCaret) return null;
|
|
1421
1621
|
const active = document.activeElement;
|
|
@@ -1460,10 +1660,10 @@
|
|
|
1460
1660
|
|
|
1461
1661
|
// The door only resolves the focused surface; every rule (the reading gate, the cross-block
|
|
1462
1662
|
// range decline, the arms themselves) lives below the seam. See `editor-props.ts`.
|
|
1463
|
-
export function runCommand(commandId: string): boolean {
|
|
1663
|
+
export function runCommand(commandId: string, arg?: unknown): boolean {
|
|
1464
1664
|
return runCommandById(
|
|
1465
1665
|
commandId as AnyCommandId,
|
|
1466
|
-
|
|
1666
|
+
arg,
|
|
1467
1667
|
focusedCommandTarget(),
|
|
1468
1668
|
commandDispatchContext,
|
|
1469
1669
|
commandErrorSink
|
|
@@ -1632,6 +1832,7 @@
|
|
|
1632
1832
|
tabindex="-1"
|
|
1633
1833
|
role="group"
|
|
1634
1834
|
aria-label={EDITOR_LABEL}
|
|
1835
|
+
oncontextmenu={onRootContextMenu}
|
|
1635
1836
|
>
|
|
1636
1837
|
{#if searchBar}
|
|
1637
1838
|
<!-- Zero-height sticky anchor, so the bar doesn't scroll away with content. Portaled
|
|
@@ -1665,6 +1866,38 @@
|
|
|
1665
1866
|
window={topWindowing.window}
|
|
1666
1867
|
reorderable={true}
|
|
1667
1868
|
/>
|
|
1869
|
+
<!-- A sibling of the list like the header: the windowing scope wants the list bare. -->
|
|
1870
|
+
<TailInsert
|
|
1871
|
+
{blockEdit}
|
|
1872
|
+
childCount={doc.children.length}
|
|
1873
|
+
readOnly={effectiveMode === 'reading'}
|
|
1874
|
+
onPlus={(button) => void onTailPlus(button)}
|
|
1875
|
+
/>
|
|
1876
|
+
{#if blockMenu}
|
|
1877
|
+
<BlockMenu
|
|
1878
|
+
x={blockMenu.x}
|
|
1879
|
+
y={blockMenu.y}
|
|
1880
|
+
anchor={blockMenu.anchor}
|
|
1881
|
+
items={blockMenu.items}
|
|
1882
|
+
label={blockMenu.label}
|
|
1883
|
+
onPick={blockMenu.pick}
|
|
1884
|
+
onClose={() => (blockMenu = null)}
|
|
1885
|
+
/>
|
|
1886
|
+
{/if}
|
|
1887
|
+
{#if selectionToolbar && effectiveMode !== 'reading'}
|
|
1888
|
+
<SelectionToolbar
|
|
1889
|
+
editor={{
|
|
1890
|
+
getSelection,
|
|
1891
|
+
getRects,
|
|
1892
|
+
getBlockKindAt,
|
|
1893
|
+
runCommand,
|
|
1894
|
+
canRunCommand,
|
|
1895
|
+
isCommandActive,
|
|
1896
|
+
getEvents
|
|
1897
|
+
}}
|
|
1898
|
+
root={editorEl}
|
|
1899
|
+
/>
|
|
1900
|
+
{/if}
|
|
1668
1901
|
<ImageOverlayHost
|
|
1669
1902
|
{widgetSelection}
|
|
1670
1903
|
{controller}
|
|
@@ -1709,7 +1942,9 @@
|
|
|
1709
1942
|
.editor {
|
|
1710
1943
|
width: 100%;
|
|
1711
1944
|
flex: 1;
|
|
1712
|
-
|
|
1945
|
+
/* Wider on the left: the drag grip lives in that gutter, 1.25rem out from the content,
|
|
1946
|
+
and the rest of this padding is what keeps it off the scroll container's border. */
|
|
1947
|
+
padding: 1rem 1rem 1rem 1.5rem;
|
|
1713
1948
|
font-family: var(--font-editor, ui-monospace, monospace);
|
|
1714
1949
|
/* The type-scale root: every construct sizes in `em` off this, so one
|
|
1715
1950
|
declaration scales the whole surface. */
|
|
@@ -1724,9 +1959,10 @@
|
|
|
1724
1959
|
double-corrects. Do NOT restore `overflow-anchor` (VR-2). */
|
|
1725
1960
|
overflow-anchor: none;
|
|
1726
1961
|
scrollbar-width: thin;
|
|
1727
|
-
scrollbar-color: var(--color-
|
|
1728
|
-
|
|
1729
|
-
|
|
1962
|
+
scrollbar-color: var(--color-border, #3e3e3b) transparent;
|
|
1963
|
+
/* No box of its own. A document is the page's content, not a widget sitting on it, and
|
|
1964
|
+
an outline around the whole editor reads as a form field the moment the host gives it
|
|
1965
|
+
a column. A host that wants the frame draws it on its own container. */
|
|
1730
1966
|
/* Containing block for the image overlay portal. */
|
|
1731
1967
|
position: relative;
|
|
1732
1968
|
}
|