edytor 3.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/block/block.svelte.js +6 -21
- package/dist/block/inlineBlock.svelte.js +4 -6
- package/dist/clipboard/moveText.js +6 -8
- package/dist/cloudflare/DocumentRoom.d.ts +23 -12
- package/dist/cloudflare/DocumentRoom.js +10 -6
- package/dist/cloudflare/index.d.ts +1 -1
- package/dist/cloudflare/room/access.d.ts +8 -3
- package/dist/cloudflare/room/access.js +44 -20
- package/dist/cloudflare/room/admission.js +28 -5
- package/dist/cloudflare/room/comments.js +28 -1
- package/dist/cloudflare/room/context.d.ts +139 -129
- package/dist/cloudflare/room/context.js +11 -4
- package/dist/cloudflare/room/forged.d.ts +14 -0
- package/dist/cloudflare/room/forged.js +30 -2
- package/dist/cloudflare/room/frames.d.ts +4 -0
- package/dist/cloudflare/room/frames.js +5 -1
- package/dist/cloudflare/room/purge.d.ts +6 -3
- package/dist/cloudflare/room/purge.js +45 -19
- package/dist/cloudflare/room/replicas.d.ts +4 -2
- package/dist/cloudflare/room/replicas.js +8 -3
- package/dist/cloudflare/room/suggest.d.ts +7 -0
- package/dist/cloudflare/room/suggest.js +143 -0
- package/dist/cloudflare/room/updates.d.ts +3 -4
- package/dist/cloudflare/room/updates.js +6 -8
- package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
- package/dist/cloudflare/routeDocumentSocket.js +5 -3
- package/dist/components/Content.svelte +9 -1
- package/dist/crdt/anchors.js +3 -2
- package/dist/crdt/attribution/attribution.d.ts +34 -12
- package/dist/crdt/attribution/attribution.js +59 -21
- package/dist/crdt/attribution/block.d.ts +3 -3
- package/dist/crdt/attribution/block.js +7 -8
- package/dist/crdt/attribution/index.d.ts +1 -1
- package/dist/crdt/attribution/index.js +1 -1
- package/dist/crdt/data.d.ts +62 -31
- package/dist/crdt/data.js +211 -103
- package/dist/crdt/doc/capability.js +9 -13
- package/dist/crdt/doc/delete.d.ts +1 -1
- package/dist/crdt/doc/delete.js +13 -30
- package/dist/crdt/doc/funnel.d.ts +1 -1
- package/dist/crdt/doc/funnel.js +34 -26
- package/dist/crdt/doc/gate.d.ts +3 -3
- package/dist/crdt/doc/history.d.ts +31 -9
- package/dist/crdt/doc/history.js +204 -73
- package/dist/crdt/doc/layout.js +16 -27
- package/dist/crdt/doc/meta.js +17 -23
- package/dist/crdt/doc/moves.d.ts +1 -1
- package/dist/crdt/doc/moves.js +52 -32
- package/dist/crdt/doc/reads.d.ts +2 -2
- package/dist/crdt/doc/reads.js +20 -30
- package/dist/crdt/doc/seed.js +2 -2
- package/dist/crdt/doc/split-merge.js +12 -15
- package/dist/crdt/doc/steps.d.ts +25 -13
- package/dist/crdt/doc/steps.js +55 -125
- package/dist/crdt/doc/suggest.d.ts +167 -0
- package/dist/crdt/doc/suggest.js +579 -0
- package/dist/crdt/doc/types.d.ts +21 -9
- package/dist/crdt/document.d.ts +26 -6
- package/dist/crdt/document.js +40 -15
- package/dist/crdt/edytor-doc.d.ts +410 -390
- package/dist/crdt/edytor-doc.js +143 -95
- package/dist/crdt/engine-api.d.ts +39 -1
- package/dist/crdt/engine.d.ts +2 -2
- package/dist/crdt/engine.js +4 -5
- package/dist/crdt/flow.d.ts +2 -2
- package/dist/crdt/flow.js +29 -37
- package/dist/crdt/index.d.ts +134 -127
- package/dist/crdt/index.js +4 -1
- package/dist/crdt/operations.d.ts +12 -3
- package/dist/crdt/parts.d.ts +43 -0
- package/dist/crdt/parts.js +44 -0
- package/dist/crdt/placement/display.d.ts +6 -31
- package/dist/crdt/placement/display.js +41 -146
- package/dist/crdt/placement/keys.d.ts +63 -0
- package/dist/crdt/placement/keys.js +95 -0
- package/dist/crdt/placement/model.d.ts +119 -118
- package/dist/crdt/placement/model.js +242 -142
- package/dist/crdt/placement/resolve.d.ts +123 -35
- package/dist/crdt/placement/resolve.js +188 -153
- package/dist/crdt/placement/slots.d.ts +134 -0
- package/dist/crdt/placement/slots.js +152 -0
- package/dist/crdt/protocol.d.ts +1 -1
- package/dist/crdt/protocol.js +1 -1
- package/dist/crdt/protocols/auth.d.ts +11 -1
- package/dist/crdt/protocols/auth.js +17 -1
- package/dist/crdt/protocols/comments.d.ts +16 -1
- package/dist/crdt/protocols/comments.js +9 -1
- package/dist/crdt/providers/index.d.ts +6 -4
- package/dist/crdt/providers/index.js +3 -1
- package/dist/crdt/providers/room.d.ts +16 -2
- package/dist/crdt/providers/room.js +64 -17
- package/dist/crdt/providers/websocket.d.ts +6 -4
- package/dist/crdt/providers/websocket.js +14 -7
- package/dist/crdt/purge.d.ts +24 -9
- package/dist/crdt/purge.js +266 -126
- package/dist/crdt/restore.d.ts +4 -3
- package/dist/crdt/restore.js +27 -34
- package/dist/crdt/schema.d.ts +30 -21
- package/dist/crdt/schema.js +39 -23
- package/dist/crdt/storage.d.ts +1 -1
- package/dist/crdt/storage.js +1 -1
- package/dist/crdt/structs.d.ts +17 -15
- package/dist/crdt/structs.js +10 -22
- package/dist/crdt/text/index/cache.js +4 -0
- package/dist/crdt/text/index/claims.js +13 -23
- package/dist/crdt/text/index/fold.d.ts +2 -3
- package/dist/crdt/text/index/fold.js +194 -44
- package/dist/crdt/text/index/layout.d.ts +2 -4
- package/dist/crdt/text/index/layout.js +14 -30
- package/dist/crdt/text/index/placement.d.ts +26 -18
- package/dist/crdt/text/index/placement.js +420 -157
- package/dist/crdt/text/index/records.d.ts +3 -3
- package/dist/crdt/text/index/records.js +24 -31
- package/dist/crdt/text/index/shared.d.ts +11 -3
- package/dist/crdt/text/index/shared.js +29 -17
- package/dist/crdt/text/index/state.d.ts +21 -17
- package/dist/crdt/text/index/state.js +15 -17
- package/dist/crdt/text/index/streams.d.ts +10 -5
- package/dist/crdt/text/index/streams.js +78 -27
- package/dist/crdt/text/items.d.ts +10 -3
- package/dist/crdt/text/items.js +7 -0
- package/dist/crdt/text/model.d.ts +87 -62
- package/dist/crdt/text/model.js +91 -169
- package/dist/crdt/text/ranges.d.ts +1 -1
- package/dist/crdt/text/ranges.js +3 -3
- package/dist/crdt/text/rows.d.ts +13 -7
- package/dist/crdt/text/rows.js +24 -7
- package/dist/crdt/text/runs.d.ts +51 -42
- package/dist/crdt/text/runs.js +24 -7
- package/dist/crdt/text/streams.d.ts +26 -13
- package/dist/crdt/text/streams.js +41 -14
- package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
- package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
- package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
- package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
- package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
- package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
- package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
- package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
- package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
- package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
- package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
- package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
- package/dist/crdt/vendor/yjs/global.d.ts +2 -0
- package/dist/crdt/vendor/yjs/src/index.js +1 -1
- package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
- package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
- package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
- package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
- package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
- package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
- package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
- package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
- package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
- package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
- package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
- package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
- package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
- package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
- package/dist/edytor.svelte.d.ts +1 -1
- package/dist/edytor.svelte.js +103 -75
- package/dist/events/beforeInputCommands.d.ts +2 -2
- package/dist/events/beforeInputCommands.js +2 -2
- package/dist/events/nativeInteractiveControl.d.ts +11 -1
- package/dist/events/nativeInteractiveControl.js +41 -1
- package/dist/events/onBeforeInput.js +3 -3
- package/dist/events/onFocus.js +9 -11
- package/dist/events/onPageHide.d.ts +3 -0
- package/dist/events/onPageHide.js +16 -0
- package/dist/export/forms.d.ts +50 -50
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +6 -1
- package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
- package/dist/plugins/comments/CommentsController.svelte.js +14 -4
- package/dist/plugins/find/search.js +2 -2
- package/dist/plugins/index.d.ts +1 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
- package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
- package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
- package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
- package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
- package/dist/plugins/suggestions/shared.svelte.js +421 -0
- package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
- package/dist/selection/domSelection.d.ts +2 -0
- package/dist/selection/domSelection.js +6 -2
- package/dist/selection/pointer.svelte.js +75 -48
- package/dist/selection/replaceSelection.js +2 -6
- package/dist/selection/selection.svelte.d.ts +3 -3
- package/dist/selection/selection.svelte.js +39 -32
- package/dist/selection/selection.utils.d.ts +14 -0
- package/dist/selection/selection.utils.js +28 -3
- package/dist/session/commands.js +21 -11
- package/dist/session/composition.svelte.d.ts +5 -3
- package/dist/session/composition.svelte.js +21 -17
- package/dist/session/handles.d.ts +4 -17
- package/dist/session/handles.js +11 -37
- package/dist/session/navigation.js +6 -9
- package/dist/session/selection.d.ts +11 -5
- package/dist/session/selection.js +14 -48
- package/dist/session/suggestions.svelte.d.ts +38 -9
- package/dist/session/suggestions.svelte.js +37 -18
- package/dist/session/virtual.d.ts +1 -1
- package/dist/surface/attributes.d.ts +5 -0
- package/dist/surface/attributes.js +2 -2
- package/dist/surface/cells.d.ts +2 -26
- package/dist/surface/cells.js +2 -33
- package/dist/surface/observer.svelte.d.ts +6 -1
- package/dist/surface/observer.svelte.js +130 -83
- package/dist/surface/pin.svelte.d.ts +9 -9
- package/dist/surface/pin.svelte.js +15 -22
- package/dist/surface/projector.svelte.d.ts +15 -3
- package/dist/surface/projector.svelte.js +67 -22
- package/dist/text/text.svelte.js +14 -19
- package/dist/themes/notion.css +29 -0
- package/package.json +4 -2
- package/dist/crdt/placement/rank.d.ts +0 -87
- package/dist/crdt/placement/rank.js +0 -332
- package/dist/crdt/placement/source.d.ts +0 -43
- package/dist/crdt/placement/source.js +0 -66
- package/dist/crdt/text/delete-index.d.ts +0 -129
- package/dist/crdt/text/delete-index.js +0 -365
- package/dist/crdt/text/deletes.d.ts +0 -76
- package/dist/crdt/text/deletes.js +0 -362
- package/dist/crdt/text/index/anchored.d.ts +0 -17
- package/dist/crdt/text/index/anchored.js +0 -197
package/dist/edytor.svelte.js
CHANGED
|
@@ -8,6 +8,7 @@ import { onInput } from './events/onInput.js';
|
|
|
8
8
|
import { onPaste } from './events/onPaste.js';
|
|
9
9
|
import { onTextDragEnd, onTextDragStart, preventUnsupportedDrop } from './events/onDrop.js';
|
|
10
10
|
import { attachFocus, selectionIsInside, takeKeys } from './events/onFocus.js';
|
|
11
|
+
import { attachPageHide } from './events/onPageHide.js';
|
|
11
12
|
import { insertLineBreak, runIntent } from './events/beforeInputCommands.js';
|
|
12
13
|
import { textPointAt } from './selection/selection.utils.js';
|
|
13
14
|
import { Attempts } from './session/attempt.js';
|
|
@@ -37,7 +38,8 @@ import { attachDocument, SemanticConflictError } from './crdt/index.js';
|
|
|
37
38
|
import { whenDocumentReady } from './collaboration/documentSync.js';
|
|
38
39
|
import { mintPresenceKey, PresenceWriter } from './collaboration/awarenessSelection.js';
|
|
39
40
|
import { canMove, move } from './session/moves.js';
|
|
40
|
-
import {
|
|
41
|
+
import { SUGGEST_ROOT } from './crdt/schema.js';
|
|
42
|
+
import { asEngineDoc } from './crdt/structs.js';
|
|
41
43
|
import { on } from 'svelte/events';
|
|
42
44
|
import { Keymap } from './session/keymap.js';
|
|
43
45
|
import { TRANSACTION } from './constants.js';
|
|
@@ -45,7 +47,7 @@ import { Dispatcher } from './session/commands.js';
|
|
|
45
47
|
import { History } from './session/history.js';
|
|
46
48
|
import { DataWrites } from './session/props.js';
|
|
47
49
|
import { kindCatalogue, kindCommand, UNKNOWN_KIND } from './kinds.js';
|
|
48
|
-
import { clearDomSelection, getActiveElement, getDomSelection, getDomSelectionSnapshot } from './selection/domSelection.js';
|
|
50
|
+
import { clearDomSelection, focusIn, getActiveElement, getDomSelection, getDomSelectionSnapshot } from './selection/domSelection.js';
|
|
49
51
|
/** A consumer that is set (a prop or a plugin hook left out is `undefined`). */
|
|
50
52
|
const isListener = (listener) => listener !== undefined;
|
|
51
53
|
/**
|
|
@@ -86,10 +88,6 @@ export class Edytor {
|
|
|
86
88
|
/** The view's handles: one id-only `Block` per live id, texts and atoms by position and id. */
|
|
87
89
|
idToBlock = new Handles(this);
|
|
88
90
|
/** @internal */
|
|
89
|
-
nodeToInlineBlock = new Map();
|
|
90
|
-
/** @internal */
|
|
91
|
-
nodeToText = new Map();
|
|
92
|
-
/** @internal */
|
|
93
91
|
transaction = new TRANSACTION();
|
|
94
92
|
/** @internal The view's keymap: consumer bindings, then the extensions', then the built-in rows. */
|
|
95
93
|
keymap;
|
|
@@ -112,8 +110,8 @@ export class Edytor {
|
|
|
112
110
|
surface: {
|
|
113
111
|
park: (text, offset) => this.projector.park(text, offset),
|
|
114
112
|
flush: () => void this.surface.flush(),
|
|
115
|
-
placed:
|
|
116
|
-
parked:
|
|
113
|
+
placed: this.projector.placed,
|
|
114
|
+
parked: this.projector.parked,
|
|
117
115
|
clear: () => clearDomSelection(this.node),
|
|
118
116
|
pointAt: (node, offset) => textPointAt.call(this.selection, node, offset),
|
|
119
117
|
sameLine: (a, b) => onOneLine(this.selection.textAt(a), this.selection.textAt(b))
|
|
@@ -210,7 +208,7 @@ export class Edytor {
|
|
|
210
208
|
*/
|
|
211
209
|
get facade() {
|
|
212
210
|
return (this.lens ??= virtualLens(this.document.facade, () => this.document.ready, () => this.document.defaultChild(null), (kind) => ({
|
|
213
|
-
|
|
211
|
+
childless: this.definitionOf(kind).void === true,
|
|
214
212
|
rendersContent: this.document.rendersContent(kind)
|
|
215
213
|
})));
|
|
216
214
|
}
|
|
@@ -297,10 +295,6 @@ export class Edytor {
|
|
|
297
295
|
};
|
|
298
296
|
constructor(options) {
|
|
299
297
|
const { readonly, hotkeys, plugins, document, actor, requireHydration, presence, sync, value, onSelectionChange, labels, onChange, onDocChange, allowNesting } = options;
|
|
300
|
-
for (const name of DOCUMENT_WRITES)
|
|
301
|
-
Object.defineProperty(this, name, {
|
|
302
|
-
value: (...args) => this.dispatcher.operate(name, args)
|
|
303
|
-
});
|
|
304
298
|
if (labels)
|
|
305
299
|
this.labels = labels;
|
|
306
300
|
this.allowNesting = allowNesting === true;
|
|
@@ -314,6 +308,11 @@ export class Edytor {
|
|
|
314
308
|
this.document = attachDocument(new Y.Doc(), { actor, requireHydration });
|
|
315
309
|
this.#owned = true;
|
|
316
310
|
}
|
|
311
|
+
// The writes (`DocumentWrites`): every op the document prepares, dispatched.
|
|
312
|
+
for (const name in this.document.facade.prepare)
|
|
313
|
+
Object.defineProperty(this, name, {
|
|
314
|
+
value: (...args) => this.dispatcher.operate(name, args)
|
|
315
|
+
});
|
|
317
316
|
this.readonly = readonly || false;
|
|
318
317
|
this.onChange = onChange;
|
|
319
318
|
this.onDocChange = onDocChange;
|
|
@@ -488,6 +487,18 @@ export class Edytor {
|
|
|
488
487
|
this.cells = createCells(this.facade, this.surface.patched);
|
|
489
488
|
this.root = this.idToBlock.root;
|
|
490
489
|
this.#offCommit = this.facade.onChange(this.#onCommit);
|
|
490
|
+
// Without its plugin, a view reads a document's shared suggestions as plain content.
|
|
491
|
+
if (DEV && !this.suggestions.shared) {
|
|
492
|
+
const warn = () => {
|
|
493
|
+
if (asEngineDoc(this.document.doc).get(SUGGEST_ROOT).attrKeys().next().done)
|
|
494
|
+
return;
|
|
495
|
+
console.warn('[edytor] this document holds shared suggestions: without createSharedSuggestionsPlugin() the view shows their text as plain content and exports it');
|
|
496
|
+
};
|
|
497
|
+
if (this.document.ready)
|
|
498
|
+
warn();
|
|
499
|
+
else
|
|
500
|
+
this.document.onReady(warn);
|
|
501
|
+
}
|
|
491
502
|
// A document that is already empty shows its virtual paragraph with the caret in it.
|
|
492
503
|
this.selection?.restoreDeadSelectionEndpoints();
|
|
493
504
|
};
|
|
@@ -532,11 +543,11 @@ export class Edytor {
|
|
|
532
543
|
if (values.length > 0)
|
|
533
544
|
callEach('[edytor] onChange', values, this.value);
|
|
534
545
|
};
|
|
535
|
-
/** The text each range `replace` suggestion removes when accepted (`Ranges`). */
|
|
546
|
+
/** The text each range `replace` suggestion removes when accepted (`Ranges`); a shared one's are its runs. */
|
|
536
547
|
#replaced = () => {
|
|
537
548
|
void this.suggestions.revision;
|
|
538
|
-
return this.suggestions.list.flatMap(({ at }) => {
|
|
539
|
-
if (!('replace' in at) || Array.isArray(at.replace))
|
|
549
|
+
return this.suggestions.list.flatMap(({ at, shared }) => {
|
|
550
|
+
if (shared || !('replace' in at) || Array.isArray(at.replace))
|
|
540
551
|
return [];
|
|
541
552
|
const { anchor, focus } = at.replace;
|
|
542
553
|
const [start, end] = [anchor, focus].map(this.selection.resolveTextAnchor);
|
|
@@ -579,15 +590,14 @@ export class Edytor {
|
|
|
579
590
|
* affordances long after the gesture ended.
|
|
580
591
|
*/
|
|
581
592
|
#userInputHandlingDepth = 0;
|
|
582
|
-
#withUserInput = (handler,
|
|
593
|
+
#withUserInput = (handler, bumpSerial = true) => {
|
|
583
594
|
return (...args) => {
|
|
584
595
|
// Depth-counted: a synchronous wrapped listener landing inside an
|
|
585
596
|
// awaited handler's hold window must not end the window the outer
|
|
586
597
|
// handler still needs.
|
|
587
598
|
this.#userInputHandlingDepth++;
|
|
588
|
-
if (
|
|
599
|
+
if (bumpSerial)
|
|
589
600
|
this.markUserGesture();
|
|
590
|
-
}
|
|
591
601
|
const clear = () => {
|
|
592
602
|
// A display the window held back gets its pass now.
|
|
593
603
|
if (--this.#userInputHandlingDepth === 0)
|
|
@@ -636,12 +646,13 @@ export class Edytor {
|
|
|
636
646
|
* fires (element already focused).
|
|
637
647
|
*/
|
|
638
648
|
#expectInternalFocusArmed = false;
|
|
639
|
-
/** @internal */
|
|
640
|
-
expectInternalFocus = () => {
|
|
649
|
+
/** @internal Armed, and `element` (the host, an editing host in it) focused without scrolling. */
|
|
650
|
+
expectInternalFocus = (element) => {
|
|
641
651
|
this.#expectInternalFocusArmed = true;
|
|
642
652
|
queueMicrotask(() => {
|
|
643
653
|
this.#expectInternalFocusArmed = false;
|
|
644
654
|
});
|
|
655
|
+
element?.focus({ preventScroll: true });
|
|
645
656
|
};
|
|
646
657
|
/** @internal A `focusin` consumes the armed flag: `true` when the editor focused itself. */
|
|
647
658
|
consumeInternalFocus = () => {
|
|
@@ -669,7 +680,7 @@ export class Edytor {
|
|
|
669
680
|
*/
|
|
670
681
|
lastUserGestureOutsideEditor = false;
|
|
671
682
|
/** @internal Run `body` as part of the user's input (deferred work of an occurrence). */
|
|
672
|
-
userInput = (body) => this.#withUserInput(body,
|
|
683
|
+
userInput = (body) => this.#withUserInput(body, false)();
|
|
673
684
|
/**
|
|
674
685
|
* A commit's caret: select it (the projector displays it once the pin's
|
|
675
686
|
* render lands) and arm the IME post-commit jump rule — a move right after
|
|
@@ -680,13 +691,12 @@ export class Edytor {
|
|
|
680
691
|
if (this.readonly || this.isComposing || !text.isInDocument)
|
|
681
692
|
return;
|
|
682
693
|
const active = getActiveElement(this.node);
|
|
683
|
-
const selection = getDomSelection(this.node);
|
|
684
694
|
const ours = (node) => Boolean(node && this.node?.contains(node));
|
|
685
695
|
// Focus left for a foreign element with no selection of ours, or moved
|
|
686
696
|
// into a nested editable island (its own caret): the user owns it.
|
|
687
697
|
if ((active &&
|
|
688
|
-
!
|
|
689
|
-
!ours(
|
|
698
|
+
!focusIn(this.node) &&
|
|
699
|
+
!ours(getDomSelection(this.node)?.anchorNode) &&
|
|
690
700
|
!ours(this.selection.textRange().startText?.node)) ||
|
|
691
701
|
isKindControl(this.node, active))
|
|
692
702
|
return;
|
|
@@ -769,21 +779,50 @@ export class Edytor {
|
|
|
769
779
|
this.lastUserGestureOutsideEditor = false;
|
|
770
780
|
this.node = node;
|
|
771
781
|
this.selection.init();
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
782
|
+
// The engine's transaction events the projector and the observer read.
|
|
783
|
+
const engine = [
|
|
784
|
+
['beforeAllTransactions', this.projector.opening],
|
|
785
|
+
['beforeTransaction', this.surface.pending],
|
|
786
|
+
['beforeTransaction', this.projector.before],
|
|
787
|
+
['afterTransaction', this.projector.after]
|
|
788
|
+
];
|
|
789
|
+
for (const [name, listener] of engine)
|
|
790
|
+
this.doc.on(name, listener);
|
|
776
791
|
this.off.push(() => {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
this.doc.off('beforeTransaction', this.projector.before);
|
|
780
|
-
this.doc.off('afterTransaction', this.projector.after);
|
|
792
|
+
for (const [name, listener] of engine)
|
|
793
|
+
this.doc.off(name, listener);
|
|
781
794
|
});
|
|
782
795
|
// Keydown serial bumps happen INSIDE onKeyDown, after the
|
|
783
796
|
// composition-phantom swallow — a swallowed trailing Enter/Backspace
|
|
784
797
|
// is a browser artifact, not a gesture, and must not disarm pending
|
|
785
798
|
// composition caret restores.
|
|
786
|
-
const
|
|
799
|
+
const user = this.#withUserInput;
|
|
800
|
+
const keydown = user(onKeyDown.bind(this), false);
|
|
801
|
+
const { pointer } = this.selection;
|
|
802
|
+
const drop = (event) => preventUnsupportedDrop(event, this);
|
|
803
|
+
/** The host's listeners; the user-input ones run inside its window (`isHandlingUserInput`). */
|
|
804
|
+
const host = [
|
|
805
|
+
['mousedown', pointer.preventTripleClick],
|
|
806
|
+
['click', pointer.tripleClick],
|
|
807
|
+
// Settle queued mutation repairs before the native menu opens —
|
|
808
|
+
// spellcheck suggestions are computed against the DOM at this
|
|
809
|
+
// moment (PM flushes its DOM observer on contextmenu for the
|
|
810
|
+
// same reason).
|
|
811
|
+
['contextmenu', () => void this.surface.flush()],
|
|
812
|
+
['beforeinput', user(this.onBeforeInput)],
|
|
813
|
+
// An `input` records what the browser did; it says nothing new about
|
|
814
|
+
// where the user wants the selection (a display still to land wins).
|
|
815
|
+
['input', user(this.onInput, false)],
|
|
816
|
+
['copy', this.onCopy],
|
|
817
|
+
['cut', user(this.onCut)],
|
|
818
|
+
['paste', user(this.onPaste)],
|
|
819
|
+
['dragstart', (event) => onTextDragStart(this, event)],
|
|
820
|
+
['dragend', () => onTextDragEnd(this)],
|
|
821
|
+
['dragover', drop],
|
|
822
|
+
['drop', user(drop)],
|
|
823
|
+
['compositionstart', this.onCompositionStart],
|
|
824
|
+
['compositionend', this.onCompositionEnd]
|
|
825
|
+
];
|
|
787
826
|
const detachSurface = this.surface.attach(node);
|
|
788
827
|
this.off.push(
|
|
789
828
|
// One handler per keyboard occurrence: keys inside the editor at
|
|
@@ -794,48 +833,45 @@ export class Edytor {
|
|
|
794
833
|
keydown(event);
|
|
795
834
|
}),
|
|
796
835
|
// Pointer and focus ownership, and the focus-time caret restore.
|
|
797
|
-
...attachFocus(this, node),
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
//
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
//
|
|
804
|
-
// where the user wants the selection (a display still to land wins).
|
|
805
|
-
on(node, 'input', this.#withUserInput(this.onInput, { bumpSerial: false })), on(node, 'copy', this.onCopy), on(node, 'cut', this.#withUserInput(this.onCut)), on(node, 'paste', this.#withUserInput(this.onPaste)), on(node, 'dragstart', (event) => onTextDragStart(this, event)), on(node, 'dragend', () => onTextDragEnd(this)), on(node, 'dragover', (event) => preventUnsupportedDrop(event, this)), on(node, 'drop', this.#withUserInput((event) => preventUnsupportedDrop(event, this))), on(node, 'compositionstart', this.onCompositionStart), on(node, 'compositionend', this.onCompositionEnd), detachSurface);
|
|
806
|
-
// Chrome lives in the overlay, outside the host: remote carets, then the plugins'.
|
|
836
|
+
...attachFocus(this, node),
|
|
837
|
+
// The page leaving for good ends the session's history.
|
|
838
|
+
attachPageHide(this), ...host.map(([type, listener]) => on(node, type, listener)), detachSurface);
|
|
839
|
+
// Chrome lives in the overlay, outside the host: remote carets, the
|
|
840
|
+
// text a `replace` suggestion removes, marked beside the host, the
|
|
841
|
+
// polite live region (what this view's block moves and deletes did);
|
|
842
|
+
// the plugins' after them.
|
|
807
843
|
const detachOverlay = this.overlay.attach(node);
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
props: { edytor: this, name: 'suggestion-range', z: 19, ranges: this.#replaced }
|
|
816
|
-
});
|
|
817
|
-
// The polite live region: what this view's block moves and deletes did.
|
|
818
|
-
const live = mount(Announcer, {
|
|
819
|
-
target: this.overlay.layer,
|
|
820
|
-
props: { announcer: this.announcer }
|
|
821
|
-
});
|
|
822
|
-
this.off.push(() => unmount(presence), () => unmount(ranges), () => unmount(live), detachOverlay);
|
|
844
|
+
const target = this.overlay.layer;
|
|
845
|
+
const mounted = [
|
|
846
|
+
[RemoteSelections, { edytor: this }],
|
|
847
|
+
[Ranges, { edytor: this, name: 'suggestion-range', z: 19, ranges: this.#replaced }],
|
|
848
|
+
[Announcer, { announcer: this.announcer }]
|
|
849
|
+
].map(([component, props]) => mount(component, { target, props: props }));
|
|
850
|
+
this.off.push(() => mounted.forEach((instance) => void unmount(instance)), detachOverlay);
|
|
823
851
|
this.off.push(...cleanups(this.plugins, (plugin) => plugin.onEdytorAttached?.({ node })));
|
|
824
852
|
return {
|
|
825
853
|
destroy: () => {
|
|
826
854
|
if (selectionIsInside(node))
|
|
827
855
|
clearDomSelection(node);
|
|
828
|
-
const active =
|
|
829
|
-
if (active instanceof HTMLElement
|
|
856
|
+
const active = focusIn(node);
|
|
857
|
+
if (active instanceof HTMLElement)
|
|
830
858
|
active.blur();
|
|
831
|
-
this
|
|
832
|
-
this.attempts.clear();
|
|
833
|
-
this.composition.reset();
|
|
834
|
-
// Drain AND clear: `destroy()` runs the same batch again.
|
|
835
|
-
this.off.splice(0).forEach((off) => off());
|
|
859
|
+
this.#detach();
|
|
836
860
|
}
|
|
837
861
|
};
|
|
838
862
|
};
|
|
863
|
+
/**
|
|
864
|
+
* Release what an attach holds: the selection's listeners, the attempts,
|
|
865
|
+
* the composition, then the attach-lifetime batch (DOM listeners, the
|
|
866
|
+
* mutation observer, plugin actions), drained AND cleared (`destroy()`
|
|
867
|
+
* runs it again).
|
|
868
|
+
*/
|
|
869
|
+
#detach = () => {
|
|
870
|
+
this.selection.destroy();
|
|
871
|
+
this.attempts.clear();
|
|
872
|
+
this.composition.reset();
|
|
873
|
+
this.off.splice(0).forEach((off) => off());
|
|
874
|
+
};
|
|
839
875
|
/** @internal */
|
|
840
876
|
destroyed = false;
|
|
841
877
|
/** @internal The key of this view's presence entry — minted here, written only by this view. */
|
|
@@ -866,21 +902,13 @@ export class Edytor {
|
|
|
866
902
|
// A pending readiness binding must not resurrect a dead view.
|
|
867
903
|
this.#readinessRelease?.();
|
|
868
904
|
this.#readinessRelease = undefined;
|
|
869
|
-
|
|
870
|
-
// observer, plugin actions — and empties the array.
|
|
871
|
-
this.off.splice(0).forEach((off) => off());
|
|
905
|
+
this.#detach();
|
|
872
906
|
this.#offCommit?.();
|
|
873
907
|
this.history.unbind();
|
|
874
|
-
// `selectionchange` listener + the published remote caret.
|
|
875
|
-
this.selection.destroy();
|
|
876
|
-
this.attempts.clear();
|
|
877
|
-
this.composition.reset();
|
|
878
908
|
this.cells?.dispose();
|
|
879
909
|
// Wrapper maps — drop every id/node→wrapper edge a shared doc or a
|
|
880
910
|
// stray mutation could still reach.
|
|
881
911
|
this.idToBlock.clear();
|
|
882
|
-
this.nodeToInlineBlock.clear();
|
|
883
|
-
this.nodeToText.clear();
|
|
884
912
|
this.root = undefined;
|
|
885
913
|
this.node = undefined;
|
|
886
914
|
// Release this view's local-edit origin from the document's
|
|
@@ -9,6 +9,6 @@ import { type Attempt } from '../session/attempt.js';
|
|
|
9
9
|
*/
|
|
10
10
|
export declare const insertLineBreak: (edytor: Edytor, snapshot: Attempt, caret?: "after" | "before") => void;
|
|
11
11
|
/** The model command for a `beforeinput`, run as one user command (undo policy, prevention scope). */
|
|
12
|
-
export declare const runBeforeInputCommand: (edytor: Edytor, snapshot: Attempt) => boolean | void |
|
|
12
|
+
export declare const runBeforeInputCommand: (edytor: Edytor, snapshot: Attempt) => boolean | void | Promise<undefined> | Text | null | undefined;
|
|
13
13
|
/** An editing intent at the current selection (a key binding's command): no event to fabricate. */
|
|
14
|
-
export declare const runIntent: (edytor: Edytor, inputType: string) => boolean | void |
|
|
14
|
+
export declare const runIntent: (edytor: Edytor, inputType: string) => boolean | void | Promise<undefined> | Text | null | undefined;
|
|
@@ -209,14 +209,14 @@ const splitKeepingChildren = (edytor, [id, offset, born, tail], child) => {
|
|
|
209
209
|
return REFUSED;
|
|
210
210
|
if (!child)
|
|
211
211
|
return facade.compose({ ...split, writes: [step] });
|
|
212
|
-
// The
|
|
212
|
+
// The document's answer on placing the kind as a first child.
|
|
213
213
|
const slot = facade.prepare.insertBlocks({ parent: id, index: 0 }, [
|
|
214
214
|
{ id: born, type: tail?.type ?? facade.defaultChild(id) }
|
|
215
215
|
]);
|
|
216
216
|
const first = 'writes' in slot ? slot.writes[0] : undefined;
|
|
217
217
|
if (first?.op !== 'insertBlocks')
|
|
218
218
|
return REFUSED;
|
|
219
|
-
const placed = { ...step, parent: id,
|
|
219
|
+
const placed = { ...step, parent: id, index: 0 };
|
|
220
220
|
return facade.compose({ ...split, writes: [placed] });
|
|
221
221
|
};
|
|
222
222
|
/**
|
|
@@ -2,9 +2,19 @@ export declare const isNativeInteractiveControl: (target: EventTarget | null) =>
|
|
|
2
2
|
export declare const isNativeInteractiveEvent: (event: Event) => boolean;
|
|
3
3
|
export declare const isNativeTextControl: (target: EventTarget | null) => boolean;
|
|
4
4
|
export declare const isNativeTextControlEvent: (event: Event) => boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Whether a press inside the host, and its whole gesture (its clicks and
|
|
7
|
+
* multi-clicks, its `selectstart`, its drag), is its target's own
|
|
8
|
+
* (`sel.press.void`): a control of a block's chrome, a link there (a
|
|
9
|
+
* bookmark's card, a file's row: its click opens it), or an element that
|
|
10
|
+
* keeps the selection (`data-edytor-keep-selection`). The one rule every
|
|
11
|
+
* press handler asks.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ownsPress: (editorRoot: Element | null | undefined, event: Event) => boolean;
|
|
5
14
|
/**
|
|
6
15
|
* A kind's own control: a form control (`input` of any type, `textarea`,
|
|
7
|
-
* `select`, `button`)
|
|
16
|
+
* `select`, `button`), a control of a block's chrome (an interactive role,
|
|
17
|
+
* a focusable element, `data-edytor-control`) or a nested editable island inside the host (a plugin
|
|
8
18
|
* chrome editor, an embedded code field: it owns its input and composition
|
|
9
19
|
* lifecycle), never the root nor the editor's own text elements (they carry
|
|
10
20
|
* their own `contentEditable="true"`). It owns its focus, selection and
|
|
@@ -32,13 +32,52 @@ export const isNativeInteractiveControl = (target) => Boolean(findClosestElement
|
|
|
32
32
|
export const isNativeInteractiveEvent = (event) => Boolean(findNativeControlInEvent(event, NATIVE_INTERACTIVE_SELECTOR));
|
|
33
33
|
export const isNativeTextControl = (target) => Boolean(findClosestElement(target, NATIVE_TEXT_SELECTOR));
|
|
34
34
|
export const isNativeTextControlEvent = (event) => Boolean(findNativeControlInEvent(event, NATIVE_TEXT_SELECTOR));
|
|
35
|
+
/**
|
|
36
|
+
* The controls of a block's chrome (`sel.press.void`): a native form
|
|
37
|
+
* control, an element whose role says it is interactive or that takes the
|
|
38
|
+
* focus, and one the app marks as its own (`data-edytor-control`, for any
|
|
39
|
+
* other element). Their press and its whole gesture, their keys and their
|
|
40
|
+
* input are theirs: the editor does nothing with them.
|
|
41
|
+
*/
|
|
42
|
+
const CHROME_CONTROL_SELECTOR = [
|
|
43
|
+
'input, textarea, select, button',
|
|
44
|
+
'[role="button"], [role="link"], [role="checkbox"], [role="switch"], [role="menuitem"]',
|
|
45
|
+
'[role="tab"], [role="option"], [role="slider"], [role="textbox"], [role="combobox"]',
|
|
46
|
+
'[tabindex]:not([tabindex^="-"])',
|
|
47
|
+
'[data-edytor-control]'
|
|
48
|
+
].join(', ');
|
|
49
|
+
/**
|
|
50
|
+
* The control of a block's chrome holding `target` inside the host, by
|
|
51
|
+
* `selector`: never the host, nor an element inside the editor's text or an
|
|
52
|
+
* inline atom (a link mark, a mention: the text's own presses).
|
|
53
|
+
*/
|
|
54
|
+
const chromeControl = (editorRoot, target, selector = CHROME_CONTROL_SELECTOR) => {
|
|
55
|
+
const control = getElementFromTarget(target)?.closest(selector);
|
|
56
|
+
return control &&
|
|
57
|
+
editorRoot &&
|
|
58
|
+
control !== editorRoot &&
|
|
59
|
+
editorRoot.contains(control) &&
|
|
60
|
+
!control.closest('[data-edytor-text], [data-edytor-inline-block]')
|
|
61
|
+
? control
|
|
62
|
+
: null;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Whether a press inside the host, and its whole gesture (its clicks and
|
|
66
|
+
* multi-clicks, its `selectstart`, its drag), is its target's own
|
|
67
|
+
* (`sel.press.void`): a control of a block's chrome, a link there (a
|
|
68
|
+
* bookmark's card, a file's row: its click opens it), or an element that
|
|
69
|
+
* keeps the selection (`data-edytor-keep-selection`). The one rule every
|
|
70
|
+
* press handler asks.
|
|
71
|
+
*/
|
|
72
|
+
export const ownsPress = (editorRoot, event) => Boolean(chromeControl(editorRoot, event.target, `${CHROME_CONTROL_SELECTOR}, [data-edytor-keep-selection], a[href]`));
|
|
35
73
|
// Any non-"false" contenteditable value — attribute selectors are
|
|
36
74
|
// case-sensitive, and "TRUE"/"Plaintext-Only"/"inherit"/garbage values
|
|
37
75
|
// all still establish a nested editable context the guard must catch.
|
|
38
76
|
const EDITABLE_TARGET_SELECTOR = '[contenteditable]:not([contenteditable="false" i])';
|
|
39
77
|
/**
|
|
40
78
|
* A kind's own control: a form control (`input` of any type, `textarea`,
|
|
41
|
-
* `select`, `button`)
|
|
79
|
+
* `select`, `button`), a control of a block's chrome (an interactive role,
|
|
80
|
+
* a focusable element, `data-edytor-control`) or a nested editable island inside the host (a plugin
|
|
42
81
|
* chrome editor, an embedded code field: it owns its input and composition
|
|
43
82
|
* lifecycle), never the root nor the editor's own text elements (they carry
|
|
44
83
|
* their own `contentEditable="true"`). It owns its focus, selection and
|
|
@@ -48,6 +87,7 @@ const EDITABLE_TARGET_SELECTOR = '[contenteditable]:not([contenteditable="false"
|
|
|
48
87
|
export const isKindControl = (editorRoot, target) => {
|
|
49
88
|
const editable = findClosestElement(target, EDITABLE_TARGET_SELECTOR);
|
|
50
89
|
const control = findClosestElement(target, NATIVE_ACTIVE_ELEMENT_SELECTOR) ??
|
|
90
|
+
chromeControl(editorRoot, target) ??
|
|
51
91
|
(editable?.hasAttribute('data-edytor-text') ? null : editable);
|
|
52
92
|
return Boolean(control && editorRoot && control !== editorRoot && editorRoot.contains(control));
|
|
53
93
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { vetoable } from '../utils.js';
|
|
2
|
-
import { createDomRange, createDomSelectionSnapshotFromRange, getActiveElement, getDomSelection } from '../selection/domSelection.js';
|
|
2
|
+
import { createDomRange, createDomSelectionSnapshotFromRange, focusIn, getActiveElement, getDomSelection } from '../selection/domSelection.js';
|
|
3
3
|
import { getYIndex } from '../selection/selection.utils.js';
|
|
4
4
|
import { sameValue } from '../session/selection.js';
|
|
5
5
|
import { INTENTS, attemptOf, intentOf, kindOf, reproject, targetless } from '../session/attempt.js';
|
|
@@ -87,9 +87,9 @@ const isForeignHistory = (edytor) => {
|
|
|
87
87
|
const node = edytor.node;
|
|
88
88
|
if (!node)
|
|
89
89
|
return true;
|
|
90
|
-
|
|
91
|
-
if (active && node.contains(active))
|
|
90
|
+
if (focusIn(node))
|
|
92
91
|
return false;
|
|
92
|
+
const active = getActiveElement(node);
|
|
93
93
|
const { body, documentElement } = node.ownerDocument;
|
|
94
94
|
const anchor = getDomSelection(node)?.anchorNode;
|
|
95
95
|
return ((!!active &&
|
package/dist/events/onFocus.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* programmatic focus returns to. Answers the listeners' teardown.
|
|
7
7
|
*/
|
|
8
8
|
import { on } from 'svelte/events';
|
|
9
|
-
import { clearDomSelection, getActiveElement, getDomSelection, getDomSelectionSnapshot } from '../selection/domSelection.js';
|
|
9
|
+
import { clearDomSelection, focusIn, getActiveElement, getDomSelection, getDomSelectionSnapshot } from '../selection/domSelection.js';
|
|
10
10
|
import { getYIndex } from '../selection/selection.utils.js';
|
|
11
11
|
import { noSelection } from '../session/selection.js';
|
|
12
12
|
import { observeInternalDragSources } from './onDrop.js';
|
|
@@ -38,8 +38,7 @@ export const takeKeys = (edytor) => {
|
|
|
38
38
|
return;
|
|
39
39
|
if (getActiveElement(node) === node)
|
|
40
40
|
return;
|
|
41
|
-
edytor.expectInternalFocus();
|
|
42
|
-
node.focus({ preventScroll: true });
|
|
41
|
+
edytor.expectInternalFocus(node);
|
|
43
42
|
edytor.projector.parked();
|
|
44
43
|
edytor.selection.display();
|
|
45
44
|
};
|
|
@@ -125,6 +124,9 @@ export const attachFocus = (edytor, node) => {
|
|
|
125
124
|
*/
|
|
126
125
|
const placingPress = (event) => {
|
|
127
126
|
const { pointer } = edytor.selection;
|
|
127
|
+
// A control of a block's chrome owns its whole press (`sel.press.void`).
|
|
128
|
+
if (pointer.ownPress(event))
|
|
129
|
+
return void (pointer.claimed = false);
|
|
128
130
|
if (pointer.marginPress(event))
|
|
129
131
|
return;
|
|
130
132
|
if (!pointer.belowPress(event))
|
|
@@ -136,8 +138,9 @@ export const attachFocus = (edytor, node) => {
|
|
|
136
138
|
edytor.composition.abandon();
|
|
137
139
|
lastPointerDownInsideEditorAt = event.timeStamp;
|
|
138
140
|
const { pointer } = edytor.selection;
|
|
139
|
-
// The margin gesture's press places no caret and starts no text drag
|
|
140
|
-
|
|
141
|
+
// The margin gesture's press places no caret and starts no text drag; a
|
|
142
|
+
// control's press is the control's (`sel.press.void`).
|
|
143
|
+
if (pointer.claimed || pointer.ownPress(event))
|
|
141
144
|
return;
|
|
142
145
|
edytor.projector.pressed();
|
|
143
146
|
pointer.capture(event);
|
|
@@ -145,13 +148,8 @@ export const attachFocus = (edytor, node) => {
|
|
|
145
148
|
pointer.collapseBlocksAt(event);
|
|
146
149
|
};
|
|
147
150
|
const clearNativeSelectionAfterExternalFocus = () => {
|
|
148
|
-
|
|
149
|
-
if (activeElement instanceof Node && node.contains(activeElement)) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (selectionIsInside(node)) {
|
|
151
|
+
if (!focusIn(node) && selectionIsInside(node))
|
|
153
152
|
clearDomSelection(node);
|
|
154
|
-
}
|
|
155
153
|
};
|
|
156
154
|
/**
|
|
157
155
|
* A keyboard or programmatic focus returns to the model's selection: the
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page leaves for good (installed by `Edytor.attach`): a `pagehide` the
|
|
3
|
+
* back/forward cache keeps nothing of. The view tells its document the
|
|
4
|
+
* session's history ended (`document.endHistory()`, `hist.release.end`): the
|
|
5
|
+
* history's rule releases what the session deleted. A page the cache keeps
|
|
6
|
+
* may come back with its history, and a lost connection fires nothing here:
|
|
7
|
+
* both keep it.
|
|
8
|
+
*
|
|
9
|
+
* The page's lifecycle is the sync layer's (`onPageLeave`, which the
|
|
10
|
+
* providers leave by too): the view subscribes to its `write` phase, run
|
|
11
|
+
* before the providers' teardown whatever order they subscribed in, so the
|
|
12
|
+
* release goes out on a socket or store that is still open.
|
|
13
|
+
*/
|
|
14
|
+
import { onPageLeave } from '../crdt/providers/room.js';
|
|
15
|
+
/** End the view's session when its page leaves for good; answers the teardown. */
|
|
16
|
+
export const attachPageHide = (edytor) => onPageLeave(() => edytor.document.endHistory(), 'write');
|