edytor 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +10 -3
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +115 -106
- package/dist/cloudflare/room/context.js +10 -3
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.js +2 -1
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +3 -0
- package/dist/crdt/attribution/attribution.js +3 -3
- package/dist/crdt/doc/capability.js +9 -13
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +5 -2
- package/dist/crdt/doc/gate.d.ts +1 -1
- package/dist/crdt/doc/history.d.ts +6 -0
- package/dist/crdt/doc/history.js +77 -56
- package/dist/crdt/doc/layout.js +5 -5
- package/dist/crdt/doc/meta.js +6 -7
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +18 -28
- package/dist/crdt/doc/steps.js +4 -4
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/document.d.ts +10 -1
- package/dist/crdt/document.js +19 -11
- package/dist/crdt/edytor-doc.d.ts +355 -333
- package/dist/crdt/edytor-doc.js +125 -82
- package/dist/crdt/engine-api.d.ts +8 -2
- package/dist/crdt/engine.d.ts +2 -1
- package/dist/crdt/engine.js +3 -1
- package/dist/crdt/flow.d.ts +1 -1
- package/dist/crdt/flow.js +4 -10
- package/dist/crdt/index.d.ts +111 -105
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/operations.d.ts +12 -3
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +1 -1
- package/dist/crdt/placement/display.js +3 -3
- package/dist/crdt/placement/keys.d.ts +5 -0
- package/dist/crdt/placement/keys.js +5 -0
- package/dist/crdt/placement/resolve.js +2 -1
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +6 -4
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/websocket.d.ts +6 -4
- package/dist/crdt/providers/websocket.js +9 -4
- package/dist/crdt/purge.d.ts +7 -1
- package/dist/crdt/purge.js +32 -2
- package/dist/crdt/restore.d.ts +2 -1
- package/dist/crdt/restore.js +9 -0
- package/dist/crdt/schema.d.ts +9 -1
- package/dist/crdt/schema.js +13 -3
- package/dist/crdt/structs.d.ts +2 -0
- package/dist/crdt/structs.js +2 -0
- package/dist/crdt/text/index/claims.js +1 -23
- package/dist/crdt/text/index/layout.d.ts +2 -4
- package/dist/crdt/text/index/layout.js +9 -22
- package/dist/crdt/text/index/placement.js +21 -24
- package/dist/crdt/text/index/shared.js +17 -14
- package/dist/crdt/text/model.d.ts +10 -0
- package/dist/crdt/text/model.js +8 -1
- package/dist/crdt/text/runs.d.ts +34 -30
- package/dist/crdt/vendor/yjs/UPSTREAM.md +83 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +13 -2
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/Item.js +44 -5
- package/dist/crdt/vendor/yjs/src/ynode.js +29 -7
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +100 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/export/forms.d.ts +25 -25
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +28 -3
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +11 -37
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +128 -81
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.js +8 -14
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/package.json +3 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Edytor } from '../edytor.svelte.js';
|
|
2
2
|
import type { Text } from '../text/text.svelte.js';
|
|
3
|
+
import type { InlineBlock } from '../block/inlineBlock.svelte.js';
|
|
3
4
|
import { type Kind, type Owned } from './attributes.js';
|
|
4
5
|
import { type Patched, type Replaced } from './cells.js';
|
|
5
6
|
/**
|
|
@@ -29,7 +30,7 @@ export declare class SurfaceObserver {
|
|
|
29
30
|
* release. A text element takes the table's attributes now (its id, its
|
|
30
31
|
* editing host inside a void).
|
|
31
32
|
*/
|
|
32
|
-
register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned) => () => void;
|
|
33
|
+
register: (node: Element, kind: Kind, block: string, name?: string, owned?: () => Owned, handle?: Text | InlineBlock) => () => void;
|
|
33
34
|
/** The render anchor (`Edytor.svelte`): a registered child of the strict root. */
|
|
34
35
|
anchor: (node: HTMLElement) => {
|
|
35
36
|
destroy: () => void;
|
|
@@ -53,6 +54,10 @@ export declare class SurfaceObserver {
|
|
|
53
54
|
flush: () => Promise<void>;
|
|
54
55
|
pre: () => void;
|
|
55
56
|
post: () => void;
|
|
57
|
+
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
58
|
+
handleOf: <K extends "text" | "atom">(element: Node | null | undefined, kind: K) => (K extends "text" ? Text : InlineBlock) | null;
|
|
59
|
+
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
60
|
+
blockOf: (node: Node | null) => import("../index.js").Block | null;
|
|
56
61
|
}
|
|
57
62
|
/**
|
|
58
63
|
* Adopt what the browser made of `text` — the only adopter: one
|
|
@@ -18,8 +18,11 @@
|
|
|
18
18
|
* - The pre pass (root `$effect.pre`, before the flush's writes) inverts a
|
|
19
19
|
* record that removed registered elements (its added nodes go, its removed
|
|
20
20
|
* nodes return at their recorded siblings, before Svelte reconciles around
|
|
21
|
-
* them) and
|
|
22
|
-
* the
|
|
21
|
+
* them), and one that removed only the render's anchors from a block list
|
|
22
|
+
* when the editor did not write it (`dom.anchors.foreign`: the records taken
|
|
23
|
+
* from the pre pass to the end of a compare pass are the editor's own), and
|
|
24
|
+
* snapshots a content the flush re-renders whose DOM holds an edit the
|
|
25
|
+
* flush would overwrite.
|
|
23
26
|
* - The compare pass (root `$effect`, after every DOM write of the flush)
|
|
24
27
|
* removes identity clones, nodes inside a content's run and the root's
|
|
25
28
|
* foreign children, heals owned attributes (bounded), and compares each
|
|
@@ -189,6 +192,10 @@ export class SurfaceObserver {
|
|
|
189
192
|
#wrote = false;
|
|
190
193
|
/** Records arrived since the last pass (the projector reads what they did: `rendered`). */
|
|
191
194
|
#fresh = false;
|
|
195
|
+
/** The flush writes the host: from the pre pass to the end of its compare pass. */
|
|
196
|
+
#rendering = false;
|
|
197
|
+
/** Passes under way (a pass's own repairs; an adoption's synchronous flush nests one). */
|
|
198
|
+
#passes = 0;
|
|
192
199
|
#window = null;
|
|
193
200
|
constructor(edytor) {
|
|
194
201
|
this.edytor = edytor;
|
|
@@ -196,11 +203,13 @@ export class SurfaceObserver {
|
|
|
196
203
|
/** Observe `root` (the host): records name what to compare. */
|
|
197
204
|
attach = (root) => {
|
|
198
205
|
const mo = (this.#mo = new MutationObserver((records) => {
|
|
199
|
-
records
|
|
206
|
+
for (const record of records)
|
|
207
|
+
this.#intake(record, this.#own());
|
|
200
208
|
this.signal();
|
|
201
209
|
}));
|
|
202
210
|
for (const child of root.childNodes)
|
|
203
211
|
this.#rooted.add(child);
|
|
212
|
+
this.#rendering = false;
|
|
204
213
|
mo.observe(root, {
|
|
205
214
|
attributes: true,
|
|
206
215
|
attributeOldValue: true,
|
|
@@ -220,8 +229,8 @@ export class SurfaceObserver {
|
|
|
220
229
|
* release. A text element takes the table's attributes now (its id, its
|
|
221
230
|
* editing host inside a void).
|
|
222
231
|
*/
|
|
223
|
-
register = (node, kind, block, name, owned) => {
|
|
224
|
-
const entry = { kind, block, name, owned };
|
|
232
|
+
register = (node, kind, block, name, owned, handle) => {
|
|
233
|
+
const entry = { kind, block, name, owned, handle };
|
|
225
234
|
this.#registry.set(node, entry);
|
|
226
235
|
if (kind === 'text')
|
|
227
236
|
heal(node, ownedOf(this.edytor, node, entry));
|
|
@@ -289,50 +298,71 @@ export class SurfaceObserver {
|
|
|
289
298
|
if (!this.edytor.readonly)
|
|
290
299
|
this.#restore();
|
|
291
300
|
this.#snapshot();
|
|
301
|
+
// From here to the end of the compare pass, what the host records is the flush's.
|
|
302
|
+
this.#rendering = true;
|
|
292
303
|
});
|
|
293
304
|
};
|
|
294
305
|
post = () => {
|
|
295
306
|
this.#deps();
|
|
296
|
-
untrack(() =>
|
|
307
|
+
untrack(() => {
|
|
308
|
+
void this.#pass(true);
|
|
309
|
+
this.#rendering = false;
|
|
310
|
+
});
|
|
297
311
|
};
|
|
298
312
|
#deps = () => {
|
|
299
313
|
const { edytor } = this;
|
|
300
314
|
void [this.epoch, this.records, edytor.readonly, edytor.composition.phase];
|
|
301
315
|
void [edytor.selection.value, edytor.suggestions.revision];
|
|
302
316
|
};
|
|
317
|
+
/**
|
|
318
|
+
* The editor writes the host now: the flush (from the pre pass to the end
|
|
319
|
+
* of its compare pass) or a pass's own repairs. A record taken then is the
|
|
320
|
+
* render's (`dom.anchors.foreign`); every other one is the browser's, a
|
|
321
|
+
* page script's or an extension's.
|
|
322
|
+
*/
|
|
323
|
+
#own = () => this.#rendering || this.#passes > 0;
|
|
303
324
|
#take = () => {
|
|
304
325
|
const records = this.#mo?.takeRecords() ?? [];
|
|
305
|
-
|
|
326
|
+
const own = this.#own();
|
|
327
|
+
for (const record of records)
|
|
328
|
+
this.#intake(record, own);
|
|
306
329
|
return records.length > 0;
|
|
307
330
|
};
|
|
308
|
-
/** The
|
|
309
|
-
#
|
|
331
|
+
/** The closest registered element holding `node` in the host: its entry (`null`: the host). */
|
|
332
|
+
#near = (node) => {
|
|
310
333
|
for (let at = node; at; at = at.parentNode) {
|
|
311
334
|
if (at === this.edytor.node)
|
|
312
335
|
return null;
|
|
313
|
-
const entry =
|
|
314
|
-
// What happens in a preview is the view's own render: nothing to compare.
|
|
336
|
+
const entry = this.#registry.get(at);
|
|
315
337
|
if (entry)
|
|
316
|
-
return entry
|
|
338
|
+
return entry;
|
|
317
339
|
}
|
|
318
|
-
return undefined;
|
|
319
340
|
};
|
|
320
|
-
/** The
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
341
|
+
/** The block an element belongs to: its closest registered element's (`null`: the root). */
|
|
342
|
+
#owner = (node) => {
|
|
343
|
+
const entry = this.#near(node);
|
|
344
|
+
// What happens in a preview is the view's own render: nothing to compare.
|
|
345
|
+
return (entry && (entry.kind === 'preview' ? undefined : entry.kind === 'anchor' ? null : entry.block));
|
|
346
|
+
};
|
|
347
|
+
/** The handle a registered element of `kind` renders (`null`: another element). */
|
|
348
|
+
handleOf = (element, kind) => {
|
|
349
|
+
const entry = element && this.#registry.get(element);
|
|
350
|
+
return (entry?.kind === kind ? entry.handle : null);
|
|
351
|
+
};
|
|
352
|
+
/** The block `node` is in (its closest registered element's), none in a preview or outside the host. */
|
|
353
|
+
blockOf = (node) => {
|
|
354
|
+
const entry = this.#near(node);
|
|
355
|
+
return entry && entry.kind !== 'preview' && entry.kind !== 'anchor'
|
|
356
|
+
? (this.edytor.idToBlock.get(entry.block) ?? null)
|
|
357
|
+
: null;
|
|
328
358
|
};
|
|
329
359
|
#registered = (node) => node instanceof Element && this.#registry.has(node);
|
|
330
360
|
/** `node` is, or holds, an element the core still renders. */
|
|
331
361
|
#holds = (node) => this.#registered(node) ||
|
|
332
362
|
(node instanceof Element &&
|
|
333
363
|
[...node.querySelectorAll('*')].some((child) => this.#registry.has(child)));
|
|
334
|
-
/** A record names elements to compare (never a verdict). */
|
|
335
|
-
#intake = (record) => {
|
|
364
|
+
/** A record names elements to compare (never a verdict); `own`: the editor wrote it. */
|
|
365
|
+
#intake = (record, own) => {
|
|
336
366
|
this.#fresh = true;
|
|
337
367
|
const id = this.#owner(record.target);
|
|
338
368
|
if (id === undefined)
|
|
@@ -355,7 +385,7 @@ export class SurfaceObserver {
|
|
|
355
385
|
return;
|
|
356
386
|
const added = [...record.addedNodes];
|
|
357
387
|
const { target: parent, nextSibling: next, previousSibling: prev } = record;
|
|
358
|
-
this.#removed.push({ parent, nodes: removed, added, next, prev });
|
|
388
|
+
this.#removed.push({ parent, nodes: removed, added, next, prev, foreign: !own });
|
|
359
389
|
// Wrapper replacement: the browser replaced a text element with its own nodes.
|
|
360
390
|
const [text] = removed;
|
|
361
391
|
if (removed.length === 1 &&
|
|
@@ -394,21 +424,33 @@ export class SurfaceObserver {
|
|
|
394
424
|
* browser removed them one record each (Firefox): an anchor-only record is
|
|
395
425
|
* inverted in a parent that gets a registered element back, else the
|
|
396
426
|
* element's recorded siblings are gone and it lands outside Svelte's block.
|
|
427
|
+
* A foreign range across blocks (`dom.anchors.foreign`: a script's
|
|
428
|
+
* `Range.deleteContents()`) takes the render's anchors between the blocks
|
|
429
|
+
* from their list (the root, a block's children) and those after its start
|
|
430
|
+
* and before its end inside the blocks it enters, and leaves the elements:
|
|
431
|
+
* Svelte's next removal of either block would start at a detached anchor
|
|
432
|
+
* and leave its element, a child added to one would find no anchor to
|
|
433
|
+
* render before. When a foreign record took anchors from a block list,
|
|
434
|
+
* every foreign anchor-only record of the batch is inverted. The render's
|
|
435
|
+
* own removals (an item leaving the list) are never undone.
|
|
397
436
|
*/
|
|
398
437
|
#restore = () => {
|
|
399
438
|
const root = this.edytor.node;
|
|
400
439
|
const removals = this.#removed.splice(0).reverse();
|
|
401
440
|
const held = ({ parent, nodes }) => nodes.some((node) => node.parentNode !== parent && this.#holds(node));
|
|
402
441
|
const parents = new Set(removals.filter(held).map(({ parent }) => parent));
|
|
442
|
+
const crossed = removals.some((removal) => this.#lost(removal) && this.#list(removal.parent));
|
|
403
443
|
for (const removal of removals) {
|
|
404
444
|
const { parent, nodes, added, next, prev } = removal;
|
|
405
445
|
if (!root?.contains(parent))
|
|
406
446
|
continue;
|
|
407
|
-
|
|
447
|
+
const anchors = parents.has(parent) && nodes.every(isAnchor);
|
|
448
|
+
if (!held(removal) && !anchors && !(crossed && this.#lost(removal)))
|
|
408
449
|
continue;
|
|
409
450
|
// A browser-owned attempt's host: its change is adopted whatever the
|
|
410
451
|
// browser did to its structure, which the re-render then restores.
|
|
411
|
-
const
|
|
452
|
+
const near = this.#near(parent);
|
|
453
|
+
const inside = near?.kind === 'text' && near.handle;
|
|
412
454
|
if (inside && this.edytor.attempts.on(inside))
|
|
413
455
|
continue;
|
|
414
456
|
for (const node of added)
|
|
@@ -428,6 +470,17 @@ export class SurfaceObserver {
|
|
|
428
470
|
}
|
|
429
471
|
this.#replaced.clear();
|
|
430
472
|
};
|
|
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');
|
|
431
484
|
/**
|
|
432
485
|
* Where an edit made at `at` of the base lands now: through the anchor
|
|
433
486
|
* minted at it (it follows its text through a re-key, split, move or
|
|
@@ -479,6 +532,19 @@ export class SurfaceObserver {
|
|
|
479
532
|
* `$effect`); otherwise contents waiting for a flush are left to it.
|
|
480
533
|
*/
|
|
481
534
|
#pass = (flushed) => {
|
|
535
|
+
this.#passes++;
|
|
536
|
+
try {
|
|
537
|
+
return this.#compare(flushed);
|
|
538
|
+
}
|
|
539
|
+
finally {
|
|
540
|
+
// What the flush and this pass wrote: the render's records, compared
|
|
541
|
+
// at the next pass (as the observer's callback would have them).
|
|
542
|
+
if (this.#take())
|
|
543
|
+
queueMicrotask(this.signal);
|
|
544
|
+
this.#passes--;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
#compare = (flushed) => {
|
|
482
548
|
const { edytor } = this;
|
|
483
549
|
const cells = edytor.cells;
|
|
484
550
|
if (!cells || !edytor.node?.isConnected || edytor.destroyed)
|
|
@@ -620,31 +686,11 @@ export class SurfaceObserver {
|
|
|
620
686
|
#invert = (id) => {
|
|
621
687
|
if (id === this.#live())
|
|
622
688
|
return;
|
|
623
|
-
if (this.#shape(id
|
|
689
|
+
if (this.#shape(id, true))
|
|
624
690
|
this.edytor.cells?.remount(id);
|
|
625
691
|
else
|
|
626
692
|
this.update();
|
|
627
693
|
};
|
|
628
|
-
/** Write each render delta's text back into its node; false when a node is not there. */
|
|
629
|
-
#rewrite = (id) => {
|
|
630
|
-
const writes = [];
|
|
631
|
-
for (const text of this.#texts(id)) {
|
|
632
|
-
const deltas = this.edytor.deltasOf(text);
|
|
633
|
-
const nodes = this.#nodes(text.node);
|
|
634
|
-
const shown = deltas.length ? deltas : [{ text: '\u200B' }];
|
|
635
|
-
for (const [i, delta] of shown.entries()) {
|
|
636
|
-
const node = nodes[i];
|
|
637
|
-
const leaf = node?.nodeType === Node.TEXT_NODE ? node : node && textLeaf(node);
|
|
638
|
-
if (!leaf)
|
|
639
|
-
return false;
|
|
640
|
-
if (leaf.data !== delta.text)
|
|
641
|
-
writes.push([leaf, delta.text]);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
for (const [leaf, value] of writes)
|
|
645
|
-
leaf.data = value;
|
|
646
|
-
return true;
|
|
647
|
-
};
|
|
648
694
|
/** A text element's children that stand for render deltas (Svelte's anchors and the newline marker aside). */
|
|
649
695
|
#nodes = (element) => [...element.childNodes].filter((node) => !isAnchor(node) &&
|
|
650
696
|
!(node instanceof Element && node.hasAttribute('data-edytor-trailing-newline')));
|
|
@@ -655,7 +701,7 @@ export class SurfaceObserver {
|
|
|
655
701
|
*/
|
|
656
702
|
#adoptContent = (id) => {
|
|
657
703
|
const { edytor } = this;
|
|
658
|
-
let broken =
|
|
704
|
+
let broken = this.#shape(id);
|
|
659
705
|
const caret = getCollapsedDomTextSelection(edytor);
|
|
660
706
|
const work = [];
|
|
661
707
|
for (const text of this.#texts(id)) {
|
|
@@ -688,7 +734,7 @@ export class SurfaceObserver {
|
|
|
688
734
|
const value = now.slice(0, local) + insert + now.slice(local + remove);
|
|
689
735
|
return adopt(this.edytor, text, value, undefined, local + insert.length);
|
|
690
736
|
};
|
|
691
|
-
#texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.
|
|
737
|
+
#texts = (id) => [...(this.#contents.get(id) ?? [])].flatMap((element) => this.handleOf(element, 'text') ?? []);
|
|
692
738
|
/** A content's DOM: its registered text and atom elements in order; null when not shown. */
|
|
693
739
|
#read = (id) => {
|
|
694
740
|
const elements = [...(this.#contents.get(id) ?? [])];
|
|
@@ -697,20 +743,17 @@ export class SurfaceObserver {
|
|
|
697
743
|
elements.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
|
|
698
744
|
const host = this.#blocks.get(id);
|
|
699
745
|
let text = '';
|
|
700
|
-
let broken = host && !host.isConnected
|
|
746
|
+
let broken = !!host && !host.isConnected;
|
|
701
747
|
for (const element of elements) {
|
|
702
748
|
const kind = this.#registry.get(element)?.kind;
|
|
703
749
|
// A mark is inside its text element: its presence is the shape check's.
|
|
704
750
|
if (kind === 'mark')
|
|
705
751
|
continue;
|
|
706
|
-
|
|
707
|
-
broken ??= 'missing';
|
|
708
|
-
else if (host && !host.contains(element))
|
|
709
|
-
broken ??= 'escaped';
|
|
752
|
+
broken ||= !element.isConnected || (!!host && !host.contains(element));
|
|
710
753
|
if (kind === 'atom')
|
|
711
754
|
text += ATOM;
|
|
712
755
|
else if (kind === 'text') {
|
|
713
|
-
const found = this.
|
|
756
|
+
const found = this.handleOf(element, 'text');
|
|
714
757
|
text += (found ? readDomText(found) : (element.textContent ?? '')).replace(ZWSP, '');
|
|
715
758
|
}
|
|
716
759
|
}
|
|
@@ -718,46 +761,50 @@ export class SurfaceObserver {
|
|
|
718
761
|
};
|
|
719
762
|
/**
|
|
720
763
|
* Equal text, compared structurally: each text element (a strict
|
|
721
|
-
* container) holds one node per render delta
|
|
722
|
-
* registered mark element whose registered marks follow
|
|
764
|
+
* container) holds one node per render delta (the empty filler's one) — a
|
|
765
|
+
* text node, or a registered mark element whose registered marks follow
|
|
766
|
+
* the delta's chain and that renders one text node. Answers whether it
|
|
767
|
+
* differs; `write`: a content that does not gets each delta's text back
|
|
768
|
+
* in its node.
|
|
723
769
|
*/
|
|
724
|
-
#shape = (id) => {
|
|
770
|
+
#shape = (id, write) => {
|
|
771
|
+
const writes = [];
|
|
725
772
|
for (const text of this.#texts(id)) {
|
|
726
773
|
const element = text.node;
|
|
727
774
|
if (!element)
|
|
728
775
|
continue;
|
|
729
776
|
const deltas = this.edytor.deltasOf(text);
|
|
777
|
+
const shown = deltas.length ? deltas : [{ text: '\u200B', marks: [] }];
|
|
730
778
|
const nodes = this.#nodes(element);
|
|
731
|
-
if (
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
if (nodes.length !== deltas.length)
|
|
737
|
-
return 'partition';
|
|
738
|
-
for (const [i, delta] of deltas.entries()) {
|
|
779
|
+
if (nodes.length !== shown.length)
|
|
780
|
+
return true;
|
|
781
|
+
for (const [i, delta] of shown.entries()) {
|
|
739
782
|
const node = nodes[i];
|
|
783
|
+
let leaf = node;
|
|
740
784
|
const chain = delta.marks
|
|
741
785
|
.map(([name]) => name)
|
|
742
786
|
.filter((name) => rendersMark(this.edytor.marks, name));
|
|
743
|
-
if (
|
|
744
|
-
if (node
|
|
745
|
-
return
|
|
746
|
-
|
|
787
|
+
if (chain.length) {
|
|
788
|
+
if (!this.#registry.has(node))
|
|
789
|
+
return true;
|
|
790
|
+
const marks = [node, ...node.querySelectorAll('[data-edytor-mark]')]
|
|
791
|
+
.map((el) => this.#registry.get(el)?.name)
|
|
792
|
+
.filter(Boolean);
|
|
793
|
+
leaf = textLeaf(node);
|
|
794
|
+
// The snippet's markup is its own, but the delta renders one text node.
|
|
795
|
+
if (marks.join() !== chain.join() || !leaf || textLeaf(node, 1))
|
|
796
|
+
return true;
|
|
747
797
|
}
|
|
748
|
-
if (
|
|
749
|
-
return
|
|
750
|
-
|
|
751
|
-
.
|
|
752
|
-
.filter(Boolean);
|
|
753
|
-
if (marks.join() !== chain.join())
|
|
754
|
-
return 'mark-chain';
|
|
755
|
-
// The snippet's markup is its own, but the delta renders one text node.
|
|
756
|
-
if (!textLeaf(node) || textLeaf(node, 1))
|
|
757
|
-
return 'mark-text';
|
|
798
|
+
else if (node.nodeType !== Node.TEXT_NODE)
|
|
799
|
+
return true;
|
|
800
|
+
if (leaf.data !== delta.text)
|
|
801
|
+
writes.push([leaf, delta.text]);
|
|
758
802
|
}
|
|
759
803
|
}
|
|
760
|
-
|
|
804
|
+
if (write)
|
|
805
|
+
for (const [leaf, value] of writes)
|
|
806
|
+
leaf.data = value;
|
|
807
|
+
return false;
|
|
761
808
|
};
|
|
762
809
|
/** The root's children differ from its registered block elements in cell order, the anchor, Svelte's anchors. */
|
|
763
810
|
#rootDiverges = () => {
|
|
@@ -23,15 +23,15 @@ export declare class Pin {
|
|
|
23
23
|
/** The session's preview (the browser draws it itself when `native`). */
|
|
24
24
|
show: (text: string, marks: Record<string, unknown> | undefined, native: boolean) => void;
|
|
25
25
|
/**
|
|
26
|
-
* The first write replaced a range across
|
|
27
|
-
* cell lost the segments and atoms it covered, and the rest of
|
|
28
|
-
* now follows the preview in
|
|
29
|
-
*
|
|
30
|
-
* host element
|
|
31
|
-
*
|
|
32
|
-
*
|
|
26
|
+
* The first write replaced a range across an inline atom inside one block:
|
|
27
|
+
* the host's cell lost the segments and atoms it covered, and the rest of
|
|
28
|
+
* its end text now follows the preview in its own element right after the
|
|
29
|
+
* host (`rest`), so the IME's node is never rewritten (the IME's write
|
|
30
|
+
* moves the host element's render anchors into the next segment's,
|
|
31
|
+
* Chromium). A range across blocks never merges: the session replaces it
|
|
32
|
+
* at its start (`Composition.#rangeless`).
|
|
33
33
|
*/
|
|
34
|
-
merge: (cell: Cell, items: Item[]
|
|
34
|
+
merge: (cell: Cell, items: Item[]) => void;
|
|
35
35
|
/** The session ended: the host renders from its cell again (the catch-up patch). */
|
|
36
36
|
release: () => null;
|
|
37
37
|
/** `node` lies inside the pinned element. */
|
|
@@ -48,7 +48,7 @@ export declare class Pin {
|
|
|
48
48
|
deltas: RenderDelta[];
|
|
49
49
|
empty: boolean;
|
|
50
50
|
};
|
|
51
|
-
/** What follows the pinned segment `key` of `block` in its own element (`merge
|
|
51
|
+
/** What follows the pinned segment `key` of `block` in its own element (`merge`). */
|
|
52
52
|
rest: (block: BlockId, key: string) => RenderDelta[];
|
|
53
53
|
/**
|
|
54
54
|
* What the IME shows in the pinned element: its text between the frozen
|
|
@@ -29,7 +29,6 @@ export class Pin {
|
|
|
29
29
|
head: cut(frozen, from)[0],
|
|
30
30
|
tail: cut(frozen, Math.max(from, to))[1],
|
|
31
31
|
merged: [],
|
|
32
|
-
beside: false,
|
|
33
32
|
element,
|
|
34
33
|
// Until the first preview the render stays as the IME found it: a
|
|
35
34
|
// write now would collapse its DOM range before it replaces it.
|
|
@@ -44,18 +43,18 @@ export class Pin {
|
|
|
44
43
|
this.#held = { ...held, preview: { text, marks: set && Object.fromEntries(set), native } };
|
|
45
44
|
};
|
|
46
45
|
/**
|
|
47
|
-
* The first write replaced a range across
|
|
48
|
-
* cell lost the segments and atoms it covered, and the rest of
|
|
49
|
-
* now follows the preview in
|
|
50
|
-
*
|
|
51
|
-
* host element
|
|
52
|
-
*
|
|
53
|
-
*
|
|
46
|
+
* The first write replaced a range across an inline atom inside one block:
|
|
47
|
+
* the host's cell lost the segments and atoms it covered, and the rest of
|
|
48
|
+
* its end text now follows the preview in its own element right after the
|
|
49
|
+
* host (`rest`), so the IME's node is never rewritten (the IME's write
|
|
50
|
+
* moves the host element's render anchors into the next segment's,
|
|
51
|
+
* Chromium). A range across blocks never merges: the session replaces it
|
|
52
|
+
* at its start (`Composition.#rangeless`).
|
|
54
53
|
*/
|
|
55
|
-
merge = (cell, items
|
|
54
|
+
merge = (cell, items) => {
|
|
56
55
|
const held = this.#held;
|
|
57
56
|
if (held)
|
|
58
|
-
this.#held = { ...held, parts: partsOf(cell.runs), merged: items
|
|
57
|
+
this.#held = { ...held, parts: partsOf(cell.runs), merged: items };
|
|
59
58
|
};
|
|
60
59
|
/** The session ended: the host renders from its cell again (the catch-up patch). */
|
|
61
60
|
release = () => (this.#held = null);
|
|
@@ -68,14 +67,10 @@ export class Pin {
|
|
|
68
67
|
const held = this.#held;
|
|
69
68
|
if (!held || held.block !== block || held.key !== key)
|
|
70
69
|
return null;
|
|
71
|
-
const { preview, head, tail
|
|
72
|
-
const merged = beside ? [] : held.merged;
|
|
70
|
+
const { preview, head, tail } = held;
|
|
73
71
|
if (!preview || preview.native)
|
|
74
|
-
return {
|
|
75
|
-
|
|
76
|
-
empty: held.empty && !textOf(merged)
|
|
77
|
-
};
|
|
78
|
-
const items = [...head, preview, ...tail, ...merged];
|
|
72
|
+
return { deltas: [...held.frozen], empty: held.empty };
|
|
73
|
+
const items = [...head, preview, ...tail];
|
|
79
74
|
return { deltas: renderDeltas(items), empty: !textOf(items) };
|
|
80
75
|
};
|
|
81
76
|
/** The render of `segment` of `cell`: pinned while it hosts the IME, else its kind's (`segmentDeltas`). */
|
|
@@ -83,11 +78,10 @@ export class Pin {
|
|
|
83
78
|
deltas: segmentDeltas(cell, segment, transform),
|
|
84
79
|
empty: segment.text === ''
|
|
85
80
|
};
|
|
86
|
-
/** What follows the pinned segment `key` of `block` in its own element (`merge
|
|
81
|
+
/** What follows the pinned segment `key` of `block` in its own element (`merge`). */
|
|
87
82
|
rest = (block, key) => {
|
|
88
83
|
const held = this.#held;
|
|
89
|
-
|
|
90
|
-
return shown ? renderDeltas(held.merged) : [];
|
|
84
|
+
return held?.block === block && held.key === key ? renderDeltas(held.merged) : [];
|
|
91
85
|
};
|
|
92
86
|
/**
|
|
93
87
|
* What the IME shows in the pinned element: its text between the frozen
|
|
@@ -102,8 +96,7 @@ export class Pin {
|
|
|
102
96
|
const dom = (held.element.textContent ?? '').replace(/\u200B/g, '');
|
|
103
97
|
if (!held.preview && dom === textOf(held.frozen))
|
|
104
98
|
return null;
|
|
105
|
-
const
|
|
106
|
-
const [head, tail] = [textOf(held.head), textOf(inside)];
|
|
99
|
+
const [head, tail] = [textOf(held.head), textOf(held.tail)];
|
|
107
100
|
if (dom.length < head.length + tail.length)
|
|
108
101
|
return null;
|
|
109
102
|
return dom.startsWith(head) && dom.endsWith(tail)
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* from the observer's records (`rendered`, `sel.drift.unreported`).
|
|
40
40
|
*/
|
|
41
41
|
import { untrack } from 'svelte';
|
|
42
|
-
import { clearDomSelection, createDomRange, getActiveElement, getDomSelection, getDomSelectionSnapshot, scrollCaretIntoView } from '../selection/domSelection.js';
|
|
42
|
+
import { clearDomSelection, createDomRange, focusIn, getActiveElement, getDomSelection, getDomSelectionSnapshot, scrollCaretIntoView } from '../selection/domSelection.js';
|
|
43
43
|
import { getMarkEdgeSide, getYIndex, isTextBoundSelectionPoint } from '../selection/selection.utils.js';
|
|
44
44
|
import { project, sameEnds } from '../session/selection.js';
|
|
45
45
|
import { isAndroidChromeBrowser } from '../events/events.utils.js';
|
|
@@ -116,9 +116,7 @@ export class Projector {
|
|
|
116
116
|
pre = () => {
|
|
117
117
|
void this.#deps();
|
|
118
118
|
untrack(() => {
|
|
119
|
-
|
|
120
|
-
const active = node && getActiveElement(node);
|
|
121
|
-
this.#noted = active && node.contains(active) ? active : null;
|
|
119
|
+
this.#noted = focusIn(this.edytor.node);
|
|
122
120
|
});
|
|
123
121
|
};
|
|
124
122
|
/** Root post-flush `$effect`: every DOM write of the flush has landed. */
|
|
@@ -430,8 +428,7 @@ export class Projector {
|
|
|
430
428
|
// whose `selectionchange` can land after that key counted as a
|
|
431
429
|
// gesture) leaves any caret the browser parks later its own
|
|
432
430
|
// (`#parking`, `placed`), whatever key comes before it.
|
|
433
|
-
|
|
434
|
-
if (requested && value.kind !== 'blocks' && active && node.contains(active))
|
|
431
|
+
if (requested && value.kind !== 'blocks' && focusIn(node))
|
|
435
432
|
this.#parking = true;
|
|
436
433
|
const inside = !!dom?.anchorNode && node.contains(dom.anchorNode);
|
|
437
434
|
const clears = value.kind === 'none' ? requested && inside : inside || dom?.rangeCount === 0;
|
|
@@ -459,15 +456,11 @@ export class Projector {
|
|
|
459
456
|
if (!this.#shows(dom, range, isReversed)) {
|
|
460
457
|
if (!this.#ours(requested))
|
|
461
458
|
return true;
|
|
462
|
-
|
|
463
|
-
if (!(active && node.contains(active))) {
|
|
459
|
+
if (!focusIn(node)) {
|
|
464
460
|
// Focus first (focusing a host can move the selection): the editing
|
|
465
461
|
// host, never a focusable element inside the text such as a link.
|
|
466
|
-
edytor.expectInternalFocus();
|
|
467
462
|
const element = start[0] instanceof HTMLElement ? start[0] : start[0].parentElement;
|
|
468
|
-
(element?.closest('[contenteditable="true"]') ?? node)
|
|
469
|
-
preventScroll: true
|
|
470
|
-
});
|
|
463
|
+
edytor.expectInternalFocus(element?.closest('[contenteditable="true"]') ?? node);
|
|
471
464
|
}
|
|
472
465
|
this.#write(getDomSelection(node) ?? dom, points, isCollapsed);
|
|
473
466
|
if (selection.scrollOnDisplay)
|
|
@@ -551,10 +544,11 @@ export class Projector {
|
|
|
551
544
|
#ours = (requested) => {
|
|
552
545
|
const node = this.edytor.node;
|
|
553
546
|
const active = getActiveElement(node);
|
|
547
|
+
const inside = focusIn(node);
|
|
554
548
|
// A form control or a foreign editable in a kind's chrome (a field bound to
|
|
555
549
|
// `block.data`) owns its focus and its selection: never written over.
|
|
556
|
-
if (
|
|
557
|
-
return !isKindControl(node,
|
|
550
|
+
if (inside)
|
|
551
|
+
return !isKindControl(node, inside);
|
|
558
552
|
const document = node.ownerDocument;
|
|
559
553
|
if (active && active !== document.body && active !== document.documentElement)
|
|
560
554
|
return false;
|
package/dist/text/text.svelte.js
CHANGED
|
@@ -27,28 +27,26 @@ export class Text {
|
|
|
27
27
|
get parent() {
|
|
28
28
|
return this.edytor.idToBlock.block(this.blockId);
|
|
29
29
|
}
|
|
30
|
-
/** This segment in its block's parts
|
|
31
|
-
get #
|
|
30
|
+
/** This segment in its block's parts. */
|
|
31
|
+
get #parts() {
|
|
32
32
|
if ($effect.tracking())
|
|
33
33
|
this.edytor.cells?.get(this.blockId);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return { part, index };
|
|
39
|
-
}
|
|
40
|
-
return { index: -1 };
|
|
34
|
+
return this.edytor.idToBlock.parts(this.blockId);
|
|
35
|
+
}
|
|
36
|
+
get #part() {
|
|
37
|
+
return this.#parts.find((part) => part.kind === 'text' && part.segOrd === this.ordinal);
|
|
41
38
|
}
|
|
42
39
|
/** Position among the block's content parts. */
|
|
43
40
|
get index() {
|
|
44
|
-
|
|
41
|
+
const parts = this.#parts;
|
|
42
|
+
return parts.indexOf(this.#part);
|
|
45
43
|
}
|
|
46
44
|
get value() {
|
|
47
|
-
return segmentJSON(this.#
|
|
45
|
+
return segmentJSON(this.#part?.runs ?? []);
|
|
48
46
|
}
|
|
49
47
|
/** The segment's text. */
|
|
50
48
|
get stringContent() {
|
|
51
|
-
return this.#
|
|
49
|
+
return this.#part?.text ?? '';
|
|
52
50
|
}
|
|
53
51
|
get isEmpty() {
|
|
54
52
|
return this.length === 0;
|
|
@@ -57,15 +55,15 @@ export class Text {
|
|
|
57
55
|
return this.stringContent.endsWith('\n');
|
|
58
56
|
}
|
|
59
57
|
get length() {
|
|
60
|
-
return this
|
|
58
|
+
return this.stringContent.length;
|
|
61
59
|
}
|
|
62
60
|
/** Display offset of this segment inside its block's content (atoms count 1). */
|
|
63
61
|
get segStart() {
|
|
64
|
-
return this.#
|
|
62
|
+
return this.#part?.start ?? 0;
|
|
65
63
|
}
|
|
66
64
|
/** The segment is shown by a live block. */
|
|
67
65
|
get isInDocument() {
|
|
68
|
-
return this.parent.isInTree && this.#
|
|
66
|
+
return this.parent.isInTree && this.#part !== undefined;
|
|
69
67
|
}
|
|
70
68
|
/**
|
|
71
69
|
* Re-create this block's text elements from its cell — the repair of a
|
|
@@ -101,9 +99,8 @@ export class Text {
|
|
|
101
99
|
/** @internal */
|
|
102
100
|
attach = (node) => {
|
|
103
101
|
this.node = node;
|
|
104
|
-
this.edytor.nodeToText.set(node, this);
|
|
105
102
|
// Registered, it takes the attribute table's attributes (`surface/attributes.ts`).
|
|
106
|
-
const release = this.edytor.surface.register(node, 'text', this.parent.id);
|
|
103
|
+
const release = this.edytor.surface.register(node, 'text', this.parent.id, undefined, undefined, this);
|
|
107
104
|
const pluginDestroy = cleanups(this.edytor.plugins, (plugin) => plugin.onTextAttached?.({ node, text: this }));
|
|
108
105
|
// A display that waited for a mounted destination runs in the next pass.
|
|
109
106
|
this.edytor.projector?.mounted(this);
|
|
@@ -111,8 +108,6 @@ export class Text {
|
|
|
111
108
|
destroy: () => {
|
|
112
109
|
if (this.node === node)
|
|
113
110
|
this.node = undefined;
|
|
114
|
-
if (this.edytor.nodeToText.get(node) === this)
|
|
115
|
-
this.edytor.nodeToText.delete(node);
|
|
116
111
|
release();
|
|
117
112
|
pluginDestroy.forEach((destroy) => destroy());
|
|
118
113
|
}
|