edytor 6.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 +11 -2
- 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/suggest.d.ts +6 -2
- package/dist/crdt/doc/suggest.js +6 -5
- package/dist/crdt/edytor-doc.d.ts +1 -1
- package/dist/crdt/edytor-doc.js +1 -1
- package/dist/crdt/engine-room.d.ts +4 -0
- package/dist/crdt/engine-room.js +6 -0
- package/dist/crdt/index.d.ts +2 -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/providers/index.d.ts +1 -0
- package/dist/crdt/providers/index.js +22 -3
- package/dist/crdt/providers/room.d.ts +15 -0
- package/dist/crdt/providers/room.js +15 -0
- package/dist/crdt/providers/websocket.d.ts +4 -2
- package/dist/crdt/providers/websocket.js +36 -9
- 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 +3 -9
- package/dist/crdt/text/index/build.js +87 -181
- 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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/selection/selection.svelte.d.ts +1 -1
- package/dist/selection/selection.svelte.js +87 -106
- package/dist/session/commands.js +49 -51
- 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 +213 -309
- package/package.json +1 -1
|
@@ -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';
|
|
47
|
+
import { diverges, heal, ownText, ownedOf } from './attributes.js';
|
|
48
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,15 +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) => textLeaves(node).filter((leaf) => leaf.data)[nth] ?? null;
|
|
77
|
-
const ZERO_WIDTH_SPACE = '\u200B';
|
|
78
73
|
/**
|
|
79
74
|
* What the browser shows for `text`: its element's text nodes in order (WebKit's
|
|
80
75
|
* converted-space wrappers read as plain spaces), the empty filler and the
|
|
@@ -82,35 +77,23 @@ const ZERO_WIDTH_SPACE = '\u200B';
|
|
|
82
77
|
*/
|
|
83
78
|
export const readDomText = (text) => {
|
|
84
79
|
let value = '';
|
|
85
|
-
const node
|
|
86
|
-
for (const leaf of node ? textLeaves(node) : [])
|
|
80
|
+
for (const leaf of text.node ? textLeaves(text.node) : [])
|
|
87
81
|
value += leaf.parentElement?.closest('span.Apple-converted-space')
|
|
88
82
|
? leaf.data.replaceAll('\u00A0', ' ')
|
|
89
83
|
: leaf.data;
|
|
90
84
|
// An empty text's filler shares its node with what the browser typed, on either side.
|
|
91
85
|
if (text.isEmpty)
|
|
92
|
-
return value.replaceAll(
|
|
93
|
-
|
|
94
|
-
value = value.slice(0, -ZERO_WIDTH_SPACE.length);
|
|
95
|
-
}
|
|
96
|
-
return value;
|
|
86
|
+
return value.replaceAll(ZWSP, '');
|
|
87
|
+
return text.endsWithNewline && value.endsWith(ZWSP) ? value.slice(0, -1) : value;
|
|
97
88
|
};
|
|
98
89
|
export const getCollapsedDomTextSelection = (edytor) => {
|
|
99
90
|
const selection = getDomSelectionSnapshot(edytor.node);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
if (!edytor.node?.contains(selection.anchorNode)) {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
const text = edytor.selection.getTextOfNode(selection.anchorNode);
|
|
107
|
-
if (!text?.node) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
91
|
+
const node = selection?.isCollapsed ? selection.anchorNode : null;
|
|
92
|
+
const text = node && edytor.node?.contains(node) ? edytor.selection.getTextOfNode(node) : null;
|
|
110
93
|
// No model rule here: the DOM caret is read BEFORE the reconcile writes
|
|
111
94
|
// the model, so a native insertion legitimately sits past `text.length`
|
|
112
95
|
// (`selectText` clamps the final write).
|
|
113
|
-
return { text, offset: domOffsetIn(text,
|
|
96
|
+
return text?.node ? { text, offset: domOffsetIn(text, node, selection.anchorOffset) } : null;
|
|
114
97
|
};
|
|
115
98
|
/** A native line break (a diff inserting only newlines at `at`) runs as the key's occurrence. */
|
|
116
99
|
export const lineBreak = async (edytor, text, value, at) => {
|
|
@@ -126,17 +109,14 @@ export const lineBreak = async (edytor, text, value, at) => {
|
|
|
126
109
|
const settle = (promises) => Promise.all(promises).then(() => undefined);
|
|
127
110
|
/** The collapsed DOM caret's text offset inside `nodes` (a replacement's), if it is there. */
|
|
128
111
|
const caretIn = (nodes) => {
|
|
129
|
-
const selection = nodes[0]
|
|
130
|
-
const anchor = selection?.isCollapsed ? selection.anchorNode : null;
|
|
131
|
-
if (!anchor)
|
|
132
|
-
return undefined;
|
|
112
|
+
const selection = nodes[0].ownerDocument?.getSelection();
|
|
133
113
|
let offset = 0;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|
|
140
120
|
};
|
|
141
121
|
export class SurfaceObserver {
|
|
142
122
|
edytor;
|
|
@@ -185,32 +165,23 @@ export class SurfaceObserver {
|
|
|
185
165
|
#rendering = false;
|
|
186
166
|
/** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
|
|
187
167
|
#passes = 0;
|
|
188
|
-
#window
|
|
168
|
+
#window;
|
|
189
169
|
constructor(edytor) {
|
|
190
170
|
this.edytor = edytor;
|
|
191
171
|
}
|
|
192
172
|
/** Observe `root` (the host): records name what to compare. */
|
|
193
173
|
attach = (root) => {
|
|
194
174
|
const mo = (this.#mo = new MutationObserver((records) => {
|
|
195
|
-
|
|
196
|
-
this.#intake(record, this.#own());
|
|
175
|
+
this.#take(records);
|
|
197
176
|
this.signal();
|
|
198
177
|
}));
|
|
199
178
|
for (const child of root.childNodes)
|
|
200
179
|
this.#rooted.add(child);
|
|
201
180
|
this.#rendering = false;
|
|
202
|
-
mo.observe(root, {
|
|
203
|
-
attributes: true,
|
|
204
|
-
attributeOldValue: true,
|
|
205
|
-
characterData: true,
|
|
206
|
-
childList: true,
|
|
207
|
-
subtree: true
|
|
208
|
-
});
|
|
181
|
+
mo.observe(root, { attributes: true, characterData: true, childList: true, subtree: true });
|
|
209
182
|
return () => {
|
|
210
183
|
mo.disconnect();
|
|
211
|
-
|
|
212
|
-
this.#mo = null;
|
|
213
|
-
clearTimeout(this.#window ?? undefined);
|
|
184
|
+
clearTimeout(this.#window);
|
|
214
185
|
};
|
|
215
186
|
};
|
|
216
187
|
/**
|
|
@@ -238,16 +209,14 @@ export class SurfaceObserver {
|
|
|
238
209
|
this.#contents.get(block)?.delete(node);
|
|
239
210
|
};
|
|
240
211
|
};
|
|
241
|
-
/**
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
return { destroy: release };
|
|
250
|
-
};
|
|
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
|
+
});
|
|
251
220
|
/** Extension view state rendered into the host, or a re-display request: an epoch bump. */
|
|
252
221
|
update = () => void this.epoch++;
|
|
253
222
|
/** Cells patched by a commit: the contents the flush re-renders (`before`: what the DOM shows). */
|
|
@@ -304,15 +273,14 @@ export class SurfaceObserver {
|
|
|
304
273
|
void [edytor.selection.value, edytor.suggestions.revision];
|
|
305
274
|
};
|
|
306
275
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
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.
|
|
311
281
|
*/
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
const records = this.#mo?.takeRecords() ?? [];
|
|
315
|
-
const own = this.#own();
|
|
282
|
+
#take = (records = this.#mo?.takeRecords() ?? []) => {
|
|
283
|
+
const own = this.#rendering || this.#passes > 0;
|
|
316
284
|
for (const record of records)
|
|
317
285
|
this.#intake(record, own);
|
|
318
286
|
return records.length > 0;
|
|
@@ -327,10 +295,13 @@ export class SurfaceObserver {
|
|
|
327
295
|
return entry;
|
|
328
296
|
}
|
|
329
297
|
};
|
|
330
|
-
/**
|
|
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
|
+
*/
|
|
331
303
|
#owner = (node) => {
|
|
332
304
|
const entry = this.#near(node);
|
|
333
|
-
// What happens in a preview is the view's own render: nothing to compare.
|
|
334
305
|
return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
|
|
335
306
|
};
|
|
336
307
|
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
@@ -340,14 +311,11 @@ export class SurfaceObserver {
|
|
|
340
311
|
};
|
|
341
312
|
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
342
313
|
blockOf = (node) => {
|
|
343
|
-
const
|
|
344
|
-
return
|
|
345
|
-
? (this.edytor.idToBlock.get(entry.block) ?? null)
|
|
346
|
-
: null;
|
|
314
|
+
const id = node && this.#owner(node);
|
|
315
|
+
return id ? (this.edytor.idToBlock.get(id) ?? null) : null;
|
|
347
316
|
};
|
|
348
|
-
#registered = (node) => node instanceof Element && this.#registry.has(node);
|
|
349
317
|
/** `node` is, or holds, an element the core still renders. */
|
|
350
|
-
#holds = (node) => this.#
|
|
318
|
+
#holds = (node) => this.#registry.has(node) ||
|
|
351
319
|
(node instanceof Element &&
|
|
352
320
|
[...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
|
|
353
321
|
/** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
|
|
@@ -359,32 +327,28 @@ export class SurfaceObserver {
|
|
|
359
327
|
this.#dirty.add(id);
|
|
360
328
|
if (id)
|
|
361
329
|
this.#seen(id);
|
|
362
|
-
|
|
330
|
+
const { type, target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
331
|
+
if (type === 'attributes')
|
|
363
332
|
this.#attributes.add(record);
|
|
364
|
-
if (
|
|
333
|
+
if (type !== 'childList')
|
|
365
334
|
return;
|
|
366
|
-
const
|
|
367
|
-
for (const node of
|
|
335
|
+
const added = [...record.addedNodes];
|
|
336
|
+
for (const node of added) {
|
|
368
337
|
this.#added.add(node);
|
|
369
|
-
if (
|
|
338
|
+
if (parent === this.edytor.node)
|
|
370
339
|
this.#rooted.add(node);
|
|
371
340
|
}
|
|
372
|
-
const
|
|
373
|
-
if (!
|
|
341
|
+
const nodes = [...record.removedNodes];
|
|
342
|
+
if (!nodes.some((node) => isAnchor(node) || this.#holds(node)))
|
|
374
343
|
return;
|
|
375
|
-
|
|
376
|
-
const { target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
377
|
-
this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
|
|
344
|
+
this.#removed.push({ parent, nodes, added, next, prev, foreign: !own });
|
|
378
345
|
// Wrapper replacement: the browser replaced a text element with its own nodes.
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
this.#registry.get(
|
|
382
|
-
|
|
383
|
-
!added.some((node) => this.#holds(node) || node.nodeName === 'BR'))
|
|
384
|
-
|
|
385
|
-
if (value)
|
|
386
|
-
this.#replaced.set(text, { nodes: added, value, caret: caretIn(added) });
|
|
387
|
-
}
|
|
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) });
|
|
388
352
|
};
|
|
389
353
|
/**
|
|
390
354
|
* Records name a content while the model still equals its render (no patch
|
|
@@ -396,15 +360,17 @@ export class SurfaceObserver {
|
|
|
396
360
|
if (!cell || this.#patched.has(id))
|
|
397
361
|
return;
|
|
398
362
|
const text = textOf(cell);
|
|
399
|
-
|
|
400
|
-
|
|
363
|
+
let base = this.#bases.get(id);
|
|
364
|
+
if (base?.text !== text)
|
|
365
|
+
this.#bases.set(id, (base = { text, edit: null }));
|
|
401
366
|
const dom = this.#read(id)?.text;
|
|
402
|
-
|
|
403
|
-
if (dom === undefined || dom === text || known.edit?.dom === dom)
|
|
367
|
+
if (dom === undefined || dom === text || base.edit?.dom === dom)
|
|
404
368
|
return;
|
|
405
369
|
const at = diffText(text, dom).at;
|
|
406
|
-
|
|
407
|
-
|
|
370
|
+
base.edit = {
|
|
371
|
+
dom,
|
|
372
|
+
anchor: this.edytor.facade.anchorAt(id, at, at < text.length ? 'right' : 'left')
|
|
373
|
+
};
|
|
408
374
|
};
|
|
409
375
|
/**
|
|
410
376
|
* A record that removed registered elements is inverted whole (newest
|
|
@@ -427,77 +393,57 @@ export class SurfaceObserver {
|
|
|
427
393
|
const root = this.edytor.node;
|
|
428
394
|
const removals = this.#removed.splice(0).reverse();
|
|
429
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);
|
|
430
402
|
const parents = new Set(removals.filter(held).map(({ parent }) => parent));
|
|
431
|
-
|
|
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')));
|
|
432
407
|
for (const removal of removals) {
|
|
433
408
|
const { parent, nodes, added, next, prev } = removal;
|
|
434
409
|
if (!root?.contains(parent))
|
|
435
410
|
continue;
|
|
436
411
|
const anchors = parents.has(parent) && nodes.every(isAnchor);
|
|
437
|
-
if (!held(removal) && !anchors && !(crossed &&
|
|
412
|
+
if (!held(removal) && !anchors && !(crossed && lost(removal)))
|
|
438
413
|
continue;
|
|
439
414
|
// A browser-owned attempt's host: its change is adopted whatever the
|
|
440
415
|
// browser did to its structure, which the re-render then restores.
|
|
441
416
|
const near = this.#near(parent);
|
|
442
|
-
|
|
443
|
-
if (inside && this.edytor.attempts.on(inside))
|
|
417
|
+
if (near?.kind === 'text' && this.edytor.attempts.on(near.handle))
|
|
444
418
|
continue;
|
|
445
419
|
for (const node of added)
|
|
446
420
|
if (node.parentNode === parent && !this.#holds(node))
|
|
447
421
|
parent.removeChild(node);
|
|
448
422
|
const before = next?.parentNode === parent ? next : prev?.parentNode === parent ? prev.nextSibling : null;
|
|
449
|
-
for (const node of nodes)
|
|
423
|
+
for (const node of nodes) {
|
|
450
424
|
if (node.parentNode !== parent)
|
|
451
425
|
parent.insertBefore(node, before);
|
|
452
|
-
this.#wrote = true;
|
|
453
|
-
for (const node of nodes) {
|
|
454
|
-
const replaced = node instanceof Element && this.#replaced.get(node);
|
|
455
426
|
// The replacement's text is the browser's edit: adopted into the element's text.
|
|
427
|
+
const replaced = this.#replaced.get(node);
|
|
456
428
|
if (replaced)
|
|
457
429
|
this.#replacements.set(node, replaced);
|
|
458
430
|
}
|
|
431
|
+
this.#wrote = true;
|
|
459
432
|
}
|
|
460
433
|
this.#replaced.clear();
|
|
461
434
|
};
|
|
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');
|
|
473
|
-
/**
|
|
474
|
-
* Where an edit made at `at` of the base lands now: through the anchor
|
|
475
|
-
* minted at it (it follows its text through a re-key, split, move or
|
|
476
|
-
* retype), else rebased within the block over the model's own change
|
|
477
|
-
* (three-way prefix/suffix).
|
|
478
|
-
*/
|
|
479
|
-
#place = (id, base, dom, at, now) => {
|
|
480
|
-
const to = base.edit?.dom === dom &&
|
|
481
|
-
base.edit.anchor &&
|
|
482
|
-
this.edytor.facade.resolveAnchor(base.edit.anchor);
|
|
483
|
-
if (to)
|
|
484
|
-
return { block: to.blockId, at: to.offset };
|
|
485
|
-
const model = diffText(base.text, now);
|
|
486
|
-
const before = model && model.at + model.remove <= at;
|
|
487
|
-
return { block: id, at: at + (before ? model.insert.length - model.remove : 0) };
|
|
488
|
-
};
|
|
489
435
|
/** Only a content this flush re-renders can lose an edit to its writes: snapshot it. */
|
|
490
436
|
#snapshot = () => {
|
|
491
|
-
|
|
437
|
+
const { edytor } = this;
|
|
438
|
+
if (edytor.readonly)
|
|
492
439
|
return;
|
|
493
440
|
for (const id of this.#rerendered) {
|
|
494
|
-
if (id === this.#host)
|
|
495
|
-
continue;
|
|
496
441
|
const base = this.#bases.get(id);
|
|
497
|
-
const
|
|
498
|
-
const cell =
|
|
499
|
-
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))
|
|
500
445
|
continue;
|
|
446
|
+
const dom = read.text;
|
|
501
447
|
// An expectation claims the edit, or it is placed through the anchor minted at it.
|
|
502
448
|
const verdict = this.#divergence(id);
|
|
503
449
|
// An edit the window defers is drift: the flush's render restores the cell.
|
|
@@ -505,9 +451,17 @@ export class SurfaceObserver {
|
|
|
505
451
|
this.#snapshots.set(id, verdict === 'defer' ? 'invert' : verdict);
|
|
506
452
|
continue;
|
|
507
453
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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 });
|
|
511
465
|
}
|
|
512
466
|
};
|
|
513
467
|
/** The live composition host's block. */
|
|
@@ -536,8 +490,9 @@ export class SurfaceObserver {
|
|
|
536
490
|
#compare = (flushed) => {
|
|
537
491
|
const { edytor } = this;
|
|
538
492
|
const cells = edytor.cells;
|
|
493
|
+
const work = [];
|
|
539
494
|
if (!cells || !edytor.node?.isConnected || edytor.destroyed)
|
|
540
|
-
return
|
|
495
|
+
return work;
|
|
541
496
|
const live = this.#live();
|
|
542
497
|
// The hand-back: the IME host returns to the compare pass at the session's end.
|
|
543
498
|
if (this.#host !== live) {
|
|
@@ -547,71 +502,62 @@ export class SurfaceObserver {
|
|
|
547
502
|
}
|
|
548
503
|
this.#heal();
|
|
549
504
|
if (edytor.readonly) {
|
|
505
|
+
// The root is checked at the flip back (its strict check finds what diverged).
|
|
550
506
|
for (const id of this.#dirty)
|
|
551
|
-
if (this.#diverges(id))
|
|
507
|
+
if (id === null || this.#diverges(id))
|
|
552
508
|
this.#readonly.add(id);
|
|
553
509
|
this.#dirty.clear();
|
|
554
|
-
if (this.#wrote)
|
|
555
|
-
this.update();
|
|
556
|
-
this.#wrote = false;
|
|
557
|
-
return [];
|
|
558
510
|
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
this.#
|
|
563
|
-
if (!edytor.attempts.busy) {
|
|
564
|
-
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)
|
|
565
515
|
this.#dirty.add(id);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
this.#dirty.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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
|
+
}
|
|
584
542
|
}
|
|
585
|
-
this.#readonly.clear();
|
|
586
543
|
if (this.#wrote) {
|
|
587
544
|
this.#wrote = false;
|
|
588
545
|
this.update();
|
|
589
546
|
}
|
|
590
|
-
if (this.#fresh) {
|
|
591
|
-
this.#fresh = false;
|
|
592
|
-
void settle(work).then(edytor.projector.rendered);
|
|
593
|
-
}
|
|
594
547
|
return work;
|
|
595
548
|
};
|
|
596
549
|
/** A content differs from its cell (text, structure or a missing element). */
|
|
597
|
-
#diverges = (id) => {
|
|
598
|
-
if (id === null)
|
|
599
|
-
return this.#rootDiverges();
|
|
550
|
+
#diverges = (id, dom = id !== this.#host && this.#read(id)) => {
|
|
600
551
|
const cell = this.edytor.cells?.get(id);
|
|
601
|
-
|
|
602
|
-
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)));
|
|
603
553
|
};
|
|
604
554
|
/** One content against its cell, and what that asks for. */
|
|
605
555
|
#resolve = (id, cell) => {
|
|
606
556
|
const snapshot = this.#snapshots.get(id);
|
|
607
557
|
this.#snapshots.delete(id);
|
|
608
|
-
if (!cell)
|
|
558
|
+
if (!cell)
|
|
609
559
|
this.#bases.delete(id);
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
if (id === this.#host)
|
|
613
|
-
return [];
|
|
614
|
-
const dom = this.#read(id);
|
|
560
|
+
const dom = cell && id !== this.#host && this.#read(id);
|
|
615
561
|
if (!dom)
|
|
616
562
|
return [];
|
|
617
563
|
// What the DOM shows once this pass acted: the base of the next edit.
|
|
@@ -625,26 +571,21 @@ export class SurfaceObserver {
|
|
|
625
571
|
this.#invert(id);
|
|
626
572
|
return [];
|
|
627
573
|
}
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
if (dom.broken || this.#readonly.has(id)) {
|
|
631
|
-
if (
|
|
632
|
-
this.#invert(id);
|
|
633
|
-
return [];
|
|
634
|
-
}
|
|
635
|
-
if (dom.text === want && !replaced) {
|
|
636
|
-
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))
|
|
637
578
|
this.#invert(id);
|
|
638
579
|
return [];
|
|
639
580
|
}
|
|
640
581
|
const verdict = this.#divergence(id);
|
|
641
582
|
if (verdict === 'defer')
|
|
642
583
|
this.#waiting.add(id);
|
|
643
|
-
if (verdict === 'invert')
|
|
584
|
+
else if (verdict === 'invert')
|
|
644
585
|
this.#invert(id);
|
|
645
|
-
|
|
646
|
-
return
|
|
647
|
-
return
|
|
586
|
+
else
|
|
587
|
+
return this.#adoptContent(id);
|
|
588
|
+
return [];
|
|
648
589
|
};
|
|
649
590
|
/**
|
|
650
591
|
* A text divergence: an expectation on its host claims it (the composition
|
|
@@ -680,9 +621,6 @@ export class SurfaceObserver {
|
|
|
680
621
|
else
|
|
681
622
|
this.update();
|
|
682
623
|
};
|
|
683
|
-
/** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
|
|
684
|
-
#nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
|
|
685
|
-
!(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
|
|
686
624
|
/**
|
|
687
625
|
* Adopt the browser's text of each text element of `id` (one command each,
|
|
688
626
|
* through the dispatcher); a native line break runs as its occurrence. A
|
|
@@ -694,20 +632,16 @@ export class SurfaceObserver {
|
|
|
694
632
|
const caret = getCollapsedDomTextSelection(edytor);
|
|
695
633
|
const work = [];
|
|
696
634
|
for (const text of this.#texts(id)) {
|
|
697
|
-
const replaced =
|
|
698
|
-
|
|
699
|
-
this.#replacements.delete(text.node);
|
|
635
|
+
const replaced = this.#replacements.get(text.node);
|
|
636
|
+
this.#replacements.delete(text.node);
|
|
700
637
|
const dom = replaced ? replaced.value : readDomText(text);
|
|
701
638
|
if (dom === text.stringContent)
|
|
702
639
|
continue;
|
|
703
|
-
broken ||=
|
|
704
|
-
const at = replaced ? replaced.caret : caret?.text === text ? caret.offset : undefined;
|
|
640
|
+
broken ||= !!replaced;
|
|
705
641
|
const line = diffText(text.stringContent, dom);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
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));
|
|
711
645
|
}
|
|
712
646
|
if (broken)
|
|
713
647
|
this.#invert(id);
|
|
@@ -734,17 +668,15 @@ export class SurfaceObserver {
|
|
|
734
668
|
let text = '';
|
|
735
669
|
let broken = !!host && !host.isConnected;
|
|
736
670
|
for (const element of elements) {
|
|
737
|
-
const
|
|
671
|
+
const entry = this.#registry.get(element);
|
|
738
672
|
// A mark is inside its text element: its presence is the shape check's.
|
|
739
|
-
if (kind === 'mark')
|
|
673
|
+
if (entry?.kind === 'mark')
|
|
740
674
|
continue;
|
|
741
675
|
broken ||= !element.isConnected || (!!host && !host.contains(element));
|
|
742
|
-
if (kind === 'atom')
|
|
676
|
+
if (entry?.kind === 'atom')
|
|
743
677
|
text += ATOM;
|
|
744
|
-
else if (kind === 'text')
|
|
745
|
-
|
|
746
|
-
text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
|
|
747
|
-
}
|
|
678
|
+
else if (entry?.kind === 'text')
|
|
679
|
+
text += readDomText(entry.handle).replaceAll(ZWSP, '');
|
|
748
680
|
}
|
|
749
681
|
return { text, broken };
|
|
750
682
|
};
|
|
@@ -752,19 +684,21 @@ export class SurfaceObserver {
|
|
|
752
684
|
* Equal text, compared structurally: each text element (a strict
|
|
753
685
|
* container) holds one node per render delta (the empty filler's one) — a
|
|
754
686
|
* text node, or a registered mark element whose registered marks follow
|
|
755
|
-
* the delta's chain and that renders one text node.
|
|
756
|
-
*
|
|
757
|
-
* 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.
|
|
758
690
|
*/
|
|
759
691
|
#shape = (id, write) => {
|
|
692
|
+
const { edytor } = this;
|
|
760
693
|
const writes = [];
|
|
761
694
|
for (const text of this.#texts(id)) {
|
|
762
695
|
const element = text.node;
|
|
763
696
|
if (!element)
|
|
764
697
|
continue;
|
|
765
|
-
const deltas =
|
|
766
|
-
const shown = deltas.length ? deltas : [{ text:
|
|
767
|
-
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')));
|
|
768
702
|
if (nodes.length !== shown.length)
|
|
769
703
|
return true;
|
|
770
704
|
for (const [i, delta] of shown.entries()) {
|
|
@@ -772,17 +706,18 @@ export class SurfaceObserver {
|
|
|
772
706
|
let leaf = node;
|
|
773
707
|
const chain = delta.marks
|
|
774
708
|
.map(([name]) => name)
|
|
775
|
-
.filter((name) => rendersMark(
|
|
709
|
+
.filter((name) => rendersMark(edytor.marks, name));
|
|
776
710
|
if (chain.length) {
|
|
777
711
|
if (!this.#registry.has(node))
|
|
778
712
|
return true;
|
|
779
713
|
const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
|
|
780
714
|
.map((el) => this.#registry.get(el)?.name)
|
|
781
715
|
.filter(Boolean);
|
|
782
|
-
|
|
716
|
+
const leaves = textLeaves(node).filter((leaf) => leaf.data);
|
|
783
717
|
// The snippet's markup is its own, but the delta renders one text node.
|
|
784
|
-
if (marks.join() !== chain.join() ||
|
|
718
|
+
if (marks.join() !== chain.join() || leaves.length !== 1)
|
|
785
719
|
return true;
|
|
720
|
+
leaf = leaves[0];
|
|
786
721
|
}
|
|
787
722
|
else if (node.nodeType !== Node.TEXT_NODE)
|
|
788
723
|
return true;
|
|
@@ -795,34 +730,14 @@ export class SurfaceObserver {
|
|
|
795
730
|
leaf.data = value;
|
|
796
731
|
return false;
|
|
797
732
|
};
|
|
798
|
-
/** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
|
|
799
|
-
#rootDiverges = () => {
|
|
800
|
-
const { node, cells } = this.edytor;
|
|
801
|
-
if (!node || !cells)
|
|
802
|
-
return false;
|
|
803
|
-
const shown = [];
|
|
804
|
-
for (const child of node.childNodes) {
|
|
805
|
-
if (isAnchor(child))
|
|
806
|
-
continue;
|
|
807
|
-
const entry = child instanceof Element ? this.#registry.get(child) : undefined;
|
|
808
|
-
if (entry?.kind === 'block')
|
|
809
|
-
shown.push(entry.block);
|
|
810
|
-
else if (entry?.kind !== 'anchor' && entry?.kind !== 'preview')
|
|
811
|
-
return true;
|
|
812
|
-
}
|
|
813
|
-
return (cells.rootIds.some((id) => this.#blocks.get(id)?.parentNode !== node) ||
|
|
814
|
-
shown.join() !== cells.rootIds.join());
|
|
815
|
-
};
|
|
816
733
|
/**
|
|
817
734
|
* The root is a strict container: a child it does not render is removed.
|
|
818
735
|
* Only a node a record put there can be one (`#rooted`).
|
|
819
736
|
*/
|
|
820
737
|
#strictRoot = () => {
|
|
821
738
|
const node = this.edytor.node;
|
|
822
|
-
if (!node)
|
|
823
|
-
return;
|
|
824
739
|
for (const child of this.#rooted)
|
|
825
|
-
if (child.parentNode === node && !isAnchor(child) && !this.#
|
|
740
|
+
if (child.parentNode === node && !isAnchor(child) && !this.#registry.has(child)) {
|
|
826
741
|
node.removeChild(child);
|
|
827
742
|
this.#wrote = true;
|
|
828
743
|
}
|
|
@@ -838,7 +753,7 @@ export class SurfaceObserver {
|
|
|
838
753
|
#clones = () => {
|
|
839
754
|
const root = this.edytor.node;
|
|
840
755
|
for (const node of this.#added) {
|
|
841
|
-
if (!
|
|
756
|
+
if (!root.contains(node) || isAnchor(node) || this.#holds(node))
|
|
842
757
|
continue;
|
|
843
758
|
if (node instanceof Element && node.matches(REST))
|
|
844
759
|
continue;
|
|
@@ -849,7 +764,7 @@ export class SurfaceObserver {
|
|
|
849
764
|
this.#claims(element));
|
|
850
765
|
if (!clone && !this.#inRun(node))
|
|
851
766
|
continue;
|
|
852
|
-
node.
|
|
767
|
+
node.remove();
|
|
853
768
|
this.#wrote = true;
|
|
854
769
|
}
|
|
855
770
|
this.#added.clear();
|
|
@@ -857,21 +772,20 @@ export class SurfaceObserver {
|
|
|
857
772
|
/** `node` sits between two registered text or atom elements of one content. */
|
|
858
773
|
#inRun = (node) => {
|
|
859
774
|
const side = (step) => {
|
|
860
|
-
for (let at = step
|
|
861
|
-
const entry =
|
|
775
|
+
for (let at = node[step]; at; at = at[step]) {
|
|
776
|
+
const entry = this.#registry.get(at);
|
|
862
777
|
if (entry)
|
|
863
778
|
return entry.kind === 'text' || entry.kind === 'atom' ? entry.block : null;
|
|
864
779
|
}
|
|
865
780
|
return null;
|
|
866
781
|
};
|
|
867
|
-
const before = side(
|
|
868
|
-
return before !== null && before === side(
|
|
782
|
+
const before = side('previousSibling');
|
|
783
|
+
return before !== null && before === side('nextSibling');
|
|
869
784
|
};
|
|
870
785
|
/** A `data-edytor-mark` naming no declared mark claims nothing: its element is foreign input. */
|
|
871
|
-
#claims = (element) =>
|
|
872
|
-
(name
|
|
873
|
-
this.edytor.marks.has(element.getAttribute(name)
|
|
874
|
-
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))));
|
|
875
789
|
/**
|
|
876
790
|
* Owned attributes against the table (read-only views included); an
|
|
877
791
|
* identity attribute on an unregistered element is stripped; damage on a
|
|
@@ -886,35 +800,33 @@ export class SurfaceObserver {
|
|
|
886
800
|
this.#attributes.delete(record);
|
|
887
801
|
continue;
|
|
888
802
|
}
|
|
889
|
-
const entry = this.#registry.get(element)
|
|
803
|
+
const entry = this.#registry.get(element);
|
|
890
804
|
const owner = this.#owner(element);
|
|
891
805
|
if (owner && owner === this.#host)
|
|
892
806
|
continue;
|
|
893
807
|
if (this.#heals > BOUND)
|
|
894
808
|
break;
|
|
895
809
|
this.#attributes.delete(record);
|
|
896
|
-
const name = record.attributeName
|
|
810
|
+
const name = record.attributeName;
|
|
897
811
|
const table = ownedOf(edytor, element, entry);
|
|
898
812
|
// A mark element's own attributes are its render: damage there re-renders the content.
|
|
899
813
|
if (entry?.kind === 'mark') {
|
|
900
814
|
if (table && owner && diverges(element, table))
|
|
901
815
|
edytor.cells?.remount(owner);
|
|
902
|
-
continue;
|
|
903
816
|
}
|
|
904
|
-
if (
|
|
905
|
-
if (
|
|
906
|
-
element.hasAttribute(name) &&
|
|
907
|
-
!element.closest(SUGGESTION)) {
|
|
908
|
-
element.removeAttribute(name);
|
|
817
|
+
else if (table) {
|
|
818
|
+
if (diverges(element, table) && heal(element, table)) {
|
|
909
819
|
this.#counted();
|
|
820
|
+
if (element === edytor.node)
|
|
821
|
+
this.#refocus();
|
|
910
822
|
}
|
|
911
|
-
continue;
|
|
912
823
|
}
|
|
913
|
-
if (
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
this.#
|
|
824
|
+
else if (IDENTITY_ATTRIBUTES.includes(name) &&
|
|
825
|
+
element.hasAttribute(name) &&
|
|
826
|
+
!element.closest(SUGGESTION)) {
|
|
827
|
+
element.removeAttribute(name);
|
|
828
|
+
this.#counted();
|
|
829
|
+
}
|
|
918
830
|
}
|
|
919
831
|
};
|
|
920
832
|
/**
|
|
@@ -935,9 +847,8 @@ export class SurfaceObserver {
|
|
|
935
847
|
};
|
|
936
848
|
if (again())
|
|
937
849
|
setTimeout(() => {
|
|
938
|
-
if (edytor.intentSerial
|
|
939
|
-
|
|
940
|
-
if (!edytor.node?.ownerDocument.getSelection()?.rangeCount)
|
|
850
|
+
if (edytor.intentSerial === serial &&
|
|
851
|
+
!edytor.node?.ownerDocument.getSelection()?.rangeCount)
|
|
941
852
|
again();
|
|
942
853
|
});
|
|
943
854
|
};
|
|
@@ -946,7 +857,6 @@ export class SurfaceObserver {
|
|
|
946
857
|
if (++this.#heals === 1)
|
|
947
858
|
this.#window = setTimeout(() => {
|
|
948
859
|
this.#heals = 0;
|
|
949
|
-
this.#window = null;
|
|
950
860
|
if (this.#attributes.size)
|
|
951
861
|
this.signal();
|
|
952
862
|
}, WINDOW_MS);
|
|
@@ -955,10 +865,9 @@ export class SurfaceObserver {
|
|
|
955
865
|
/** The rendered marks (a tag or a snippet) the browser shows at `at` of `text`: the element chain of its node there. */
|
|
956
866
|
const shownMarks = (text, at) => {
|
|
957
867
|
const element = text.node;
|
|
958
|
-
if (!element)
|
|
959
|
-
return null;
|
|
960
868
|
let offset = 0;
|
|
961
|
-
const node =
|
|
869
|
+
const node = element &&
|
|
870
|
+
textLeaves(element).find((leaf) => (offset += leaf.data.replaceAll(ZWSP, '').length) > at);
|
|
962
871
|
if (!node)
|
|
963
872
|
return null;
|
|
964
873
|
const names = [];
|
|
@@ -988,35 +897,35 @@ const modelMarks = (edytor, text, at) => Object.keys(activeMarks(text.getMarksAt
|
|
|
988
897
|
*/
|
|
989
898
|
export const adopt = async (edytor, text, dom, domCaret, prefer) => {
|
|
990
899
|
if (!text.isInDocument)
|
|
991
|
-
return
|
|
900
|
+
return;
|
|
901
|
+
const { attempts, selection } = edytor;
|
|
992
902
|
const serial = edytor.intentSerial;
|
|
993
|
-
const attempt =
|
|
903
|
+
const attempt = attempts.on(text);
|
|
994
904
|
const after = attempt?.expect?.kind === 'change' ? attempt.expect.after : null;
|
|
995
|
-
const drifted = after !== null && dom !== after &&
|
|
905
|
+
const drifted = after !== null && dom !== after && attempts.drifting(text);
|
|
996
906
|
const value = drifted ? after : dom;
|
|
997
907
|
const caret = drifted ? undefined : domCaret;
|
|
998
908
|
if (attempt)
|
|
999
|
-
|
|
1000
|
-
const { startText, yStart } =
|
|
909
|
+
selection.select(attempt.target);
|
|
910
|
+
const { startText, yStart } = selection.textRange();
|
|
1001
911
|
const grow = value.length - text.length;
|
|
1002
912
|
const back = attempt?.isCollapsed && INTENTS[attempt.inputType]?.dir === 'back';
|
|
1003
913
|
const change = diffText(text.stringContent, value, attempt && startText === text ? yStart + (back ? grow : Math.max(0, grow)) : (prefer ?? caret));
|
|
1004
914
|
if (!change)
|
|
1005
|
-
return
|
|
915
|
+
return;
|
|
1006
916
|
const { at, remove, insert } = change;
|
|
1007
917
|
// The command runs at the change (a hook reads the selection).
|
|
1008
918
|
if (startText !== text && (attempt || caret !== undefined))
|
|
1009
|
-
|
|
919
|
+
selection.select(selection.textValue(text, at));
|
|
920
|
+
const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
|
|
1010
921
|
// A deletion of uniformly marked text keeps its marks pending.
|
|
1011
|
-
const removed = insert ? [] :
|
|
922
|
+
const removed = insert ? [] : replaced;
|
|
1012
923
|
const marks = activeMarks(removed[0]?.marks);
|
|
1013
924
|
const same = removed.every((part) => jsonEquals(activeMarks(part.marks), marks));
|
|
1014
925
|
const shown = insert && shownMarks(text, at);
|
|
1015
|
-
|
|
1016
|
-
edytor.dispatcher.run(kind ?? 'foreignChange', () => {
|
|
926
|
+
edytor.dispatcher.run(attempt ? attempt.inputType || (insert ? 'insertText' : 'deleteContent') : 'foreignChange', () => {
|
|
1017
927
|
const [id, from] = [text.parent.id, text.segStart + at];
|
|
1018
928
|
// A replaced range's common marks; else the view's insertion rule.
|
|
1019
|
-
const replaced = remove ? text.getMarksAtRange(at, at + remove) : undefined;
|
|
1020
929
|
const marks = replaced && marksForInsertion(text, at, { replaced });
|
|
1021
930
|
if (remove)
|
|
1022
931
|
edytor.deleteText(id, from, remove);
|
|
@@ -1027,28 +936,23 @@ export const adopt = async (edytor, text, dom, domCaret, prefer) => {
|
|
|
1027
936
|
if (attempt)
|
|
1028
937
|
attempt.phase = adopted ? 'applied' : 'failed';
|
|
1029
938
|
if (!text.isInDocument)
|
|
1030
|
-
return
|
|
939
|
+
return;
|
|
1031
940
|
// The render patches only the runs the command changed: when the one
|
|
1032
941
|
// insertion rule (`marksForInsertion`) gave the text other marks than the
|
|
1033
942
|
// node the browser wrote, that node keeps its copy — re-render.
|
|
1034
|
-
|
|
1035
|
-
if (!adopted || drifted || moved)
|
|
943
|
+
if (!adopted || drifted || (insert && shown !== modelMarks(edytor, text, at)))
|
|
1036
944
|
text.refreshFromModel();
|
|
1037
945
|
// Deferred past the re-render (selecting in the same turn left the
|
|
1038
946
|
// native caret at 0), so it checks the gesture serial: a gesture since the
|
|
1039
947
|
// adoption owns the selection, and its caret and pending marks stand
|
|
1040
948
|
// (review 2026-09-29).
|
|
1041
949
|
await tick();
|
|
1042
|
-
if (edytor.intentSerial !== serial)
|
|
1043
|
-
return
|
|
950
|
+
if (!adopted || edytor.intentSerial !== serial)
|
|
951
|
+
return;
|
|
1044
952
|
const end = caret ?? (attempt ? at + insert.length : undefined);
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
953
|
+
if (end !== undefined)
|
|
954
|
+
selection.selectText(text, Math.min(end, text.length));
|
|
1047
955
|
// A deletion of uniformly marked text keeps its marks pending at the caret.
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1050
|
-
Object.keys(marks).length > 0 &&
|
|
1051
|
-
edytor.selection.textRange().startText === text)
|
|
1052
|
-
edytor.selection.stage(marks);
|
|
1053
|
-
return true;
|
|
956
|
+
if (same && Object.keys(marks).length > 0 && selection.textRange().startText === text)
|
|
957
|
+
selection.stage(marks);
|
|
1054
958
|
};
|