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
package/dist/edytor.svelte.js
CHANGED
|
@@ -38,7 +38,8 @@ import { attachDocument, SemanticConflictError } from './crdt/index.js';
|
|
|
38
38
|
import { whenDocumentReady } from './collaboration/documentSync.js';
|
|
39
39
|
import { mintPresenceKey, PresenceWriter } from './collaboration/awarenessSelection.js';
|
|
40
40
|
import { canMove, move } from './session/moves.js';
|
|
41
|
-
import {
|
|
41
|
+
import { SUGGEST_ROOT } from './crdt/schema.js';
|
|
42
|
+
import { asEngineDoc } from './crdt/structs.js';
|
|
42
43
|
import { on } from 'svelte/events';
|
|
43
44
|
import { Keymap } from './session/keymap.js';
|
|
44
45
|
import { TRANSACTION } from './constants.js';
|
|
@@ -46,7 +47,7 @@ import { Dispatcher } from './session/commands.js';
|
|
|
46
47
|
import { History } from './session/history.js';
|
|
47
48
|
import { DataWrites } from './session/props.js';
|
|
48
49
|
import { kindCatalogue, kindCommand, UNKNOWN_KIND } from './kinds.js';
|
|
49
|
-
import { clearDomSelection, getActiveElement, getDomSelection, getDomSelectionSnapshot } from './selection/domSelection.js';
|
|
50
|
+
import { clearDomSelection, focusIn, getActiveElement, getDomSelection, getDomSelectionSnapshot } from './selection/domSelection.js';
|
|
50
51
|
/** A consumer that is set (a prop or a plugin hook left out is `undefined`). */
|
|
51
52
|
const isListener = (listener) => listener !== undefined;
|
|
52
53
|
/**
|
|
@@ -87,10 +88,6 @@ export class Edytor {
|
|
|
87
88
|
/** The view's handles: one id-only `Block` per live id, texts and atoms by position and id. */
|
|
88
89
|
idToBlock = new Handles(this);
|
|
89
90
|
/** @internal */
|
|
90
|
-
nodeToInlineBlock = new Map();
|
|
91
|
-
/** @internal */
|
|
92
|
-
nodeToText = new Map();
|
|
93
|
-
/** @internal */
|
|
94
91
|
transaction = new TRANSACTION();
|
|
95
92
|
/** @internal The view's keymap: consumer bindings, then the extensions', then the built-in rows. */
|
|
96
93
|
keymap;
|
|
@@ -113,8 +110,8 @@ export class Edytor {
|
|
|
113
110
|
surface: {
|
|
114
111
|
park: (text, offset) => this.projector.park(text, offset),
|
|
115
112
|
flush: () => void this.surface.flush(),
|
|
116
|
-
placed:
|
|
117
|
-
parked:
|
|
113
|
+
placed: this.projector.placed,
|
|
114
|
+
parked: this.projector.parked,
|
|
118
115
|
clear: () => clearDomSelection(this.node),
|
|
119
116
|
pointAt: (node, offset) => textPointAt.call(this.selection, node, offset),
|
|
120
117
|
sameLine: (a, b) => onOneLine(this.selection.textAt(a), this.selection.textAt(b))
|
|
@@ -211,7 +208,7 @@ export class Edytor {
|
|
|
211
208
|
*/
|
|
212
209
|
get facade() {
|
|
213
210
|
return (this.lens ??= virtualLens(this.document.facade, () => this.document.ready, () => this.document.defaultChild(null), (kind) => ({
|
|
214
|
-
|
|
211
|
+
childless: this.definitionOf(kind).void === true,
|
|
215
212
|
rendersContent: this.document.rendersContent(kind)
|
|
216
213
|
})));
|
|
217
214
|
}
|
|
@@ -298,10 +295,6 @@ export class Edytor {
|
|
|
298
295
|
};
|
|
299
296
|
constructor(options) {
|
|
300
297
|
const { readonly, hotkeys, plugins, document, actor, requireHydration, presence, sync, value, onSelectionChange, labels, onChange, onDocChange, allowNesting } = options;
|
|
301
|
-
for (const name of DOCUMENT_WRITES)
|
|
302
|
-
Object.defineProperty(this, name, {
|
|
303
|
-
value: (...args) => this.dispatcher.operate(name, args)
|
|
304
|
-
});
|
|
305
298
|
if (labels)
|
|
306
299
|
this.labels = labels;
|
|
307
300
|
this.allowNesting = allowNesting === true;
|
|
@@ -315,6 +308,11 @@ export class Edytor {
|
|
|
315
308
|
this.document = attachDocument(new Y.Doc(), { actor, requireHydration });
|
|
316
309
|
this.#owned = true;
|
|
317
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
|
+
});
|
|
318
316
|
this.readonly = readonly || false;
|
|
319
317
|
this.onChange = onChange;
|
|
320
318
|
this.onDocChange = onDocChange;
|
|
@@ -489,6 +487,18 @@ export class Edytor {
|
|
|
489
487
|
this.cells = createCells(this.facade, this.surface.patched);
|
|
490
488
|
this.root = this.idToBlock.root;
|
|
491
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
|
+
}
|
|
492
502
|
// A document that is already empty shows its virtual paragraph with the caret in it.
|
|
493
503
|
this.selection?.restoreDeadSelectionEndpoints();
|
|
494
504
|
};
|
|
@@ -533,11 +543,11 @@ export class Edytor {
|
|
|
533
543
|
if (values.length > 0)
|
|
534
544
|
callEach('[edytor] onChange', values, this.value);
|
|
535
545
|
};
|
|
536
|
-
/** 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. */
|
|
537
547
|
#replaced = () => {
|
|
538
548
|
void this.suggestions.revision;
|
|
539
|
-
return this.suggestions.list.flatMap(({ at }) => {
|
|
540
|
-
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))
|
|
541
551
|
return [];
|
|
542
552
|
const { anchor, focus } = at.replace;
|
|
543
553
|
const [start, end] = [anchor, focus].map(this.selection.resolveTextAnchor);
|
|
@@ -580,15 +590,14 @@ export class Edytor {
|
|
|
580
590
|
* affordances long after the gesture ended.
|
|
581
591
|
*/
|
|
582
592
|
#userInputHandlingDepth = 0;
|
|
583
|
-
#withUserInput = (handler,
|
|
593
|
+
#withUserInput = (handler, bumpSerial = true) => {
|
|
584
594
|
return (...args) => {
|
|
585
595
|
// Depth-counted: a synchronous wrapped listener landing inside an
|
|
586
596
|
// awaited handler's hold window must not end the window the outer
|
|
587
597
|
// handler still needs.
|
|
588
598
|
this.#userInputHandlingDepth++;
|
|
589
|
-
if (
|
|
599
|
+
if (bumpSerial)
|
|
590
600
|
this.markUserGesture();
|
|
591
|
-
}
|
|
592
601
|
const clear = () => {
|
|
593
602
|
// A display the window held back gets its pass now.
|
|
594
603
|
if (--this.#userInputHandlingDepth === 0)
|
|
@@ -637,12 +646,13 @@ export class Edytor {
|
|
|
637
646
|
* fires (element already focused).
|
|
638
647
|
*/
|
|
639
648
|
#expectInternalFocusArmed = false;
|
|
640
|
-
/** @internal */
|
|
641
|
-
expectInternalFocus = () => {
|
|
649
|
+
/** @internal Armed, and `element` (the host, an editing host in it) focused without scrolling. */
|
|
650
|
+
expectInternalFocus = (element) => {
|
|
642
651
|
this.#expectInternalFocusArmed = true;
|
|
643
652
|
queueMicrotask(() => {
|
|
644
653
|
this.#expectInternalFocusArmed = false;
|
|
645
654
|
});
|
|
655
|
+
element?.focus({ preventScroll: true });
|
|
646
656
|
};
|
|
647
657
|
/** @internal A `focusin` consumes the armed flag: `true` when the editor focused itself. */
|
|
648
658
|
consumeInternalFocus = () => {
|
|
@@ -670,7 +680,7 @@ export class Edytor {
|
|
|
670
680
|
*/
|
|
671
681
|
lastUserGestureOutsideEditor = false;
|
|
672
682
|
/** @internal Run `body` as part of the user's input (deferred work of an occurrence). */
|
|
673
|
-
userInput = (body) => this.#withUserInput(body,
|
|
683
|
+
userInput = (body) => this.#withUserInput(body, false)();
|
|
674
684
|
/**
|
|
675
685
|
* A commit's caret: select it (the projector displays it once the pin's
|
|
676
686
|
* render lands) and arm the IME post-commit jump rule — a move right after
|
|
@@ -681,13 +691,12 @@ export class Edytor {
|
|
|
681
691
|
if (this.readonly || this.isComposing || !text.isInDocument)
|
|
682
692
|
return;
|
|
683
693
|
const active = getActiveElement(this.node);
|
|
684
|
-
const selection = getDomSelection(this.node);
|
|
685
694
|
const ours = (node) => Boolean(node && this.node?.contains(node));
|
|
686
695
|
// Focus left for a foreign element with no selection of ours, or moved
|
|
687
696
|
// into a nested editable island (its own caret): the user owns it.
|
|
688
697
|
if ((active &&
|
|
689
|
-
!
|
|
690
|
-
!ours(
|
|
698
|
+
!focusIn(this.node) &&
|
|
699
|
+
!ours(getDomSelection(this.node)?.anchorNode) &&
|
|
691
700
|
!ours(this.selection.textRange().startText?.node)) ||
|
|
692
701
|
isKindControl(this.node, active))
|
|
693
702
|
return;
|
|
@@ -770,21 +779,50 @@ export class Edytor {
|
|
|
770
779
|
this.lastUserGestureOutsideEditor = false;
|
|
771
780
|
this.node = node;
|
|
772
781
|
this.selection.init();
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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);
|
|
777
791
|
this.off.push(() => {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
this.doc.off('beforeTransaction', this.projector.before);
|
|
781
|
-
this.doc.off('afterTransaction', this.projector.after);
|
|
792
|
+
for (const [name, listener] of engine)
|
|
793
|
+
this.doc.off(name, listener);
|
|
782
794
|
});
|
|
783
795
|
// Keydown serial bumps happen INSIDE onKeyDown, after the
|
|
784
796
|
// composition-phantom swallow — a swallowed trailing Enter/Backspace
|
|
785
797
|
// is a browser artifact, not a gesture, and must not disarm pending
|
|
786
798
|
// composition caret restores.
|
|
787
|
-
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
|
+
];
|
|
788
826
|
const detachSurface = this.surface.attach(node);
|
|
789
827
|
this.off.push(
|
|
790
828
|
// One handler per keyboard occurrence: keys inside the editor at
|
|
@@ -797,48 +835,43 @@ export class Edytor {
|
|
|
797
835
|
// Pointer and focus ownership, and the focus-time caret restore.
|
|
798
836
|
...attachFocus(this, node),
|
|
799
837
|
// The page leaving for good ends the session's history.
|
|
800
|
-
attachPageHide(this),
|
|
801
|
-
//
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
//
|
|
805
|
-
on(node, 'contextmenu', () => void this.surface.flush()), on(node, 'beforeinput', this.#withUserInput(this.onBeforeInput)),
|
|
806
|
-
// An `input` records what the browser did; it says nothing new about
|
|
807
|
-
// where the user wants the selection (a display still to land wins).
|
|
808
|
-
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);
|
|
809
|
-
// Chrome lives in the overlay, outside the host: remote carets, then the plugins'.
|
|
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.
|
|
810
843
|
const detachOverlay = this.overlay.attach(node);
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
props: { edytor: this, name: 'suggestion-range', z: 19, ranges: this.#replaced }
|
|
819
|
-
});
|
|
820
|
-
// The polite live region: what this view's block moves and deletes did.
|
|
821
|
-
const live = mount(Announcer, {
|
|
822
|
-
target: this.overlay.layer,
|
|
823
|
-
props: { announcer: this.announcer }
|
|
824
|
-
});
|
|
825
|
-
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);
|
|
826
851
|
this.off.push(...cleanups(this.plugins, (plugin) => plugin.onEdytorAttached?.({ node })));
|
|
827
852
|
return {
|
|
828
853
|
destroy: () => {
|
|
829
854
|
if (selectionIsInside(node))
|
|
830
855
|
clearDomSelection(node);
|
|
831
|
-
const active =
|
|
832
|
-
if (active instanceof HTMLElement
|
|
856
|
+
const active = focusIn(node);
|
|
857
|
+
if (active instanceof HTMLElement)
|
|
833
858
|
active.blur();
|
|
834
|
-
this
|
|
835
|
-
this.attempts.clear();
|
|
836
|
-
this.composition.reset();
|
|
837
|
-
// Drain AND clear: `destroy()` runs the same batch again.
|
|
838
|
-
this.off.splice(0).forEach((off) => off());
|
|
859
|
+
this.#detach();
|
|
839
860
|
}
|
|
840
861
|
};
|
|
841
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
|
+
};
|
|
842
875
|
/** @internal */
|
|
843
876
|
destroyed = false;
|
|
844
877
|
/** @internal The key of this view's presence entry — minted here, written only by this view. */
|
|
@@ -869,21 +902,13 @@ export class Edytor {
|
|
|
869
902
|
// A pending readiness binding must not resurrect a dead view.
|
|
870
903
|
this.#readinessRelease?.();
|
|
871
904
|
this.#readinessRelease = undefined;
|
|
872
|
-
|
|
873
|
-
// observer, plugin actions — and empties the array.
|
|
874
|
-
this.off.splice(0).forEach((off) => off());
|
|
905
|
+
this.#detach();
|
|
875
906
|
this.#offCommit?.();
|
|
876
907
|
this.history.unbind();
|
|
877
|
-
// `selectionchange` listener + the published remote caret.
|
|
878
|
-
this.selection.destroy();
|
|
879
|
-
this.attempts.clear();
|
|
880
|
-
this.composition.reset();
|
|
881
908
|
this.cells?.dispose();
|
|
882
909
|
// Wrapper maps — drop every id/node→wrapper edge a shared doc or a
|
|
883
910
|
// stray mutation could still reach.
|
|
884
911
|
this.idToBlock.clear();
|
|
885
|
-
this.nodeToInlineBlock.clear();
|
|
886
|
-
this.nodeToText.clear();
|
|
887
912
|
this.root = undefined;
|
|
888
913
|
this.node = undefined;
|
|
889
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;
|
|
@@ -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
|