edytor 4.0.0 → 6.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 +10 -3
- 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 +106 -109
- package/dist/cloudflare/room/context.js +10 -3
- 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.js +2 -1
- 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/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 +3 -0
- package/dist/crdt/attribution/attribution.js +3 -3
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +5 -2
- package/dist/crdt/doc/gate.d.ts +1 -1
- package/dist/crdt/doc/history.d.ts +6 -0
- package/dist/crdt/doc/history.js +79 -59
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +18 -28
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +19 -11
- package/dist/crdt/edytor-doc.d.ts +294 -272
- package/dist/crdt/edytor-doc.js +123 -108
- package/dist/crdt/engine-api.d.ts +8 -2
- package/dist/crdt/engine.d.ts +2 -1
- package/dist/crdt/engine.js +3 -1
- package/dist/crdt/flow.d.ts +5 -47
- package/dist/crdt/flow.js +3 -269
- package/dist/crdt/index.d.ts +102 -108
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/operations.d.ts +13 -4
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +1 -1
- package/dist/crdt/placement/display.js +3 -3
- package/dist/crdt/placement/keys.d.ts +5 -0
- package/dist/crdt/placement/keys.js +5 -0
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/placement/resolve.js +2 -1
- 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/awareness.js +46 -81
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +12 -22
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +11 -0
- package/dist/crdt/providers/room.js +26 -12
- package/dist/crdt/providers/websocket.d.ts +8 -12
- package/dist/crdt/providers/websocket.js +17 -19
- package/dist/crdt/purge.d.ts +7 -1
- package/dist/crdt/purge.js +32 -2
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/restore.d.ts +2 -1
- package/dist/crdt/restore.js +9 -0
- package/dist/crdt/schema.d.ts +9 -1
- package/dist/crdt/schema.js +13 -3
- package/dist/crdt/structs.d.ts +2 -0
- package/dist/crdt/structs.js +2 -0
- package/dist/crdt/text/index/build.d.ts +125 -0
- package/dist/crdt/text/index/build.js +2895 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +19 -19
- package/dist/crdt/text/model.d.ts +10 -0
- package/dist/crdt/text/model.js +10 -4
- package/dist/crdt/text/runs.d.ts +43 -42
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
- package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +100 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- 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/export/forms.d.ts +75 -75
- 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/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- 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 +33 -11
- 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 +14 -44
- 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 +143 -111
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.js +10 -16
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +3 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -211
- package/dist/crdt/doc/content.d.ts +0 -16
- package/dist/crdt/doc/content.js +0 -77
- package/dist/crdt/doc/delete.d.ts +0 -15
- package/dist/crdt/doc/delete.js +0 -84
- package/dist/crdt/doc/layout.d.ts +0 -9
- package/dist/crdt/doc/layout.js +0 -178
- package/dist/crdt/doc/meta.d.ts +0 -19
- package/dist/crdt/doc/meta.js +0 -199
- package/dist/crdt/doc/moves.d.ts +0 -21
- package/dist/crdt/doc/moves.js +0 -274
- package/dist/crdt/doc/split-merge.d.ts +0 -10
- package/dist/crdt/doc/split-merge.js +0 -179
- package/dist/crdt/doc/steps.d.ts +0 -65
- package/dist/crdt/doc/steps.js +0 -311
- package/dist/crdt/text/index/cache.d.ts +0 -16
- package/dist/crdt/text/index/cache.js +0 -175
- package/dist/crdt/text/index/claims.d.ts +0 -18
- package/dist/crdt/text/index/claims.js +0 -225
- package/dist/crdt/text/index/fold.d.ts +0 -21
- package/dist/crdt/text/index/fold.js +0 -656
- package/dist/crdt/text/index/layout.d.ts +0 -24
- package/dist/crdt/text/index/layout.js +0 -271
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -741
- package/dist/crdt/text/index/records.d.ts +0 -13
- package/dist/crdt/text/index/records.js +0 -139
- package/dist/crdt/text/index/report.d.ts +0 -18
- package/dist/crdt/text/index/report.js +0 -337
- package/dist/crdt/text/index/state.d.ts +0 -197
- package/dist/crdt/text/index/state.js +0 -116
- package/dist/crdt/text/index/streams.d.ts +0 -25
- package/dist/crdt/text/index/streams.js +0 -248
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Edytor } from '../edytor.svelte.js';
|
|
2
2
|
import type { Text } from '../text/text.svelte.js';
|
|
3
|
+
import type { InlineBlock } from '../block/inlineBlock.svelte.js';
|
|
3
4
|
import { type Kind, type Owned } from './attributes.js';
|
|
4
5
|
import { type Patched, type Replaced } from './cells.js';
|
|
5
6
|
/**
|
|
@@ -29,7 +30,7 @@ export declare class SurfaceObserver {
|
|
|
29
30
|
* release. A text element takes the table's attributes now (its id, its
|
|
30
31
|
* editing host inside a void).
|
|
31
32
|
*/
|
|
32
|
-
register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned) => () => void;
|
|
33
|
+
register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned, handle?: Text | InlineBlock) => () => void;
|
|
33
34
|
/** The render anchor (`Edytor.svelte`): a registered child of the strict root. */
|
|
34
35
|
anchor: (node: HTMLElement) => {
|
|
35
36
|
destroy: () => void;
|
|
@@ -53,6 +54,10 @@ export declare class SurfaceObserver {
|
|
|
53
54
|
flush: () => Promise<void>;
|
|
54
55
|
pre: () => void;
|
|
55
56
|
post: () => void;
|
|
57
|
+
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
58
|
+
handleOf: <K extends "text" | "atom">(element: Node | null | undefined, kind: K) => (K extends "text" ? Text : InlineBlock) | null;
|
|
59
|
+
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
60
|
+
blockOf: (node: Node | null) => import("../index.js").Block | null;
|
|
56
61
|
}
|
|
57
62
|
/**
|
|
58
63
|
* Adopt what the browser made of `text` — the only adopter: one
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* - The pre pass (root `$effect.pre`, before the flush's writes) inverts a
|
|
19
19
|
* record that removed registered elements (its added nodes go, its removed
|
|
20
20
|
* nodes return at their recorded siblings, before Svelte reconciles around
|
|
21
|
-
* them) and
|
|
22
|
-
* the
|
|
21
|
+
* them), and one that removed only the render's anchors from a block list
|
|
22
|
+
* when the editor did not write it (`dom.anchors.foreign`: the records taken
|
|
23
|
+
* from the pre pass to the end of a compare pass are the editor's own), and
|
|
24
|
+
* snapshots a content the flush re-renders whose DOM holds an edit the
|
|
25
|
+
* flush would overwrite.
|
|
23
26
|
* - The compare pass (root `$effect`, after every DOM write of the flush)
|
|
24
27
|
* removes identity clones, nodes inside a content's run and the root's
|
|
25
28
|
* foreign children, heals owned attributes (bounded), and compares each
|
|
@@ -42,7 +45,7 @@ import { runOccurrence } from '../events/onBeforeInput.js';
|
|
|
42
45
|
import { getDomSelectionSnapshot } from '../selection/domSelection.js';
|
|
43
46
|
import { domOffsetIn } from '../selection/selection.utils.js';
|
|
44
47
|
import { diverges, heal, ownedOf } from './attributes.js';
|
|
45
|
-
import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
|
|
48
|
+
import { getTextContentOffsetAtPoint, textLeaves } from '../events/domTextOffset.js';
|
|
46
49
|
import { rendersMark } from './cells.js';
|
|
47
50
|
const ZWSP = /\u200B/g;
|
|
48
51
|
const ATOM = '\uFFFC';
|
|
@@ -70,13 +73,7 @@ const textOf = (cell) => cell.runs
|
|
|
70
73
|
/** Svelte's anchors: comments and empty text nodes are never input and never removed. */
|
|
71
74
|
const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
|
|
72
75
|
/** The `nth` non-empty text node under `node`. */
|
|
73
|
-
const textLeaf = (node, nth = 0) =>
|
|
74
|
-
const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
75
|
-
while (walker.nextNode())
|
|
76
|
-
if (walker.currentNode.data && nth-- === 0)
|
|
77
|
-
return walker.currentNode;
|
|
78
|
-
return null;
|
|
79
|
-
};
|
|
76
|
+
const textLeaf = (node, nth = 0) => textLeaves(node).filter((leaf) => leaf.data)[nth] ?? null;
|
|
80
77
|
const ZERO_WIDTH_SPACE = '\u200B';
|
|
81
78
|
/**
|
|
82
79
|
* What the browser shows for `text`: its element's text nodes in order (WebKit's
|
|
@@ -86,15 +83,10 @@ const ZERO_WIDTH_SPACE = '\u200B';
|
|
|
86
83
|
export const readDomText = (text) => {
|
|
87
84
|
let value = '';
|
|
88
85
|
const node = text.node;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
94
|
-
? leaf.data.replaceAll('\u00A0', ' ')
|
|
95
|
-
: leaf.data;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
86
|
+
for (const leaf of node ? textLeaves(node) : [])
|
|
87
|
+
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
88
|
+
? leaf.data.replaceAll('\u00A0', ' ')
|
|
89
|
+
: leaf.data;
|
|
98
90
|
// An empty text's filler shares its node with what the browser typed, on either side.
|
|
99
91
|
if (text.isEmpty)
|
|
100
92
|
return value.replaceAll(ZERO_WIDTH_SPACE, '');
|
|
@@ -189,6 +181,10 @@ export class SurfaceObserver {
|
|
|
189
181
|
#wrote = false;
|
|
190
182
|
/** Records arrived since the last pass (the projector reads what they did: `rendered`). */
|
|
191
183
|
#fresh = false;
|
|
184
|
+
/** The flush writes the host: from the pre pass to the end of its compare pass. */
|
|
185
|
+
#rendering = false;
|
|
186
|
+
/** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
|
|
187
|
+
#passes = 0;
|
|
192
188
|
#window = null;
|
|
193
189
|
constructor(edytor) {
|
|
194
190
|
this.edytor = edytor;
|
|
@@ -196,11 +192,13 @@ export class SurfaceObserver {
|
|
|
196
192
|
/** Observe `root` (the host): records name what to compare. */
|
|
197
193
|
attach = (root) => {
|
|
198
194
|
const mo = (this.#mo = new MutationObserver((records) => {
|
|
199
|
-
records
|
|
195
|
+
for (const record of records)
|
|
196
|
+
this.#intake(record, this.#own());
|
|
200
197
|
this.signal();
|
|
201
198
|
}));
|
|
202
199
|
for (const child of root.childNodes)
|
|
203
200
|
this.#rooted.add(child);
|
|
201
|
+
this.#rendering = false;
|
|
204
202
|
mo.observe(root, {
|
|
205
203
|
attributes: true,
|
|
206
204
|
attributeOldValue: true,
|
|
@@ -220,8 +218,8 @@ export class SurfaceObserver {
|
|
|
220
218
|
* release. A text element takes the table's attributes now (its id, its
|
|
221
219
|
* editing host inside a void).
|
|
222
220
|
*/
|
|
223
|
-
register = (node, kind, block, name, owned) => {
|
|
224
|
-
const entry = { kind, block, name, owned };
|
|
221
|
+
register = (node, kind, block, name, owned, handle) => {
|
|
222
|
+
const entry = { kind, block, name, owned, handle };
|
|
225
223
|
this.#registry.set(node, entry);
|
|
226
224
|
if (kind === 'text')
|
|
227
225
|
heal(node, ownedOf(this.edytor, node, entry));
|
|
@@ -289,50 +287,71 @@ export class SurfaceObserver {
|
|
|
289
287
|
if (!this.edytor.readonly)
|
|
290
288
|
this.#restore();
|
|
291
289
|
this.#snapshot();
|
|
290
|
+
// From here to the end of the compare pass, what the host records is the flush's.
|
|
291
|
+
this.#rendering = true;
|
|
292
292
|
});
|
|
293
293
|
};
|
|
294
294
|
post = () => {
|
|
295
295
|
this.#deps();
|
|
296
|
-
untrack(() =>
|
|
296
|
+
untrack(() => {
|
|
297
|
+
void this.#pass(true);
|
|
298
|
+
this.#rendering = false;
|
|
299
|
+
});
|
|
297
300
|
};
|
|
298
301
|
#deps = () => {
|
|
299
302
|
const { edytor } = this;
|
|
300
303
|
void [this.epoch, this.records, edytor.readonly, edytor.composition.phase];
|
|
301
304
|
void [edytor.selection.value, edytor.suggestions.revision];
|
|
302
305
|
};
|
|
306
|
+
/**
|
|
307
|
+
* The editor writes the host now: the flush (from the pre pass to the end
|
|
308
|
+
* of its compare pass) or a pass's own repairs. A record taken then is the
|
|
309
|
+
* render's (`dom.anchors.foreign`); every other one is the browser's, a
|
|
310
|
+
* page script's or an extension's.
|
|
311
|
+
*/
|
|
312
|
+
#own = () => this.#rendering || this.#passes > 0;
|
|
303
313
|
#take = () => {
|
|
304
314
|
const records = this.#mo?.takeRecords() ?? [];
|
|
305
|
-
|
|
315
|
+
const own = this.#own();
|
|
316
|
+
for (const record of records)
|
|
317
|
+
this.#intake(record, own);
|
|
306
318
|
return records.length > 0;
|
|
307
319
|
};
|
|
308
|
-
/** The
|
|
309
|
-
#
|
|
320
|
+
/** The closest registered element holding `node` in the host: its entry (`null`: the host). */
|
|
321
|
+
#near = (node) => {
|
|
310
322
|
for (let at = node; at; at = at.parentNode) {
|
|
311
323
|
if (at === this.edytor.node)
|
|
312
324
|
return null;
|
|
313
|
-
const entry =
|
|
314
|
-
// What happens in a preview is the view's own render: nothing to compare.
|
|
325
|
+
const entry = this.#registry.get(at);
|
|
315
326
|
if (entry)
|
|
316
|
-
return entry
|
|
327
|
+
return entry;
|
|
317
328
|
}
|
|
318
|
-
return undefined;
|
|
319
329
|
};
|
|
320
|
-
/** The
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
/** The block an element belongs to: its closest registered element's (`null`: the root). */
|
|
331
|
+
#owner = (node) => {
|
|
332
|
+
const entry = this.#near(node);
|
|
333
|
+
// What happens in a preview is the view's own render: nothing to compare.
|
|
334
|
+
return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
|
|
335
|
+
};
|
|
336
|
+
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
337
|
+
handleOf = (element, kind) => {
|
|
338
|
+
const entry = element && this.#registry.get(element);
|
|
339
|
+
return (entry?.kind === kind ? entry.handle : null);
|
|
340
|
+
};
|
|
341
|
+
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
342
|
+
blockOf = (node) => {
|
|
343
|
+
const entry = this.#near(node);
|
|
344
|
+
return entry && entry.kind !== 'preview' && entry.kind !== 'anchor'
|
|
345
|
+
? (this.edytor.idToBlock.get(entry.block) ?? null)
|
|
346
|
+
: null;
|
|
328
347
|
};
|
|
329
348
|
#registered = (node) => node instanceof Element && this.#registry.has(node);
|
|
330
349
|
/** `node` is, or holds, an element the core still renders. */
|
|
331
350
|
#holds = (node) => this.#registered(node) ||
|
|
332
351
|
(node instanceof Element &&
|
|
333
352
|
[...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
|
|
334
|
-
/** A record names elements to compare (never a verdict). */
|
|
335
|
-
#intake = (record) => {
|
|
353
|
+
/** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
|
|
354
|
+
#intake = (record, own) => {
|
|
336
355
|
this.#fresh = true;
|
|
337
356
|
const id = this.#owner(record.target);
|
|
338
357
|
if (id === undefined)
|
|
@@ -355,7 +374,7 @@ export class SurfaceObserver {
|
|
|
355
374
|
return;
|
|
356
375
|
const added = [...record.addedNodes];
|
|
357
376
|
const { target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
358
|
-
this.#removed.push({ parent, nodes: removed, added, next, prev });
|
|
377
|
+
this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
|
|
359
378
|
// Wrapper replacement: the browser replaced a text element with its own nodes.
|
|
360
379
|
const [text] = removed;
|
|
361
380
|
if (removed.length === 1 &&
|
|
@@ -394,21 +413,33 @@ export class SurfaceObserver {
|
|
|
394
413
|
* browser removed them one record each (Firefox): an anchor-only record is
|
|
395
414
|
* inverted in a parent that gets a registered element back, else the
|
|
396
415
|
* element's recorded siblings are gone and it lands outside Svelte's block.
|
|
416
|
+
* A foreign range across blocks (`dom.anchors.foreign`: a script's
|
|
417
|
+
* `Range.deleteContents()`) takes the render's anchors between the blocks
|
|
418
|
+
* from their list (the root, a block's children) and those after its start
|
|
419
|
+
* and before its end inside the blocks it enters, and leaves the elements:
|
|
420
|
+
* Svelte's next removal of either block would start at a detached anchor
|
|
421
|
+
* and leave its element, a child added to one would find no anchor to
|
|
422
|
+
* render before. When a foreign record took anchors from a block list,
|
|
423
|
+
* every foreign anchor-only record of the batch is inverted. The render's
|
|
424
|
+
* own removals (an item leaving the list) are never undone.
|
|
397
425
|
*/
|
|
398
426
|
#restore = () => {
|
|
399
427
|
const root = this.edytor.node;
|
|
400
428
|
const removals = this.#removed.splice(0).reverse();
|
|
401
429
|
const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
|
|
402
430
|
const parents = new Set(removals.filter(held).map(({ parent }) => parent));
|
|
431
|
+
const crossed = removals.some((removal) => this.#lost(removal) && this.#list(removal.parent));
|
|
403
432
|
for (const removal of removals) {
|
|
404
433
|
const { parent, nodes, added, next, prev } = removal;
|
|
405
434
|
if (!root?.contains(parent))
|
|
406
435
|
continue;
|
|
407
|
-
|
|
436
|
+
const anchors = parents.has(parent) && nodes.every(isAnchor);
|
|
437
|
+
if (!held(removal) && !anchors && !(crossed && this.#lost(removal)))
|
|
408
438
|
continue;
|
|
409
439
|
// A browser-owned attempt's host: its change is adopted whatever the
|
|
410
440
|
// browser did to its structure, which the re-render then restores.
|
|
411
|
-
const
|
|
441
|
+
const near = this.#near(parent);
|
|
442
|
+
const inside = near?.kind === 'text' && near.handle;
|
|
412
443
|
if (inside && this.edytor.attempts.on(inside))
|
|
413
444
|
continue;
|
|
414
445
|
for (const node of added)
|
|
@@ -428,6 +459,17 @@ export class SurfaceObserver {
|
|
|
428
459
|
}
|
|
429
460
|
this.#replaced.clear();
|
|
430
461
|
};
|
|
462
|
+
/**
|
|
463
|
+
* A foreign record took only the render's anchors, and they are gone from
|
|
464
|
+
* the page (not moved): they can return beside a recorded sibling still
|
|
465
|
+
* there (never at the parent's end).
|
|
466
|
+
*/
|
|
467
|
+
#lost = ({ parent, nodes, next, prev, foreign }) => foreign &&
|
|
468
|
+
nodes.every((node) => isAnchor(node) && !node.isConnected) &&
|
|
469
|
+
(next?.parentNode === parent || prev?.parentNode === parent);
|
|
470
|
+
/** A block list: the root, or a parent of block elements (a block's children). */
|
|
471
|
+
#list = (parent) => parent === this.edytor.node ||
|
|
472
|
+
[...parent.childNodes].some((child) => child instanceof Element && this.#registry.get(child)?.kind === 'block');
|
|
431
473
|
/**
|
|
432
474
|
* Where an edit made at `at` of the base lands now: through the anchor
|
|
433
475
|
* minted at it (it follows its text through a re-key, split, move or
|
|
@@ -479,6 +521,19 @@ export class SurfaceObserver {
|
|
|
479
521
|
* `$effect`); otherwise contents waiting for a flush are left to it.
|
|
480
522
|
*/
|
|
481
523
|
#pass = (flushed) => {
|
|
524
|
+
this.#passes++;
|
|
525
|
+
try {
|
|
526
|
+
return this.#compare(flushed);
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
// What the flush and this pass wrote: the render's records, compared
|
|
530
|
+
// at the next pass (as the observer's callback would have them).
|
|
531
|
+
if (this.#take())
|
|
532
|
+
queueMicrotask(this.signal);
|
|
533
|
+
this.#passes--;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
#compare = (flushed) => {
|
|
482
537
|
const { edytor } = this;
|
|
483
538
|
const cells = edytor.cells;
|
|
484
539
|
if (!cells || !edytor.node?.isConnected || edytor.destroyed)
|
|
@@ -620,31 +675,11 @@ export class SurfaceObserver {
|
|
|
620
675
|
#invert = (id) => {
|
|
621
676
|
if (id === this.#live())
|
|
622
677
|
return;
|
|
623
|
-
if (this.#shape(id
|
|
678
|
+
if (this.#shape(id, true))
|
|
624
679
|
this.edytor.cells?.remount(id);
|
|
625
680
|
else
|
|
626
681
|
this.update();
|
|
627
682
|
};
|
|
628
|
-
/** Write each render delta's text back into its node; false when a node is not there. */
|
|
629
|
-
#rewrite = (id) => {
|
|
630
|
-
const writes = [];
|
|
631
|
-
for (const text of this.#texts(id)) {
|
|
632
|
-
const deltas = this.edytor.deltasOf(text);
|
|
633
|
-
const nodes = this.#nodes(text.node);
|
|
634
|
-
const shown = deltas.length ? deltas : [{ text: '\u200B' }];
|
|
635
|
-
for (const [i, delta] of shown.entries()) {
|
|
636
|
-
const node = nodes[i];
|
|
637
|
-
const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && textLeaf(node);
|
|
638
|
-
if (!leaf)
|
|
639
|
-
return false;
|
|
640
|
-
if (leaf.data !== delta.text)
|
|
641
|
-
writes.push([leaf, delta.text]);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (const [leaf, value] of writes)
|
|
645
|
-
leaf.data = value;
|
|
646
|
-
return true;
|
|
647
|
-
};
|
|
648
683
|
/** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
|
|
649
684
|
#nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
|
|
650
685
|
!(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
|
|
@@ -655,7 +690,7 @@ export class SurfaceObserver {
|
|
|
655
690
|
*/
|
|
656
691
|
#adoptContent = (id) => {
|
|
657
692
|
const { edytor } = this;
|
|
658
|
-
let broken =
|
|
693
|
+
let broken = this.#shape(id);
|
|
659
694
|
const caret = getCollapsedDomTextSelection(edytor);
|
|
660
695
|
const work = [];
|
|
661
696
|
for (const text of this.#texts(id)) {
|
|
@@ -688,7 +723,7 @@ export class SurfaceObserver {
|
|
|
688
723
|
const value = now.slice(0, local) + insert + now.slice(local + remove);
|
|
689
724
|
return adopt(this.edytor, text, value, undefined, local + insert.length);
|
|
690
725
|
};
|
|
691
|
-
#texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.
|
|
726
|
+
#texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.handleOf(element, 'text') ?? []);
|
|
692
727
|
/** A content's DOM: its registered text and atom elements in order; null when not shown. */
|
|
693
728
|
#read = (id) => {
|
|
694
729
|
const elements = [...(this.#contents.get(id) ?? [])];
|
|
@@ -697,20 +732,17 @@ export class SurfaceObserver {
|
|
|
697
732
|
elements.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
698
733
|
const host = this.#blocks.get(id);
|
|
699
734
|
let text = '';
|
|
700
|
-
let broken = host && !host.isConnected
|
|
735
|
+
let broken = !!host && !host.isConnected;
|
|
701
736
|
for (const element of elements) {
|
|
702
737
|
const kind = this.#registry.get(element)?.kind;
|
|
703
738
|
// A mark is inside its text element: its presence is the shape check's.
|
|
704
739
|
if (kind === 'mark')
|
|
705
740
|
continue;
|
|
706
|
-
|
|
707
|
-
broken ??= 'missing';
|
|
708
|
-
else if (host && !host.contains(element))
|
|
709
|
-
broken ??= 'escaped';
|
|
741
|
+
broken ||= !element.isConnected || (!!host && !host.contains(element));
|
|
710
742
|
if (kind === 'atom')
|
|
711
743
|
text += ATOM;
|
|
712
744
|
else if (kind === 'text') {
|
|
713
|
-
const found = this.
|
|
745
|
+
const found = this.handleOf(element, 'text');
|
|
714
746
|
text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
|
|
715
747
|
}
|
|
716
748
|
}
|
|
@@ -718,46 +750,50 @@ export class SurfaceObserver {
|
|
|
718
750
|
};
|
|
719
751
|
/**
|
|
720
752
|
* Equal text, compared structurally: each text element (a strict
|
|
721
|
-
* container) holds one node per render delta
|
|
722
|
-
* registered mark element whose registered marks follow
|
|
753
|
+
* container) holds one node per render delta (the empty filler's one) — a
|
|
754
|
+
* text node, or a registered mark element whose registered marks follow
|
|
755
|
+
* the delta's chain and that renders one text node. Answers whether it
|
|
756
|
+
* differs; `write`: a content that does not gets each delta's text back
|
|
757
|
+
* in its node.
|
|
723
758
|
*/
|
|
724
|
-
#shape = (id) => {
|
|
759
|
+
#shape = (id, write) => {
|
|
760
|
+
const writes = [];
|
|
725
761
|
for (const text of this.#texts(id)) {
|
|
726
762
|
const element = text.node;
|
|
727
763
|
if (!element)
|
|
728
764
|
continue;
|
|
729
765
|
const deltas = this.edytor.deltasOf(text);
|
|
766
|
+
const shown = deltas.length ? deltas : [{ text: '\u200B', marks: [] }];
|
|
730
767
|
const nodes = this.#nodes(element);
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (nodes.length !== deltas.length)
|
|
737
|
-
return 'partition';
|
|
738
|
-
for (const [i, delta] of deltas.entries()) {
|
|
768
|
+
if (nodes.length !== shown.length)
|
|
769
|
+
return true;
|
|
770
|
+
for (const [i, delta] of shown.entries()) {
|
|
739
771
|
const node = nodes[i];
|
|
772
|
+
let leaf = node;
|
|
740
773
|
const chain = delta.marks
|
|
741
774
|
.map(([name]) => name)
|
|
742
775
|
.filter((name) => rendersMark(this.edytor.marks, name));
|
|
743
|
-
if (
|
|
744
|
-
if (node
|
|
745
|
-
return
|
|
746
|
-
|
|
776
|
+
if (chain.length) {
|
|
777
|
+
if (!this.#registry.has(node))
|
|
778
|
+
return true;
|
|
779
|
+
const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
|
|
780
|
+
.map((el) => this.#registry.get(el)?.name)
|
|
781
|
+
.filter(Boolean);
|
|
782
|
+
leaf = textLeaf(node);
|
|
783
|
+
// The snippet's markup is its own, but the delta renders one text node.
|
|
784
|
+
if (marks.join() !== chain.join() || !leaf || textLeaf(node, 1))
|
|
785
|
+
return true;
|
|
747
786
|
}
|
|
748
|
-
if (
|
|
749
|
-
return
|
|
750
|
-
|
|
751
|
-
.
|
|
752
|
-
.filter(Boolean);
|
|
753
|
-
if (marks.join() !== chain.join())
|
|
754
|
-
return 'mark-chain';
|
|
755
|
-
// The snippet's markup is its own, but the delta renders one text node.
|
|
756
|
-
if (!textLeaf(node) || textLeaf(node, 1))
|
|
757
|
-
return 'mark-text';
|
|
787
|
+
else if (node.nodeType !== Node.TEXT_NODE)
|
|
788
|
+
return true;
|
|
789
|
+
if (leaf.data !== delta.text)
|
|
790
|
+
writes.push([leaf, delta.text]);
|
|
758
791
|
}
|
|
759
792
|
}
|
|
760
|
-
|
|
793
|
+
if (write)
|
|
794
|
+
for (const [leaf, value] of writes)
|
|
795
|
+
leaf.data = value;
|
|
796
|
+
return false;
|
|
761
797
|
};
|
|
762
798
|
/** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
|
|
763
799
|
#rootDiverges = () => {
|
|
@@ -921,21 +957,17 @@ const shownMarks = (text, at) => {
|
|
|
921
957
|
const element = text.node;
|
|
922
958
|
if (!element)
|
|
923
959
|
return null;
|
|
924
|
-
const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
925
960
|
let offset = 0;
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
names.push(name);
|
|
935
|
-
}
|
|
936
|
-
return names.sort().join();
|
|
961
|
+
const node = textLeaves(element).find((leaf) => (offset += leaf.data.replace(ZWSP, '').length) > at);
|
|
962
|
+
if (!node)
|
|
963
|
+
return null;
|
|
964
|
+
const names = [];
|
|
965
|
+
for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
|
|
966
|
+
const name = el.getAttribute('data-edytor-mark');
|
|
967
|
+
if (name)
|
|
968
|
+
names.push(name);
|
|
937
969
|
}
|
|
938
|
-
return
|
|
970
|
+
return names.sort().join();
|
|
939
971
|
};
|
|
940
972
|
/** The rendered marks the model gives the character at `at` of `text`. */
|
|
941
973
|
const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAtRange(at, at + 1)[0]?.marks))
|
|
@@ -23,15 +23,15 @@ export declare class Pin {
|
|
|
23
23
|
/** The session's preview (the browser draws it itself when `native`). */
|
|
24
24
|
show: (text: string, marks: Record<string, unknown> | undefined, native: boolean) => void;
|
|
25
25
|
/**
|
|
26
|
-
* The first write replaced a range across
|
|
27
|
-
* cell lost the segments and atoms it covered, and the rest of
|
|
28
|
-
* now follows the preview in
|
|
29
|
-
*
|
|
30
|
-
* host element
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* The first write replaced a range across an inline atom inside one block:
|
|
27
|
+
* the host's cell lost the segments and atoms it covered, and the rest of
|
|
28
|
+
* its end text now follows the preview in its own element right after the
|
|
29
|
+
* host (`rest`), so the IME's node is never rewritten (the IME's write
|
|
30
|
+
* moves the host element's render anchors into the next segment's,
|
|
31
|
+
* Chromium). A range across blocks never merges: the session replaces it
|
|
32
|
+
* at its start (`Composition.#rangeless`).
|
|
33
33
|
*/
|
|
34
|
-
merge: (cell: Cell, items: Item[]
|
|
34
|
+
merge: (cell: Cell, items: Item[]) => void;
|
|
35
35
|
/** The session ended: the host renders from its cell again (the catch-up patch). */
|
|
36
36
|
release: () => null;
|
|
37
37
|
/** `node` lies inside the pinned element. */
|
|
@@ -48,7 +48,7 @@ export declare class Pin {
|
|
|
48
48
|
deltas: RenderDelta[];
|
|
49
49
|
empty: boolean;
|
|
50
50
|
};
|
|
51
|
-
/** What follows the pinned segment `key` of `block` in its own element (`merge
|
|
51
|
+
/** What follows the pinned segment `key` of `block` in its own element (`merge`). */
|
|
52
52
|
rest: (block: BlockId, key: string) => RenderDelta[];
|
|
53
53
|
/**
|
|
54
54
|
* What the IME shows in the pinned element: its text between the frozen
|
|
@@ -29,7 +29,6 @@ export class Pin {
|
|
|
29
29
|
head: cut(frozen, from)[0],
|
|
30
30
|
tail: cut(frozen, Math.max(from, to))[1],
|
|
31
31
|
merged: [],
|
|
32
|
-
beside: false,
|
|
33
32
|
element,
|
|
34
33
|
// Until the first preview the render stays as the IME found it: a
|
|
35
34
|
// write now would collapse its DOM range before it replaces it.
|
|
@@ -44,18 +43,18 @@ export class Pin {
|
|
|
44
43
|
this.#held = { ...held, preview: { text, marks: set && Object.fromEntries(set), native } };
|
|
45
44
|
};
|
|
46
45
|
/**
|
|
47
|
-
* The first write replaced a range across
|
|
48
|
-
* cell lost the segments and atoms it covered, and the rest of
|
|
49
|
-
* now follows the preview in
|
|
50
|
-
*
|
|
51
|
-
* host element
|
|
52
|
-
*
|
|
53
|
-
*
|
|
46
|
+
* The first write replaced a range across an inline atom inside one block:
|
|
47
|
+
* the host's cell lost the segments and atoms it covered, and the rest of
|
|
48
|
+
* its end text now follows the preview in its own element right after the
|
|
49
|
+
* host (`rest`), so the IME's node is never rewritten (the IME's write
|
|
50
|
+
* moves the host element's render anchors into the next segment's,
|
|
51
|
+
* Chromium). A range across blocks never merges: the session replaces it
|
|
52
|
+
* at its start (`Composition.#rangeless`).
|
|
54
53
|
*/
|
|
55
|
-
merge = (cell, items
|
|
54
|
+
merge = (cell, items) => {
|
|
56
55
|
const held = this.#held;
|
|
57
56
|
if (held)
|
|
58
|
-
this.#held = { ...held, parts: partsOf(cell.runs), merged: items
|
|
57
|
+
this.#held = { ...held, parts: partsOf(cell.runs), merged: items };
|
|
59
58
|
};
|
|
60
59
|
/** The session ended: the host renders from its cell again (the catch-up patch). */
|
|
61
60
|
release = () => (this.#held = null);
|
|
@@ -68,14 +67,10 @@ export class Pin {
|
|
|
68
67
|
const held = this.#held;
|
|
69
68
|
if (!held || held.block !== block || held.key !== key)
|
|
70
69
|
return null;
|
|
71
|
-
const { preview, head, tail
|
|
72
|
-
const merged = beside ? [] : held.merged;
|
|
70
|
+
const { preview, head, tail } = held;
|
|
73
71
|
if (!preview || preview.native)
|
|
74
|
-
return {
|
|
75
|
-
|
|
76
|
-
empty: held.empty && !textOf(merged)
|
|
77
|
-
};
|
|
78
|
-
const items = [...head, preview, ...tail, ...merged];
|
|
72
|
+
return { deltas: [...held.frozen], empty: held.empty };
|
|
73
|
+
const items = [...head, preview, ...tail];
|
|
79
74
|
return { deltas: renderDeltas(items), empty: !textOf(items) };
|
|
80
75
|
};
|
|
81
76
|
/** The render of `segment` of `cell`: pinned while it hosts the IME, else its kind's (`segmentDeltas`). */
|
|
@@ -83,11 +78,10 @@ export class Pin {
|
|
|
83
78
|
deltas: segmentDeltas(cell, segment, transform),
|
|
84
79
|
empty: segment.text === ''
|
|
85
80
|
};
|
|
86
|
-
/** What follows the pinned segment `key` of `block` in its own element (`merge
|
|
81
|
+
/** What follows the pinned segment `key` of `block` in its own element (`merge`). */
|
|
87
82
|
rest = (block, key) => {
|
|
88
83
|
const held = this.#held;
|
|
89
|
-
|
|
90
|
-
return shown ? renderDeltas(held.merged) : [];
|
|
84
|
+
return held?.block === block && held.key === key ? renderDeltas(held.merged) : [];
|
|
91
85
|
};
|
|
92
86
|
/**
|
|
93
87
|
* What the IME shows in the pinned element: its text between the frozen
|
|
@@ -102,8 +96,7 @@ export class Pin {
|
|
|
102
96
|
const dom = (held.element.textContent ?? '').replace(/\u200B/g, '');
|
|
103
97
|
if (!held.preview && dom === textOf(held.frozen))
|
|
104
98
|
return null;
|
|
105
|
-
const
|
|
106
|
-
const [head, tail] = [textOf(held.head), textOf(inside)];
|
|
99
|
+
const [head, tail] = [textOf(held.head), textOf(held.tail)];
|
|
107
100
|
if (dom.length < head.length + tail.length)
|
|
108
101
|
return null;
|
|
109
102
|
return dom.startsWith(head) && dom.endsWith(tail)
|