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.
Files changed (231) hide show
  1. package/README.md +2 -2
  2. package/dist/block/block.svelte.js +6 -21
  3. package/dist/block/inlineBlock.svelte.js +4 -6
  4. package/dist/clipboard/moveText.js +6 -8
  5. package/dist/cloudflare/DocumentRoom.d.ts +23 -12
  6. package/dist/cloudflare/DocumentRoom.js +10 -6
  7. package/dist/cloudflare/index.d.ts +1 -1
  8. package/dist/cloudflare/room/access.d.ts +8 -3
  9. package/dist/cloudflare/room/access.js +44 -20
  10. package/dist/cloudflare/room/admission.js +28 -5
  11. package/dist/cloudflare/room/comments.js +28 -1
  12. package/dist/cloudflare/room/context.d.ts +139 -129
  13. package/dist/cloudflare/room/context.js +11 -4
  14. package/dist/cloudflare/room/forged.d.ts +14 -0
  15. package/dist/cloudflare/room/forged.js +30 -2
  16. package/dist/cloudflare/room/frames.d.ts +4 -0
  17. package/dist/cloudflare/room/frames.js +5 -1
  18. package/dist/cloudflare/room/purge.d.ts +6 -3
  19. package/dist/cloudflare/room/purge.js +45 -19
  20. package/dist/cloudflare/room/replicas.d.ts +4 -2
  21. package/dist/cloudflare/room/replicas.js +8 -3
  22. package/dist/cloudflare/room/suggest.d.ts +7 -0
  23. package/dist/cloudflare/room/suggest.js +143 -0
  24. package/dist/cloudflare/room/updates.d.ts +3 -4
  25. package/dist/cloudflare/room/updates.js +6 -8
  26. package/dist/cloudflare/routeDocumentSocket.d.ts +10 -1
  27. package/dist/cloudflare/routeDocumentSocket.js +5 -3
  28. package/dist/components/Content.svelte +9 -1
  29. package/dist/crdt/anchors.js +3 -2
  30. package/dist/crdt/attribution/attribution.d.ts +34 -12
  31. package/dist/crdt/attribution/attribution.js +59 -21
  32. package/dist/crdt/attribution/block.d.ts +3 -3
  33. package/dist/crdt/attribution/block.js +7 -8
  34. package/dist/crdt/attribution/index.d.ts +1 -1
  35. package/dist/crdt/attribution/index.js +1 -1
  36. package/dist/crdt/data.d.ts +62 -31
  37. package/dist/crdt/data.js +211 -103
  38. package/dist/crdt/doc/capability.js +9 -13
  39. package/dist/crdt/doc/delete.d.ts +1 -1
  40. package/dist/crdt/doc/delete.js +13 -30
  41. package/dist/crdt/doc/funnel.d.ts +1 -1
  42. package/dist/crdt/doc/funnel.js +34 -26
  43. package/dist/crdt/doc/gate.d.ts +3 -3
  44. package/dist/crdt/doc/history.d.ts +31 -9
  45. package/dist/crdt/doc/history.js +204 -73
  46. package/dist/crdt/doc/layout.js +16 -27
  47. package/dist/crdt/doc/meta.js +17 -23
  48. package/dist/crdt/doc/moves.d.ts +1 -1
  49. package/dist/crdt/doc/moves.js +52 -32
  50. package/dist/crdt/doc/reads.d.ts +2 -2
  51. package/dist/crdt/doc/reads.js +20 -30
  52. package/dist/crdt/doc/seed.js +2 -2
  53. package/dist/crdt/doc/split-merge.js +12 -15
  54. package/dist/crdt/doc/steps.d.ts +25 -13
  55. package/dist/crdt/doc/steps.js +55 -125
  56. package/dist/crdt/doc/suggest.d.ts +167 -0
  57. package/dist/crdt/doc/suggest.js +579 -0
  58. package/dist/crdt/doc/types.d.ts +21 -9
  59. package/dist/crdt/document.d.ts +26 -6
  60. package/dist/crdt/document.js +40 -15
  61. package/dist/crdt/edytor-doc.d.ts +410 -390
  62. package/dist/crdt/edytor-doc.js +143 -95
  63. package/dist/crdt/engine-api.d.ts +39 -1
  64. package/dist/crdt/engine.d.ts +2 -2
  65. package/dist/crdt/engine.js +4 -5
  66. package/dist/crdt/flow.d.ts +2 -2
  67. package/dist/crdt/flow.js +29 -37
  68. package/dist/crdt/index.d.ts +134 -127
  69. package/dist/crdt/index.js +4 -1
  70. package/dist/crdt/operations.d.ts +12 -3
  71. package/dist/crdt/parts.d.ts +43 -0
  72. package/dist/crdt/parts.js +44 -0
  73. package/dist/crdt/placement/display.d.ts +6 -31
  74. package/dist/crdt/placement/display.js +41 -146
  75. package/dist/crdt/placement/keys.d.ts +63 -0
  76. package/dist/crdt/placement/keys.js +95 -0
  77. package/dist/crdt/placement/model.d.ts +119 -118
  78. package/dist/crdt/placement/model.js +242 -142
  79. package/dist/crdt/placement/resolve.d.ts +123 -35
  80. package/dist/crdt/placement/resolve.js +188 -153
  81. package/dist/crdt/placement/slots.d.ts +134 -0
  82. package/dist/crdt/placement/slots.js +152 -0
  83. package/dist/crdt/protocol.d.ts +1 -1
  84. package/dist/crdt/protocol.js +1 -1
  85. package/dist/crdt/protocols/auth.d.ts +11 -1
  86. package/dist/crdt/protocols/auth.js +17 -1
  87. package/dist/crdt/protocols/comments.d.ts +16 -1
  88. package/dist/crdt/protocols/comments.js +9 -1
  89. package/dist/crdt/providers/index.d.ts +6 -4
  90. package/dist/crdt/providers/index.js +3 -1
  91. package/dist/crdt/providers/room.d.ts +16 -2
  92. package/dist/crdt/providers/room.js +64 -17
  93. package/dist/crdt/providers/websocket.d.ts +6 -4
  94. package/dist/crdt/providers/websocket.js +14 -7
  95. package/dist/crdt/purge.d.ts +24 -9
  96. package/dist/crdt/purge.js +266 -126
  97. package/dist/crdt/restore.d.ts +4 -3
  98. package/dist/crdt/restore.js +27 -34
  99. package/dist/crdt/schema.d.ts +30 -21
  100. package/dist/crdt/schema.js +39 -23
  101. package/dist/crdt/storage.d.ts +1 -1
  102. package/dist/crdt/storage.js +1 -1
  103. package/dist/crdt/structs.d.ts +17 -15
  104. package/dist/crdt/structs.js +10 -22
  105. package/dist/crdt/text/index/cache.js +4 -0
  106. package/dist/crdt/text/index/claims.js +13 -23
  107. package/dist/crdt/text/index/fold.d.ts +2 -3
  108. package/dist/crdt/text/index/fold.js +194 -44
  109. package/dist/crdt/text/index/layout.d.ts +2 -4
  110. package/dist/crdt/text/index/layout.js +14 -30
  111. package/dist/crdt/text/index/placement.d.ts +26 -18
  112. package/dist/crdt/text/index/placement.js +420 -157
  113. package/dist/crdt/text/index/records.d.ts +3 -3
  114. package/dist/crdt/text/index/records.js +24 -31
  115. package/dist/crdt/text/index/shared.d.ts +11 -3
  116. package/dist/crdt/text/index/shared.js +29 -17
  117. package/dist/crdt/text/index/state.d.ts +21 -17
  118. package/dist/crdt/text/index/state.js +15 -17
  119. package/dist/crdt/text/index/streams.d.ts +10 -5
  120. package/dist/crdt/text/index/streams.js +78 -27
  121. package/dist/crdt/text/items.d.ts +10 -3
  122. package/dist/crdt/text/items.js +7 -0
  123. package/dist/crdt/text/model.d.ts +87 -62
  124. package/dist/crdt/text/model.js +91 -169
  125. package/dist/crdt/text/ranges.d.ts +1 -1
  126. package/dist/crdt/text/ranges.js +3 -3
  127. package/dist/crdt/text/rows.d.ts +13 -7
  128. package/dist/crdt/text/rows.js +24 -7
  129. package/dist/crdt/text/runs.d.ts +51 -42
  130. package/dist/crdt/text/runs.js +24 -7
  131. package/dist/crdt/text/streams.d.ts +26 -13
  132. package/dist/crdt/text/streams.js +41 -14
  133. package/dist/crdt/vendor/yjs/UPSTREAM.md +651 -21
  134. package/dist/crdt/vendor/yjs/dts/global.d.ts +2 -0
  135. package/dist/crdt/vendor/yjs/dts/index.d.ts +1 -1
  136. package/dist/crdt/vendor/yjs/dts/structs/AbstractStruct.d.ts +7 -0
  137. package/dist/crdt/vendor/yjs/dts/structs/Item.d.ts +197 -10
  138. package/dist/crdt/vendor/yjs/dts/utils/Doc.d.ts +6 -3
  139. package/dist/crdt/vendor/yjs/dts/utils/StructStore.d.ts +5 -18
  140. package/dist/crdt/vendor/yjs/dts/utils/Transaction.d.ts +15 -1
  141. package/dist/crdt/vendor/yjs/dts/utils/UpdateDecoder.d.ts +14 -6
  142. package/dist/crdt/vendor/yjs/dts/utils/UpdateEncoder.d.ts +33 -6
  143. package/dist/crdt/vendor/yjs/dts/utils/ids.d.ts +1 -65
  144. package/dist/crdt/vendor/yjs/dts/utils/marks.d.ts +6 -1
  145. package/dist/crdt/vendor/yjs/dts/ynode.d.ts +54 -7
  146. package/dist/crdt/vendor/yjs/global.d.ts +2 -0
  147. package/dist/crdt/vendor/yjs/src/index.js +1 -1
  148. package/dist/crdt/vendor/yjs/src/structs/AbstractStruct.js +10 -0
  149. package/dist/crdt/vendor/yjs/src/structs/Item.js +649 -51
  150. package/dist/crdt/vendor/yjs/src/utils/BlockSet.js +18 -9
  151. package/dist/crdt/vendor/yjs/src/utils/Doc.js +5 -3
  152. package/dist/crdt/vendor/yjs/src/utils/StructStore.js +5 -0
  153. package/dist/crdt/vendor/yjs/src/utils/Transaction.js +36 -13
  154. package/dist/crdt/vendor/yjs/src/utils/UpdateDecoder.js +88 -18
  155. package/dist/crdt/vendor/yjs/src/utils/UpdateEncoder.js +119 -32
  156. package/dist/crdt/vendor/yjs/src/utils/encoding.js +13 -1
  157. package/dist/crdt/vendor/yjs/src/utils/ids.js +7 -7
  158. package/dist/crdt/vendor/yjs/src/utils/marks.js +193 -8
  159. package/dist/crdt/vendor/yjs/src/utils/redo.js +1 -1
  160. package/dist/crdt/vendor/yjs/src/utils/transaction-helpers.js +1 -1
  161. package/dist/crdt/vendor/yjs/src/utils/updates.js +3 -3
  162. package/dist/crdt/vendor/yjs/src/ynode.js +199 -12
  163. package/dist/edytor.svelte.d.ts +1 -1
  164. package/dist/edytor.svelte.js +103 -75
  165. package/dist/events/beforeInputCommands.d.ts +2 -2
  166. package/dist/events/beforeInputCommands.js +2 -2
  167. package/dist/events/nativeInteractiveControl.d.ts +11 -1
  168. package/dist/events/nativeInteractiveControl.js +41 -1
  169. package/dist/events/onBeforeInput.js +3 -3
  170. package/dist/events/onFocus.js +9 -11
  171. package/dist/events/onPageHide.d.ts +3 -0
  172. package/dist/events/onPageHide.js +16 -0
  173. package/dist/export/forms.d.ts +50 -50
  174. package/dist/index.d.ts +4 -2
  175. package/dist/index.js +1 -1
  176. package/dist/labels.d.ts +8 -0
  177. package/dist/labels.js +6 -1
  178. package/dist/plugins/comments/CommentsController.svelte.d.ts +5 -1
  179. package/dist/plugins/comments/CommentsController.svelte.js +14 -4
  180. package/dist/plugins/find/search.js +2 -2
  181. package/dist/plugins/index.d.ts +1 -0
  182. package/dist/plugins/index.js +1 -0
  183. package/dist/plugins/suggestions/SuggestionBars.svelte +40 -23
  184. package/dist/plugins/suggestions/SuggestionBars.svelte.d.ts +3 -1
  185. package/dist/plugins/suggestions/SuggestionRuns.svelte +77 -0
  186. package/dist/plugins/suggestions/SuggestionRuns.svelte.d.ts +9 -0
  187. package/dist/plugins/suggestions/shared.svelte.d.ts +58 -0
  188. package/dist/plugins/suggestions/shared.svelte.js +421 -0
  189. package/dist/plugins/suggestions/suggestionsPlugin.d.ts +15 -4
  190. package/dist/selection/domSelection.d.ts +2 -0
  191. package/dist/selection/domSelection.js +6 -2
  192. package/dist/selection/pointer.svelte.js +75 -48
  193. package/dist/selection/replaceSelection.js +2 -6
  194. package/dist/selection/selection.svelte.d.ts +3 -3
  195. package/dist/selection/selection.svelte.js +39 -32
  196. package/dist/selection/selection.utils.d.ts +14 -0
  197. package/dist/selection/selection.utils.js +28 -3
  198. package/dist/session/commands.js +21 -11
  199. package/dist/session/composition.svelte.d.ts +5 -3
  200. package/dist/session/composition.svelte.js +21 -17
  201. package/dist/session/handles.d.ts +4 -17
  202. package/dist/session/handles.js +11 -37
  203. package/dist/session/navigation.js +6 -9
  204. package/dist/session/selection.d.ts +11 -5
  205. package/dist/session/selection.js +14 -48
  206. package/dist/session/suggestions.svelte.d.ts +38 -9
  207. package/dist/session/suggestions.svelte.js +37 -18
  208. package/dist/session/virtual.d.ts +1 -1
  209. package/dist/surface/attributes.d.ts +5 -0
  210. package/dist/surface/attributes.js +2 -2
  211. package/dist/surface/cells.d.ts +2 -26
  212. package/dist/surface/cells.js +2 -33
  213. package/dist/surface/observer.svelte.d.ts +6 -1
  214. package/dist/surface/observer.svelte.js +130 -83
  215. package/dist/surface/pin.svelte.d.ts +9 -9
  216. package/dist/surface/pin.svelte.js +15 -22
  217. package/dist/surface/projector.svelte.d.ts +15 -3
  218. package/dist/surface/projector.svelte.js +67 -22
  219. package/dist/text/text.svelte.js +14 -19
  220. package/dist/themes/notion.css +29 -0
  221. package/package.json +4 -2
  222. package/dist/crdt/placement/rank.d.ts +0 -87
  223. package/dist/crdt/placement/rank.js +0 -332
  224. package/dist/crdt/placement/source.d.ts +0 -43
  225. package/dist/crdt/placement/source.js +0 -66
  226. package/dist/crdt/text/delete-index.d.ts +0 -129
  227. package/dist/crdt/text/delete-index.js +0 -365
  228. package/dist/crdt/text/deletes.d.ts +0 -76
  229. package/dist/crdt/text/deletes.js +0 -362
  230. package/dist/crdt/text/index/anchored.d.ts +0 -17
  231. package/dist/crdt/text/index/anchored.js +0 -197
