edytor 5.0.0 → 7.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 +3 -3
- package/dist/cloudflare/DocumentRoom.d.ts +77 -2
- package/dist/cloudflare/DocumentRoom.js +37 -1
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/index.js +1 -1
- package/dist/cloudflare/room/access.d.ts +6 -1
- package/dist/cloudflare/room/access.js +30 -4
- package/dist/cloudflare/room/admission.d.ts +11 -0
- package/dist/cloudflare/room/admission.js +43 -10
- package/dist/cloudflare/room/context.d.ts +73 -76
- package/dist/cloudflare/room/context.js +17 -6
- package/dist/cloudflare/room/forged.d.ts +1 -1
- package/dist/cloudflare/room/forged.js +11 -9
- package/dist/cloudflare/room/frames.d.ts +2 -0
- package/dist/cloudflare/room/frames.js +2 -0
- package/dist/cloudflare/room/presence.js +1 -4
- package/dist/cloudflare/room/reseed.d.ts +93 -0
- package/dist/cloudflare/room/reseed.js +500 -0
- package/dist/cloudflare/room/scheduler.d.ts +1 -1
- package/dist/cloudflare/room/scheduler.js +9 -1
- package/dist/cloudflare/room/shared.d.ts +10 -0
- package/dist/cloudflare/room/shared.js +10 -0
- package/dist/cloudflare/room/storage.d.ts +7 -0
- package/dist/cloudflare/room/storage.js +33 -1
- package/dist/components/Edytor.svelte +18 -3
- package/dist/components/Edytor.svelte.d.ts +10 -1
- package/dist/components/EdytorRoot.svelte +17 -0
- package/dist/components/EdytorRoot.svelte.d.ts +4 -0
- package/dist/components/Suggestion.svelte +1 -1
- package/dist/crdt/attribution/block.d.ts +3 -0
- package/dist/crdt/attribution/block.js +3 -2
- package/dist/crdt/doc/history.js +2 -3
- package/dist/crdt/doc/ops.d.ts +81 -0
- package/dist/crdt/doc/ops.js +1898 -0
- package/dist/crdt/doc/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +157 -157
- package/dist/crdt/edytor-doc.js +15 -43
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/flow.d.ts +4 -46
- package/dist/crdt/flow.js +2 -262
- package/dist/crdt/index.d.ts +64 -75
- package/dist/crdt/operations.d.ts +1 -1
- package/dist/crdt/placement/resolve.d.ts +1 -1
- package/dist/crdt/protocol.d.ts +2 -2
- package/dist/crdt/protocol.js +2 -2
- package/dist/crdt/protocols/auth.d.ts +8 -1
- package/dist/crdt/protocols/auth.js +14 -1
- package/dist/crdt/protocols/awareness.js +46 -81
- package/dist/crdt/providers/index.d.ts +11 -22
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/indexeddb.d.ts +6 -10
- package/dist/crdt/providers/indexeddb.js +10 -16
- package/dist/crdt/providers/room.d.ts +26 -0
- package/dist/crdt/providers/room.js +41 -12
- package/dist/crdt/providers/websocket.d.ts +10 -14
- package/dist/crdt/providers/websocket.js +43 -23
- package/dist/crdt/rangeDelete.d.ts +2 -47
- package/dist/crdt/rangeDelete.js +1 -169
- package/dist/crdt/reseed.d.ts +127 -0
- package/dist/crdt/reseed.js +533 -0
- package/dist/crdt/restore.d.ts +13 -1
- package/dist/crdt/restore.js +8 -3
- package/dist/crdt/schema.d.ts +6 -0
- package/dist/crdt/schema.js +6 -0
- package/dist/crdt/structs.d.ts +8 -11
- package/dist/crdt/structs.js +25 -44
- package/dist/crdt/text/index/build.d.ts +119 -0
- package/dist/crdt/text/index/build.js +2801 -0
- package/dist/crdt/text/index/shared.d.ts +2 -6
- package/dist/crdt/text/index/shared.js +2 -5
- package/dist/crdt/text/model.js +2 -3
- package/dist/crdt/text/runs.d.ts +9 -12
- package/dist/crdt/text/runs.js +2 -165
- package/dist/crdt/vendor/yjs/UPSTREAM.md +101 -5
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +10 -10
- package/dist/crdt/vendor/yjs/dts/utils/encoding.d.ts +1 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +24 -13
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +12 -10
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +42 -1
- package/dist/events/domTextOffset.d.ts +2 -0
- package/dist/events/domTextOffset.js +8 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugins/richtext/RichTextPlugin.svelte +63 -61
- package/dist/plugins/richtext/richTextOperations.js +26 -32
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/selection/selection.utils.js +5 -8
- package/dist/session/commands.js +49 -51
- package/dist/session/handles.js +3 -7
- package/dist/surface/attributes.d.ts +3 -3
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/observer.svelte.d.ts +7 -7
- package/dist/surface/observer.svelte.js +225 -336
- package/dist/surface/projector.svelte.js +2 -2
- package/dist/utils.d.ts +5 -0
- package/dist/utils.js +7 -0
- package/package.json +1 -1
- package/dist/crdt/doc/capability.d.ts +0 -56
- package/dist/crdt/doc/capability.js +0 -207
- 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 -198
- 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 -203
- 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 -22
- package/dist/crdt/text/index/layout.js +0 -258
- package/dist/crdt/text/index/placement.d.ts +0 -47
- package/dist/crdt/text/index/placement.js +0 -738
- 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
|
@@ -44,10 +44,11 @@ import { INTENTS } from '../session/attempt.js';
|
|
|
44
44
|
import { runOccurrence } from '../events/onBeforeInput.js';
|
|
45
45
|
import { getDomSelectionSnapshot } from '../selection/domSelection.js';
|
|
46
46
|
import { domOffsetIn } from '../selection/selection.utils.js';
|
|
47
|
-
import { diverges, heal, ownedOf } from './attributes.js';
|
|
48
|
-
import { getTextContentOffsetAtPoint } from '../events/domTextOffset.js';
|
|
47
|
+
import { diverges, heal, ownText, ownedOf } from './attributes.js';
|
|
48
|
+
import { getTextContentOffsetAtPoint, textLeaves } from '../events/domTextOffset.js';
|
|
49
49
|
import { rendersMark } from './cells.js';
|
|
50
|
-
|
|
50
|
+
/** The empty filler (and the trailing newline's marker). */
|
|
51
|
+
const ZWSP = '\u200B';
|
|
51
52
|
const ATOM = '\uFFFC';
|
|
52
53
|
/** Attributes that claim a core element's identity: never on an unregistered element. */
|
|
53
54
|
const IDENTITY_ATTRIBUTES = [
|
|
@@ -66,21 +67,9 @@ const REST = '[data-edytor-composition-rest]';
|
|
|
66
67
|
const BOUND = 100;
|
|
67
68
|
const WINDOW_MS = 250;
|
|
68
69
|
/** The text a cell renders: characters, an atom one unit, fillers stripped. */
|
|
69
|
-
const textOf = (cell) => cell.runs
|
|
70
|
-
.map((run) => (run.kind === 'text' ? run.text : ATOM))
|
|
71
|
-
.join('')
|
|
72
|
-
.replace(ZWSP, '');
|
|
70
|
+
const textOf = (cell) => ownText(cell.runs, ATOM).replaceAll(ZWSP, '');
|
|
73
71
|
/** Svelte's anchors: comments and empty text nodes are never input and never removed. */
|
|
74
72
|
const isAnchor = (node) => node.nodeType === Node.COMMENT_NODE || (node.nodeType === Node.TEXT_NODE && !node.nodeValue);
|
|
75
|
-
/** The `nth` non-empty text node under `node`. */
|
|
76
|
-
const textLeaf = (node, nth = 0) => {
|
|
77
|
-
const walker = (node.ownerDocument ?? document).createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
|
78
|
-
while (walker.nextNode())
|
|
79
|
-
if (walker.currentNode.data && nth-- === 0)
|
|
80
|
-
return walker.currentNode;
|
|
81
|
-
return null;
|
|
82
|
-
};
|
|
83
|
-
const ZERO_WIDTH_SPACE = '\u200B';
|
|
84
73
|
/**
|
|
85
74
|
* What the browser shows for `text`: its element's text nodes in order (WebKit's
|
|
86
75
|
* converted-space wrappers read as plain spaces), the empty filler and the
|
|
@@ -88,40 +77,23 @@ const ZERO_WIDTH_SPACE = '\u200B';
|
|
|
88
77
|
*/
|
|
89
78
|
export const readDomText = (text) => {
|
|
90
79
|
let value = '';
|
|
91
|
-
const node
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const leaf = walker.currentNode;
|
|
96
|
-
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
97
|
-
? leaf.data.replaceAll('\u00A0', ' ')
|
|
98
|
-
: leaf.data;
|
|
99
|
-
}
|
|
100
|
-
}
|
|
80
|
+
for (const leaf of text.node ? textLeaves(text.node) : [])
|
|
81
|
+
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
82
|
+
? leaf.data.replaceAll('\u00A0', ' ')
|
|
83
|
+
: leaf.data;
|
|
101
84
|
// An empty text's filler shares its node with what the browser typed, on either side.
|
|
102
85
|
if (text.isEmpty)
|
|
103
|
-
return value.replaceAll(
|
|
104
|
-
|
|
105
|
-
value = value.slice(0, -ZERO_WIDTH_SPACE.length);
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
86
|
+
return value.replaceAll(ZWSP, '');
|
|
87
|
+
return text.endsWithNewline && value.endsWith(ZWSP) ? value.slice(0, -1) : value;
|
|
108
88
|
};
|
|
109
89
|
export const getCollapsedDomTextSelection = (edytor) => {
|
|
110
90
|
const selection = getDomSelectionSnapshot(edytor.node);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
if (!edytor.node?.contains(selection.anchorNode)) {
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
const text = edytor.selection.getTextOfNode(selection.anchorNode);
|
|
118
|
-
if (!text?.node) {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
91
|
+
const node = selection?.isCollapsed ? selection.anchorNode : null;
|
|
92
|
+
const text = node && edytor.node?.contains(node) ? edytor.selection.getTextOfNode(node) : null;
|
|
121
93
|
// No model rule here: the DOM caret is read BEFORE the reconcile writes
|
|
122
94
|
// the model, so a native insertion legitimately sits past `text.length`
|
|
123
95
|
// (`selectText` clamps the final write).
|
|
124
|
-
return { text, offset: domOffsetIn(text,
|
|
96
|
+
return text?.node ? { text, offset: domOffsetIn(text, node, selection.anchorOffset) } : null;
|
|
125
97
|
};
|
|
126
98
|
/** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
|
|
127
99
|
export const lineBreak = async (edytor, text, value, at) => {
|
|
@@ -137,17 +109,14 @@ export const lineBreak = async (edytor, text, value, at) => {
|
|
|
137
109
|
const settle = (promises) => Promise.all(promises).then(() => undefined);
|
|
138
110
|
/** The collapsed DOM caret's text offset inside `nodes` (a replacement's), if it is there. */
|
|
139
111
|
const caretIn = (nodes) => {
|
|
140
|
-
const selection = nodes[0]
|
|
141
|
-
const anchor = selection?.isCollapsed ? selection.anchorNode : null;
|
|
142
|
-
if (!anchor)
|
|
143
|
-
return undefined;
|
|
112
|
+
const selection = nodes[0].ownerDocument?.getSelection();
|
|
144
113
|
let offset = 0;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
114
|
+
if (selection?.isCollapsed)
|
|
115
|
+
for (const node of nodes) {
|
|
116
|
+
if (node.contains(selection.anchorNode))
|
|
117
|
+
return (offset + getTextContentOffsetAtPoint(node, selection.anchorNode, selection.anchorOffset));
|
|
118
|
+
offset += node.textContent.length;
|
|
119
|
+
}
|
|
151
120
|
};
|
|
152
121
|
export class SurfaceObserver {
|
|
153
122
|
edytor;
|
|
@@ -196,32 +165,23 @@ export class SurfaceObserver {
|
|
|
196
165
|
#rendering = false;
|
|
197
166
|
/** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
|
|
198
167
|
#passes = 0;
|
|
199
|
-
#window
|
|
168
|
+
#window;
|
|
200
169
|
constructor(edytor) {
|
|
201
170
|
this.edytor = edytor;
|
|
202
171
|
}
|
|
203
172
|
/** Observe `root` (the host): records name what to compare. */
|
|
204
173
|
attach = (root) => {
|
|
205
174
|
const mo = (this.#mo = new MutationObserver((records) => {
|
|
206
|
-
|
|
207
|
-
this.#intake(record, this.#own());
|
|
175
|
+
this.#take(records);
|
|
208
176
|
this.signal();
|
|
209
177
|
}));
|
|
210
178
|
for (const child of root.childNodes)
|
|
211
179
|
this.#rooted.add(child);
|
|
212
180
|
this.#rendering = false;
|
|
213
|
-
mo.observe(root, {
|
|
214
|
-
attributes: true,
|
|
215
|
-
attributeOldValue: true,
|
|
216
|
-
characterData: true,
|
|
217
|
-
childList: true,
|
|
218
|
-
subtree: true
|
|
219
|
-
});
|
|
181
|
+
mo.observe(root, { attributes: true, characterData: true, childList: true, subtree: true });
|
|
220
182
|
return () => {
|
|
221
183
|
mo.disconnect();
|
|
222
|
-
|
|
223
|
-
this.#mo = null;
|
|
224
|
-
clearTimeout(this.#window ?? undefined);
|
|
184
|
+
clearTimeout(this.#window);
|
|
225
185
|
};
|
|
226
186
|
};
|
|
227
187
|
/**
|
|
@@ -249,16 +209,14 @@ export class SurfaceObserver {
|
|
|
249
209
|
this.#contents.get(block)?.delete(node);
|
|
250
210
|
};
|
|
251
211
|
};
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
return { destroy: release };
|
|
261
|
-
};
|
|
212
|
+
/**
|
|
213
|
+
* A view-only element the core renders in the host, never compared: the
|
|
214
|
+
* render anchor (`Edytor.svelte`, a registered child of the strict root),
|
|
215
|
+
* or with its suggestion's `id`, a preview group (`components/Suggestion`).
|
|
216
|
+
*/
|
|
217
|
+
own = (node, id) => ({
|
|
218
|
+
destroy: this.register(node, id ? 'preview' : 'anchor', id ?? '')
|
|
219
|
+
});
|
|
262
220
|
/** Extension view state rendered into the host, or a re-display request: an epoch bump. */
|
|
263
221
|
update = () => void this.epoch++;
|
|
264
222
|
/** Cells patched by a commit: the contents the flush re-renders (`before`: what the DOM shows). */
|
|
@@ -315,15 +273,14 @@ export class SurfaceObserver {
|
|
|
315
273
|
void [edytor.selection.value, edytor.suggestions.revision];
|
|
316
274
|
};
|
|
317
275
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
276
|
+
* Take `records` (the callback's; else the queue's): each names elements to
|
|
277
|
+
* compare. The editor writes the host now: the flush (from the pre pass to
|
|
278
|
+
* the end of its compare pass) or a pass's own repairs. A record taken then
|
|
279
|
+
* is the render's (`dom.anchors.foreign`); every other one is the browser's,
|
|
280
|
+
* a page script's or an extension's.
|
|
322
281
|
*/
|
|
323
|
-
#
|
|
324
|
-
|
|
325
|
-
const records = this.#mo?.takeRecords() ?? [];
|
|
326
|
-
const own = this.#own();
|
|
282
|
+
#take = (records = this.#mo?.takeRecords() ?? []) => {
|
|
283
|
+
const own = this.#rendering || this.#passes > 0;
|
|
327
284
|
for (const record of records)
|
|
328
285
|
this.#intake(record, own);
|
|
329
286
|
return records.length > 0;
|
|
@@ -338,10 +295,13 @@ export class SurfaceObserver {
|
|
|
338
295
|
return entry;
|
|
339
296
|
}
|
|
340
297
|
};
|
|
341
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* The block an element belongs to: its closest registered element's
|
|
300
|
+
* (`null`: the root; `undefined`: a preview, the view's own render, or
|
|
301
|
+
* outside the host).
|
|
302
|
+
*/
|
|
342
303
|
#owner = (node) => {
|
|
343
304
|
const entry = this.#near(node);
|
|
344
|
-
// What happens in a preview is the view's own render: nothing to compare.
|
|
345
305
|
return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
|
|
346
306
|
};
|
|
347
307
|
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
@@ -351,14 +311,11 @@ export class SurfaceObserver {
|
|
|
351
311
|
};
|
|
352
312
|
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
353
313
|
blockOf = (node) => {
|
|
354
|
-
const
|
|
355
|
-
return
|
|
356
|
-
? (this.edytor.idToBlock.get(entry.block) ?? null)
|
|
357
|
-
: null;
|
|
314
|
+
const id = node && this.#owner(node);
|
|
315
|
+
return id ? (this.edytor.idToBlock.get(id) ?? null) : null;
|
|
358
316
|
};
|
|
359
|
-
#registered = (node) => node instanceof Element && this.#registry.has(node);
|
|
360
317
|
/** `node` is, or holds, an element the core still renders. */
|
|
361
|
-
#holds = (node) => this.#
|
|
318
|
+
#holds = (node) => this.#registry.has(node) ||
|
|
362
319
|
(node instanceof Element &&
|
|
363
320
|
[...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
|
|
364
321
|
/** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
|
|
@@ -370,32 +327,28 @@ export class SurfaceObserver {
|
|
|
370
327
|
this.#dirty.add(id);
|
|
371
328
|
if (id)
|
|
372
329
|
this.#seen(id);
|
|
373
|
-
|
|
330
|
+
const { type, target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
331
|
+
if (type === 'attributes')
|
|
374
332
|
this.#attributes.add(record);
|
|
375
|
-
if (
|
|
333
|
+
if (type !== 'childList')
|
|
376
334
|
return;
|
|
377
|
-
const
|
|
378
|
-
for (const node of
|
|
335
|
+
const added = [...record.addedNodes];
|
|
336
|
+
for (const node of added) {
|
|
379
337
|
this.#added.add(node);
|
|
380
|
-
if (
|
|
338
|
+
if (parent === this.edytor.node)
|
|
381
339
|
this.#rooted.add(node);
|
|
382
340
|
}
|
|
383
|
-
const
|
|
384
|
-
if (!
|
|
341
|
+
const nodes = [...record.removedNodes];
|
|
342
|
+
if (!nodes.some((node) => isAnchor(node) || this.#holds(node)))
|
|
385
343
|
return;
|
|
386
|
-
|
|
387
|
-
const { target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
388
|
-
this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
|
|
344
|
+
this.#removed.push({ parent, nodes, added, next, prev, foreign: !own });
|
|
389
345
|
// Wrapper replacement: the browser replaced a text element with its own nodes.
|
|
390
|
-
const
|
|
391
|
-
if (
|
|
392
|
-
this.#registry.get(
|
|
393
|
-
|
|
394
|
-
!added.some((node) => this.#holds(node) || node.nodeName === 'BR'))
|
|
395
|
-
|
|
396
|
-
if (value)
|
|
397
|
-
this.#replaced.set(text, { nodes: added, value, caret: caretIn(added) });
|
|
398
|
-
}
|
|
346
|
+
const value = added.map((node) => node.textContent).join('');
|
|
347
|
+
if (nodes.length === 1 &&
|
|
348
|
+
this.#registry.get(nodes[0])?.kind === 'text' &&
|
|
349
|
+
value &&
|
|
350
|
+
!added.some((node) => this.#holds(node) || node.nodeName === 'BR'))
|
|
351
|
+
this.#replaced.set(nodes[0], { nodes: added, value, caret: caretIn(added) });
|
|
399
352
|
};
|
|
400
353
|
/**
|
|
401
354
|
* Records name a content while the model still equals its render (no patch
|
|
@@ -407,15 +360,17 @@ export class SurfaceObserver {
|
|
|
407
360
|
if (!cell || this.#patched.has(id))
|
|
408
361
|
return;
|
|
409
362
|
const text = textOf(cell);
|
|
410
|
-
|
|
411
|
-
|
|
363
|
+
let base = this.#bases.get(id);
|
|
364
|
+
if (base?.text !== text)
|
|
365
|
+
this.#bases.set(id, (base = { text, edit: null }));
|
|
412
366
|
const dom = this.#read(id)?.text;
|
|
413
|
-
|
|
414
|
-
if (dom === undefined || dom === text || known.edit?.dom === dom)
|
|
367
|
+
if (dom === undefined || dom === text || base.edit?.dom === dom)
|
|
415
368
|
return;
|
|
416
369
|
const at = diffText(text, dom).at;
|
|
417
|
-
|
|
418
|
-
|
|
370
|
+
base.edit = {
|
|
371
|
+
dom,
|
|
372
|
+
anchor: this.edytor.facade.anchorAt(id, at, at < text.length ? 'right' : 'left')
|
|
373
|
+
};
|
|
419
374
|
};
|
|
420
375
|
/**
|
|
421
376
|
* A record that removed registered elements is inverted whole (newest
|
|
@@ -438,77 +393,57 @@ export class SurfaceObserver {
|
|
|
438
393
|
const root = this.edytor.node;
|
|
439
394
|
const removals = this.#removed.splice(0).reverse();
|
|
440
395
|
const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
|
|
396
|
+
// A foreign record took only the render's anchors, and they are gone from
|
|
397
|
+
// the page (not moved): they can return beside a recorded sibling still
|
|
398
|
+
// there (never at the parent's end).
|
|
399
|
+
const lost = ({ parent, nodes, next, prev, foreign }) => foreign &&
|
|
400
|
+
nodes.every((node) => isAnchor(node) && !node.isConnected) &&
|
|
401
|
+
(next?.parentNode === parent || prev?.parentNode === parent);
|
|
441
402
|
const parents = new Set(removals.filter(held).map(({ parent }) => parent));
|
|
442
|
-
|
|
403
|
+
// Took anchors from a block list: the root, or a parent of block elements (a block's children).
|
|
404
|
+
const crossed = removals.some((removal) => lost(removal) &&
|
|
405
|
+
(removal.parent === root ||
|
|
406
|
+
[...removal.parent.childNodes].some((child) => this.#registry.get(child)?.kind === 'block')));
|
|
443
407
|
for (const removal of removals) {
|
|
444
408
|
const { parent, nodes, added, next, prev } = removal;
|
|
445
409
|
if (!root?.contains(parent))
|
|
446
410
|
continue;
|
|
447
411
|
const anchors = parents.has(parent) && nodes.every(isAnchor);
|
|
448
|
-
if (!held(removal) && !anchors && !(crossed &&
|
|
412
|
+
if (!held(removal) && !anchors && !(crossed && lost(removal)))
|
|
449
413
|
continue;
|
|
450
414
|
// A browser-owned attempt's host: its change is adopted whatever the
|
|
451
415
|
// browser did to its structure, which the re-render then restores.
|
|
452
416
|
const near = this.#near(parent);
|
|
453
|
-
|
|
454
|
-
if (inside && this.edytor.attempts.on(inside))
|
|
417
|
+
if (near?.kind === 'text' && this.edytor.attempts.on(near.handle))
|
|
455
418
|
continue;
|
|
456
419
|
for (const node of added)
|
|
457
420
|
if (node.parentNode === parent && !this.#holds(node))
|
|
458
421
|
parent.removeChild(node);
|
|
459
422
|
const before = next?.parentNode === parent ? next : prev?.parentNode === parent ? prev.nextSibling : null;
|
|
460
|
-
for (const node of nodes)
|
|
423
|
+
for (const node of nodes) {
|
|
461
424
|
if (node.parentNode !== parent)
|
|
462
425
|
parent.insertBefore(node, before);
|
|
463
|
-
this.#wrote = true;
|
|
464
|
-
for (const node of nodes) {
|
|
465
|
-
const replaced = node instanceof Element && this.#replaced.get(node);
|
|
466
426
|
// The replacement's text is the browser's edit: adopted into the element's text.
|
|
427
|
+
const replaced = this.#replaced.get(node);
|
|
467
428
|
if (replaced)
|
|
468
429
|
this.#replacements.set(node, replaced);
|
|
469
430
|
}
|
|
431
|
+
this.#wrote = true;
|
|
470
432
|
}
|
|
471
433
|
this.#replaced.clear();
|
|
472
434
|
};
|
|
473
|
-
/**
|
|
474
|
-
* A foreign record took only the render's anchors, and they are gone from
|
|
475
|
-
* the page (not moved): they can return beside a recorded sibling still
|
|
476
|
-
* there (never at the parent's end).
|
|
477
|
-
*/
|
|
478
|
-
#lost = ({ parent, nodes, next, prev, foreign }) => foreign &&
|
|
479
|
-
nodes.every((node) => isAnchor(node) && !node.isConnected) &&
|
|
480
|
-
(next?.parentNode === parent || prev?.parentNode === parent);
|
|
481
|
-
/** A block list: the root, or a parent of block elements (a block's children). */
|
|
482
|
-
#list = (parent) => parent === this.edytor.node ||
|
|
483
|
-
[...parent.childNodes].some((child) => child instanceof Element && this.#registry.get(child)?.kind === 'block');
|
|
484
|
-
/**
|
|
485
|
-
* Where an edit made at `at` of the base lands now: through the anchor
|
|
486
|
-
* minted at it (it follows its text through a re-key, split, move or
|
|
487
|
-
* retype), else rebased within the block over the model's own change
|
|
488
|
-
* (three-way prefix/suffix).
|
|
489
|
-
*/
|
|
490
|
-
#place = (id, base, dom, at, now) => {
|
|
491
|
-
const to = base.edit?.dom === dom &&
|
|
492
|
-
base.edit.anchor &&
|
|
493
|
-
this.edytor.facade.resolveAnchor(base.edit.anchor);
|
|
494
|
-
if (to)
|
|
495
|
-
return { block: to.blockId, at: to.offset };
|
|
496
|
-
const model = diffText(base.text, now);
|
|
497
|
-
const before = model && model.at + model.remove <= at;
|
|
498
|
-
return { block: id, at: at + (before ? model.insert.length - model.remove : 0) };
|
|
499
|
-
};
|
|
500
435
|
/** Only a content this flush re-renders can lose an edit to its writes: snapshot it. */
|
|
501
436
|
#snapshot = () => {
|
|
502
|
-
|
|
437
|
+
const { edytor } = this;
|
|
438
|
+
if (edytor.readonly)
|
|
503
439
|
return;
|
|
504
440
|
for (const id of this.#rerendered) {
|
|
505
|
-
if (id === this.#host)
|
|
506
|
-
continue;
|
|
507
441
|
const base = this.#bases.get(id);
|
|
508
|
-
const
|
|
509
|
-
const cell =
|
|
510
|
-
if (!
|
|
442
|
+
const read = base && id !== this.#host && this.#read(id);
|
|
443
|
+
const cell = edytor.cells?.get(id);
|
|
444
|
+
if (!read || !cell || read.text === base.text || read.text === textOf(cell))
|
|
511
445
|
continue;
|
|
446
|
+
const dom = read.text;
|
|
512
447
|
// An expectation claims the edit, or it is placed through the anchor minted at it.
|
|
513
448
|
const verdict = this.#divergence(id);
|
|
514
449
|
// An edit the window defers is drift: the flush's render restores the cell.
|
|
@@ -516,9 +451,17 @@ export class SurfaceObserver {
|
|
|
516
451
|
this.#snapshots.set(id, verdict === 'defer' ? 'invert' : verdict);
|
|
517
452
|
continue;
|
|
518
453
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
454
|
+
// Where it lands now: through the anchor minted at it (it follows its text
|
|
455
|
+
// through a re-key, split, move or retype), else rebased within the block
|
|
456
|
+
// over the model's own change (three-way prefix/suffix).
|
|
457
|
+
const change = diffText(base.text, dom);
|
|
458
|
+
const to = base.edit?.dom === dom && base.edit.anchor && edytor.facade.resolveAnchor(base.edit.anchor);
|
|
459
|
+
const model = !to && diffText(base.text, textOf(cell));
|
|
460
|
+
const at = to
|
|
461
|
+
? to.offset
|
|
462
|
+
: change.at +
|
|
463
|
+
(model && model.at + model.remove <= change.at ? model.insert.length - model.remove : 0);
|
|
464
|
+
this.#snapshots.set(id, { ...change, block: to ? to.blockId : id, at });
|
|
522
465
|
}
|
|
523
466
|
};
|
|
524
467
|
/** The live composition host's block. */
|
|
@@ -547,8 +490,9 @@ export class SurfaceObserver {
|
|
|
547
490
|
#compare = (flushed) => {
|
|
548
491
|
const { edytor } = this;
|
|
549
492
|
const cells = edytor.cells;
|
|
493
|
+
const work = [];
|
|
550
494
|
if (!cells || !edytor.node?.isConnected || edytor.destroyed)
|
|
551
|
-
return
|
|
495
|
+
return work;
|
|
552
496
|
const live = this.#live();
|
|
553
497
|
// The hand-back: the IME host returns to the compare pass at the session's end.
|
|
554
498
|
if (this.#host !== live) {
|
|
@@ -558,71 +502,62 @@ export class SurfaceObserver {
|
|
|
558
502
|
}
|
|
559
503
|
this.#heal();
|
|
560
504
|
if (edytor.readonly) {
|
|
505
|
+
// The root is checked at the flip back (its strict check finds what diverged).
|
|
561
506
|
for (const id of this.#dirty)
|
|
562
|
-
if (this.#diverges(id))
|
|
507
|
+
if (id === null || this.#diverges(id))
|
|
563
508
|
this.#readonly.add(id);
|
|
564
509
|
this.#dirty.clear();
|
|
565
|
-
if (this.#wrote)
|
|
566
|
-
this.update();
|
|
567
|
-
this.#wrote = false;
|
|
568
|
-
return [];
|
|
569
510
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.#
|
|
574
|
-
if (!edytor.attempts.busy) {
|
|
575
|
-
for (const id of this.#waiting)
|
|
511
|
+
else {
|
|
512
|
+
// The flip back: what diverged while read-only is compared again; and
|
|
513
|
+
// what waited for a model-owned attempt's drift window, once it closed.
|
|
514
|
+
for (const id of this.#readonly)
|
|
576
515
|
this.#dirty.add(id);
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
this.#dirty.
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
516
|
+
if (!edytor.attempts.busy) {
|
|
517
|
+
for (const id of this.#waiting)
|
|
518
|
+
this.#dirty.add(id);
|
|
519
|
+
this.#waiting.clear();
|
|
520
|
+
}
|
|
521
|
+
this.#restore();
|
|
522
|
+
this.#clones();
|
|
523
|
+
const dirty = [...this.#dirty].filter((id) => flushed || id === null || !this.#patched.has(id));
|
|
524
|
+
for (const id of dirty)
|
|
525
|
+
this.#dirty.delete(id);
|
|
526
|
+
if (flushed) {
|
|
527
|
+
this.#patched.clear();
|
|
528
|
+
this.#rerendered.clear();
|
|
529
|
+
}
|
|
530
|
+
for (const id of dirty) {
|
|
531
|
+
if (id === null)
|
|
532
|
+
this.#strictRoot();
|
|
533
|
+
// A content this pass's own adoption re-rendered waits for that flush.
|
|
534
|
+
else if (!this.#patched.has(id))
|
|
535
|
+
work.push(...this.#resolve(id, cells.get(id)));
|
|
536
|
+
}
|
|
537
|
+
this.#readonly.clear();
|
|
538
|
+
if (this.#fresh) {
|
|
539
|
+
this.#fresh = false;
|
|
540
|
+
void settle(work).then(edytor.projector.rendered);
|
|
541
|
+
}
|
|
595
542
|
}
|
|
596
|
-
this.#readonly.clear();
|
|
597
543
|
if (this.#wrote) {
|
|
598
544
|
this.#wrote = false;
|
|
599
545
|
this.update();
|
|
600
546
|
}
|
|
601
|
-
if (this.#fresh) {
|
|
602
|
-
this.#fresh = false;
|
|
603
|
-
void settle(work).then(edytor.projector.rendered);
|
|
604
|
-
}
|
|
605
547
|
return work;
|
|
606
548
|
};
|
|
607
549
|
/** A content differs from its cell (text, structure or a missing element). */
|
|
608
|
-
#diverges = (id) => {
|
|
609
|
-
if (id === null)
|
|
610
|
-
return this.#rootDiverges();
|
|
550
|
+
#diverges = (id, dom = id !== this.#host && this.#read(id)) => {
|
|
611
551
|
const cell = this.edytor.cells?.get(id);
|
|
612
|
-
|
|
613
|
-
return Boolean(dom && (dom.broken || dom.text !== textOf(cell) || this.#shape(id)));
|
|
552
|
+
return !!(cell && dom && (dom.broken || dom.text !== textOf(cell) || this.#shape(id)));
|
|
614
553
|
};
|
|
615
554
|
/** One content against its cell, and what that asks for. */
|
|
616
555
|
#resolve = (id, cell) => {
|
|
617
556
|
const snapshot = this.#snapshots.get(id);
|
|
618
557
|
this.#snapshots.delete(id);
|
|
619
|
-
if (!cell)
|
|
558
|
+
if (!cell)
|
|
620
559
|
this.#bases.delete(id);
|
|
621
|
-
|
|
622
|
-
}
|
|
623
|
-
if (id === this.#host)
|
|
624
|
-
return [];
|
|
625
|
-
const dom = this.#read(id);
|
|
560
|
+
const dom = cell && id !== this.#host && this.#read(id);
|
|
626
561
|
if (!dom)
|
|
627
562
|
return [];
|
|
628
563
|
// What the DOM shows once this pass acted: the base of the next edit.
|
|
@@ -636,26 +571,21 @@ export class SurfaceObserver {
|
|
|
636
571
|
this.#invert(id);
|
|
637
572
|
return [];
|
|
638
573
|
}
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
if (dom.broken || this.#readonly.has(id)) {
|
|
642
|
-
if (
|
|
643
|
-
this.#invert(id);
|
|
644
|
-
return [];
|
|
645
|
-
}
|
|
646
|
-
if (dom.text === want && !replaced) {
|
|
647
|
-
if (this.#shape(id))
|
|
574
|
+
const replaced = this.#texts(id).some((text) => this.#replacements.has(text.node));
|
|
575
|
+
// Broken, read-only or equal text: the cell is restored when it differs.
|
|
576
|
+
if (dom.broken || this.#readonly.has(id) || (dom.text === textOf(cell) && !replaced)) {
|
|
577
|
+
if (this.#diverges(id, dom))
|
|
648
578
|
this.#invert(id);
|
|
649
579
|
return [];
|
|
650
580
|
}
|
|
651
581
|
const verdict = this.#divergence(id);
|
|
652
582
|
if (verdict === 'defer')
|
|
653
583
|
this.#waiting.add(id);
|
|
654
|
-
if (verdict === 'invert')
|
|
584
|
+
else if (verdict === 'invert')
|
|
655
585
|
this.#invert(id);
|
|
656
|
-
|
|
657
|
-
return
|
|
658
|
-
return
|
|
586
|
+
else
|
|
587
|
+
return this.#adoptContent(id);
|
|
588
|
+
return [];
|
|
659
589
|
};
|
|
660
590
|
/**
|
|
661
591
|
* A text divergence: an expectation on its host claims it (the composition
|
|
@@ -691,9 +621,6 @@ export class SurfaceObserver {
|
|
|
691
621
|
else
|
|
692
622
|
this.update();
|
|
693
623
|
};
|
|
694
|
-
/** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
|
|
695
|
-
#nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
|
|
696
|
-
!(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
|
|
697
624
|
/**
|
|
698
625
|
* Adopt the browser's text of each text element of `id` (one command each,
|
|
699
626
|
* through the dispatcher); a native line break runs as its occurrence. A
|
|
@@ -705,20 +632,16 @@ export class SurfaceObserver {
|
|
|
705
632
|
const caret = getCollapsedDomTextSelection(edytor);
|
|
706
633
|
const work = [];
|
|
707
634
|
for (const text of this.#texts(id)) {
|
|
708
|
-
const replaced =
|
|
709
|
-
|
|
710
|
-
this.#replacements.delete(text.node);
|
|
635
|
+
const replaced = this.#replacements.get(text.node);
|
|
636
|
+
this.#replacements.delete(text.node);
|
|
711
637
|
const dom = replaced ? replaced.value : readDomText(text);
|
|
712
638
|
if (dom === text.stringContent)
|
|
713
639
|
continue;
|
|
714
|
-
broken ||=
|
|
715
|
-
const at = replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined;
|
|
640
|
+
broken ||= !!replaced;
|
|
716
641
|
const line = diffText(text.stringContent, dom);
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
721
|
-
work.push(adopt(edytor, text, dom, at));
|
|
642
|
+
work.push(line && !line.remove && /^[\r\n]+$/.test(line.insert)
|
|
643
|
+
? lineBreak(edytor, text, dom, line.at)
|
|
644
|
+
: adopt(edytor, text, dom, replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined));
|
|
722
645
|
}
|
|
723
646
|
if (broken)
|
|
724
647
|
this.#invert(id);
|
|
@@ -745,17 +668,15 @@ export class SurfaceObserver {
|
|
|
745
668
|
let text = '';
|
|
746
669
|
let broken = !!host && !host.isConnected;
|
|
747
670
|
for (const element of elements) {
|
|
748
|
-
const
|
|
671
|
+
const entry = this.#registry.get(element);
|
|
749
672
|
// A mark is inside its text element: its presence is the shape check's.
|
|
750
|
-
if (kind === 'mark')
|
|
673
|
+
if (entry?.kind === 'mark')
|
|
751
674
|
continue;
|
|
752
675
|
broken ||= !element.isConnected || (!!host && !host.contains(element));
|
|
753
|
-
if (kind === 'atom')
|
|
676
|
+
if (entry?.kind === 'atom')
|
|
754
677
|
text += ATOM;
|
|
755
|
-
else if (kind === 'text')
|
|
756
|
-
|
|
757
|
-
text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
|
|
758
|
-
}
|
|
678
|
+
else if (entry?.kind === 'text')
|
|
679
|
+
text += readDomText(entry.handle).replaceAll(ZWSP, '');
|
|
759
680
|
}
|
|
760
681
|
return { text, broken };
|
|
761
682
|
};
|
|
@@ -763,19 +684,21 @@ export class SurfaceObserver {
|
|
|
763
684
|
* Equal text, compared structurally: each text element (a strict
|
|
764
685
|
* container) holds one node per render delta (the empty filler's one) — a
|
|
765
686
|
* text node, or a registered mark element whose registered marks follow
|
|
766
|
-
* the delta's chain and that renders one text node.
|
|
767
|
-
*
|
|
768
|
-
* in its node.
|
|
687
|
+
* the delta's chain and that renders one text node. Svelte's anchors and
|
|
688
|
+
* the newline marker stand for none. Answers whether it differs; `write`:
|
|
689
|
+
* a content that does not gets each delta's text back in its node.
|
|
769
690
|
*/
|
|
770
691
|
#shape = (id, write) => {
|
|
692
|
+
const { edytor } = this;
|
|
771
693
|
const writes = [];
|
|
772
694
|
for (const text of this.#texts(id)) {
|
|
773
695
|
const element = text.node;
|
|
774
696
|
if (!element)
|
|
775
697
|
continue;
|
|
776
|
-
const deltas =
|
|
777
|
-
const shown = deltas.length ? deltas : [{ text:
|
|
778
|
-
const nodes =
|
|
698
|
+
const deltas = edytor.deltasOf(text);
|
|
699
|
+
const shown = deltas.length ? deltas : [{ text: ZWSP, marks: [] }];
|
|
700
|
+
const nodes = [...element.childNodes].filter((node) => !isAnchor(node) &&
|
|
701
|
+
!(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
|
|
779
702
|
if (nodes.length !== shown.length)
|
|
780
703
|
return true;
|
|
781
704
|
for (const [i, delta] of shown.entries()) {
|
|
@@ -783,17 +706,18 @@ export class SurfaceObserver {
|
|
|
783
706
|
let leaf = node;
|
|
784
707
|
const chain = delta.marks
|
|
785
708
|
.map(([name]) => name)
|
|
786
|
-
.filter((name) => rendersMark(
|
|
709
|
+
.filter((name) => rendersMark(edytor.marks, name));
|
|
787
710
|
if (chain.length) {
|
|
788
711
|
if (!this.#registry.has(node))
|
|
789
712
|
return true;
|
|
790
713
|
const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
|
|
791
714
|
.map((el) => this.#registry.get(el)?.name)
|
|
792
715
|
.filter(Boolean);
|
|
793
|
-
|
|
716
|
+
const leaves = textLeaves(node).filter((leaf) => leaf.data);
|
|
794
717
|
// The snippet's markup is its own, but the delta renders one text node.
|
|
795
|
-
if (marks.join() !== chain.join() ||
|
|
718
|
+
if (marks.join() !== chain.join() || leaves.length !== 1)
|
|
796
719
|
return true;
|
|
720
|
+
leaf = leaves[0];
|
|
797
721
|
}
|
|
798
722
|
else if (node.nodeType !== Node.TEXT_NODE)
|
|
799
723
|
return true;
|
|
@@ -806,34 +730,14 @@ export class SurfaceObserver {
|
|
|
806
730
|
leaf.data = value;
|
|
807
731
|
return false;
|
|
808
732
|
};
|
|
809
|
-
/** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
|
|
810
|
-
#rootDiverges = () => {
|
|
811
|
-
const { node, cells } = this.edytor;
|
|
812
|
-
if (!node || !cells)
|
|
813
|
-
return false;
|
|
814
|
-
const shown = [];
|
|
815
|
-
for (const child of node.childNodes) {
|
|
816
|
-
if (isAnchor(child))
|
|
817
|
-
continue;
|
|
818
|
-
const entry = child instanceof Element ? this.#registry.get(child) : undefined;
|
|
819
|
-
if (entry?.kind === 'block')
|
|
820
|
-
shown.push(entry.block);
|
|
821
|
-
else if (entry?.kind !== 'anchor' && entry?.kind !== 'preview')
|
|
822
|
-
return true;
|
|
823
|
-
}
|
|
824
|
-
return (cells.rootIds.some((id) => this.#blocks.get(id)?.parentNode !== node) ||
|
|
825
|
-
shown.join() !== cells.rootIds.join());
|
|
826
|
-
};
|
|
827
733
|
/**
|
|
828
734
|
* The root is a strict container: a child it does not render is removed.
|
|
829
735
|
* Only a node a record put there can be one (`#rooted`).
|
|
830
736
|
*/
|
|
831
737
|
#strictRoot = () => {
|
|
832
738
|
const node = this.edytor.node;
|
|
833
|
-
if (!node)
|
|
834
|
-
return;
|
|
835
739
|
for (const child of this.#rooted)
|
|
836
|
-
if (child.parentNode === node && !isAnchor(child) && !this.#
|
|
740
|
+
if (child.parentNode === node && !isAnchor(child) && !this.#registry.has(child)) {
|
|
837
741
|
node.removeChild(child);
|
|
838
742
|
this.#wrote = true;
|
|
839
743
|
}
|
|
@@ -849,7 +753,7 @@ export class SurfaceObserver {
|
|
|
849
753
|
#clones = () => {
|
|
850
754
|
const root = this.edytor.node;
|
|
851
755
|
for (const node of this.#added) {
|
|
852
|
-
if (!
|
|
756
|
+
if (!root.contains(node) || isAnchor(node) || this.#holds(node))
|
|
853
757
|
continue;
|
|
854
758
|
if (node instanceof Element && node.matches(REST))
|
|
855
759
|
continue;
|
|
@@ -860,7 +764,7 @@ export class SurfaceObserver {
|
|
|
860
764
|
this.#claims(element));
|
|
861
765
|
if (!clone && !this.#inRun(node))
|
|
862
766
|
continue;
|
|
863
|
-
node.
|
|
767
|
+
node.remove();
|
|
864
768
|
this.#wrote = true;
|
|
865
769
|
}
|
|
866
770
|
this.#added.clear();
|
|
@@ -868,21 +772,20 @@ export class SurfaceObserver {
|
|
|
868
772
|
/** `node` sits between two registered text or atom elements of one content. */
|
|
869
773
|
#inRun = (node) => {
|
|
870
774
|
const side = (step) => {
|
|
871
|
-
for (let at = step
|
|
872
|
-
const entry =
|
|
775
|
+
for (let at = node[step]; at; at = at[step]) {
|
|
776
|
+
const entry = this.#registry.get(at);
|
|
873
777
|
if (entry)
|
|
874
778
|
return entry.kind === 'text' || entry.kind === 'atom' ? entry.block : null;
|
|
875
779
|
}
|
|
876
780
|
return null;
|
|
877
781
|
};
|
|
878
|
-
const before = side(
|
|
879
|
-
return before !== null && before === side(
|
|
782
|
+
const before = side('previousSibling');
|
|
783
|
+
return before !== null && before === side('nextSibling');
|
|
880
784
|
};
|
|
881
785
|
/** A `data-edytor-mark` naming no declared mark claims nothing: its element is foreign input. */
|
|
882
|
-
#claims = (element) =>
|
|
883
|
-
(name
|
|
884
|
-
this.edytor.marks.has(element.getAttribute(name)
|
|
885
|
-
element.hasAttribute('data-edytor'))) || element.hasAttribute('data-edytor');
|
|
786
|
+
#claims = (element) => element.hasAttribute('data-edytor') ||
|
|
787
|
+
IDENTITY_ATTRIBUTES.some((name) => element.hasAttribute(name) &&
|
|
788
|
+
(name !== 'data-edytor-mark' || this.edytor.marks.has(element.getAttribute(name))));
|
|
886
789
|
/**
|
|
887
790
|
* Owned attributes against the table (read-only views included); an
|
|
888
791
|
* identity attribute on an unregistered element is stripped; damage on a
|
|
@@ -897,35 +800,33 @@ export class SurfaceObserver {
|
|
|
897
800
|
this.#attributes.delete(record);
|
|
898
801
|
continue;
|
|
899
802
|
}
|
|
900
|
-
const entry = this.#registry.get(element)
|
|
803
|
+
const entry = this.#registry.get(element);
|
|
901
804
|
const owner = this.#owner(element);
|
|
902
805
|
if (owner && owner === this.#host)
|
|
903
806
|
continue;
|
|
904
807
|
if (this.#heals > BOUND)
|
|
905
808
|
break;
|
|
906
809
|
this.#attributes.delete(record);
|
|
907
|
-
const name = record.attributeName
|
|
810
|
+
const name = record.attributeName;
|
|
908
811
|
const table = ownedOf(edytor, element, entry);
|
|
909
812
|
// A mark element's own attributes are its render: damage there re-renders the content.
|
|
910
813
|
if (entry?.kind === 'mark') {
|
|
911
814
|
if (table && owner && diverges(element, table))
|
|
912
815
|
edytor.cells?.remount(owner);
|
|
913
|
-
continue;
|
|
914
816
|
}
|
|
915
|
-
if (
|
|
916
|
-
if (
|
|
917
|
-
element.hasAttribute(name) &&
|
|
918
|
-
!element.closest(SUGGESTION)) {
|
|
919
|
-
element.removeAttribute(name);
|
|
817
|
+
else if (table) {
|
|
818
|
+
if (diverges(element, table) && heal(element, table)) {
|
|
920
819
|
this.#counted();
|
|
820
|
+
if (element === edytor.node)
|
|
821
|
+
this.#refocus();
|
|
921
822
|
}
|
|
922
|
-
continue;
|
|
923
823
|
}
|
|
924
|
-
if (
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
this.#
|
|
824
|
+
else if (IDENTITY_ATTRIBUTES.includes(name) &&
|
|
825
|
+
element.hasAttribute(name) &&
|
|
826
|
+
!element.closest(SUGGESTION)) {
|
|
827
|
+
element.removeAttribute(name);
|
|
828
|
+
this.#counted();
|
|
829
|
+
}
|
|
929
830
|
}
|
|
930
831
|
};
|
|
931
832
|
/**
|
|
@@ -946,9 +847,8 @@ export class SurfaceObserver {
|
|
|
946
847
|
};
|
|
947
848
|
if (again())
|
|
948
849
|
setTimeout(() => {
|
|
949
|
-
if (edytor.intentSerial
|
|
950
|
-
|
|
951
|
-
if (!edytor.node?.ownerDocument.getSelection()?.rangeCount)
|
|
850
|
+
if (edytor.intentSerial === serial &&
|
|
851
|
+
!edytor.node?.ownerDocument.getSelection()?.rangeCount)
|
|
952
852
|
again();
|
|
953
853
|
});
|
|
954
854
|
};
|
|
@@ -957,7 +857,6 @@ export class SurfaceObserver {
|
|
|
957
857
|
if (++this.#heals === 1)
|
|
958
858
|
this.#window = setTimeout(() => {
|
|
959
859
|
this.#heals = 0;
|
|
960
|
-
this.#window = null;
|
|
961
860
|
if (this.#attributes.size)
|
|
962
861
|
this.signal();
|
|
963
862
|
}, WINDOW_MS);
|
|
@@ -966,23 +865,18 @@ export class SurfaceObserver {
|
|
|
966
865
|
/** The rendered marks (a tag or a snippet) the browser shows at `at` of `text`: the element chain of its node there. */
|
|
967
866
|
const shownMarks = (text, at) => {
|
|
968
867
|
const element = text.node;
|
|
969
|
-
if (!element)
|
|
970
|
-
return null;
|
|
971
|
-
const walker = element.ownerDocument.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
972
868
|
let offset = 0;
|
|
973
|
-
|
|
974
|
-
offset +=
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
return names.sort().join();
|
|
869
|
+
const node = element &&
|
|
870
|
+
textLeaves(element).find((leaf) => (offset += leaf.data.replaceAll(ZWSP, '').length) > at);
|
|
871
|
+
if (!node)
|
|
872
|
+
return null;
|
|
873
|
+
const names = [];
|
|
874
|
+
for (let el = node.parentElement; el && el !== element; el = el.parentElement) {
|
|
875
|
+
const name = el.getAttribute('data-edytor-mark');
|
|
876
|
+
if (name)
|
|
877
|
+
names.push(name);
|
|
984
878
|
}
|
|
985
|
-
return
|
|
879
|
+
return names.sort().join();
|
|
986
880
|
};
|
|
987
881
|
/** The rendered marks the model gives the character at `at` of `text`. */
|
|
988
882
|
const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAtRange(at, at + 1)[0]?.marks))
|
|
@@ -1003,35 +897,35 @@ const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAt
|
|
|
1003
897
|
*/
|
|
1004
898
|
export const adopt = async (edytor, text, dom, domCaret, prefer) => {
|
|
1005
899
|
if (!text.isInDocument)
|
|
1006
|
-
return
|
|
900
|
+
return;
|
|
901
|
+
const { attempts, selection } = edytor;
|
|
1007
902
|
const serial = edytor.intentSerial;
|
|
1008
|
-
const attempt =
|
|
903
|
+
const attempt = attempts.on(text);
|
|
1009
904
|
const after = attempt?.expect?.kind === 'change' ? attempt.expect.after : null;
|
|
1010
|
-
const drifted = after !== null && dom !== after &&
|
|
905
|
+
const drifted = after !== null && dom !== after && attempts.drifting(text);
|
|
1011
906
|
const value = drifted ? after : dom;
|
|
1012
907
|
const caret = drifted ? undefined : domCaret;
|
|
1013
908
|
if (attempt)
|
|
1014
|
-
|
|
1015
|
-
const { startText, yStart } =
|
|
909
|
+
selection.select(attempt.target);
|
|
910
|
+
const { startText, yStart } = selection.textRange();
|
|
1016
911
|
const grow = value.length - text.length;
|
|
1017
912
|
const back = attempt?.isCollapsed && INTENTS[attempt.inputType]?.dir === 'back';
|
|
1018
913
|
const change = diffText(text.stringContent, value, attempt && startText === text ? yStart + (back ? grow : Math.max(0, grow)) : (prefer ?? caret));
|
|
1019
914
|
if (!change)
|
|
1020
|
-
return
|
|
915
|
+
return;
|
|
1021
916
|
const { at, remove, insert } = change;
|
|
1022
917
|
// The command runs at the change (a hook reads the selection).
|
|
1023
918
|
if (startText !== text && (attempt || caret !== undefined))
|
|
1024
|
-
|
|
919
|
+
selection.select(selection.textValue(text, at));
|
|
920
|
+
const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
|
|
1025
921
|
// A deletion of uniformly marked text keeps its marks pending.
|
|
1026
|
-
const removed = insert ? [] :
|
|
922
|
+
const removed = insert ? [] : replaced;
|
|
1027
923
|
const marks = activeMarks(removed[0]?.marks);
|
|
1028
924
|
const same = removed.every((part) => jsonEquals(activeMarks(part.marks), marks));
|
|
1029
925
|
const shown = insert && shownMarks(text, at);
|
|
1030
|
-
|
|
1031
|
-
edytor.dispatcher.run(kind ?? 'foreignChange', () => {
|
|
926
|
+
edytor.dispatcher.run(attempt ? attempt.inputType || (insert ? 'insertText' : 'deleteContent') : 'foreignChange', () => {
|
|
1032
927
|
const [id, from] = [text.parent.id, text.segStart + at];
|
|
1033
928
|
// A replaced range's common marks; else the view's insertion rule.
|
|
1034
|
-
const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
|
|
1035
929
|
const marks = replaced && marksForInsertion(text, at, { replaced });
|
|
1036
930
|
if (remove)
|
|
1037
931
|
edytor.deleteText(id, from, remove);
|
|
@@ -1042,28 +936,23 @@ export const adopt = async (edytor, text, dom, domCaret, prefer) => {
|
|
|
1042
936
|
if (attempt)
|
|
1043
937
|
attempt.phase = adopted ? 'applied' : 'failed';
|
|
1044
938
|
if (!text.isInDocument)
|
|
1045
|
-
return
|
|
939
|
+
return;
|
|
1046
940
|
// The render patches only the runs the command changed: when the one
|
|
1047
941
|
// insertion rule (`marksForInsertion`) gave the text other marks than the
|
|
1048
942
|
// node the browser wrote, that node keeps its copy — re-render.
|
|
1049
|
-
|
|
1050
|
-
if (!adopted || drifted || moved)
|
|
943
|
+
if (!adopted || drifted || (insert && shown !== modelMarks(edytor, text, at)))
|
|
1051
944
|
text.refreshFromModel();
|
|
1052
945
|
// Deferred past the re-render (selecting in the same turn left the
|
|
1053
946
|
// native caret at 0), so it checks the gesture serial: a gesture since the
|
|
1054
947
|
// adoption owns the selection, and its caret and pending marks stand
|
|
1055
948
|
// (review 2026-09-29).
|
|
1056
949
|
await tick();
|
|
1057
|
-
if (edytor.intentSerial !== serial)
|
|
1058
|
-
return
|
|
950
|
+
if (!adopted || edytor.intentSerial !== serial)
|
|
951
|
+
return;
|
|
1059
952
|
const end = caret ?? (attempt ? at + insert.length : undefined);
|
|
1060
|
-
if (
|
|
1061
|
-
|
|
953
|
+
if (end !== undefined)
|
|
954
|
+
selection.selectText(text, Math.min(end, text.length));
|
|
1062
955
|
// A deletion of uniformly marked text keeps its marks pending at the caret.
|
|
1063
|
-
if (
|
|
1064
|
-
|
|
1065
|
-
Object.keys(marks).length > 0 &&
|
|
1066
|
-
edytor.selection.textRange().startText === text)
|
|
1067
|
-
edytor.selection.stage(marks);
|
|
1068
|
-
return true;
|
|
956
|
+
if (same && Object.keys(marks).length > 0 && selection.textRange().startText === text)
|
|
957
|
+
selection.stage(marks);
|
|
1069
958
|
};
|