edytor 3.0.0 → 5.0.0
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 +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +23 -12
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +139 -129
- package/dist/cloudflare/room/context.js +11 -4
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +45 -19
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +34 -12
- package/dist/crdt/attribution/attribution.js +59 -21
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/capability.js +9 -13
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +34 -26
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +31 -9
- package/dist/crdt/doc/history.js +204 -73
- package/dist/crdt/doc/layout.js +16 -27
- package/dist/crdt/doc/meta.js +17 -23
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +20 -30
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +55 -125
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +26 -6
- package/dist/crdt/document.js +40 -15
- package/dist/crdt/edytor-doc.d.ts +410 -390
- package/dist/crdt/edytor-doc.js +143 -95
- package/dist/crdt/engine-api.d.ts +39 -1
- package/dist/crdt/engine.d.ts +2 -2
- package/dist/crdt/engine.js +4 -5
- package/dist/crdt/flow.d.ts +2 -2
- package/dist/crdt/flow.js +29 -37
- package/dist/crdt/index.d.ts +134 -127
- package/dist/crdt/index.js +4 -1
- package/dist/crdt/operations.d.ts +12 -3
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +6 -31
- package/dist/crdt/placement/display.js +41 -146
- package/dist/crdt/placement/keys.d.ts +63 -0
- package/dist/crdt/placement/keys.js +95 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +188 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +6 -4
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.d.ts +6 -4
- package/dist/crdt/providers/websocket.js +14 -7
- package/dist/crdt/purge.d.ts +24 -9
- package/dist/crdt/purge.js +266 -126
- package/dist/crdt/restore.d.ts +4 -3
- package/dist/crdt/restore.js +27 -34
- package/dist/crdt/schema.d.ts +30 -21
- package/dist/crdt/schema.js +39 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +17 -15
- package/dist/crdt/structs.js +10 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +13 -23
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.d.ts +2 -4
- package/dist/crdt/text/index/layout.js +14 -30
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +420 -157
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +29 -17
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +87 -62
- package/dist/crdt/text/model.js +91 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +51 -42
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +103 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +28 -3
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +11 -37
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +130 -83
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +67 -22
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/package.json +4 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
|
@@ -1,15 +1,26 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { Plugin } from '../../plugins.js';
|
|
3
|
-
import type {
|
|
4
|
-
/** What a `bar` snippet receives: one block suggestion and its actions. */
|
|
3
|
+
import type { AnySuggestion } from '../../session/suggestions.svelte.js';
|
|
4
|
+
/** What a `bar` snippet receives: one suggestion (a block suggestion, or a shared one) and its actions. */
|
|
5
5
|
export type SuggestionBarPayload = {
|
|
6
|
-
suggestion:
|
|
7
|
-
/**
|
|
6
|
+
suggestion: AnySuggestion;
|
|
7
|
+
/** Accept it (a view-local one: place it in the document) and give the editor its focus back. */
|
|
8
8
|
accept: () => void;
|
|
9
|
+
/** Drop it (a shared one: its author withdraws it, a decider rejects it). */
|
|
9
10
|
discard: () => void;
|
|
11
|
+
/** Reject a shared one (a view-local one: drop it). */
|
|
12
|
+
reject: () => void;
|
|
10
13
|
/** Ask for another answer (`onRetry`); `undefined` without one. */
|
|
11
14
|
retry: (() => void) | undefined;
|
|
12
15
|
readonly: boolean;
|
|
16
|
+
/** This view may accept or reject it (a shared one; else: not readonly). */
|
|
17
|
+
canDecide: boolean;
|
|
18
|
+
/** A shared one's author, as their profile names them ("Ada", with `suggestion.via`: "Ada (AI)"). */
|
|
19
|
+
author: {
|
|
20
|
+
id: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
color?: string;
|
|
23
|
+
} | undefined;
|
|
13
24
|
/** The view's keys are a Mac's: Mod is ⌘ (name the Accept key with it). */
|
|
14
25
|
mac: boolean;
|
|
15
26
|
};
|
|
@@ -3,6 +3,8 @@ export type DomSelectionSnapshot = Pick<Selection, 'anchorNode' | 'anchorOffset'
|
|
|
3
3
|
};
|
|
4
4
|
export declare const getDomSelection: (node?: Node | null) => Selection | null;
|
|
5
5
|
export declare const getActiveElement: (node?: Node | null) => Element | null;
|
|
6
|
+
/** The focused element when it is `node` or inside it (the editor's own focus), else `null`. */
|
|
7
|
+
export declare const focusIn: (node?: Node | null) => Element | null;
|
|
6
8
|
export declare const clearDomSelection: (node?: Node | null) => void;
|
|
7
9
|
/**
|
|
8
10
|
* The caret point under (`x`, `y`): `caretPositionFromPoint` (Firefox,
|
|
@@ -7,6 +7,11 @@ const shadowOf = (node) => {
|
|
|
7
7
|
const documentSelection = (node) => ownerOf(node)?.getSelection() ?? null;
|
|
8
8
|
export const getDomSelection = (node) => shadowOf(node)?.getSelection?.() ?? documentSelection(node);
|
|
9
9
|
export const getActiveElement = (node) => shadowOf(node)?.activeElement ?? ownerOf(node)?.activeElement ?? null;
|
|
10
|
+
/** The focused element when it is `node` or inside it (the editor's own focus), else `null`. */
|
|
11
|
+
export const focusIn = (node) => {
|
|
12
|
+
const active = getActiveElement(node);
|
|
13
|
+
return active && node?.contains(active) ? active : null;
|
|
14
|
+
};
|
|
10
15
|
export const clearDomSelection = (node) => {
|
|
11
16
|
getDomSelection(node)?.removeAllRanges();
|
|
12
17
|
};
|
|
@@ -96,8 +101,7 @@ export const scrollCaretIntoView = (root) => {
|
|
|
96
101
|
const focusNode = selection?.focusNode;
|
|
97
102
|
if (!selection || !focusNode || selection.rangeCount === 0 || !root.contains(focusNode))
|
|
98
103
|
return;
|
|
99
|
-
|
|
100
|
-
if (!active || !root.contains(active))
|
|
104
|
+
if (!focusIn(root))
|
|
101
105
|
return;
|
|
102
106
|
const element = focusNode instanceof Element ? focusNode : focusNode.parentElement;
|
|
103
107
|
if (!element)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { blockSelection } from '../session/selection.js';
|
|
2
2
|
import { toTrailingParagraph } from '../session/navigation.js';
|
|
3
|
-
import { isNativeInteractiveControl, isNativeInteractiveEvent } from '../events/nativeInteractiveControl.js';
|
|
3
|
+
import { isNativeInteractiveControl, isNativeInteractiveEvent, ownsPress } from '../events/nativeInteractiveControl.js';
|
|
4
4
|
import { acrossColumns } from './replaceSelection.js';
|
|
5
5
|
import { caretAtPoint, getDomSelectionSnapshot } from './domSelection.js';
|
|
6
6
|
import { domPointOf } from '../surface/projector.svelte.js';
|
|
7
|
-
import {
|
|
7
|
+
import { editableBoundary, isTextBoundSelectionPoint, offsetIn, SUGGESTION } from './selection.utils.js';
|
|
8
8
|
export class SelectionPointer {
|
|
9
9
|
selection;
|
|
10
10
|
/** The press's text point while a primary press (a drag) lasts; `null` after its release. */
|
|
@@ -24,13 +24,17 @@ export class SelectionPointer {
|
|
|
24
24
|
hold = () => {
|
|
25
25
|
this.#held = true;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The block whose non-editable chrome `node` is in: its nearest
|
|
29
|
+
* `contenteditable` boundary is `false`. A nested editable between them
|
|
30
|
+
* (a void block's caption) is no chrome: the browser places the caret
|
|
31
|
+
* at the press, in one step (`sel.press.nested-editable`).
|
|
32
|
+
*/
|
|
27
33
|
#chromeBlock = (node) => {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (!(nonEditableElement instanceof HTMLElement) ||
|
|
31
|
-
!this.selection.edytor.node?.contains(nonEditableElement)) {
|
|
34
|
+
const boundary = editableBoundary(node, this.selection.edytor.node);
|
|
35
|
+
if (!boundary || boundary.editable)
|
|
32
36
|
return null;
|
|
33
|
-
|
|
37
|
+
const nonEditableElement = boundary.element;
|
|
34
38
|
// A suggestion's preview is no block's chrome: a press there is its own (cancelled).
|
|
35
39
|
if (isNativeInteractiveControl(nonEditableElement) || nonEditableElement.closest(SUGGESTION)) {
|
|
36
40
|
return null;
|
|
@@ -75,17 +79,17 @@ export class SelectionPointer {
|
|
|
75
79
|
};
|
|
76
80
|
#trailing = () => {
|
|
77
81
|
const { edytor } = this.selection;
|
|
78
|
-
edytor.expectInternalFocus();
|
|
79
|
-
edytor.node?.focus({ preventScroll: true });
|
|
82
|
+
edytor.expectInternalFocus(edytor.node);
|
|
80
83
|
toTrailingParagraph(edytor);
|
|
81
84
|
};
|
|
82
85
|
/** The gesture a press in the host's own area starts (`claimMargins`): the marquee's. */
|
|
83
86
|
#margin = null;
|
|
84
87
|
/**
|
|
85
|
-
* The host's last press went to the
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
88
|
+
* The host's last press went to a gesture of the model's (the margin
|
|
89
|
+
* gesture's, `marginPress`; a void block's selection, `chromePress`),
|
|
90
|
+
* until its release: its compatibility `mousedown` is cancelled (no
|
|
91
|
+
* native caret, focus or selection), and it places no caret (`pressed`)
|
|
92
|
+
* and starts no text drag (`capture`).
|
|
89
93
|
* @internal
|
|
90
94
|
*/
|
|
91
95
|
claimed = false;
|
|
@@ -159,14 +163,26 @@ export class SelectionPointer {
|
|
|
159
163
|
const point = this.#pointAt(x, at.clientY);
|
|
160
164
|
if (!point)
|
|
161
165
|
return;
|
|
162
|
-
edytor.expectInternalFocus();
|
|
163
|
-
host.focus({ preventScroll: true });
|
|
166
|
+
edytor.expectInternalFocus(host);
|
|
164
167
|
this.selection.selectText(point.text, point.offset);
|
|
165
168
|
};
|
|
166
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* A press on a block's non-editable chrome. A void block's
|
|
171
|
+
* (`sel.press.void`, Notion): the block is selected, the host focused,
|
|
172
|
+
* no caret placed — unless the press is a control's of its own
|
|
173
|
+
* (`ownsPress`: a native control, an interactive role, an element the
|
|
174
|
+
* app marks `data-edytor-control`), which keeps it whole; Shift or Mod
|
|
175
|
+
* adds the block to a block selection. On the line of the block's text
|
|
176
|
+
* (a caption's, beside its text), the caret at the nearest point of it.
|
|
177
|
+
* Any other block's: the caret at the start of its text. A press a
|
|
178
|
+
* control owns (`ownPress`) never reaches it.
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
167
181
|
chromePress = (event) => {
|
|
168
182
|
const targetNode = event.target instanceof Node ? event.target : null;
|
|
169
183
|
const targetBlock = this.#chromeBlock(targetNode);
|
|
184
|
+
if (targetBlock && this.selection.edytor.facade.isVoid(targetBlock.id))
|
|
185
|
+
return this.#voidPress(event, targetBlock);
|
|
170
186
|
const targetText = targetBlock?.firstText;
|
|
171
187
|
// A native control inside the chrome (a void block's input) owns its press.
|
|
172
188
|
if (!targetText || isNativeInteractiveEvent(event)) {
|
|
@@ -175,10 +191,40 @@ export class SelectionPointer {
|
|
|
175
191
|
// The model answers this press: the browser's own caret placement on
|
|
176
192
|
// the chrome is cancelled (a canceled pointerdown moves no selection).
|
|
177
193
|
event.preventDefault();
|
|
178
|
-
this.selection.edytor.expectInternalFocus();
|
|
179
|
-
this.selection.edytor.node?.focus({ preventScroll: true });
|
|
194
|
+
this.selection.edytor.expectInternalFocus(this.selection.edytor.node);
|
|
180
195
|
this.selection.selectText(targetText, 0);
|
|
181
196
|
};
|
|
197
|
+
/**
|
|
198
|
+
* A press (and its whole gesture: its clicks, multi-clicks, `selectstart`
|
|
199
|
+
* and drag) a control of a block's chrome owns (`ownsPress`,
|
|
200
|
+
* `sel.press.void`): the editor does nothing with it. Every press handler
|
|
201
|
+
* asks this one rule first.
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
ownPress = (event) => ownsPress(this.selection.edytor.node, event);
|
|
205
|
+
/** A press on void block `block`'s chrome selects it (`sel.press.void`). */
|
|
206
|
+
#voidPress = (event, block) => {
|
|
207
|
+
const { edytor, value } = this.selection;
|
|
208
|
+
if (event.button !== 0)
|
|
209
|
+
return;
|
|
210
|
+
// The model answers it: no native caret, focus or selection; no text drag (`claimed`).
|
|
211
|
+
event.preventDefault();
|
|
212
|
+
this.claimed = true;
|
|
213
|
+
edytor.expectInternalFocus(edytor.node);
|
|
214
|
+
// On the line of its text (a caption's, beside the text): the caret at the nearest point there.
|
|
215
|
+
const target = event.target instanceof Element && event.target !== block.node ? event.target : null;
|
|
216
|
+
const line = target?.querySelector('[data-edytor-text]');
|
|
217
|
+
const text = line && this.selection.getTextOfNode(line);
|
|
218
|
+
const box = line?.getBoundingClientRect();
|
|
219
|
+
if (text &&
|
|
220
|
+
text.parent === block &&
|
|
221
|
+
box &&
|
|
222
|
+
event.clientY >= box.top &&
|
|
223
|
+
event.clientY <= box.bottom)
|
|
224
|
+
return this.placeAt(text, event.clientX, event.clientY);
|
|
225
|
+
const adding = (event.shiftKey || event.metaKey || event.ctrlKey) && value.kind === 'blocks';
|
|
226
|
+
this.selection.select(blockSelection(adding ? [...new Set([...value.ids, block.id])] : [block.id]));
|
|
227
|
+
};
|
|
182
228
|
/**
|
|
183
229
|
* A triple click selects the block's content in the model; the browser's
|
|
184
230
|
* own multi-click selection is cancelled at its `mousedown`
|
|
@@ -186,7 +232,7 @@ export class SelectionPointer {
|
|
|
186
232
|
* @internal
|
|
187
233
|
*/
|
|
188
234
|
tripleClick = (e) => {
|
|
189
|
-
if (e.detail < 3)
|
|
235
|
+
if (e.detail < 3 || this.ownPress(e))
|
|
190
236
|
return;
|
|
191
237
|
const targetNode = e.target instanceof Node ? e.target : null;
|
|
192
238
|
const clickedBlock = this.selection.getBlockOfNode(targetNode);
|
|
@@ -202,7 +248,7 @@ export class SelectionPointer {
|
|
|
202
248
|
};
|
|
203
249
|
/** @internal */
|
|
204
250
|
preventTripleClick = (e) => {
|
|
205
|
-
if (e.detail >= 3)
|
|
251
|
+
if (e.detail >= 3 && !this.ownPress(e))
|
|
206
252
|
e.preventDefault();
|
|
207
253
|
};
|
|
208
254
|
/** The text point under the pointer, if any. */
|
|
@@ -361,39 +407,20 @@ export class SelectionPointer {
|
|
|
361
407
|
* anchor) would anchor the DOM selection on nodes the model cannot
|
|
362
408
|
* represent — block-level selection owns that surface instead.
|
|
363
409
|
*
|
|
364
|
-
* The
|
|
365
|
-
*
|
|
366
|
-
* re-enable editing on their text
|
|
367
|
-
* The root itself is excluded so readonly
|
|
410
|
+
* The nearest `[contenteditable]` boundary decides (`editableBoundary`,
|
|
411
|
+
* the chrome press's rule too): `false` means non-editable chrome, while
|
|
412
|
+
* a nested editable (void-block captions re-enable editing on their text
|
|
413
|
+
* spans) keeps native selection. The root itself is excluded so readonly
|
|
414
|
+
* mode stays selectable.
|
|
368
415
|
* @internal
|
|
369
416
|
*/
|
|
370
417
|
onSelectStart = (event) => {
|
|
371
418
|
// Native controls (todo checkboxes, plugin inputs…) keep their own
|
|
372
419
|
// selection behavior.
|
|
373
|
-
if (isNativeInteractiveEvent(event))
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
const element = getElementFromNode(event.target instanceof Node ? event.target : null);
|
|
377
|
-
if (!element || !this.selection.edytor.node?.contains(element)) {
|
|
420
|
+
if (isNativeInteractiveEvent(event) || this.ownPress(event))
|
|
378
421
|
return;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
// attach hooks of inline/void blocks set the property, which jsdom
|
|
383
|
-
// does not reflect onto the attribute.
|
|
384
|
-
let boundary = element instanceof HTMLElement ? element : null;
|
|
385
|
-
while (boundary && boundary !== this.selection.edytor.node) {
|
|
386
|
-
const state = boundary.getAttribute('contenteditable') ?? boundary.contentEditable;
|
|
387
|
-
if (state === 'false') {
|
|
388
|
-
event.preventDefault();
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if (state === 'true' || state === 'plaintext-only' || state === '') {
|
|
392
|
-
// Nested editable region (e.g. void-block captions re-enabling
|
|
393
|
-
// editing on their text spans) keeps native selection.
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
boundary = boundary.parentElement;
|
|
397
|
-
}
|
|
422
|
+
const target = event.target instanceof Node ? event.target : null;
|
|
423
|
+
if (editableBoundary(target, this.selection.edytor.node)?.editable === false)
|
|
424
|
+
event.preventDefault();
|
|
398
425
|
};
|
|
399
426
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from '../text/text.svelte.js';
|
|
2
2
|
import { id, vetoable } from '../utils.js';
|
|
3
|
-
import { blockSelection, noSelection } from '../session/selection.js';
|
|
3
|
+
import { blockSelection, noSelection, pointOf } from '../session/selection.js';
|
|
4
4
|
import { hidden, selectedMembers, shown } from './visibility.js';
|
|
5
5
|
/**
|
|
6
6
|
* The caret a write that decides one left (a range deletion, a placed flow:
|
|
@@ -211,11 +211,7 @@ const deleteRange = (edytor, replace, selection) => {
|
|
|
211
211
|
const { startText, endText, yStart, yEnd } = selection ?? edytor.selection.textRange();
|
|
212
212
|
if (!startText || !endText)
|
|
213
213
|
return null;
|
|
214
|
-
const
|
|
215
|
-
block: text.parent.id,
|
|
216
|
-
offset: text.segStart + offset
|
|
217
|
-
});
|
|
218
|
-
const [from, to] = [at(startText, yStart), at(endText, yEnd)];
|
|
214
|
+
const [from, to] = [pointOf(startText, yStart), pointOf(endText, yEnd)];
|
|
219
215
|
const result = replace ? edytor.replaceRange(from, to) : edytor.deleteRange(from, to);
|
|
220
216
|
if (result.status === 'refused')
|
|
221
217
|
return result.reason === 'view' ? null : { text: startText, offset: yStart };
|
|
@@ -90,9 +90,9 @@ export declare class EdytorSelection {
|
|
|
90
90
|
/** Stage (or, with `undefined`, clear) the caret's pending marks: a new value, same anchors. */
|
|
91
91
|
stage: (pending: Marks | undefined) => void;
|
|
92
92
|
/**
|
|
93
|
-
* A text value from wrapper endpoints (offsets inside text segments):
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
* A text value from wrapper endpoints (offsets inside text segments): the
|
|
94
|
+
* start binds right (boundary inserts stay outside), the end and a caret
|
|
95
|
+
* bind left; `none` when the start text is not bound to the document.
|
|
96
96
|
*/
|
|
97
97
|
textValue: (startText: Text, yStart: number, endText?: Text, yEnd?: number, isReversed?: boolean, startAffinity?: "left" | "right") => SelectionValue;
|
|
98
98
|
/**
|
|
@@ -5,7 +5,7 @@ import { getDomSelectionSnapshot } from './domSelection.js';
|
|
|
5
5
|
import { SvelteSet } from 'svelte/reactivity';
|
|
6
6
|
import { InlineBlock } from '../block/inlineBlock.svelte.js';
|
|
7
7
|
import { isKindControl, isNativeInteractiveControl, isOwnPoint } from '../events/nativeInteractiveControl.js';
|
|
8
|
-
import { atomSelection, blockSelection, noSelection, project, sameEnds, sameValue, serialize, textSelection } from '../session/selection.js';
|
|
8
|
+
import { atomSelection, blockSelection, noSelection, pointOf, project, sameEnds, sameValue, serialize, textSelection } from '../session/selection.js';
|
|
9
9
|
import { seam } from '../crdt/anchors.js';
|
|
10
10
|
import { landed } from '../session/navigation.js';
|
|
11
11
|
import * as visibility from './visibility.js';
|
|
@@ -272,9 +272,8 @@ export class EdytorSelection {
|
|
|
272
272
|
* shows no text (a divider) keeps the current value. Displayed after the flush.
|
|
273
273
|
*/
|
|
274
274
|
setCaret = (caret) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.selectText(at.text, at.offset);
|
|
275
|
+
if (caret)
|
|
276
|
+
this.selectAt({ block: caret.block.id, offset: Math.max(0, caret.offset) });
|
|
278
277
|
};
|
|
279
278
|
/** The marks the next insertion at the caret takes (values kept). */
|
|
280
279
|
get pending() {
|
|
@@ -385,20 +384,39 @@ export class EdytorSelection {
|
|
|
385
384
|
}
|
|
386
385
|
};
|
|
387
386
|
/**
|
|
388
|
-
* A text value from
|
|
389
|
-
*
|
|
390
|
-
*
|
|
387
|
+
* A text value from block-offset points (an inline atom counting 1): the
|
|
388
|
+
* start binds right (boundary inserts stay outside), the end and a caret
|
|
389
|
+
* bind left; `none` when the start does not anchor (the root, an unknown block).
|
|
390
|
+
* @internal
|
|
391
391
|
*/
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
const collapsed =
|
|
395
|
-
const
|
|
396
|
-
if (!
|
|
392
|
+
valueAt = (start, end = start, isReversed = false, startAffinity) => {
|
|
393
|
+
const { facade } = this.edytor;
|
|
394
|
+
const collapsed = start.block === end.block && start.offset === end.offset;
|
|
395
|
+
const a = facade.anchorAt(start.block, start.offset, startAffinity ?? (collapsed ? 'left' : 'right'));
|
|
396
|
+
if (!a)
|
|
397
397
|
return noSelection;
|
|
398
398
|
if (collapsed)
|
|
399
|
-
return textSelection(
|
|
400
|
-
const
|
|
401
|
-
return isReversed ? textSelection(
|
|
399
|
+
return textSelection(a);
|
|
400
|
+
const b = facade.anchorAt(end.block, end.offset, 'left') ?? a;
|
|
401
|
+
return isReversed ? textSelection(b, a) : textSelection(a, b);
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* A text value from wrapper endpoints (offsets inside text segments): the
|
|
405
|
+
* start binds right (boundary inserts stay outside), the end and a caret
|
|
406
|
+
* bind left; `none` when the start text is not bound to the document.
|
|
407
|
+
*/
|
|
408
|
+
textValue = (startText, yStart, endText = startText, yEnd = yStart, isReversed = false, startAffinity) => this.valueAt(pointOf(startText, yStart), pointOf(endText, yEnd), isReversed, startAffinity);
|
|
409
|
+
/**
|
|
410
|
+
* Select from `start` to `end` (block offsets, a caret by default;
|
|
411
|
+
* `isReversed` makes the end the anchor), else the seam of the start's
|
|
412
|
+
* block. The projector displays it after the flush.
|
|
413
|
+
* @internal
|
|
414
|
+
*/
|
|
415
|
+
selectAt = (start, end = start, isReversed = false) => {
|
|
416
|
+
const value = this.valueAt(start, end, isReversed);
|
|
417
|
+
const target = project(value, this.edytor.facade).start ? value : this.#seamValue(start.block);
|
|
418
|
+
if (target)
|
|
419
|
+
this.select(target);
|
|
402
420
|
};
|
|
403
421
|
/**
|
|
404
422
|
* Select from `yStart` of `startText` to `yEnd` of `endText` (a caret by
|
|
@@ -413,13 +431,8 @@ export class EdytorSelection {
|
|
|
413
431
|
selectText = (startText, yStart, endText = startText ?? null, yEnd = yStart, isReversed = false) => {
|
|
414
432
|
if (!startText || !endText)
|
|
415
433
|
return;
|
|
416
|
-
const at = (text, offset) => Math.min(Math.max(offset, 0), text.length);
|
|
417
|
-
|
|
418
|
-
const target = project(value, this.edytor.facade).start
|
|
419
|
-
? value
|
|
420
|
-
: this.#seamValue(startText.parent?.id ?? null);
|
|
421
|
-
if (target)
|
|
422
|
-
this.select(target);
|
|
434
|
+
const at = (text, offset) => pointOf(text, Math.min(Math.max(offset, 0), text.length));
|
|
435
|
+
this.selectAt(at(startText, yStart), at(endText, yEnd), isReversed);
|
|
423
436
|
};
|
|
424
437
|
/**
|
|
425
438
|
* Select `block`'s lines (Mod+A, a triple click): from its first shown
|
|
@@ -468,8 +481,7 @@ export class EdytorSelection {
|
|
|
468
481
|
if (value.kind !== 'atom')
|
|
469
482
|
return;
|
|
470
483
|
const { start } = this.projection;
|
|
471
|
-
|
|
472
|
-
this.select(before ? this.textValue(before.text, before.offset) : noSelection);
|
|
484
|
+
this.select(start ? this.valueAt(start) : noSelection);
|
|
473
485
|
};
|
|
474
486
|
selectInlineBlock = (inlineBlock, from = 'before') => {
|
|
475
487
|
this.select(atomSelection(inlineBlock.parent.id, inlineBlock.id, from));
|
|
@@ -477,11 +489,7 @@ export class EdytorSelection {
|
|
|
477
489
|
/** @internal */
|
|
478
490
|
getTextOfNode = getTextOfNode.bind(this);
|
|
479
491
|
/** @internal The block whose element holds `node`, in this editor. */
|
|
480
|
-
getBlockOfNode = (node) =>
|
|
481
|
-
const element = getElementFromNode(node)?.closest('[data-edytor-block]');
|
|
482
|
-
const id = element && this.edytor.node?.contains(element) && element.dataset.edytorId;
|
|
483
|
-
return (id && this.edytor.idToBlock.get(id)) || null;
|
|
484
|
-
};
|
|
492
|
+
getBlockOfNode = (node) => this.edytor.surface.blockOf(node);
|
|
485
493
|
/** @internal */
|
|
486
494
|
init = () => {
|
|
487
495
|
if (typeof document === 'undefined')
|
|
@@ -656,9 +664,8 @@ export class EdytorSelection {
|
|
|
656
664
|
* for the root's own text.
|
|
657
665
|
*/
|
|
658
666
|
createTextAnchor = (text, offset, affinity = 'left') => {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
return this.edytor.facade.anchorAt(text.blockId, text.segStart + offset, affinity);
|
|
667
|
+
const { block, offset: at } = pointOf(text, offset);
|
|
668
|
+
return this.edytor.facade.anchorAt(block, at, affinity);
|
|
662
669
|
};
|
|
663
670
|
/**
|
|
664
671
|
* Resolve a `TextAnchor` → `{text, offset}` (offset within the text
|
|
@@ -7,6 +7,20 @@ export declare const SYNTHETIC_TEXT_OVERLAY_SELECTOR = "[data-edytor-text-sugges
|
|
|
7
7
|
/** A block suggestion's preview group: view-only, never a selection endpoint (`session/suggestions`). */
|
|
8
8
|
export declare const SUGGESTION = "[data-edytor-suggestion]";
|
|
9
9
|
export declare const getElementFromNode: (node: Node | null) => Element | null;
|
|
10
|
+
/**
|
|
11
|
+
* The nearest `contenteditable` boundary of `node` inside `host` (the host
|
|
12
|
+
* itself excluded), and whether it is editable: `false` is non-editable
|
|
13
|
+
* chrome (a void block's wrapper, an atom, a control's icon); a nested
|
|
14
|
+
* `true`, `plaintext-only` or `''` (a void block's caption, which turns
|
|
15
|
+
* editing on again for its text) is an editable region whose presses and
|
|
16
|
+
* drags are the browser's (`sel.press.nested-editable`). The attribute or,
|
|
17
|
+
* without one, the property (an attach hook sets it; jsdom reflects
|
|
18
|
+
* neither way). `null`: no boundary below the host.
|
|
19
|
+
*/
|
|
20
|
+
export declare const editableBoundary: (node: Node | null, host: Element | null | undefined) => {
|
|
21
|
+
element: Element;
|
|
22
|
+
editable: boolean;
|
|
23
|
+
} | null;
|
|
10
24
|
export declare const isInsideTrailingNewlineMarker: (node: Node) => boolean;
|
|
11
25
|
/**
|
|
12
26
|
* A selection endpoint counts as text-bound when it lives inside a
|
|
@@ -13,6 +13,31 @@ export const getElementFromNode = (node) => !node || typeof Element === 'undefin
|
|
|
13
13
|
: node.nodeType === Node.TEXT_NODE
|
|
14
14
|
? node.parentElement
|
|
15
15
|
: null;
|
|
16
|
+
/**
|
|
17
|
+
* The nearest `contenteditable` boundary of `node` inside `host` (the host
|
|
18
|
+
* itself excluded), and whether it is editable: `false` is non-editable
|
|
19
|
+
* chrome (a void block's wrapper, an atom, a control's icon); a nested
|
|
20
|
+
* `true`, `plaintext-only` or `''` (a void block's caption, which turns
|
|
21
|
+
* editing on again for its text) is an editable region whose presses and
|
|
22
|
+
* drags are the browser's (`sel.press.nested-editable`). The attribute or,
|
|
23
|
+
* without one, the property (an attach hook sets it; jsdom reflects
|
|
24
|
+
* neither way). `null`: no boundary below the host.
|
|
25
|
+
*/
|
|
26
|
+
export const editableBoundary = (node, host) => {
|
|
27
|
+
if (!host)
|
|
28
|
+
return null;
|
|
29
|
+
for (let at = getElementFromNode(node); at && at !== host; at = at.parentElement) {
|
|
30
|
+
if (!host.contains(at))
|
|
31
|
+
return null;
|
|
32
|
+
const attribute = at.getAttribute('contenteditable');
|
|
33
|
+
const state = attribute?.toLowerCase() ?? at.contentEditable;
|
|
34
|
+
if (state === 'false')
|
|
35
|
+
return { element: at, editable: false };
|
|
36
|
+
if (state === 'true' || state === 'plaintext-only' || state === '')
|
|
37
|
+
return { element: at, editable: true };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
16
41
|
/** The element around `node` (itself included) matching `selector`. */
|
|
17
42
|
const closestTo = (node, selector) => getElementFromNode(node)?.closest(selector) ?? null;
|
|
18
43
|
/** `node` is an element matching `selector` or holding one. */
|
|
@@ -120,7 +145,7 @@ export function getTextOfNode(node, offset) {
|
|
|
120
145
|
if (!node)
|
|
121
146
|
return null;
|
|
122
147
|
const { edytor } = this;
|
|
123
|
-
const textOf = (element) =>
|
|
148
|
+
const textOf = (element) => edytor.surface.handleOf(element, 'text');
|
|
124
149
|
// A point in a suggestion's preview is the stray boundary the preview sits at.
|
|
125
150
|
const preview = closestTo(node, SUGGESTION);
|
|
126
151
|
if (preview)
|
|
@@ -138,7 +163,7 @@ export const getInlineBlockOfNode = (edytor, node) => {
|
|
|
138
163
|
const element = getElementFromNode(node);
|
|
139
164
|
const around = element?.closest(INLINE);
|
|
140
165
|
const inline = around && edytor.node?.contains(around) ? around : element?.querySelector(INLINE);
|
|
141
|
-
return
|
|
166
|
+
return edytor.surface.handleOf(inline, 'atom');
|
|
142
167
|
};
|
|
143
168
|
/** The one inline atom a non-collapsed range covers, touching no text. */
|
|
144
169
|
export const getInlineBlockInSelectedRange = (edytor, range) => {
|
|
@@ -146,7 +171,7 @@ export const getInlineBlockInSelectedRange = (edytor, range) => {
|
|
|
146
171
|
if (!root)
|
|
147
172
|
return null;
|
|
148
173
|
const inside = (selector) => [...(root.matches(selector) ? [root] : []), ...root.querySelectorAll(selector)].filter((element) => range.intersectsNode(element));
|
|
149
|
-
const atoms = new Set(inside(INLINE).flatMap((element) => edytor.
|
|
174
|
+
const atoms = new Set(inside(INLINE).flatMap((element) => edytor.surface.handleOf(element, 'atom') ?? []));
|
|
150
175
|
return atoms.size === 1 && !inside(TEXT).length ? [...atoms][0] : null;
|
|
151
176
|
};
|
|
152
177
|
/**
|
package/dist/session/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { refused } from '../crdt/doc/plan.js';
|
|
2
|
+
import { pointOf } from './selection.js';
|
|
2
3
|
import { viewOf } from '../selection/visibility.js';
|
|
3
4
|
import { reveal } from '../selection/replaceSelection.js';
|
|
4
5
|
import { DEV } from 'esm-env';
|
|
@@ -171,6 +172,13 @@ export class Dispatcher {
|
|
|
171
172
|
patched = null;
|
|
172
173
|
/** `outside` is running: its commands cut no step and enter none. */
|
|
173
174
|
untracked = false;
|
|
175
|
+
/**
|
|
176
|
+
* A plugin that writes plans itself (the shared suggestions' suggesting
|
|
177
|
+
* mode): a plan's result (its `at` set to where its caret stands now),
|
|
178
|
+
* or `null` to write it as prepared. Set by the plugin.
|
|
179
|
+
* @internal
|
|
180
|
+
*/
|
|
181
|
+
writer;
|
|
174
182
|
/** @internal */
|
|
175
183
|
constructor(edytor) {
|
|
176
184
|
this.edytor = edytor;
|
|
@@ -402,7 +410,7 @@ export class Dispatcher {
|
|
|
402
410
|
try {
|
|
403
411
|
// An unplanned command runs after its lead, reading the state it leaves.
|
|
404
412
|
if (lead && !plan)
|
|
405
|
-
this.edytor.facade.apply(lead);
|
|
413
|
+
void (this.writer?.(lead) ?? this.edytor.facade.apply(lead));
|
|
406
414
|
return body(payload, plan);
|
|
407
415
|
}
|
|
408
416
|
finally {
|
|
@@ -442,8 +450,10 @@ export class Dispatcher {
|
|
|
442
450
|
plan = (operation, payload, prepare, block, text) => {
|
|
443
451
|
let at;
|
|
444
452
|
const out = this.dispatch(operation, payload, { block: block ?? this.edytor.root, ...(text && { text }) }, (p, plan = prepare(p)) => {
|
|
453
|
+
const out = this.write(plan);
|
|
454
|
+
// Read once written: a plan a plugin wrote says where its caret stands now.
|
|
445
455
|
at = 'writes' in plan ? plan.at : undefined;
|
|
446
|
-
return
|
|
456
|
+
return out;
|
|
447
457
|
}, prepare);
|
|
448
458
|
if (at && !this.active && out && out.status !== 'refused')
|
|
449
459
|
this.caret(at);
|
|
@@ -493,13 +503,14 @@ export class Dispatcher {
|
|
|
493
503
|
!!caret &&
|
|
494
504
|
plan.writes.some((w) => w.op === op && w.id === caret.block.id && w.offset === caret.offset);
|
|
495
505
|
const [typed, atom] = [at('insertText'), at('insertInline')];
|
|
496
|
-
const result = facade.apply(plan);
|
|
506
|
+
const result = this.writer?.(plan) ?? facade.apply(plan);
|
|
497
507
|
for (const [id, hook] of touched)
|
|
498
508
|
this.request(id, hook);
|
|
499
509
|
if (typed)
|
|
500
510
|
selection.stage(undefined);
|
|
501
|
-
const
|
|
502
|
-
|
|
511
|
+
const value = atom
|
|
512
|
+
? selection.valueAt({ block: caret.block.id, offset: caret.offset + 1 })
|
|
513
|
+
: null;
|
|
503
514
|
if (value?.kind === 'text')
|
|
504
515
|
selection.select(Object.freeze({ ...value, pending }));
|
|
505
516
|
for (const id of plan.effect.moves) {
|
|
@@ -575,14 +586,13 @@ export class Dispatcher {
|
|
|
575
586
|
*/
|
|
576
587
|
caret = (caret, ops) => {
|
|
577
588
|
const block = typeof caret?.block === 'string' ? this.edytor.idToBlock.get(caret.block) : caret?.block;
|
|
578
|
-
|
|
579
|
-
return this.caretIn(at?.text, at?.offset ?? 0, ops);
|
|
589
|
+
return this.#caretAt(block && { block: block.id, offset: Math.max(0, caret.offset) }, ops);
|
|
580
590
|
};
|
|
581
591
|
/** @internal `caret` at an offset inside a text segment. */
|
|
582
|
-
caretIn = (text, offset, ops) =>
|
|
592
|
+
caretIn = (text, offset, ops) => this.#caretAt(text && pointOf(text, Math.max(0, Math.min(offset, text.length))), ops);
|
|
593
|
+
#caretAt = (point, ops) => {
|
|
583
594
|
const selection = this.edytor.selection;
|
|
584
|
-
const
|
|
585
|
-
const value = text ? selection.textValue(text, at) : null;
|
|
595
|
+
const value = point ? selection.valueAt(point) : null;
|
|
586
596
|
let out;
|
|
587
597
|
if (ops) {
|
|
588
598
|
const outer = this.authoring;
|
|
@@ -596,7 +606,7 @@ export class Dispatcher {
|
|
|
596
606
|
if (out.status === 'refused')
|
|
597
607
|
return out;
|
|
598
608
|
}
|
|
599
|
-
if (!
|
|
609
|
+
if (!value || value.kind === 'none')
|
|
600
610
|
return out;
|
|
601
611
|
selection.select(value);
|
|
602
612
|
// Declared before its operations: gone with them after all, the seam.
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* One IME composition is one session. Its start target (the selection at
|
|
5
5
|
* `compositionstart`) is replaced at its first write through an ordinary
|
|
6
|
-
* command (hooks, veto, tracked) — over a block or atom
|
|
7
|
-
* shows no DOM range,
|
|
8
|
-
*
|
|
6
|
+
* command (hooks, veto, tracked) — at the start over a block or atom
|
|
7
|
+
* selection, which shows no DOM range, and over a text range across blocks,
|
|
8
|
+
* whose native replacement would join the blocks' elements (`ime.range.blocks`),
|
|
9
|
+
* so the IME writes in its place — and the marks of the composed text are
|
|
10
|
+
* captured once, at the start. Previews are mechanical tracked writes (no
|
|
9
11
|
* hooks) inside one capture group: the group opens with the session's first
|
|
10
12
|
* write and is held open before each later one (`UndoManager.lastChange`),
|
|
11
13
|
* so the session and its ending are one undo step. Like a typed
|