edytor 0.0.11 → 0.1.0-next.16
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 +77 -197
- package/dist/block/block.svelte.d.ts +156 -69
- package/dist/block/block.svelte.js +324 -355
- package/dist/block/block.utils.d.ts +191 -24
- package/dist/block/block.utils.js +327 -460
- package/dist/block/contentRange.d.ts +4 -0
- package/dist/block/contentRange.js +20 -0
- package/dist/block/inlineBlock.svelte.d.ts +26 -19
- package/dist/block/inlineBlock.svelte.js +91 -39
- package/dist/clipboard/clipboard.d.ts +4 -0
- package/dist/clipboard/clipboard.js +4 -0
- package/dist/clipboard/createClipboardFragment.d.ts +3 -0
- package/dist/clipboard/createClipboardFragment.js +103 -0
- package/dist/clipboard/fragmentData.d.ts +6 -0
- package/dist/clipboard/fragmentData.js +74 -0
- package/dist/clipboard/htmlFlow.d.ts +22 -0
- package/dist/clipboard/htmlFlow.js +167 -0
- package/dist/clipboard/insertClipboardFragment.d.ts +18 -0
- package/dist/clipboard/insertClipboardFragment.js +48 -0
- package/dist/clipboard/serializeClipboardFragment.d.ts +7 -0
- package/dist/clipboard/serializeClipboardFragment.js +53 -0
- package/dist/clipboard/textBlocks.d.ts +23 -0
- package/dist/clipboard/textBlocks.js +103 -0
- package/dist/clipboard/types.d.ts +20 -0
- package/dist/clipboard/types.js +2 -0
- package/dist/cloudflare/DocumentRoom.d.ts +586 -0
- package/dist/cloudflare/DocumentRoom.js +1706 -0
- package/dist/cloudflare/index.d.ts +8 -0
- package/dist/cloudflare/index.js +8 -0
- package/dist/cloudflare/routeDocumentSocket.d.ts +33 -0
- package/dist/cloudflare/routeDocumentSocket.js +51 -0
- package/dist/collaboration/RemoteSelections.svelte +94 -0
- package/dist/collaboration/RemoteSelections.svelte.d.ts +7 -0
- package/dist/collaboration/awarenessSelection.d.ts +127 -0
- package/dist/collaboration/awarenessSelection.js +199 -0
- package/dist/collaboration/documentSync.d.ts +20 -0
- package/dist/collaboration/documentSync.js +13 -0
- package/dist/collaboration/index.d.ts +3 -0
- package/dist/collaboration/index.js +3 -0
- package/dist/collaboration/providers.d.ts +13 -0
- package/dist/collaboration/providers.js +21 -0
- package/dist/collaboration/remoteSelection.d.ts +21 -0
- package/dist/collaboration/remoteSelection.js +159 -0
- package/dist/components/Block.svelte +223 -15
- package/dist/components/Block.svelte.d.ts +4 -2
- package/dist/components/Content.svelte +128 -32
- package/dist/components/Content.svelte.d.ts +6 -2
- package/dist/components/Edytor.svelte +308 -110
- package/dist/components/Edytor.svelte.d.ts +64 -20
- package/dist/components/InlineBlock.svelte +38 -7
- package/dist/components/InlineBlock.svelte.d.ts +5 -2
- package/dist/components/Mark.svelte +29 -13
- package/dist/components/Mark.svelte.d.ts +4 -4
- package/dist/components/Suggestion.svelte +48 -0
- package/dist/components/Suggestion.svelte.d.ts +7 -0
- package/dist/components/SuggestionRanges.svelte +58 -0
- package/dist/components/SuggestionRanges.svelte.d.ts +7 -0
- package/dist/components/Text.svelte +107 -14
- package/dist/components/Text.svelte.d.ts +8 -1
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/crdt/admission.d.ts +141 -0
- package/dist/crdt/admission.js +147 -0
- package/dist/crdt/anchors.d.ts +47 -0
- package/dist/crdt/anchors.js +42 -0
- package/dist/crdt/attribution/attribution.d.ts +169 -0
- package/dist/crdt/attribution/attribution.js +136 -0
- package/dist/crdt/attribution/block.d.ts +200 -0
- package/dist/crdt/attribution/block.js +327 -0
- package/dist/crdt/attribution/index.d.ts +9 -0
- package/dist/crdt/attribution/index.js +9 -0
- package/dist/crdt/data.d.ts +81 -0
- package/dist/crdt/data.js +383 -0
- package/dist/crdt/document.d.ts +590 -0
- package/dist/crdt/document.js +1160 -0
- package/dist/crdt/edytor-doc.d.ts +752 -0
- package/dist/crdt/edytor-doc.js +2382 -0
- package/dist/crdt/engine-api.d.ts +153 -0
- package/dist/crdt/engine.d.ts +37 -0
- package/dist/crdt/engine.js +50 -0
- package/dist/crdt/flow.d.ts +104 -0
- package/dist/crdt/flow.js +229 -0
- package/dist/crdt/index.d.ts +630 -0
- package/dist/crdt/index.js +168 -0
- package/dist/crdt/migration/legacy-schema.d.ts +61 -0
- package/dist/crdt/migration/legacy-schema.js +149 -0
- package/dist/crdt/migration/migrate.d.ts +67 -0
- package/dist/crdt/migration/migrate.js +219 -0
- package/dist/crdt/nodes.d.ts +120 -0
- package/dist/crdt/nodes.js +56 -0
- package/dist/crdt/placement/model.d.ts +383 -0
- package/dist/crdt/placement/model.js +811 -0
- package/dist/crdt/placement/rank.d.ts +57 -0
- package/dist/crdt/placement/rank.js +209 -0
- package/dist/crdt/protocols/auth.d.ts +28 -0
- package/dist/crdt/protocols/auth.js +43 -0
- package/dist/crdt/protocols/awareness.d.ts +87 -0
- package/dist/crdt/protocols/awareness.js +271 -0
- package/dist/crdt/protocols/envelope.d.ts +75 -0
- package/dist/crdt/protocols/envelope.js +89 -0
- package/dist/crdt/protocols/observable.d.ts +19 -0
- package/dist/crdt/protocols/observable.js +26 -0
- package/dist/crdt/protocols/sync.d.ts +82 -0
- package/dist/crdt/protocols/sync.js +288 -0
- package/dist/crdt/providers/container.d.ts +23 -0
- package/dist/crdt/providers/container.js +64 -0
- package/dist/crdt/providers/index.d.ts +282 -0
- package/dist/crdt/providers/index.js +118 -0
- package/dist/crdt/providers/indexeddb.d.ts +196 -0
- package/dist/crdt/providers/indexeddb.js +332 -0
- package/dist/crdt/providers/room.d.ts +193 -0
- package/dist/crdt/providers/room.js +444 -0
- package/dist/crdt/providers/websocket.d.ts +223 -0
- package/dist/crdt/providers/websocket.js +601 -0
- package/dist/crdt/rand.d.ts +22 -0
- package/dist/crdt/rand.js +22 -0
- package/dist/crdt/rangeDelete.d.ts +85 -0
- package/dist/crdt/rangeDelete.js +126 -0
- package/dist/crdt/schema.d.ts +96 -0
- package/dist/crdt/schema.js +79 -0
- package/dist/crdt/semantics.d.ts +81 -0
- package/dist/crdt/semantics.js +57 -0
- package/dist/crdt/structs.d.ts +106 -0
- package/dist/crdt/structs.js +119 -0
- package/dist/crdt/text/deletes.d.ts +23 -0
- package/dist/crdt/text/deletes.js +608 -0
- package/dist/crdt/text/model.d.ts +280 -0
- package/dist/crdt/text/model.js +554 -0
- package/dist/crdt/text/runs.d.ts +212 -0
- package/dist/crdt/text/runs.js +1142 -0
- package/dist/crdt/vendor/yjs/API-NOTES.md +59 -0
- package/dist/crdt/vendor/yjs/LICENSE +23 -0
- package/dist/crdt/vendor/yjs/UPSTREAM.md +653 -0
- package/dist/crdt/vendor/yjs/dts/global.d.ts +52 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +25 -0
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +38 -0
- package/dist/crdt/vendor/yjs/dts/structs/GC.d.ts +25 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +748 -0
- package/dist/crdt/vendor/yjs/dts/structs/Skip.d.ts +30 -0
- package/dist/crdt/vendor/yjs/dts/utils/BlockSet.d.ts +34 -0
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +198 -0
- package/dist/crdt/vendor/yjs/dts/utils/EventHandler.d.ts +17 -0
- package/dist/crdt/vendor/yjs/dts/utils/ID.d.ts +20 -0
- package/dist/crdt/vendor/yjs/dts/utils/RangeCursor.d.ts +198 -0
- package/dist/crdt/vendor/yjs/dts/utils/RelativePosition.d.ts +80 -0
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +61 -0
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +129 -0
- package/dist/crdt/vendor/yjs/dts/utils/UndoManager.d.ts +205 -0
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +170 -0
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +164 -0
- package/dist/crdt/vendor/yjs/dts/utils/YEvent.d.ts +93 -0
- package/dist/crdt/vendor/yjs/dts/utils/content-helper.d.ts +1 -0
- package/dist/crdt/vendor/yjs/dts/utils/encoding-helpers.d.ts +5 -0
- package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +26 -0
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +407 -0
- package/dist/crdt/vendor/yjs/dts/utils/isParentOf.d.ts +1 -0
- package/dist/crdt/vendor/yjs/dts/utils/meta.d.ts +25 -0
- package/dist/crdt/vendor/yjs/dts/utils/renderer-helpers.d.ts +98 -0
- package/dist/crdt/vendor/yjs/dts/utils/schemas.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/utils/transaction-helpers.d.ts +15 -0
- package/dist/crdt/vendor/yjs/dts/utils/updates.d.ts +70 -0
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +655 -0
- package/dist/crdt/vendor/yjs/global.d.ts +52 -0
- package/dist/crdt/vendor/yjs/src/index.js +64 -0
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +64 -0
- package/dist/crdt/vendor/yjs/src/structs/GC.js +75 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +1538 -0
- package/dist/crdt/vendor/yjs/src/structs/Skip.js +72 -0
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +291 -0
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +255 -0
- package/dist/crdt/vendor/yjs/src/utils/EventHandler.js +75 -0
- package/dist/crdt/vendor/yjs/src/utils/ID.js +59 -0
- package/dist/crdt/vendor/yjs/src/utils/RangeCursor.js +407 -0
- package/dist/crdt/vendor/yjs/src/utils/RelativePosition.js +301 -0
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +134 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +458 -0
- package/dist/crdt/vendor/yjs/src/utils/UndoManager.js +554 -0
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +284 -0
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +323 -0
- package/dist/crdt/vendor/yjs/src/utils/YEvent.js +158 -0
- package/dist/crdt/vendor/yjs/src/utils/content-helper.js +0 -0
- package/dist/crdt/vendor/yjs/src/utils/encoding-helpers.js +110 -0
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +662 -0
- package/dist/crdt/vendor/yjs/src/utils/ids.js +1504 -0
- package/dist/crdt/vendor/yjs/src/utils/isParentOf.js +19 -0
- package/dist/crdt/vendor/yjs/src/utils/meta.js +74 -0
- package/dist/crdt/vendor/yjs/src/utils/renderer-helpers.js +186 -0
- package/dist/crdt/vendor/yjs/src/utils/schemas.js +3 -0
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +391 -0
- package/dist/crdt/vendor/yjs/src/utils/updates.js +264 -0
- package/dist/crdt/vendor/yjs/src/ynode.js +2897 -0
- package/dist/dnd/pragmatic.d.ts +22 -0
- package/dist/dnd/pragmatic.js +26 -0
- package/dist/edytor.svelte.d.ts +323 -54
- package/dist/edytor.svelte.js +771 -197
- package/dist/edytor.utils.d.ts +49 -2
- package/dist/edytor.utils.js +65 -76
- package/dist/events/beforeInputCommands.d.ts +21 -0
- package/dist/events/beforeInputCommands.js +328 -0
- package/dist/events/beforeInputDeleteCommands.d.ts +3 -0
- package/dist/events/beforeInputDeleteCommands.js +245 -0
- package/dist/events/dataTransferPayload.d.ts +6 -0
- package/dist/events/dataTransferPayload.js +9 -0
- package/dist/events/domTextOffset.d.ts +12 -0
- package/dist/events/domTextOffset.js +52 -0
- package/dist/events/events.utils.d.ts +3 -0
- package/dist/events/events.utils.js +13 -0
- package/dist/events/nativeInteractiveControl.d.ts +31 -0
- package/dist/events/nativeInteractiveControl.js +98 -0
- package/dist/events/onBeforeInput.d.ts +10 -1
- package/dist/events/onBeforeInput.js +449 -330
- package/dist/events/onCopy.d.ts +7 -0
- package/dist/events/onCopy.js +30 -0
- package/dist/events/onCut.d.ts +8 -0
- package/dist/events/onCut.js +13 -0
- package/dist/events/onDrop.d.ts +16 -0
- package/dist/events/onDrop.js +158 -0
- package/dist/events/onFocus.d.ts +4 -0
- package/dist/events/onFocus.js +166 -0
- package/dist/events/onInput.d.ts +20 -0
- package/dist/events/onInput.js +249 -0
- package/dist/events/onKeyDown.js +191 -11
- package/dist/events/onPaste.d.ts +3 -0
- package/dist/events/onPaste.js +108 -0
- package/dist/events/wordBoundary.d.ts +2 -0
- package/dist/events/wordBoundary.js +58 -0
- package/dist/index.d.ts +38 -1
- package/dist/index.js +36 -1
- package/dist/kinds.d.ts +100 -0
- package/dist/kinds.js +198 -0
- package/dist/plugins/arrowMove/arrowMove.d.ts +1 -1
- package/dist/plugins/arrowMove/arrowMove.js +30 -65
- package/dist/plugins/blockHandles/BlockHandle.svelte +152 -0
- package/dist/plugins/blockHandles/BlockHandle.svelte.d.ts +12 -0
- package/dist/plugins/blockHandles/BlockHandleController.svelte.d.ts +208 -0
- package/dist/plugins/blockHandles/BlockHandleController.svelte.js +823 -0
- package/dist/plugins/blockHandles/BlockHandles.svelte +151 -0
- package/dist/plugins/blockHandles/BlockHandles.svelte.d.ts +20 -0
- package/dist/plugins/blockHandles/autoScroll.d.ts +8 -0
- package/dist/plugins/blockHandles/autoScroll.js +25 -0
- package/dist/plugins/blockHandles/blockHandlesPlugin.d.ts +37 -0
- package/dist/plugins/blockHandles/blockHandlesPlugin.js +100 -0
- package/dist/plugins/blockHandles/dragPreview.d.ts +33 -0
- package/dist/plugins/blockHandles/dragPreview.js +228 -0
- package/dist/plugins/blockMenu/BlockMenu.svelte +301 -0
- package/dist/plugins/blockMenu/BlockMenu.svelte.d.ts +9 -0
- package/dist/plugins/blockMenu/BlockMenuController.svelte.d.ts +102 -0
- package/dist/plugins/blockMenu/BlockMenuController.svelte.js +245 -0
- package/dist/plugins/blockMenu/blockMenuPlugin.d.ts +11 -0
- package/dist/plugins/blockMenu/blockMenuPlugin.js +89 -0
- package/dist/plugins/code/CodePlugin.svelte +210 -163
- package/dist/plugins/code/CodePlugin.svelte.d.ts +1 -3
- package/dist/plugins/code/code.css +92 -0
- package/dist/plugins/icons.d.ts +2 -0
- package/dist/plugins/icons.js +31 -0
- package/dist/plugins/image/ImageEmpty.svelte +69 -0
- package/dist/plugins/image/ImageEmpty.svelte.d.ts +8 -0
- package/dist/plugins/image/ImagePlugin.svelte +74 -12
- package/dist/plugins/image/ImagePlugin.svelte.d.ts +15 -0
- package/dist/plugins/image/image.d.ts +2 -0
- package/dist/plugins/image/image.js +7 -0
- package/dist/plugins/index.d.ts +14 -1
- package/dist/plugins/index.js +10 -1
- package/dist/plugins/keepInView.d.ts +4 -0
- package/dist/plugins/keepInView.js +6 -0
- package/dist/plugins/markdownShortcuts.d.ts +3 -0
- package/dist/plugins/markdownShortcuts.js +104 -0
- package/dist/plugins/mention/MentionPlugin.svelte +28 -31
- package/dist/plugins/richtext/RichTextPlugin.svelte +411 -200
- package/dist/plugins/richtext/RichTextPlugin.svelte.d.ts +8 -7
- package/dist/plugins/richtext/richTextOperations.d.ts +53 -0
- package/dist/plugins/richtext/richTextOperations.js +200 -0
- package/dist/plugins/slashMenu/SlashMenu.svelte +297 -0
- package/dist/plugins/slashMenu/SlashMenu.svelte.d.ts +11 -0
- package/dist/plugins/slashMenu/SlashMenuController.svelte.d.ts +56 -0
- package/dist/plugins/slashMenu/SlashMenuController.svelte.js +275 -0
- package/dist/plugins/slashMenu/slashMenuPlugin.d.ts +25 -0
- package/dist/plugins/slashMenu/slashMenuPlugin.js +105 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +142 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +10 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +28 -0
- package/dist/plugins/suggestions/suggestionsPlugin.js +53 -0
- package/dist/plugins/toolbar/Toolbar.svelte +341 -0
- package/dist/plugins/toolbar/Toolbar.svelte.d.ts +9 -0
- package/dist/plugins/toolbar/ToolbarController.svelte.d.ts +85 -0
- package/dist/plugins/toolbar/ToolbarController.svelte.js +150 -0
- package/dist/plugins/toolbar/toolbarPlugin.d.ts +16 -0
- package/dist/plugins/toolbar/toolbarPlugin.js +40 -0
- package/dist/plugins.d.ts +249 -40
- package/dist/plugins.js +11 -5
- package/dist/selection/domSelection.d.ts +29 -0
- package/dist/selection/domSelection.js +230 -0
- package/dist/selection/replaceSelection.d.ts +143 -0
- package/dist/selection/replaceSelection.js +316 -0
- package/dist/selection/selection.svelte.d.ts +275 -37
- package/dist/selection/selection.svelte.js +1162 -331
- package/dist/selection/selection.utils.d.ts +35 -4
- package/dist/selection/selection.utils.js +309 -100
- package/dist/selection/visibility.d.ts +88 -0
- package/dist/selection/visibility.js +140 -0
- package/dist/session/attempt.d.ts +190 -0
- package/dist/session/attempt.js +188 -0
- package/dist/session/bindings.d.ts +16 -0
- package/dist/session/bindings.js +262 -0
- package/dist/session/commands.d.ts +158 -0
- package/dist/session/commands.js +601 -0
- package/dist/session/composition.svelte.d.ts +114 -0
- package/dist/session/composition.svelte.js +489 -0
- package/dist/session/editing/text.d.ts +33 -0
- package/dist/session/editing/text.js +36 -0
- package/dist/session/handles.d.ts +63 -0
- package/dist/session/handles.js +123 -0
- package/dist/session/history.d.ts +49 -0
- package/dist/session/history.js +131 -0
- package/dist/session/keymap.d.ts +42 -0
- package/dist/session/keymap.js +144 -0
- package/dist/session/moves.d.ts +32 -0
- package/dist/session/moves.js +116 -0
- package/dist/session/navigation.d.ts +51 -0
- package/dist/session/navigation.js +312 -0
- package/dist/session/props.d.ts +28 -0
- package/dist/session/props.js +162 -0
- package/dist/session/selection.d.ts +126 -0
- package/dist/session/selection.js +227 -0
- package/dist/session/suggestions.svelte.d.ts +125 -0
- package/dist/session/suggestions.svelte.js +329 -0
- package/dist/session/virtual.d.ts +33 -0
- package/dist/session/virtual.js +182 -0
- package/dist/surface/attributes.d.ts +36 -0
- package/dist/surface/attributes.js +143 -0
- package/dist/surface/cells.d.ts +103 -0
- package/dist/surface/cells.js +215 -0
- package/dist/surface/observer.svelte.d.ts +70 -0
- package/dist/surface/observer.svelte.js +969 -0
- package/dist/surface/overlay.d.ts +33 -0
- package/dist/surface/overlay.js +91 -0
- package/dist/surface/pin.svelte.d.ts +56 -0
- package/dist/surface/pin.svelte.js +108 -0
- package/dist/surface/projector.svelte.d.ts +69 -0
- package/dist/surface/projector.svelte.js +486 -0
- package/dist/text/deltas.d.ts +14 -3
- package/dist/text/deltas.js +27 -90
- package/dist/text/text.svelte.d.ts +43 -31
- package/dist/text/text.svelte.js +88 -69
- package/dist/text/text.utils.d.ts +7 -2
- package/dist/text/text.utils.js +121 -97
- package/dist/themes/notion.css +473 -0
- package/dist/utils/diffText.d.ts +15 -0
- package/dist/utils/diffText.js +26 -0
- package/dist/utils/json.d.ts +90 -3
- package/dist/utils/json.js +369 -2
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +2 -0
- package/package.json +112 -50
- package/dist/block/content.svelte.d.ts +0 -14
- package/dist/block/content.svelte.js +0 -40
- package/dist/components/ReadonlyEditor.svelte +0 -46
- package/dist/components/ReadonlyEditor.svelte.d.ts +0 -4
- package/dist/components/readonlyElements.svelte.d.ts +0 -82
- package/dist/components/readonlyElements.svelte.js +0 -144
- package/dist/dnd.svelte.d.ts +0 -17
- package/dist/dnd.svelte.js +0 -125
- package/dist/hotkeys.d.ts +0 -27
- package/dist/hotkeys.js +0 -282
- package/dist/localProvider.d.ts +0 -82
- package/dist/localProvider.js +0 -267
- package/dist/plugins/code/prism.css +0 -141
- package/dist/utils/jsx.d.ts +0 -25
- package/dist/utils/jsx.js +0 -66
- package/dist/utils/serialize.js +0 -1
- /package/dist/{utils/serialize.d.ts → crdt/engine-api.js} +0 -0
|
@@ -1,391 +1,1222 @@
|
|
|
1
|
-
import {} from 'yjs';
|
|
2
|
-
import * as Y from 'yjs';
|
|
3
1
|
import { Text } from '../text/text.svelte.js';
|
|
4
|
-
import {
|
|
2
|
+
import { climbDom, getElementFromNode, getInlineBlockOfNode, getInlineBlockInSelectedRange, getRangesFromSelection, getTextOfNode, getTextsInSelection, getYIndex, getMarkEdgeSide, isTextBoundSelectionPoint, normalizeUtf16Boundary, SYNTHETIC_TEXT_OVERLAY_SELECTOR, SUGGESTION } from './selection.utils.js';
|
|
3
|
+
import { clearDomSelection, getActiveElement, getDomSelectionSnapshot } from './domSelection.js';
|
|
4
|
+
import { domPointOf } from '../surface/projector.svelte.js';
|
|
5
5
|
import { Block } from '../block/block.svelte.js';
|
|
6
6
|
import { SvelteSet } from 'svelte/reactivity';
|
|
7
|
-
import { tick } from 'svelte';
|
|
8
7
|
import { InlineBlock } from '../block/inlineBlock.svelte.js';
|
|
8
|
+
import { isNativeFormControl, isNativeInteractiveEvent, isNestedForeignEditableTarget } from '../events/nativeInteractiveControl.js';
|
|
9
|
+
import { atomSelection, blockSelection, noSelection, project, sameValue, segmentsOf, serialize, textSelection } from '../session/selection.js';
|
|
10
|
+
import { seam } from '../crdt/anchors.js';
|
|
11
|
+
import { getTextPath } from '../events/events.utils.js';
|
|
12
|
+
import { landed } from '../session/navigation.js';
|
|
13
|
+
import * as visibility from './visibility.js';
|
|
14
|
+
import { caretBeside, shownText } from './replaceSelection.js';
|
|
15
|
+
/** The compatibility state of no (or an unresolvable) selection. */
|
|
16
|
+
const EMPTY_STATE = Object.freeze({
|
|
17
|
+
yStart: 0,
|
|
18
|
+
yEnd: 0,
|
|
19
|
+
isCollapsed: true,
|
|
20
|
+
isReversed: false,
|
|
21
|
+
texts: [],
|
|
22
|
+
blocks: [],
|
|
23
|
+
startText: null,
|
|
24
|
+
endText: null,
|
|
25
|
+
startBlock: null,
|
|
26
|
+
endBlock: null,
|
|
27
|
+
isBlockSpanning: false,
|
|
28
|
+
isVoidEditableElement: false
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Whether a DOM selection runs backward (focus before anchor in document
|
|
32
|
+
* order), comparing boundary points — a focus on an ancestor element of the
|
|
33
|
+
* anchor (`(textElement, childCount)`) is after it (F-S6).
|
|
34
|
+
*/
|
|
35
|
+
export const isBackward = (selection) => {
|
|
36
|
+
const { anchorNode, focusNode } = selection;
|
|
37
|
+
if (!anchorNode || !focusNode)
|
|
38
|
+
return false;
|
|
39
|
+
if (anchorNode === focusNode)
|
|
40
|
+
return selection.focusOffset < selection.anchorOffset;
|
|
41
|
+
try {
|
|
42
|
+
const range = (anchorNode.ownerDocument ?? document).createRange();
|
|
43
|
+
range.setStart(anchorNode, selection.anchorOffset);
|
|
44
|
+
return range.comparePoint(focusNode, selection.focusOffset) < 0;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return Boolean(anchorNode.compareDocumentPosition(focusNode) & Node.DOCUMENT_POSITION_PRECEDING);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const getSyntheticTextOverlayElement = (node, offset) => {
|
|
51
|
+
let syntheticOverlay = null;
|
|
52
|
+
climbDom(node, (currentNode) => {
|
|
53
|
+
if (currentNode instanceof HTMLElement &&
|
|
54
|
+
currentNode.matches(SYNTHETIC_TEXT_OVERLAY_SELECTOR)) {
|
|
55
|
+
syntheticOverlay = currentNode;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (syntheticOverlay || !(node instanceof Element)) {
|
|
60
|
+
return syntheticOverlay;
|
|
61
|
+
}
|
|
62
|
+
for (const boundaryNode of [node.childNodes[offset], node.childNodes[offset - 1]]) {
|
|
63
|
+
if (!(boundaryNode instanceof HTMLElement)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (boundaryNode.matches(SYNTHETIC_TEXT_OVERLAY_SELECTOR)) {
|
|
67
|
+
return boundaryNode;
|
|
68
|
+
}
|
|
69
|
+
const descendant = boundaryNode.querySelector(SYNTHETIC_TEXT_OVERLAY_SELECTOR);
|
|
70
|
+
if (descendant) {
|
|
71
|
+
return descendant;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
};
|
|
76
|
+
const getInlineBlockBetweenBoundaryTexts = (startText, endText, yStart, yEnd) => {
|
|
77
|
+
if (!endText || startText.parent !== endText.parent) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (yStart !== startText.length || yEnd !== 0) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const content = startText.parent.content;
|
|
84
|
+
const startIndex = content.indexOf(startText);
|
|
85
|
+
const endIndex = content.indexOf(endText);
|
|
86
|
+
if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const selectedParts = content.slice(startIndex + 1, endIndex);
|
|
90
|
+
return selectedParts.length === 1 && selectedParts[0] instanceof InlineBlock
|
|
91
|
+
? selectedParts[0]
|
|
92
|
+
: null;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Where a range over `block` starts (`first`) or ends (`last`): its first or
|
|
96
|
+
* last shown line (a list's items, a code block's lines, GX-03). A block that
|
|
97
|
+
* shows none (a divider) passes to its neighbours as a caret does, the next
|
|
98
|
+
* line first; to `cover` it, a start passes to the line before it and an end
|
|
99
|
+
* to the line after it, so the range still holds it — when a line lies on
|
|
100
|
+
* that side: a divider with none beyond it (it starts or ends the
|
|
101
|
+
* document, or only dividers follow to that edge) is left out.
|
|
102
|
+
*/
|
|
103
|
+
const rangeEdge = (block, edge, cover = false) => {
|
|
104
|
+
const text = shownText(block, edge);
|
|
105
|
+
if (text)
|
|
106
|
+
return { text, offset: edge === 'first' ? 0 : text.length };
|
|
107
|
+
const before = cover && edge === 'first';
|
|
108
|
+
return (caretBeside(block, before ? 'blockBefore' : 'blockAfter') ??
|
|
109
|
+
caretBeside(block, before ? 'blockAfter' : 'blockBefore'));
|
|
110
|
+
};
|
|
9
111
|
export class EdytorSelection {
|
|
10
112
|
edytorOnSelectionChange;
|
|
11
113
|
edytor;
|
|
12
114
|
focusedBlocks = new SvelteSet();
|
|
115
|
+
/** The selected blocks as clicked: a grip-selected list is one block. */
|
|
13
116
|
selectedBlocks = new SvelteSet();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The blocks a command over the block selection acts on, in document
|
|
119
|
+
* order: the selected blocks, a selected list or code block with its
|
|
120
|
+
* whole subtree (`sel.blocks.exact`). Delete, cut, copy and Turn into read it.
|
|
121
|
+
*/
|
|
122
|
+
get selectedMembers() {
|
|
123
|
+
return visibility.selectedMembers(this.edytor);
|
|
124
|
+
}
|
|
125
|
+
selectedInlineBlock = new SvelteSet();
|
|
126
|
+
inlineBlockDeletionTarget = null;
|
|
127
|
+
/**
|
|
128
|
+
* Set for the synchronous window of THIS view's history command
|
|
129
|
+
* (`session/history`): the replay commits under the history's origin, so
|
|
130
|
+
* the remote-apply and repair restores stand aside — the view's recorded
|
|
131
|
+
* value is selected right after.
|
|
132
|
+
*/
|
|
133
|
+
expectHistoryRestore = false;
|
|
134
|
+
pointerDragStart = null;
|
|
135
|
+
selectionDocument = null;
|
|
136
|
+
shouldKeepModelSelectionForNextTextInsertion = false;
|
|
137
|
+
modelSelectionPreservationBlock = null;
|
|
138
|
+
/**
|
|
139
|
+
* The selection (R9, L4): a value — none, a text range of two anchors,
|
|
140
|
+
* one inline atom, or a set of block ids. Only `select()` replaces it.
|
|
141
|
+
*/
|
|
142
|
+
value = $state.raw(noSelection);
|
|
143
|
+
/** Advanced by every `select()`. */
|
|
144
|
+
epoch = 0;
|
|
145
|
+
/**
|
|
146
|
+
* Advanced by every `select()` that did not come from the DOM: the
|
|
147
|
+
* projector displays the current value after the flush (R10).
|
|
148
|
+
*/
|
|
149
|
+
request = $state(0);
|
|
150
|
+
/** The last display request came from a user-input frame: the display may scroll (O54). */
|
|
151
|
+
scrollOnDisplay = false;
|
|
152
|
+
/** The intent serial (gestures but `input`) when the last display was requested. */
|
|
153
|
+
requestSerial = 0;
|
|
154
|
+
/** Why the last `select()` ran. */
|
|
155
|
+
cause = 'model';
|
|
156
|
+
/** DOM fields (Surface) observed with the value they describe. */
|
|
157
|
+
/** The mark-edge side a display or a DOM derive observed the value with. */
|
|
158
|
+
#surface = null;
|
|
159
|
+
#compat = new WeakMap();
|
|
48
160
|
constructor(edytor, edytorOnSelectionChange) {
|
|
49
161
|
this.edytorOnSelectionChange = edytorOnSelectionChange;
|
|
50
162
|
this.edytor = edytor;
|
|
51
163
|
}
|
|
164
|
+
/** The projection of the current value at the current document version. */
|
|
165
|
+
get projection() {
|
|
166
|
+
void this.edytor.valueRevision;
|
|
167
|
+
return project(this.value, this.edytor.facade);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The read-only wrapper view of the projection (D3): every field is a
|
|
171
|
+
* projection of (value, document version); `edge` comes from the Surface
|
|
172
|
+
* for the value it was observed with.
|
|
173
|
+
*/
|
|
174
|
+
get state() {
|
|
175
|
+
const projection = this.projection;
|
|
176
|
+
const surface = this.#surface?.value === this.value ? this.#surface : null;
|
|
177
|
+
const hit = this.#compat.get(projection);
|
|
178
|
+
if (hit && hit.surface === surface)
|
|
179
|
+
return hit.state;
|
|
180
|
+
const state = this.#view(this.value, projection, surface?.edge);
|
|
181
|
+
this.#compat.set(projection, { surface, state });
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
#textOf = (block, segment) => {
|
|
185
|
+
if (segment?.kind !== 'text')
|
|
186
|
+
return null;
|
|
187
|
+
return this.edytor.idToBlock.text(block.id, segment.segOrd);
|
|
188
|
+
};
|
|
189
|
+
#view = (value, projection, edge) => {
|
|
190
|
+
const { start, end } = projection;
|
|
191
|
+
const blockOf = (id) => this.edytor.idToBlock.get(id) ?? null;
|
|
192
|
+
const startBlock = start && blockOf(start.block);
|
|
193
|
+
const endBlock = end && blockOf(end.block);
|
|
194
|
+
if (!start || !end || !startBlock || !endBlock)
|
|
195
|
+
return EMPTY_STATE;
|
|
196
|
+
let parts = projection.segments;
|
|
197
|
+
if (value.kind === 'atom') {
|
|
198
|
+
const all = segmentsOf(this.edytor.facade, start.block);
|
|
199
|
+
const index = all.findIndex((part) => part.kind === 'inline' && part.id === value.atomId);
|
|
200
|
+
parts = all.slice(index - 1, index + 2);
|
|
201
|
+
}
|
|
202
|
+
const segments = parts.filter((part) => part.kind === 'text');
|
|
203
|
+
const [startSegment, endSegment] = [segments[0], segments.at(-1)];
|
|
204
|
+
const startText = this.#textOf(startBlock, startSegment);
|
|
205
|
+
const endText = this.#textOf(endBlock, endSegment);
|
|
206
|
+
if (!startText || !endText || !startSegment || !endSegment)
|
|
207
|
+
return EMPTY_STATE;
|
|
208
|
+
const texts = segments.flatMap((part) => {
|
|
209
|
+
const block = blockOf(part.block);
|
|
210
|
+
return (block && this.#textOf(block, part)) ?? [];
|
|
211
|
+
});
|
|
212
|
+
const blocks = value.kind === 'blocks'
|
|
213
|
+
? value.ids
|
|
214
|
+
.filter((id) => this.edytor.facade.isVisibleBlock(id))
|
|
215
|
+
.sort((a, b) => this.edytor.facade.compare(a, b))
|
|
216
|
+
.flatMap((id) => blockOf(id) ?? [])
|
|
217
|
+
: projection.blocks.flatMap((id) => blockOf(id) ?? []);
|
|
218
|
+
const node = this.edytor.node;
|
|
219
|
+
return Object.defineProperty({
|
|
220
|
+
yStart: start.offset - startSegment.start,
|
|
221
|
+
yEnd: end.offset - endSegment.start,
|
|
222
|
+
isCollapsed: projection.isCollapsed,
|
|
223
|
+
isReversed: projection.isReversed,
|
|
224
|
+
isBlockSpanning: projection.isBlockSpanning,
|
|
225
|
+
texts,
|
|
226
|
+
blocks,
|
|
227
|
+
edge,
|
|
228
|
+
startText,
|
|
229
|
+
endText,
|
|
230
|
+
startBlock,
|
|
231
|
+
endBlock
|
|
232
|
+
}, 'isVoidEditableElement', {
|
|
233
|
+
enumerable: true,
|
|
234
|
+
get: () => projection.voidRoot !== null &&
|
|
235
|
+
['INPUT', 'TEXTAREA'].includes(getActiveElement(node)?.tagName.toUpperCase() ?? '')
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* The one commit point (R9): replaces the value, advances the epoch and
|
|
240
|
+
* applies every side effect once — the selected, atom and focused sets
|
|
241
|
+
* (hooks and attributes), and, when the value changed, presence and `onSelectionChange`.
|
|
242
|
+
* `surface` carries the mark-edge side the value was observed with.
|
|
243
|
+
*/
|
|
244
|
+
select = (next, cause = 'model', surface) => {
|
|
245
|
+
next = this.#keepPending(this.#shown(next));
|
|
246
|
+
const changed = !sameValue(this.value, next);
|
|
247
|
+
if (changed)
|
|
248
|
+
this.value = next;
|
|
249
|
+
const value = this.value;
|
|
250
|
+
this.epoch++;
|
|
251
|
+
// A repair is a background display (it never takes focus); a command,
|
|
252
|
+
// history or host code asks for one (R10).
|
|
253
|
+
if (cause === 'repair')
|
|
254
|
+
this.edytor.surface.update();
|
|
255
|
+
else if (cause !== 'dom')
|
|
256
|
+
this.display();
|
|
257
|
+
this.cause = cause;
|
|
258
|
+
if (surface)
|
|
259
|
+
this.#surface = { value, ...surface };
|
|
260
|
+
const projection = project(value, this.edytor.facade);
|
|
261
|
+
const blockOf = (id) => this.edytor.idToBlock.get(id);
|
|
262
|
+
const selected = value.kind === 'blocks'
|
|
263
|
+
? value.ids
|
|
264
|
+
.filter((id) => this.edytor.facade.isVisibleBlock(id))
|
|
265
|
+
.flatMap((id) => blockOf(id) ?? [])
|
|
266
|
+
: [];
|
|
267
|
+
const atom = value.kind === 'atom'
|
|
268
|
+
? blockOf(value.blockId)?.content.find((part) => part instanceof InlineBlock && part.id === value.atomId)
|
|
269
|
+
: undefined;
|
|
270
|
+
const focused = value.kind === 'text'
|
|
271
|
+
? projection.blocks.flatMap((id) => blockOf(id) ?? [])
|
|
272
|
+
: value.kind === 'atom'
|
|
273
|
+
? [blockOf(value.blockId) ?? []].flat()
|
|
274
|
+
: [];
|
|
275
|
+
this.#sync(this.selectedBlocks, selected, 'selected');
|
|
276
|
+
this.#sync(this.focusedBlocks, focused, 'focused');
|
|
277
|
+
if (atom !== this.inlineBlockDeletionTarget || (!atom && this.selectedInlineBlock.size)) {
|
|
278
|
+
this.selectedInlineBlock.clear();
|
|
279
|
+
if (atom)
|
|
280
|
+
this.selectedInlineBlock.add(atom);
|
|
281
|
+
this.inlineBlockDeletionTarget = atom ?? null;
|
|
282
|
+
}
|
|
283
|
+
const state = this.state;
|
|
284
|
+
this.edytor.history?.selected(value);
|
|
285
|
+
if (state.startText) {
|
|
286
|
+
this.#lastText = state.startText;
|
|
287
|
+
this.#lastBlock = state.startBlock?.id ?? null;
|
|
288
|
+
}
|
|
289
|
+
if (!changed)
|
|
290
|
+
return;
|
|
291
|
+
// A dead view never publishes: its entry went with its teardown.
|
|
292
|
+
if (!this.edytor.destroyed) {
|
|
293
|
+
const focus = projection.isReversed ? projection.start : projection.end;
|
|
294
|
+
this.edytor.presence.write(serialize(value, projection), focus?.block ?? null);
|
|
295
|
+
}
|
|
296
|
+
this.edytorOnSelectionChange?.(this);
|
|
297
|
+
this.edytor.plugins.forEach((plugin) => {
|
|
298
|
+
plugin.onSelectionChange?.(this);
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
/** The marks the next insertion at the caret takes (L4, values kept). */
|
|
302
|
+
get pending() {
|
|
303
|
+
return this.value.kind === 'text' ? this.value.pending : undefined;
|
|
304
|
+
}
|
|
305
|
+
/** Stage (or, with `undefined`, clear) the caret's pending marks: a new value, same anchors. */
|
|
306
|
+
stage = (pending) => {
|
|
307
|
+
const value = this.value;
|
|
308
|
+
if (value.kind === 'text')
|
|
309
|
+
this.select(Object.freeze({ ...value, pending }));
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* No text endpoint rests in a block that renders no content (FX-01): its
|
|
313
|
+
* slot never mounts, so typing there is saved and never shown. Such a
|
|
314
|
+
* caret moves to the nearest shown line — a container's first item, else
|
|
315
|
+
* the next line, else the end of the one before (a line just created is
|
|
316
|
+
* displayed once it mounts). A range's start moves to the block's first
|
|
317
|
+
* shown line and its end to its last (a list's items, `rangeEdge`, GX-03);
|
|
318
|
+
* a void's passes on as a caret does. With none, the value stays as it
|
|
319
|
+
* was. Every caret write passes here, and so does the repair after a
|
|
320
|
+
* change this view did not make.
|
|
321
|
+
*/
|
|
322
|
+
#shown = (next) => {
|
|
323
|
+
if (next.kind !== 'text')
|
|
324
|
+
return next;
|
|
325
|
+
const { start, end, isReversed } = project(next, this.edytor.facade);
|
|
326
|
+
const caret = next.focus === next.anchor;
|
|
327
|
+
const moved = (at, anchor, edge) => {
|
|
328
|
+
const block = at && this.edytor.idToBlock.get(at.block);
|
|
329
|
+
if (!block || block.rendersContent)
|
|
330
|
+
return anchor;
|
|
331
|
+
const to = caret
|
|
332
|
+
? (caretBeside(block, 'blockAfter') ?? caretBeside(block, 'blockBefore'))
|
|
333
|
+
: rangeEdge(block, edge);
|
|
334
|
+
return to && this.createTextAnchor(to.text, to.offset);
|
|
335
|
+
};
|
|
336
|
+
const [first, last] = isReversed ? ['last', 'first'] : ['first', 'last'];
|
|
337
|
+
const anchor = moved(isReversed ? end : start, next.anchor, first);
|
|
338
|
+
const focus = caret ? anchor : moved(isReversed ? start : end, next.focus, last);
|
|
339
|
+
if (anchor === next.anchor && focus === next.focus)
|
|
340
|
+
return next;
|
|
341
|
+
return anchor && focus ? textSelection(anchor, focus, next.pending) : this.value;
|
|
342
|
+
};
|
|
343
|
+
/** A caret that did not move keeps its pending marks (L4); a value that names them wins. */
|
|
344
|
+
#keepPending = (next) => {
|
|
345
|
+
const prev = this.value;
|
|
346
|
+
if (prev.kind !== 'text' || !prev.pending || next.kind !== 'text' || 'pending' in next)
|
|
347
|
+
return next;
|
|
348
|
+
const [a, b] = [project(prev, this.edytor.facade), project(next, this.edytor.facade)];
|
|
349
|
+
const at = (p, q) => p !== null && q !== null && p.block === q.block && p.offset === q.offset;
|
|
350
|
+
return at(a.start, b.start) && at(a.end, b.end)
|
|
351
|
+
? textSelection(next.anchor, next.focus, prev.pending)
|
|
352
|
+
: next;
|
|
353
|
+
};
|
|
354
|
+
/** Diff one block set against its new members: hooks only for changes (the renderer draws the attributes). */
|
|
355
|
+
#sync = (set, blocks, kind) => {
|
|
356
|
+
const next = new Set(blocks);
|
|
357
|
+
for (const block of set) {
|
|
358
|
+
if (next.has(block))
|
|
359
|
+
continue;
|
|
360
|
+
set.delete(block);
|
|
361
|
+
// `onDeselect` pairs with `onSelect`; `onBlur` keeps firing on both
|
|
362
|
+
// (it has historically doubled as the selection-loss hook).
|
|
363
|
+
if (kind === 'selected')
|
|
364
|
+
block.definition.onDeselect?.({ block });
|
|
365
|
+
block.definition.onBlur?.({ block });
|
|
366
|
+
}
|
|
367
|
+
for (const block of next) {
|
|
368
|
+
if (set.has(block))
|
|
369
|
+
continue;
|
|
370
|
+
set.add(block);
|
|
371
|
+
if (kind === 'selected')
|
|
372
|
+
block.definition.onSelect?.({ block });
|
|
373
|
+
else
|
|
374
|
+
block.definition.onFocus?.({ block });
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* A text value from wrapper endpoints (offsets inside text segments):
|
|
379
|
+
* the start binds right (boundary inserts stay outside), the end and a
|
|
380
|
+
* caret bind left; `none` when the start text is not bound to the document.
|
|
381
|
+
*/
|
|
382
|
+
textValue = (startText, yStart, endText = startText, yEnd = yStart, isReversed = false, startAffinity, mint = this.createTextAnchor) => {
|
|
383
|
+
const collapsed = startText === endText && yStart === yEnd;
|
|
384
|
+
const start = mint(startText, yStart, startAffinity ?? (collapsed ? 'left' : 'right'));
|
|
385
|
+
if (!start)
|
|
386
|
+
return noSelection;
|
|
387
|
+
if (collapsed)
|
|
388
|
+
return textSelection(start);
|
|
389
|
+
const end = mint(endText, yEnd, 'left') ?? start;
|
|
390
|
+
return isReversed ? textSelection(end, start) : textSelection(start, end);
|
|
391
|
+
};
|
|
392
|
+
/**
|
|
393
|
+
* A write's intent (R4): `textValue` minted from each text's block record,
|
|
394
|
+
* live or not — the atoms of a text that died to a merge live on in the
|
|
395
|
+
* block that claimed them, so resolution follows them before any seam.
|
|
396
|
+
*/
|
|
397
|
+
#intent = (startText, yStart, endText = startText, yEnd = yStart, isReversed = false) => this.textValue(startText, yStart, endText, yEnd, isReversed, undefined, (text, at, side) => {
|
|
398
|
+
return text.parent.isRoot
|
|
399
|
+
? null
|
|
400
|
+
: this.edytor.facade.anchorAt(text.blockId, text.segStart + at, side);
|
|
401
|
+
});
|
|
402
|
+
/** Ask the projector to display the current value after the flush (R10). */
|
|
403
|
+
display = () => {
|
|
404
|
+
this.scrollOnDisplay =
|
|
405
|
+
this.edytor.isHandlingUserInput && this.edytor.suppressCaretScrollDepth === 0;
|
|
406
|
+
this.requestSerial = this.edytor.intentSerial;
|
|
407
|
+
this.request++;
|
|
408
|
+
};
|
|
409
|
+
/** The mark-edge side a display showed the current value with. */
|
|
410
|
+
observed = (surface) => {
|
|
411
|
+
this.#surface = { value: this.value, ...surface };
|
|
412
|
+
};
|
|
413
|
+
/** A pointer drag is in progress: the projector does not display under it (O57). */
|
|
414
|
+
get dragging() {
|
|
415
|
+
return this.pointerDragStart !== null;
|
|
416
|
+
}
|
|
417
|
+
getBlockByPath = (path) => {
|
|
418
|
+
if (!path?.length) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
let block = this.edytor.root;
|
|
422
|
+
for (const index of path) {
|
|
423
|
+
block = block?.children[index];
|
|
424
|
+
}
|
|
425
|
+
return block ?? null;
|
|
426
|
+
};
|
|
52
427
|
destroy = () => {
|
|
53
|
-
|
|
428
|
+
if (this.selectionDocument) {
|
|
429
|
+
this.selectionDocument.removeEventListener('selectionchange', this.onSelectionChange);
|
|
430
|
+
this.selectionDocument = null;
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* `selectedInlineBlock` and `inlineBlockDeletionTarget` are one
|
|
435
|
+
* selection — every mutation must go through these two helpers so the
|
|
436
|
+
* armed Backspace/delete target can never outlive the cleared set
|
|
437
|
+
* (a stale target feeds `deleteSelectedInlineBlock` and makes
|
|
438
|
+
* `restoreSelectionAfterRepair` bail forever).
|
|
439
|
+
*/
|
|
440
|
+
clearInlineBlockSelection = () => {
|
|
441
|
+
const value = this.value;
|
|
442
|
+
if (value.kind !== 'atom')
|
|
443
|
+
return;
|
|
444
|
+
// Leaving the atom: a caret at its start (the end of the text before it).
|
|
445
|
+
const atom = this.edytor.idToBlock
|
|
446
|
+
.get(value.blockId)
|
|
447
|
+
?.content.find((part) => part instanceof InlineBlock && part.id === value.atomId);
|
|
448
|
+
const before = atom?.parent.content[atom.parent.content.indexOf(atom) - 1];
|
|
449
|
+
this.select(before instanceof Text ? this.textValue(before, before.length) : noSelection);
|
|
450
|
+
};
|
|
451
|
+
selectInlineBlock = (inlineBlock, from = 'before') => {
|
|
452
|
+
this.select(atomSelection(inlineBlock.parent.id, inlineBlock.id, from));
|
|
453
|
+
};
|
|
454
|
+
clearModelSelectionPreservation = () => {
|
|
455
|
+
this.shouldKeepModelSelectionForNextTextInsertion = false;
|
|
456
|
+
this.modelSelectionPreservationBlock = null;
|
|
457
|
+
};
|
|
458
|
+
isStateBlockContentRange = (block) => Boolean(!this.state.isCollapsed &&
|
|
459
|
+
this.state.startBlock === block &&
|
|
460
|
+
this.state.endBlock === block &&
|
|
461
|
+
this.state.startText === block.firstText &&
|
|
462
|
+
this.state.endText === block.lastText &&
|
|
463
|
+
this.state.yStart === 0 &&
|
|
464
|
+
this.state.yEnd === block.lastText.length);
|
|
465
|
+
consumeModelSelectionPreservationForTextInsertion = () => {
|
|
466
|
+
const preservedBlock = this.modelSelectionPreservationBlock;
|
|
467
|
+
const shouldKeepPreservedBlock = this.shouldKeepModelSelectionForNextTextInsertion &&
|
|
468
|
+
preservedBlock instanceof Block &&
|
|
469
|
+
this.getBlockByPath(preservedBlock.path) === preservedBlock;
|
|
470
|
+
const shouldKeepSelection = shouldKeepPreservedBlock ||
|
|
471
|
+
(this.shouldKeepModelSelectionForNextTextInsertion &&
|
|
472
|
+
this.state.startBlock instanceof Block &&
|
|
473
|
+
this.isStateBlockContentRange(this.state.startBlock));
|
|
474
|
+
if (shouldKeepPreservedBlock) {
|
|
475
|
+
this.setStateFromBlockContentRange(preservedBlock);
|
|
476
|
+
}
|
|
477
|
+
this.shouldKeepModelSelectionForNextTextInsertion = false;
|
|
478
|
+
this.modelSelectionPreservationBlock = null;
|
|
479
|
+
return shouldKeepSelection;
|
|
54
480
|
};
|
|
55
481
|
getTextOfNode = getTextOfNode.bind(this);
|
|
56
482
|
getTextsInSelection = getTextsInSelection.bind(this);
|
|
483
|
+
getInlineBlockOfNode = getInlineBlockOfNode.bind(this);
|
|
484
|
+
getInlineBlockInSelectedRange = getInlineBlockInSelectedRange.bind(this);
|
|
485
|
+
getBlockOfNode = (node) => {
|
|
486
|
+
const element = getElementFromNode(node);
|
|
487
|
+
const blockElement = element?.closest('[data-edytor-block]');
|
|
488
|
+
if (!(blockElement instanceof HTMLElement) || !this.edytor.node?.contains(blockElement)) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const id = blockElement.dataset.edytorId;
|
|
492
|
+
return id ? (this.edytor.idToBlock.get(id) ?? null) : null;
|
|
493
|
+
};
|
|
494
|
+
isInsideEditableText = (node) => {
|
|
495
|
+
const element = getElementFromNode(node);
|
|
496
|
+
return Boolean(element?.closest('[data-edytor-text]'));
|
|
497
|
+
};
|
|
498
|
+
getNonNativeEditableIslandBlock = (node) => {
|
|
499
|
+
const element = getElementFromNode(node);
|
|
500
|
+
const island = element?.closest('[contenteditable="false"]');
|
|
501
|
+
if (!(island instanceof HTMLElement) || !this.edytor.node?.contains(island)) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
if (island.closest('input, textarea, select, button, a[href]')) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
return this.getBlockOfNode(island);
|
|
508
|
+
};
|
|
509
|
+
getNonNativeEditableBlockChromeBlock = (node) => {
|
|
510
|
+
const element = getElementFromNode(node);
|
|
511
|
+
const nonEditableElement = element?.closest('[contenteditable="false"]');
|
|
512
|
+
if (!(nonEditableElement instanceof HTMLElement) ||
|
|
513
|
+
!this.edytor.node?.contains(nonEditableElement)) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
// A suggestion's preview is no block's chrome: a press there is its own (cancelled).
|
|
517
|
+
if (nonEditableElement.closest(`input, textarea, select, button, a[href], ${SUGGESTION}`)) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const block = this.getBlockOfNode(nonEditableElement);
|
|
521
|
+
if (nonEditableElement.closest('[data-edytor-text], [data-edytor-inline-block]') || !block) {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
return block;
|
|
525
|
+
};
|
|
526
|
+
handleNonNativeEditableBlockChromePointerDown = (event) => {
|
|
527
|
+
const targetNode = event.target instanceof Node ? event.target : null;
|
|
528
|
+
const targetBlock = this.getNonNativeEditableBlockChromeBlock(targetNode);
|
|
529
|
+
const targetText = targetBlock?.firstText;
|
|
530
|
+
// A native control inside the chrome (a void block's input) owns its press.
|
|
531
|
+
if (!targetText || isNativeInteractiveEvent(event)) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// The model answers this press: the browser's own caret placement on
|
|
535
|
+
// the chrome is cancelled (a canceled pointerdown moves no selection).
|
|
536
|
+
event.preventDefault();
|
|
537
|
+
this.edytor.expectInternalFocus();
|
|
538
|
+
this.edytor.node?.focus({ preventScroll: true });
|
|
539
|
+
this.setAtTextOffset(targetText, 0);
|
|
540
|
+
};
|
|
541
|
+
normalizeTextRangePoints = (startText, startOffset, endText, endOffset, isReversed) => {
|
|
542
|
+
const clampedStart = Math.min(Math.max(startOffset, 0), startText.length);
|
|
543
|
+
const clampedEnd = Math.min(Math.max(endOffset, 0), endText.length);
|
|
544
|
+
const order = startText === endText
|
|
545
|
+
? clampedStart - clampedEnd
|
|
546
|
+
: startText.parent === endText.parent
|
|
547
|
+
? startText.index - endText.index
|
|
548
|
+
: this.edytor.compareBlocks(startText.parent, endText.parent);
|
|
549
|
+
if (order <= 0) {
|
|
550
|
+
return {
|
|
551
|
+
startText,
|
|
552
|
+
startOffset: clampedStart,
|
|
553
|
+
endText,
|
|
554
|
+
endOffset: clampedEnd,
|
|
555
|
+
isReversed: order === 0 ? false : isReversed
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
startText: endText,
|
|
560
|
+
startOffset: clampedEnd,
|
|
561
|
+
endText: startText,
|
|
562
|
+
endOffset: clampedStart,
|
|
563
|
+
// Text atoms never reorder inside one text. Undo can nevertheless
|
|
564
|
+
// resolve the two deleted-boundary anchors on opposite sides of
|
|
565
|
+
// their restored atoms. Sort those offsets without changing the
|
|
566
|
+
// selection direction stored independently in the snapshot.
|
|
567
|
+
isReversed: startText === endText ? isReversed : !isReversed
|
|
568
|
+
};
|
|
569
|
+
};
|
|
57
570
|
init = () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
this.edytor.undoManager.on('stack-item-popped', (event) => {
|
|
65
|
-
const cursorLocation = event.stackItem.meta.get('cursor-location');
|
|
66
|
-
const text = this.edytor.getTextById(cursorLocation.id);
|
|
67
|
-
text && this.setAtTextOffset(text, cursorLocation.offset);
|
|
68
|
-
});
|
|
69
|
-
document?.addEventListener('selectionchange', this.onSelectionChange);
|
|
571
|
+
if (typeof document !== 'undefined') {
|
|
572
|
+
this.selectionDocument = this.edytor.node?.ownerDocument ?? document;
|
|
573
|
+
this.selectionDocument.addEventListener('selectionchange', this.onSelectionChange);
|
|
574
|
+
}
|
|
70
575
|
};
|
|
71
|
-
|
|
576
|
+
/**
|
|
577
|
+
* A triple click selects the block's content in the model; the browser's
|
|
578
|
+
* own multi-click selection is cancelled at its `mousedown`
|
|
579
|
+
* (`preventNativeTripleClick`), so no native selection competes with the display.
|
|
580
|
+
*/
|
|
581
|
+
handleTripleClick = (e) => {
|
|
72
582
|
if (e.detail < 3)
|
|
73
583
|
return;
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
584
|
+
const targetNode = e.target instanceof Node ? e.target : null;
|
|
585
|
+
const clickedBlock = this.getBlockOfNode(targetNode);
|
|
586
|
+
if (clickedBlock?.definition.void && !this.isInsideEditableText(targetNode)) {
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
this.selectBlocks(clickedBlock);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const targetBlock = targetNode instanceof Node
|
|
592
|
+
? (this.getTextOfNode(targetNode)?.parent ?? this.getBlockOfNode(targetNode))
|
|
593
|
+
: this.state.startText?.parent;
|
|
594
|
+
if (targetBlock) {
|
|
595
|
+
this.setStateFromBlockContentRange(targetBlock);
|
|
596
|
+
this.shouldKeepModelSelectionForNextTextInsertion = true;
|
|
597
|
+
this.modelSelectionPreservationBlock = targetBlock;
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
preventNativeTripleClick = (e) => {
|
|
601
|
+
if (e.detail >= 3)
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
};
|
|
604
|
+
getTextPointFromClientPoint = (clientX, clientY) => {
|
|
605
|
+
const ownerDocument = (this.edytor.node?.ownerDocument ?? document);
|
|
606
|
+
const caretPosition = ownerDocument.caretPositionFromPoint?.(clientX, clientY);
|
|
607
|
+
const caretRange = caretPosition ? null : ownerDocument.caretRangeFromPoint?.(clientX, clientY);
|
|
608
|
+
const node = caretPosition?.offsetNode ?? caretRange?.startContainer ?? null;
|
|
609
|
+
const domOffset = caretPosition?.offset ?? caretRange?.startOffset ?? null;
|
|
610
|
+
const text = this.getTextOfNode(node, domOffset ?? undefined);
|
|
611
|
+
if (!text || typeof domOffset !== 'number') {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
text,
|
|
616
|
+
offset: Math.min(Math.max(getYIndex(text, node, domOffset), 0), text.length),
|
|
617
|
+
clientX,
|
|
618
|
+
clientY
|
|
619
|
+
};
|
|
620
|
+
};
|
|
621
|
+
getTextOffsetFromClientPoint = (text, clientX, clientY) => {
|
|
622
|
+
const ownerDocument = text.node?.ownerDocument;
|
|
623
|
+
if (!text.node || !ownerDocument) {
|
|
624
|
+
return 0;
|
|
625
|
+
}
|
|
626
|
+
const caretPoint = this.getTextPointFromClientPoint(clientX, clientY);
|
|
627
|
+
if (caretPoint?.text === text) {
|
|
628
|
+
return caretPoint.offset;
|
|
629
|
+
}
|
|
630
|
+
let closestOffset = 0;
|
|
631
|
+
let closestDistance = Number.POSITIVE_INFINITY;
|
|
632
|
+
for (let offset = 0; offset <= text.length; offset++) {
|
|
633
|
+
const [textNode, nodeOffset] = domPointOf(text.node, offset);
|
|
634
|
+
const range = ownerDocument.createRange();
|
|
635
|
+
range.setStart(textNode, nodeOffset);
|
|
636
|
+
range.collapse(true);
|
|
637
|
+
const rect = range.getBoundingClientRect();
|
|
638
|
+
const fallbackRect = text.node.getBoundingClientRect();
|
|
639
|
+
const x = rect.width || rect.height ? rect.left : fallbackRect.left;
|
|
640
|
+
const y = rect.width || rect.height ? rect.top + rect.height / 2 : fallbackRect.top;
|
|
641
|
+
const distance = Math.hypot(clientX - x, clientY - y);
|
|
642
|
+
if (distance < closestDistance) {
|
|
643
|
+
closestDistance = distance;
|
|
644
|
+
closestOffset = offset;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return closestOffset;
|
|
77
648
|
};
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
this.state.yEnd += length;
|
|
81
|
-
this.state.start += length;
|
|
82
|
-
this.state.end += length;
|
|
83
|
-
this.state.yTextContent = this.state.startText?.yText.toJSON();
|
|
649
|
+
setTextSelectionFromPointer = (text, clientX, clientY) => {
|
|
650
|
+
this.setAtTextOffset(text, this.getTextOffsetFromClientPoint(text, clientX, clientY));
|
|
84
651
|
};
|
|
652
|
+
capturePointerDragStart = (event) => {
|
|
653
|
+
if (event.button !== 0) {
|
|
654
|
+
this.pointerDragStart = null;
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
this.pointerDragStart = this.getTextPointFromClientPoint(event.clientX, event.clientY);
|
|
658
|
+
};
|
|
659
|
+
clearPointerDragStart = () => {
|
|
660
|
+
this.pointerDragStart = null;
|
|
661
|
+
this.#dropped();
|
|
662
|
+
};
|
|
663
|
+
/** A normalization a pointer drag held back (the gesture is the user's, O57). */
|
|
664
|
+
#held = false;
|
|
665
|
+
/** The drag ended: the DOM selection it left is derived again, now normalized. */
|
|
666
|
+
#dropped = () => {
|
|
667
|
+
if (!this.#held)
|
|
668
|
+
return;
|
|
669
|
+
this.#held = false;
|
|
670
|
+
this.applySelectionSnapshot(getDomSelectionSnapshot(this.edytor.node));
|
|
671
|
+
};
|
|
672
|
+
collapseSelectedBlocksAtPointer = (event) => {
|
|
673
|
+
if (event.button !== 0 || this.selectedBlocks.size === 0) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const point = this.getTextPointFromClientPoint(event.clientX, event.clientY);
|
|
677
|
+
if (!point) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.setAtTextOffset(point.text, point.offset);
|
|
681
|
+
};
|
|
682
|
+
restoreInlineAtomDragRange = (event) => {
|
|
683
|
+
const dragStart = this.pointerDragStart;
|
|
684
|
+
this.pointerDragStart = null;
|
|
685
|
+
this.#dropped();
|
|
686
|
+
if (!dragStart || event.button !== 0) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const dragEnd = this.getTextPointFromClientPoint(event.clientX, event.clientY);
|
|
690
|
+
if (!dragEnd) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const movement = Math.hypot(event.clientX - dragStart.clientX, event.clientY - dragStart.clientY);
|
|
694
|
+
if (movement < 4) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
window.setTimeout(() => {
|
|
698
|
+
if (dragStart.text.parent !== dragEnd.text.parent) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const content = dragStart.text.parent.content;
|
|
702
|
+
const startIndex = content.indexOf(dragStart.text);
|
|
703
|
+
const endIndex = content.indexOf(dragEnd.text);
|
|
704
|
+
const firstTextIndex = Math.min(startIndex, endIndex);
|
|
705
|
+
const lastTextIndex = Math.max(startIndex, endIndex);
|
|
706
|
+
const hasInlineAtomBetweenTextPoints = content.some((part, index) => part instanceof InlineBlock && index > firstTextIndex && index < lastTextIndex);
|
|
707
|
+
if (startIndex === -1 || endIndex === -1 || !hasInlineAtomBetweenTextPoints) {
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const isReversed = startIndex > endIndex;
|
|
711
|
+
const rangeStart = isReversed ? dragEnd : dragStart;
|
|
712
|
+
const rangeEnd = isReversed ? dragStart : dragEnd;
|
|
713
|
+
this.clearInlineBlockSelection();
|
|
714
|
+
this.setAtRange(rangeStart.text, rangeStart.offset, rangeEnd.text, rangeEnd.offset, {
|
|
715
|
+
isReversed
|
|
716
|
+
});
|
|
717
|
+
this.clearInlineBlockSelection();
|
|
718
|
+
});
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* `selectstart` is the only event fired before a drag-selection begins.
|
|
722
|
+
* A drag starting on `contenteditable=false` chrome (void/island
|
|
723
|
+
* chrome, inline atoms, placeholders, block handles, the render
|
|
724
|
+
* anchor) would anchor the DOM selection on nodes the model cannot
|
|
725
|
+
* represent — block-level selection owns that surface instead.
|
|
726
|
+
*
|
|
727
|
+
* The NEAREST `[contenteditable]` boundary decides: `false` means
|
|
728
|
+
* non-editable chrome, while a nested `true` (void-block captions
|
|
729
|
+
* re-enable editing on their text spans) keeps native selection.
|
|
730
|
+
* The root itself is excluded so readonly mode stays selectable.
|
|
731
|
+
*/
|
|
732
|
+
onSelectStart = (event) => {
|
|
733
|
+
// Native controls (todo checkboxes, plugin inputs…) keep their own
|
|
734
|
+
// selection behavior.
|
|
735
|
+
if (isNativeInteractiveEvent(event)) {
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const element = getElementFromNode(event.target instanceof Node ? event.target : null);
|
|
739
|
+
if (!element || !this.edytor.node?.contains(element)) {
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
// Walk ancestors to the nearest contenteditable boundary. Both the
|
|
743
|
+
// attribute and the `contentEditable` property are checked — the
|
|
744
|
+
// attach hooks of inline/void blocks set the property, which jsdom
|
|
745
|
+
// does not reflect onto the attribute.
|
|
746
|
+
let boundary = element instanceof HTMLElement ? element : null;
|
|
747
|
+
while (boundary && boundary !== this.edytor.node) {
|
|
748
|
+
const state = boundary.getAttribute('contenteditable') ?? boundary.contentEditable;
|
|
749
|
+
if (state === 'false') {
|
|
750
|
+
event.preventDefault();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (state === 'true' || state === 'plaintext-only' || state === '') {
|
|
754
|
+
// Nested editable region (e.g. void-block captions re-enabling
|
|
755
|
+
// editing on their text spans) keeps native selection.
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
boundary = boundary.parentElement;
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
/**
|
|
762
|
+
* One classifier (R10, the projector): an echo or a DOM state older than a
|
|
763
|
+
* display still to land is ignored, and so is a move while a composition
|
|
764
|
+
* session is live (the IME's; the session's end displays); drift is
|
|
765
|
+
* displayed again; a foreign write or intent is adopted.
|
|
766
|
+
*/
|
|
85
767
|
onSelectionChange = () => {
|
|
86
|
-
const selection =
|
|
87
|
-
|
|
768
|
+
const selection = getDomSelectionSnapshot(this.edytor.node);
|
|
769
|
+
const observation = this.edytor.projector.classify(selection);
|
|
770
|
+
if (observation === 'echo' || observation === 'composition')
|
|
771
|
+
return;
|
|
772
|
+
if (observation === 'drift')
|
|
773
|
+
return this.display();
|
|
774
|
+
this.applySelectionSnapshot(selection);
|
|
775
|
+
};
|
|
776
|
+
applySelectionSnapshot = (selection, options = {}) => {
|
|
777
|
+
if (this.selectedBlocks.size > 0)
|
|
778
|
+
return;
|
|
779
|
+
const container = this.edytor.node;
|
|
780
|
+
if (!selection?.anchorNode ||
|
|
781
|
+
!selection.focusNode ||
|
|
782
|
+
!container?.contains(selection.anchorNode) ||
|
|
783
|
+
!container.contains(selection.focusNode)) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const activeElement = getActiveElement(this.edytor.node);
|
|
787
|
+
if (activeElement instanceof Element &&
|
|
788
|
+
this.edytor.node?.contains(activeElement) &&
|
|
789
|
+
isNativeFormControl(activeElement)) {
|
|
88
790
|
return;
|
|
89
791
|
}
|
|
90
|
-
|
|
792
|
+
// A selection inside a nested foreign editable belongs to the
|
|
793
|
+
// island, not the model — deriving it would map the caret into the
|
|
794
|
+
// island's host text and corrupt the next model-routed write. Both
|
|
795
|
+
// endpoints are checked: a range anchored in host text but focused
|
|
796
|
+
// inside an island must not resolve either.
|
|
797
|
+
if (isNestedForeignEditableTarget(this.edytor.node, selection.anchorNode) ||
|
|
798
|
+
isNestedForeignEditableTarget(this.edytor.node, selection.focusNode)) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const { anchorNode, focusNode, anchorOffset, focusOffset } = selection;
|
|
802
|
+
const isCollapsed = selection.isCollapsed;
|
|
803
|
+
// A native vertical move that landed on no caret stop takes the key's line stop (R9).
|
|
804
|
+
if (isCollapsed &&
|
|
805
|
+
!isTextBoundSelectionPoint(anchorNode) &&
|
|
806
|
+
!this.getInlineBlockOfNode(anchorNode) &&
|
|
807
|
+
landed(this.edytor))
|
|
808
|
+
return;
|
|
91
809
|
const ranges = getRangesFromSelection(selection);
|
|
92
|
-
const isReversed =
|
|
93
|
-
? focusOffset < anchorOffset
|
|
94
|
-
: focusNode
|
|
95
|
-
? anchorNode.compareDocumentPosition(focusNode) === Node.DOCUMENT_POSITION_PRECEDING
|
|
96
|
-
: false;
|
|
97
|
-
const content = selection?.toString() || '';
|
|
810
|
+
const isReversed = isBackward(selection);
|
|
98
811
|
const startNode = isReversed ? focusNode : anchorNode;
|
|
99
812
|
const endNode = isReversed ? anchorNode : focusNode;
|
|
100
813
|
const start = isReversed ? focusOffset : anchorOffset;
|
|
101
814
|
const end = isReversed ? anchorOffset : focusOffset;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let
|
|
107
|
-
if (startText
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
let isIsland = false;
|
|
112
|
-
let islandRoot = null;
|
|
113
|
-
climb(startText?.parent, (block) => {
|
|
114
|
-
if (block instanceof Block && block.definition.island) {
|
|
115
|
-
isIsland = true;
|
|
116
|
-
islandRoot = block;
|
|
117
|
-
return true;
|
|
815
|
+
const selectedInlineBlock = this.getInlineBlockInSelectedRange(ranges[0]);
|
|
816
|
+
const selectionParts = selectedInlineBlock
|
|
817
|
+
? { startText: null, endText: null, inlineBlock: selectedInlineBlock }
|
|
818
|
+
: this.getTextsInSelection(startNode, endNode, start, end);
|
|
819
|
+
let { startText, endText, inlineBlock } = selectionParts;
|
|
820
|
+
if (!startText) {
|
|
821
|
+
const islandBlock = this.getNonNativeEditableIslandBlock(startNode);
|
|
822
|
+
if (islandBlock?.firstText) {
|
|
823
|
+
return this.setAtTextOffset(islandBlock.firstText, 0);
|
|
118
824
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (block instanceof Block && block.definition.void) {
|
|
124
|
-
isVoid = true;
|
|
125
|
-
voidRoot = block;
|
|
126
|
-
return true;
|
|
825
|
+
// A point on a white space node: the model caret is displayed again.
|
|
826
|
+
if (this.state.startText && !inlineBlock) {
|
|
827
|
+
this.display();
|
|
828
|
+
return;
|
|
127
829
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const id = node.dataset.edytorId;
|
|
133
|
-
const block = id && this.edytor.idToBlock.get(id);
|
|
134
|
-
if (block) {
|
|
135
|
-
isVoid = true;
|
|
136
|
-
voidRoot = block;
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
830
|
+
else {
|
|
831
|
+
clearDomSelection(this.edytor.node);
|
|
832
|
+
if (inlineBlock) {
|
|
833
|
+
this.selectInlineBlock(inlineBlock);
|
|
139
834
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const isAtStartOfText = yStart === 0;
|
|
143
|
-
const isAtEndOfText = yEnd === endText?.yText.length;
|
|
144
|
-
const isAtStartOfBlock = (startText && startText === startText.parent.firstText && isAtStartOfText) || false;
|
|
145
|
-
const isAtEndOfBlock = (endText && endText === endText.parent.lastText && isAtEndOfText) || false;
|
|
146
|
-
const startBlock = startText?.parent || null;
|
|
147
|
-
const endBlock = endText?.parent || null;
|
|
148
|
-
const { startBlock: previousStartBlock, endBlock: previousEndBlock } = this.state;
|
|
149
|
-
if ((previousStartBlock !== startBlock || previousEndBlock !== endBlock) &&
|
|
150
|
-
[previousStartBlock, previousEndBlock].some((block) => block?.suggestions)) {
|
|
151
|
-
[previousStartBlock, previousEndBlock].forEach((block) => {
|
|
152
|
-
if (block?.suggestions) {
|
|
153
|
-
block.suggestions = null;
|
|
835
|
+
else {
|
|
836
|
+
this.clearInlineBlockSelection();
|
|
154
837
|
}
|
|
155
|
-
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
156
840
|
}
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
841
|
+
const syntheticOverlay = isCollapsed ? getSyntheticTextOverlayElement(startNode, start) : null;
|
|
842
|
+
if (syntheticOverlay) {
|
|
843
|
+
const overlayBlock = this.getBlockOfNode(syntheticOverlay);
|
|
844
|
+
const targetText = overlayBlock?.lastText ?? startText;
|
|
845
|
+
return this.setAtTextOffset(targetText, targetText.length);
|
|
846
|
+
}
|
|
847
|
+
const rawYStart = getYIndex(startText, startNode, start);
|
|
848
|
+
const rawYEnd = isCollapsed ? rawYStart : getYIndex(endText, endNode, end);
|
|
849
|
+
let yStart = normalizeUtf16Boundary(startText.stringContent, rawYStart, isCollapsed ? 'forward' : 'backward');
|
|
850
|
+
let yEnd = isCollapsed
|
|
851
|
+
? yStart
|
|
852
|
+
: endText
|
|
853
|
+
? normalizeUtf16Boundary(endText.stringContent, rawYEnd, 'forward')
|
|
854
|
+
: rawYEnd;
|
|
855
|
+
let shouldRestoreNormalizedDomRange = yStart !== rawYStart || yEnd !== rawYEnd;
|
|
856
|
+
if (!isCollapsed &&
|
|
857
|
+
endText &&
|
|
858
|
+
startText.parent !== endText.parent &&
|
|
859
|
+
endText === endText.parent.firstText &&
|
|
860
|
+
yEnd === 0) {
|
|
861
|
+
// The shown block before: a closed toggle's header, not its hidden body.
|
|
862
|
+
const previousEndText = visibility.shown(endText.parent, 'blockBefore')?.lastText ?? null;
|
|
863
|
+
if (previousEndText?.node?.isConnected) {
|
|
864
|
+
endText = previousEndText;
|
|
865
|
+
yEnd = previousEndText.length;
|
|
866
|
+
shouldRestoreNormalizedDomRange = true;
|
|
167
867
|
}
|
|
168
868
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
let currentPart = startText;
|
|
173
|
-
let i = 0;
|
|
174
|
-
while (currentPart && currentPart !== endText) {
|
|
175
|
-
contentParts.push(currentPart);
|
|
176
|
-
currentPart = allContentParts[i + 1] || null;
|
|
177
|
-
i++;
|
|
178
|
-
}
|
|
179
|
-
if (endText) {
|
|
180
|
-
contentParts.push(endText);
|
|
181
|
-
}
|
|
182
|
-
this.state = {
|
|
183
|
-
selection,
|
|
184
|
-
start,
|
|
185
|
-
end,
|
|
186
|
-
yStart,
|
|
187
|
-
yEnd,
|
|
188
|
-
length: content.length,
|
|
189
|
-
content,
|
|
190
|
-
isCollapsed,
|
|
191
|
-
ranges,
|
|
192
|
-
texts: contentParts.filter((part) => part instanceof Text),
|
|
193
|
-
contentParts,
|
|
194
|
-
blocks,
|
|
195
|
-
isBlockSpanning,
|
|
196
|
-
startText,
|
|
197
|
-
endText,
|
|
198
|
-
isAtStartOfText,
|
|
199
|
-
isAtEndOfText,
|
|
200
|
-
isAtEndOfBlock,
|
|
201
|
-
isAtStartOfBlock,
|
|
202
|
-
isTextSpanning,
|
|
203
|
-
startNode,
|
|
204
|
-
endNode,
|
|
205
|
-
startBlock,
|
|
206
|
-
endBlock,
|
|
207
|
-
isVoid,
|
|
208
|
-
isIsland,
|
|
209
|
-
islandRoot,
|
|
210
|
-
voidRoot,
|
|
211
|
-
relativePosition: startText
|
|
212
|
-
? Y.createRelativePositionFromTypeIndex(startText.yText, yStart, -1)
|
|
213
|
-
: null,
|
|
214
|
-
// TOREMOVE
|
|
215
|
-
yTextContent: startText?.yText.toJSON()
|
|
216
|
-
};
|
|
217
|
-
this.edytorOnSelectionChange?.(this);
|
|
218
|
-
this.edytor.plugins.forEach((plugin) => {
|
|
219
|
-
plugin.onSelectionChange?.(this);
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
restoreRelativePosition = (text) => {
|
|
223
|
-
if (text !== this.state.startText || !this.state.relativePosition) {
|
|
224
|
-
return;
|
|
869
|
+
if (!isCollapsed && startText === endText && yStart === yEnd) {
|
|
870
|
+
// An empty native range reads as a caret; the DOM is normalized to one.
|
|
871
|
+
shouldRestoreNormalizedDomRange = true;
|
|
225
872
|
}
|
|
226
|
-
|
|
227
|
-
|
|
873
|
+
if (startText?.parent.isEmpty && yStart > startText.length) {
|
|
874
|
+
// In case the placeholder or suggestion is not absolutely positioned, we need to set the selection to the start of the text because caret may be placed after the placeholder which is deceptive
|
|
875
|
+
return this.setAtTextOffset(startText, startText.length);
|
|
876
|
+
}
|
|
877
|
+
const inlineBlockSelectedByBoundaryRange = getInlineBlockBetweenBoundaryTexts(startText, endText, yStart, yEnd);
|
|
878
|
+
if (inlineBlockSelectedByBoundaryRange) {
|
|
879
|
+
clearDomSelection(this.edytor.node);
|
|
880
|
+
this.selectInlineBlock(inlineBlockSelectedByBoundaryRange, isReversed ? 'after' : 'before');
|
|
228
881
|
return;
|
|
229
882
|
}
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
selectBlocks = (...blocks) => {
|
|
233
|
-
const difference = this.selectedBlocks.difference(new Set(blocks));
|
|
234
|
-
difference.forEach((block) => {
|
|
235
|
-
this.selectedBlocks.delete(block);
|
|
236
|
-
block.definition.onBlur?.({ block });
|
|
237
|
-
block.node?.removeAttribute('data-edytor-selected');
|
|
883
|
+
this.select(this.textValue(startText, yStart, endText ?? startText, yEnd, isReversed), 'dom', {
|
|
884
|
+
edge: isCollapsed ? getMarkEdgeSide(startText, startNode, yStart) : undefined
|
|
238
885
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
886
|
+
this.edytor.projector.observe();
|
|
887
|
+
if (shouldRestoreNormalizedDomRange && options.restoreNormalizedDomRange !== false && endText) {
|
|
888
|
+
// Writing the DOM under a pointer drag would reset its anchor: after it.
|
|
889
|
+
if (this.dragging)
|
|
890
|
+
this.#held = true;
|
|
891
|
+
else
|
|
892
|
+
this.setAtRange(startText, yStart, endText, yEnd, { isReversed });
|
|
246
893
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
});
|
|
260
|
-
};
|
|
261
|
-
findTextNode = (node, offset = 0) => {
|
|
262
|
-
let nodeOffset = 0;
|
|
263
|
-
const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, (node) => {
|
|
264
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
265
|
-
return NodeFilter.FILTER_ACCEPT;
|
|
266
|
-
}
|
|
267
|
-
return NodeFilter.FILTER_SKIP;
|
|
268
|
-
});
|
|
269
|
-
let currentNode = treeWalker.nextNode();
|
|
270
|
-
let textNode = null;
|
|
271
|
-
let currentOffset = 0;
|
|
272
|
-
while (currentNode) {
|
|
273
|
-
const endOffset = currentOffset + currentNode.length;
|
|
274
|
-
if (offset >= currentOffset && offset <= endOffset) {
|
|
275
|
-
textNode = currentNode;
|
|
276
|
-
nodeOffset = offset - currentOffset;
|
|
277
|
-
break;
|
|
278
|
-
}
|
|
279
|
-
currentOffset = endOffset;
|
|
280
|
-
currentNode = treeWalker.nextNode();
|
|
894
|
+
// Remember when the derived selection came from a node-bound
|
|
895
|
+
// native range (endpoints outside text elements — e.g. a block
|
|
896
|
+
// node selection or a stray boundary node). Engines asynchronously
|
|
897
|
+
// normalize such DOM selections into text points, so by the time a
|
|
898
|
+
// keydown arrives the live DOM selection may no longer look
|
|
899
|
+
// node-bound — the signature pins the flag to the derived
|
|
900
|
+
// selection it describes and any differently-shaped derive clears
|
|
901
|
+
// it.
|
|
902
|
+
const derivedKey = `${startText.id}:${yStart}:${endText?.id}:${yEnd}:${isReversed ? 1 : 0}`;
|
|
903
|
+
if (!isTextBoundSelectionPoint(selection.anchorNode) ||
|
|
904
|
+
!isTextBoundSelectionPoint(selection.focusNode)) {
|
|
905
|
+
this.#nodeBoundSelectionKey = derivedKey;
|
|
281
906
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
setAtTextOffset = async (textOrId, textOffset = this.state.yStart) => {
|
|
285
|
-
if (!textOrId || typeof textOffset !== 'number') {
|
|
286
|
-
return;
|
|
907
|
+
else if (this.#nodeBoundSelectionKey !== derivedKey) {
|
|
908
|
+
this.#nodeBoundSelectionKey = null;
|
|
287
909
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
910
|
+
};
|
|
911
|
+
#nodeBoundSelectionKey = null;
|
|
912
|
+
/**
|
|
913
|
+
* Whether the live model selection was derived from a node-bound
|
|
914
|
+
* native range — the shape whose Shift+Arrow extension is
|
|
915
|
+
* engine-defined and therefore intercepted by the hotkey layer.
|
|
916
|
+
*/
|
|
917
|
+
hasNativeNodeSelection = () => {
|
|
918
|
+
const { startText, endText, yStart, yEnd, isReversed } = this.state;
|
|
919
|
+
if (!startText || !endText) {
|
|
920
|
+
return false;
|
|
292
921
|
}
|
|
922
|
+
return (this.#nodeBoundSelectionKey ===
|
|
923
|
+
`${startText.id}:${yStart}:${endText.id}:${yEnd}:${isReversed ? 1 : 0}`);
|
|
293
924
|
};
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
925
|
+
/**
|
|
926
|
+
* Anchor a text position into the BACKING text that owns the atom at
|
|
927
|
+
* that display position — the v14 replacement for
|
|
928
|
+
* `Y.createRelativePositionFromTypeIndex(yText, offset, -1)`.
|
|
929
|
+
*
|
|
930
|
+
* `affinity` decides which side of the position the anchor binds to
|
|
931
|
+
* (`'left'` = bound to the preceding atom, inserts at the position
|
|
932
|
+
* land on the anchor's right — the caret baseline `-1`; `'right'` =
|
|
933
|
+
* bound to the following atom, inserts land outside a range starting
|
|
934
|
+
* here). `offset` is an offset inside `text`'s display segment;
|
|
935
|
+
* `text.segStart + offset` is the block-level display offset the
|
|
936
|
+
* facade maps to the owning backing text — content routed from other
|
|
937
|
+
* blocks' backing texts (merges/splits) anchors correctly.
|
|
938
|
+
*
|
|
939
|
+
* `null` while the text isn't bound to a live block (mirrors the
|
|
940
|
+
* baseline where a non-integrated Y.Text couldn't produce an absolute
|
|
941
|
+
* position).
|
|
942
|
+
*/
|
|
943
|
+
createTextAnchor = (text, offset, affinity = 'left') => {
|
|
944
|
+
if (text.parent.isRoot || !text.isInDocument)
|
|
945
|
+
return null;
|
|
946
|
+
return this.edytor.facade.anchorAt(text.blockId, text.segStart + offset, affinity);
|
|
947
|
+
};
|
|
948
|
+
/**
|
|
949
|
+
* Resolve a `TextAnchor` → `{text, offset}` (offset within the text
|
|
950
|
+
* segment). The anchor is resolved through ownership — when the bound
|
|
951
|
+
* atoms moved to another block (merge/split), the returned text lives
|
|
952
|
+
* in that block; deleted atoms resolve to the gap where they lived;
|
|
953
|
+
* fully-unowned backing resolves to the owner seam. `null` when the
|
|
954
|
+
* anchor is unresolvable (deleted/hidden target or not-yet-integrated
|
|
955
|
+
* bound item — callers fall back to id/path restore).
|
|
956
|
+
*/
|
|
957
|
+
resolveTextAnchor = (anchor) => {
|
|
958
|
+
const facade = this.edytor.facade;
|
|
959
|
+
if (!facade) {
|
|
960
|
+
return null;
|
|
297
961
|
}
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const selection = window.getSelection();
|
|
302
|
-
const range = document.createRange();
|
|
303
|
-
range.setStart(startTextNode, startNodeOffset);
|
|
304
|
-
range.setEnd(endTextNode, endNodeOffset);
|
|
305
|
-
selection?.removeAllRanges();
|
|
306
|
-
selection?.addRange(range);
|
|
307
|
-
startTextNode.parentElement?.focus();
|
|
962
|
+
const resolved = facade.resolveAnchor(anchor);
|
|
963
|
+
if (!resolved) {
|
|
964
|
+
return null;
|
|
308
965
|
}
|
|
966
|
+
return this.edytor.idToBlock.get(resolved.blockId)?.textAtOffset(resolved.offset) ?? null;
|
|
967
|
+
};
|
|
968
|
+
/** The text and block the selection's start last resolved to (the seam origin once they die). */
|
|
969
|
+
#lastText = null;
|
|
970
|
+
#lastBlock = null;
|
|
971
|
+
/**
|
|
972
|
+
* Displayable (§2.4, a Surface fact): the block's own content is mounted
|
|
973
|
+
* and not hidden by view state — a collapsed toggle's body, a `hidden`
|
|
974
|
+
* subtree. A phantom content slot (a snippet that renders no `content()`)
|
|
975
|
+
* never mounts.
|
|
976
|
+
*/
|
|
977
|
+
displayable = (id) => {
|
|
978
|
+
const node = this.edytor.idToBlock
|
|
979
|
+
.get(id)
|
|
980
|
+
?.content.find((part) => part instanceof Text && part.node != null)?.node;
|
|
981
|
+
return !!node && !node.closest(visibility.HIDDEN);
|
|
309
982
|
};
|
|
310
|
-
|
|
311
|
-
|
|
983
|
+
/** The view's visibility rule (`selection/visibility.ts`), for extensions: hidden by view state, and the shown neighbour. */
|
|
984
|
+
hidden = visibility.hidden;
|
|
985
|
+
shown = visibility.shown;
|
|
986
|
+
/** The text's computed direction (a Surface fact): arrow and word keys are visual. */
|
|
987
|
+
rtl = (text) => !!text.node?.isConnected && getComputedStyle(text.node).direction === 'rtl';
|
|
988
|
+
/** A caret at the seam `dead` vacated (`doc/anchors`); `null` when nothing displays. */
|
|
989
|
+
#seamValue = (dead) => {
|
|
990
|
+
const { facade } = this.edytor;
|
|
991
|
+
const at = seam(facade, dead, this.displayable);
|
|
992
|
+
const anchor = at && facade.anchorAt(at.block, at.offset, 'left');
|
|
993
|
+
return anchor ? textSelection(anchor) : null;
|
|
994
|
+
};
|
|
995
|
+
/**
|
|
996
|
+
* Post-mirror-flush repair of the endpoints this view did not author
|
|
997
|
+
* (R9: a command that declared its result selection authors its own), through
|
|
998
|
+
* `select()`. Text endpoints follow their anchors by projection; what is
|
|
999
|
+
* repaired is a range whose content died (a caret at that point) and a
|
|
1000
|
+
* value that no longer resolves: a block set keeps its live
|
|
1001
|
+
* members, an atom that vanished leaves a caret at its block's start, and
|
|
1002
|
+
* otherwise the selection lands at the seam of the block it last resolved in
|
|
1003
|
+
* (`doc/anchors`: the replicated slot, displayable stops only). The model is
|
|
1004
|
+
* written at once; the projector displays it (also when the text the caret
|
|
1005
|
+
* was displayed in died while its anchor moved on).
|
|
1006
|
+
*/
|
|
1007
|
+
restoreDeadSelectionEndpoints = () => {
|
|
1008
|
+
if (this.expectHistoryRestore) {
|
|
312
1009
|
return;
|
|
313
1010
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (!endOffset) {
|
|
318
|
-
endOffset = block.lastText.length;
|
|
319
|
-
}
|
|
320
|
-
const startNode = await this.edytor.getTextNode(block.firstText);
|
|
321
|
-
const [startTextNode, startNodeOffset] = this.findTextNode(startNode, startOffset);
|
|
322
|
-
const endNode = await this.edytor.getTextNode(block.lastText);
|
|
323
|
-
const [endTextNode, endNodeOffset] = this.findTextNode(endNode, endOffset);
|
|
324
|
-
if (startTextNode && endTextNode) {
|
|
325
|
-
const selection = window.getSelection();
|
|
326
|
-
const range = document.createRange();
|
|
327
|
-
range.setStart(startTextNode, startNodeOffset);
|
|
328
|
-
range.setEnd(endTextNode, endNodeOffset);
|
|
329
|
-
selection?.removeAllRanges();
|
|
330
|
-
selection?.addRange(range);
|
|
331
|
-
// this.edytor.node!.focus();
|
|
332
|
-
startTextNode.parentElement?.focus();
|
|
333
|
-
}
|
|
334
|
-
};
|
|
335
|
-
setAtTextRange = async (text, start, end) => {
|
|
336
|
-
if (!text || typeof start !== 'number' || typeof end !== 'number') {
|
|
1011
|
+
// Mid pointer-drag the user's in-progress range owns the selection; a
|
|
1012
|
+
// command that declared its result selection owns this view's endpoints.
|
|
1013
|
+
if (this.pointerDragStart !== null || this.edytor.dispatcher.authoring) {
|
|
337
1014
|
return;
|
|
338
1015
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
let
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1016
|
+
const { value } = this;
|
|
1017
|
+
const facade = this.edytor.facade;
|
|
1018
|
+
let dead;
|
|
1019
|
+
if (value.kind === 'blocks') {
|
|
1020
|
+
const live = value.ids.filter((id) => facade.isVisibleBlock(id));
|
|
1021
|
+
if (live.length === value.ids.length)
|
|
1022
|
+
return;
|
|
1023
|
+
if (live.length) {
|
|
1024
|
+
this.select(blockSelection(live), 'repair');
|
|
1025
|
+
return;
|
|
347
1026
|
}
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
358
|
-
|
|
1027
|
+
dead = value.ids[0] ?? null;
|
|
1028
|
+
}
|
|
1029
|
+
else if (value.kind === 'atom') {
|
|
1030
|
+
if (this.state.startText)
|
|
1031
|
+
return;
|
|
1032
|
+
const anchor = facade.isVisibleBlock(value.blockId)
|
|
1033
|
+
? facade.anchorAt(value.blockId, 0, 'left')
|
|
1034
|
+
: null;
|
|
1035
|
+
if (anchor) {
|
|
1036
|
+
this.#land(textSelection(anchor));
|
|
1037
|
+
return;
|
|
359
1038
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1039
|
+
dead = value.blockId;
|
|
1040
|
+
}
|
|
1041
|
+
else if (value.kind === 'text') {
|
|
1042
|
+
// A peer or host code turned the block it rests in into one that shows
|
|
1043
|
+
// no content (an empty line into a divider): the nearest shown line.
|
|
1044
|
+
const shown = this.#shown(value);
|
|
1045
|
+
if (shown !== value) {
|
|
1046
|
+
this.#land(shown);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const state = this.state;
|
|
1050
|
+
const { start, isCollapsed } = this.projection;
|
|
1051
|
+
if (state.startText && start && isCollapsed && value.anchor !== value.focus) {
|
|
1052
|
+
// A range whose content died is a caret at the seam (`sel.seam.covered-atom`):
|
|
1053
|
+
// its two anchors would re-open around text re-inserted there.
|
|
1054
|
+
const caret = facade.anchorAt(start.block, start.offset, 'left');
|
|
1055
|
+
if (caret) {
|
|
1056
|
+
this.#land(textSelection(caret));
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
364
1059
|
}
|
|
365
|
-
if (
|
|
366
|
-
|
|
1060
|
+
if (state.startText) {
|
|
1061
|
+
// The text it was displayed in died while its anchor moved on: display again.
|
|
1062
|
+
if (this.#lastText !== null && !this.#lastText.isInDocument)
|
|
1063
|
+
this.display();
|
|
1064
|
+
this.#lastText = state.startText;
|
|
1065
|
+
this.#lastBlock = state.startBlock?.id ?? null;
|
|
1066
|
+
return;
|
|
367
1067
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1068
|
+
dead = this.#lastBlock;
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
// An emptied document rests the caret in its virtual paragraph (`doc.empty.virtual`).
|
|
1072
|
+
const virtual = facade.virtual();
|
|
1073
|
+
if (virtual !== null)
|
|
1074
|
+
this.#land(textSelection(facade.anchorAt(virtual, 0, 'left')));
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
// A live origin means the anchors are not integrated yet: they converge.
|
|
1078
|
+
if (dead !== null && facade.isVisibleBlock(dead))
|
|
1079
|
+
return;
|
|
1080
|
+
// With no displayable stop yet (a whole-document remote delete runs this
|
|
1081
|
+
// pass before the replacement paragraph mounts), the value stays: the
|
|
1082
|
+
// projector's pass after the flush that mounts it runs this repair again.
|
|
1083
|
+
const target = this.#seamValue(dead);
|
|
1084
|
+
if (target)
|
|
1085
|
+
this.#land(target);
|
|
1086
|
+
};
|
|
1087
|
+
/** Select a repaired caret (the projector displays it). */
|
|
1088
|
+
#land = (target) => this.select(target, 'repair');
|
|
1089
|
+
/**
|
|
1090
|
+
* A text range from `first`'s first shown line to `last`'s last (the
|
|
1091
|
+
* triple-click shape; `rangeEdge`); the start binds left. None when no
|
|
1092
|
+
* line is shown.
|
|
1093
|
+
*/
|
|
1094
|
+
#contentRange = (first, last = first) => {
|
|
1095
|
+
const [from, to] = [rangeEdge(first, 'first', true), rangeEdge(last, 'last', true)];
|
|
1096
|
+
return from && to
|
|
1097
|
+
? this.textValue(from.text, from.offset, to.text, to.offset, false, 'left')
|
|
1098
|
+
: null;
|
|
1099
|
+
};
|
|
1100
|
+
/** Select `block`'s content range (the triple-click shape), when it shows a line. */
|
|
1101
|
+
setStateFromBlockContentRange = (block) => {
|
|
1102
|
+
const range = this.#contentRange(block);
|
|
1103
|
+
if (range)
|
|
1104
|
+
this.select(range);
|
|
1105
|
+
};
|
|
1106
|
+
/**
|
|
1107
|
+
* Select a set of whole blocks. With no block, leave block selection:
|
|
1108
|
+
* the value becomes the text range the set showed, from its first shown
|
|
1109
|
+
* line to its last (a list's items; a divider at an edge stays covered
|
|
1110
|
+
* unless no line lies beyond it, `rangeEdge`), and stays as it is
|
|
1111
|
+
* when no line is shown anywhere.
|
|
1112
|
+
*/
|
|
1113
|
+
selectBlocks = (...blocks) => {
|
|
1114
|
+
if (blocks.length) {
|
|
1115
|
+
this.select(blockSelection(blocks.map((block) => block.id)));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const { value } = this;
|
|
1119
|
+
if (value.kind !== 'blocks')
|
|
1120
|
+
return;
|
|
1121
|
+
const [first, last] = [this.state.blocks[0], this.state.blocks.at(-1)];
|
|
1122
|
+
const range = first && last ? this.#contentRange(first, last) : noSelection;
|
|
1123
|
+
if (range)
|
|
1124
|
+
this.select(range);
|
|
1125
|
+
};
|
|
1126
|
+
addBlockToSelection = (block) => {
|
|
1127
|
+
const ids = this.value.kind === 'blocks' ? this.value.ids : [];
|
|
1128
|
+
if (!ids.includes(block.id))
|
|
1129
|
+
this.select(blockSelection([...ids, block.id]));
|
|
1130
|
+
};
|
|
1131
|
+
removeBlockFromSelection = (block) => {
|
|
1132
|
+
const ids = this.value.kind === 'blocks' ? this.value.ids : [];
|
|
1133
|
+
if (ids.includes(block.id))
|
|
1134
|
+
this.select(blockSelection(ids.filter((id) => id !== block.id)));
|
|
1135
|
+
};
|
|
1136
|
+
/**
|
|
1137
|
+
* Select a caret at `offset` of `text`; the projector displays it after
|
|
1138
|
+
* the flush (R10). The value is minted now (R4): a text that dies before the
|
|
1139
|
+
* display is followed through its atoms, else the seam of its block.
|
|
1140
|
+
*/
|
|
1141
|
+
setAtTextOffset = (text, textOffset = this.state.yStart) => {
|
|
1142
|
+
if (!text || typeof textOffset !== 'number')
|
|
1143
|
+
return;
|
|
1144
|
+
this.#admit(this.#intent(text, Math.min(Math.max(textOffset, 0), text.length)), text);
|
|
1145
|
+
};
|
|
1146
|
+
/**
|
|
1147
|
+
* A write's model-side fallback: the value minted when the write was
|
|
1148
|
+
* requested (R4: anchors are captured at call time, before any await, so
|
|
1149
|
+
* a target merged away meanwhile is followed through its atoms), or the
|
|
1150
|
+
* seam of `origin`'s block when nothing of it resolves any more.
|
|
1151
|
+
*/
|
|
1152
|
+
#admit = (intended, origin) => {
|
|
1153
|
+
this.clearModelSelectionPreservation();
|
|
1154
|
+
if (project(intended, this.edytor.facade).start) {
|
|
1155
|
+
this.select(intended);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const target = origin && this.#seamValue(origin.parent?.id ?? null);
|
|
1159
|
+
if (target)
|
|
1160
|
+
this.select(target);
|
|
1161
|
+
};
|
|
1162
|
+
/** Select the whole content from `startText` to `endText` (the code block's select-all). */
|
|
1163
|
+
setAtTextsRange = (startText, endText) => {
|
|
1164
|
+
this.setAtRange(startText, 0, endText, endText.length);
|
|
1165
|
+
};
|
|
1166
|
+
/** Select a text range; the projector displays it after the flush (R10). */
|
|
1167
|
+
setAtRange = (startText, startOffset, endText, endOffset, options = {}) => {
|
|
1168
|
+
if (!startText ||
|
|
1169
|
+
!endText ||
|
|
1170
|
+
typeof startOffset !== 'number' ||
|
|
1171
|
+
typeof endOffset !== 'number') {
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
const state = this.state;
|
|
1175
|
+
const range = this.normalizeTextRangePoints(startText, startOffset, endText, endOffset, options.isReversed ??
|
|
1176
|
+
(!state.isCollapsed &&
|
|
1177
|
+
state.isReversed &&
|
|
1178
|
+
state.startText === startText &&
|
|
1179
|
+
state.endText === endText &&
|
|
1180
|
+
state.yStart === startOffset &&
|
|
1181
|
+
state.yEnd === endOffset));
|
|
1182
|
+
this.#admit(this.#intent(range.startText, range.startOffset, range.endText, range.endOffset, range.isReversed), range.startText);
|
|
1183
|
+
};
|
|
1184
|
+
/**
|
|
1185
|
+
* Select `block`'s content (the whole of it by default): from its first
|
|
1186
|
+
* shown line to its last (a list's items, a code block's lines, GX-03), the
|
|
1187
|
+
* offsets in those lines. A block that shows no line (a divider) keeps the
|
|
1188
|
+
* current value. Displayed after the flush.
|
|
1189
|
+
*/
|
|
1190
|
+
setAtBlockRange = (block, startOffset = 0, endOffset) => {
|
|
1191
|
+
const [first, last] = block ? [shownText(block, 'first'), shownText(block, 'last')] : [];
|
|
1192
|
+
if (!first || !last)
|
|
1193
|
+
return;
|
|
1194
|
+
const end = endOffset || last.length;
|
|
1195
|
+
if (!startOffset && end === last.length)
|
|
1196
|
+
this.select(this.textValue(first, 0, last, last.length, false, 'left'));
|
|
1197
|
+
else
|
|
1198
|
+
this.setAtRange(first, startOffset, last, end);
|
|
1199
|
+
};
|
|
1200
|
+
/**
|
|
1201
|
+
* Anchors from a DOM selection, without selecting them: the projector mints
|
|
1202
|
+
* an unobserved native move before a transaction it did not issue (BI-3).
|
|
1203
|
+
*/
|
|
1204
|
+
mint = (snapshot) => {
|
|
1205
|
+
const container = this.edytor.node;
|
|
1206
|
+
const { anchorNode, focusNode } = snapshot;
|
|
1207
|
+
if (!container || !anchorNode || !focusNode)
|
|
1208
|
+
return null;
|
|
1209
|
+
for (const node of [anchorNode, focusNode])
|
|
1210
|
+
if (!container.contains(node) || isNestedForeignEditableTarget(container, node))
|
|
1211
|
+
return null;
|
|
1212
|
+
const reversed = isBackward(snapshot);
|
|
1213
|
+
const [startNode, start, endNode, end] = reversed
|
|
1214
|
+
? [focusNode, snapshot.focusOffset, anchorNode, snapshot.anchorOffset]
|
|
1215
|
+
: [anchorNode, snapshot.anchorOffset, focusNode, snapshot.focusOffset];
|
|
1216
|
+
const { startText, endText } = this.getTextsInSelection(startNode, endNode, start, end);
|
|
1217
|
+
if (!startText || !endText)
|
|
1218
|
+
return null;
|
|
1219
|
+
const at = (text, node, offset) => Math.min(Math.max(getYIndex(text, node, offset), 0), text.length);
|
|
1220
|
+
return this.textValue(startText, at(startText, startNode, start), endText, at(endText, endNode, end), reversed);
|
|
390
1221
|
};
|
|
391
1222
|
}
|