@@ -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 { DOCUMENT_WRITES } from './crdt/operations.js';
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: () => this.projector.placed(),
116
- parked: () => this.projector.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
- void: this.definitionOf(kind).void === true,
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, opts = {}) => {
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 (opts.bumpSerial !== false) {
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, { bumpSerial: false })();
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
- !ours(active) &&
689
- !ours(selection?.anchorNode) &&
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
- this.doc.on('beforeAllTransactions', this.projector.opening);
773
- this.doc.on('beforeTransaction', this.surface.pending);
774
- this.doc.on('beforeTransaction', this.projector.before);
775
- this.doc.on('afterTransaction', this.projector.after);
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
- this.doc.off('beforeAllTransactions', this.projector.opening);
778
- this.doc.off('beforeTransaction', this.surface.pending);
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 keydown = this.#withUserInput(onKeyDown.bind(this), { bumpSerial: false });
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), on(node, 'mousedown', this.selection.pointer.preventTripleClick), on(node, 'click', this.selection.pointer.tripleClick),
798
- // Settle queued mutation repairs before the native menu opens —
799
- // spellcheck suggestions are computed against the DOM at this
800
- // moment (PM flushes its DOM observer on contextmenu for the
801
- // same reason).
802
- on(node, 'contextmenu', () => void this.surface.flush()), on(node, 'beforeinput', this.#withUserInput(this.onBeforeInput)),
803
- // An `input` records what the browser did; it says nothing new about
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 presence = mount(RemoteSelections, {
809
- target: this.overlay.layer,
810
- props: { edytor: this }
811
- });
812
- // The text a `replace` suggestion removes, marked beside the host.
813
- const ranges = mount(Ranges, {
814
- target: this.overlay.layer,
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 = getActiveElement(node);
829
- if (active instanceof HTMLElement && node.contains(active))
856
+ const active = focusIn(node);
857
+ if (active instanceof HTMLElement)
830
858
  active.blur();
831
- this.selection.destroy();
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
- // Runs the whole attach-lifetime batch — DOM listeners, the mutation
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 | Text | Promise<undefined> | null | undefined;
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 | Text | Promise<undefined> | null | undefined;
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 rank of a first child, and the document's answer on placing the kind there.
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, rank: first.ranks[0] };
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`) or a nested editable island inside the host (a plugin
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`) or a nested editable island inside the host (a plugin
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
- const active = getActiveElement(node);
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 &&
@@ -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
- if (pointer.claimed)
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
- const activeElement = getActiveElement(node);
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,3 @@
1
+ import type { Edytor } from '../edytor.svelte.js';
2
+ /** End the view's session when its page leaves for good; answers the teardown. */
3
+ export declare const attachPageHide: (edytor: Edytor) => (() => void);
@@ -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');