@seamlet/astro 0.11.3 → 0.12.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/dist/bridge.js CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  shortcutOf,
19
19
  toggleList,
20
20
  toggleMark
21
- } from "./chunk-2E7VDSUK.js";
21
+ } from "./chunk-MW3BE5O6.js";
22
22
  import {
23
23
  FIELD_PATH_ATTRIBUTE,
24
24
  REVEALED_ATTRIBUTE,
@@ -26,8 +26,9 @@ import {
26
26
  REVEAL_EVENT,
27
27
  asOneParagraph,
28
28
  readRichText,
29
- resolveFieldPath
30
- } from "./chunk-DRDPKFHZ.js";
29
+ resolveFieldPath,
30
+ safeHref
31
+ } from "./chunk-7GYJS7ZT.js";
31
32
 
32
33
  // src/bridge/typed-fields.ts
33
34
  function typedKindAt(schema, path) {
@@ -55,6 +56,51 @@ function typedKindAt(schema, path) {
55
56
  }
56
57
  }
57
58
 
59
+ // src/bridge/event-listening.ts
60
+ function onElement(element, type, listener) {
61
+ return {
62
+ target: element,
63
+ type,
64
+ listener,
65
+ isCapturing: false
66
+ };
67
+ }
68
+ function onDocument(document2, type, listener) {
69
+ return {
70
+ target: document2,
71
+ type,
72
+ listener,
73
+ isCapturing: true
74
+ };
75
+ }
76
+ function onWindow(view, type, listener) {
77
+ if (view === null) return [];
78
+ return [
79
+ {
80
+ target: view,
81
+ type,
82
+ listener,
83
+ isCapturing: false
84
+ }
85
+ ];
86
+ }
87
+ function listenToAll(listenings) {
88
+ for (const { target, type, listener, isCapturing } of listenings) {
89
+ target.addEventListener(type, listener, isCapturing);
90
+ }
91
+ return () => {
92
+ for (const { target, type, listener, isCapturing } of listenings) {
93
+ target.removeEventListener(type, listener, isCapturing);
94
+ }
95
+ };
96
+ }
97
+
98
+ // src/bridge/dom-nodes.ts
99
+ function asElement(value) {
100
+ const isElement = typeof value === "object" && value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
101
+ return isElement ? value : void 0;
102
+ }
103
+
58
104
  // src/bridge/page-markers.ts
59
105
  var MARKER_SELECTOR = `[${FIELD_PATH_ATTRIBUTE}]`;
60
106
  var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
@@ -100,15 +146,13 @@ function findClosestMarkedElement(document2, path) {
100
146
  function isHtmlElement(value) {
101
147
  return asElement(value)?.namespaceURI === HTML_NAMESPACE;
102
148
  }
103
- function asElement(value) {
104
- const isElement2 = typeof value === "object" && value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
105
- return isElement2 ? value : void 0;
106
- }
107
149
 
108
150
  // src/bridge/page-reveals.ts
109
151
  var DETAILS_SELECTOR = "details";
110
152
  var REVEAL_SELECTOR = `[${REVEAL_ATTRIBUTE}]`;
111
153
  var SWITCH_SELECTOR = 'input[type="radio"], input[type="checkbox"]';
154
+ var MARKER_SELECTOR2 = `[${FIELD_PATH_ATTRIBUTE}]`;
155
+ var KEY_SEPARATOR = "\n";
112
156
  function revealAround(element) {
113
157
  for (let part = element; part !== null; ) {
114
158
  if (isDetails(part)) part.open = true;
@@ -116,70 +160,77 @@ function revealAround(element) {
116
160
  part = part.parentElement;
117
161
  }
118
162
  }
119
- function toggleDetailsAt(target, isOnField) {
163
+ function revealPartAt(target) {
120
164
  const details = detailsOfSummaryAround(target);
121
- if (details === void 0) return;
122
- if (!details.open) {
123
- details.open = true;
124
- return;
125
- }
126
- if (!isOnField) details.open = false;
165
+ if (details !== void 0) details.open = true;
166
+ const control = switchAround(target);
167
+ const isUncheckedRadio = control?.type === "radio" && !control.checked;
168
+ if (control !== void 0 && isUncheckedRadio) checkSwitch(control, true);
127
169
  }
128
- function flipSwitchAt(target, isOnField) {
170
+ function togglePartAt(target) {
171
+ const details = detailsOfSummaryAround(target);
172
+ if (details !== void 0) details.open = !details.open;
129
173
  const control = switchAround(target);
130
174
  if (control === void 0) return;
131
175
  const isRadio = control.type === "radio";
132
- const isFlipped = isRadio ? !control.checked : !isOnField;
133
- if (!isFlipped) return;
134
- control.checked = isRadio || !control.checked;
135
- announceSwitch(control);
176
+ if (isRadio && control.checked) return;
177
+ checkSwitch(control, isRadio || !control.checked);
136
178
  }
137
179
  function revealedPartsOf(document2) {
138
180
  const details = [...document2.querySelectorAll(DETAILS_SELECTOR)];
139
181
  const parts = [...document2.querySelectorAll(REVEAL_SELECTOR)];
140
182
  const switches = [...document2.querySelectorAll(SWITCH_SELECTOR)];
141
183
  return {
142
- openDetails: indexesWhere(details, (part) => isDetails(part) && part.open),
143
- revealed: indexesWhere(
144
- parts,
145
- (part) => part.hasAttribute(REVEALED_ATTRIBUTE)
146
- ),
147
- flippedSwitches: indexesWhere(
184
+ openDetails: keysWhere(details, (part) => isDetails(part) && part.open),
185
+ revealed: keysWhere(parts, (part) => part.hasAttribute(REVEALED_ATTRIBUTE)),
186
+ flippedSwitches: keysWhere(
148
187
  switches,
149
188
  (control) => isSwitch(control) && control.checked !== control.defaultChecked
150
189
  )
151
190
  };
152
191
  }
153
192
  function restoreRevealedParts(document2, revealed) {
154
- const details = [...document2.querySelectorAll(DETAILS_SELECTOR)];
155
- const parts = [...document2.querySelectorAll(REVEAL_SELECTOR)];
156
- for (const index of revealed.openDetails) {
157
- const part = details[index];
158
- if (part !== void 0 && isDetails(part)) part.open = true;
193
+ const details = partsWithKeys(
194
+ document2,
195
+ DETAILS_SELECTOR,
196
+ revealed.openDetails
197
+ );
198
+ const parts = partsWithKeys(document2, REVEAL_SELECTOR, revealed.revealed);
199
+ for (const part of details) {
200
+ if (isDetails(part)) part.open = true;
159
201
  }
160
- for (const index of revealed.revealed) {
161
- const part = parts[index];
162
- if (part !== void 0) reveal(part);
202
+ for (const part of parts) reveal(part);
203
+ const switches = partsWithKeys(
204
+ document2,
205
+ SWITCH_SELECTOR,
206
+ revealed.flippedSwitches
207
+ );
208
+ for (const control of switches) {
209
+ if (isSwitch(control)) flipFromDefault(control);
163
210
  }
164
- restoreSwitches(document2, revealed.flippedSwitches);
165
211
  }
166
- function restoreSwitches(document2, flipped) {
167
- const switches = [...document2.querySelectorAll(SWITCH_SELECTOR)];
168
- for (const index of flipped) {
169
- const control = switches[index];
170
- if (control === void 0 || !isSwitch(control)) continue;
171
- control.checked = !control.defaultChecked;
172
- announceSwitch(control);
173
- }
212
+ function flipFromDefault(control) {
213
+ checkSwitch(control, !control.defaultChecked);
214
+ }
215
+ function checkSwitch(control, isChecked) {
216
+ control.checked = isChecked;
217
+ announceSwitch(control);
218
+ }
219
+ function partsWithKeys(document2, selector, keys) {
220
+ const wanted = new Set(keys);
221
+ const parts = [...document2.querySelectorAll(selector)];
222
+ const partKeys = partKeysOf(parts);
223
+ return parts.filter((_, index) => wanted.has(partKeys[index] ?? ""));
174
224
  }
175
225
  function announceSwitch(control) {
176
226
  control.dispatchEvent(new Event("input", { bubbles: true }));
177
227
  control.dispatchEvent(new Event("change", { bubbles: true }));
178
228
  }
179
229
  function switchAround(target) {
180
- if (!isElement(target)) return void 0;
181
- const label = target.closest("label");
182
- const control = isLabel(label) ? label.control : target;
230
+ const element = asElement(target);
231
+ if (element === void 0) return void 0;
232
+ const label = element.closest("label");
233
+ const control = isLabel(label) ? label.control : element;
183
234
  return control !== null && isSwitch(control) ? control : void 0;
184
235
  }
185
236
  function reveal(part) {
@@ -188,16 +239,29 @@ function reveal(part) {
188
239
  part.dispatchEvent(new CustomEvent(REVEAL_EVENT, { bubbles: true }));
189
240
  }
190
241
  function detailsOfSummaryAround(target) {
191
- const summary = isElement(target) ? target.closest("summary") : null;
242
+ const summary = asElement(target)?.closest("summary");
192
243
  const details = summary?.parentElement;
193
244
  const isSummaryOfDetails = details !== null && details !== void 0 && isDetails(details);
194
245
  return isSummaryOfDetails ? details : void 0;
195
246
  }
196
- function indexesWhere(elements, isPicked) {
247
+ function keysWhere(elements, isPicked) {
248
+ const keys = partKeysOf(elements);
197
249
  return elements.flatMap(
198
- (element, index) => isPicked(element) ? [index] : []
250
+ (element, index) => isPicked(element) ? [keys[index] ?? ""] : []
199
251
  );
200
252
  }
253
+ function partKeysOf(parts) {
254
+ const names = parts.map(partNameOf);
255
+ return names.map((name, index) => {
256
+ const place = names.slice(0, index).filter((each) => each === name).length;
257
+ return `${name}${KEY_SEPARATOR}${String(place)}`;
258
+ });
259
+ }
260
+ function partNameOf(part) {
261
+ if (part.id !== "") return `#${part.id}`;
262
+ const marked = part.closest(MARKER_SELECTOR2) ?? part.querySelector(MARKER_SELECTOR2);
263
+ return marked?.getAttribute(FIELD_PATH_ATTRIBUTE) ?? "";
264
+ }
201
265
  function isDetails(element) {
202
266
  return element.localName === "details";
203
267
  }
@@ -207,43 +271,70 @@ function isLabel(element) {
207
271
  function isSwitch(element) {
208
272
  return element.matches(SWITCH_SELECTOR);
209
273
  }
210
- function isElement(value) {
211
- return value !== null && "nodeType" in value && value.nodeType === Node.ELEMENT_NODE;
212
- }
213
274
 
214
275
  // src/bridge/page-events.ts
215
276
  var DRAG_GONE_AFTER_MS = 150;
216
277
  function listenToPage(document2, listeners) {
217
- const view = document2.defaultView;
218
- const hover = (event) => {
219
- const link = linkToFollow(event.target);
220
- listeners.onHover(
221
- link === void 0 ? markedElementAround(event.target)?.path : void 0
222
- );
223
- listeners.onLinkHover(link);
224
- };
278
+ const fileDrags = followFileDrags(listeners);
279
+ const { onLayoutChange } = listeners;
225
280
  const leave = () => {
226
281
  listeners.onHover(void 0);
227
282
  listeners.onLinkHover(void 0);
228
283
  };
229
- const click = (event) => {
230
- event.preventDefault();
231
- const link = linkToFollow(event.target);
232
- if (link !== void 0) {
233
- listeners.onLinkClick(link);
234
- return;
235
- }
236
- const marked = markedElementAround(event.target);
237
- const isOnField = marked !== void 0;
238
- toggleDetailsAt(event.target, isOnField);
239
- flipSwitchAt(event.target, isOnField);
240
- listeners.onClick(
241
- marked === void 0 ? void 0 : { ...marked, point: { x: event.clientX, y: event.clientY } }
242
- );
243
- };
244
- const preventDefault = (event) => {
245
- event.preventDefault();
284
+ const stopListening = listenToAll([
285
+ onDocument(document2, "mouseover", (event) => {
286
+ hover(event, listeners);
287
+ }),
288
+ onDocument(document2, "click", (event) => {
289
+ click(event, listeners);
290
+ }),
291
+ onDocument(document2, "auxclick", preventDefault),
292
+ onDocument(document2, "submit", preventDefault),
293
+ onDocument(document2, "dragstart", preventDefault),
294
+ onDocument(document2, "keydown", listeners.onKeyDown),
295
+ ...fileDrags.listenings(document2),
296
+ onDocument(document2, "scroll", onLayoutChange),
297
+ // An opened or closed `<details>` moves what is below it; toggle does not bubble.
298
+ onDocument(document2, "toggle", onLayoutChange),
299
+ // A flipped switch shows and hides parts of the page.
300
+ onDocument(document2, "change", onLayoutChange),
301
+ // Images load after the page is shown and move what is below them; load does not bubble.
302
+ onDocument(document2, "load", onLayoutChange),
303
+ onElement(document2.documentElement, "mouseleave", leave),
304
+ ...onWindow(document2.defaultView, "resize", onLayoutChange)
305
+ ]);
306
+ return () => {
307
+ fileDrags.stop();
308
+ stopListening();
246
309
  };
310
+ }
311
+ function hover(event, listeners) {
312
+ const link = linkToFollow(event.target);
313
+ const path = link === void 0 ? markedElementAround(event.target)?.path : void 0;
314
+ listeners.onHover(path);
315
+ listeners.onLinkHover(link);
316
+ }
317
+ function click(event, listeners) {
318
+ event.preventDefault();
319
+ const link = linkToFollow(event.target);
320
+ if (link !== void 0) {
321
+ listeners.onLinkClick(link);
322
+ return;
323
+ }
324
+ const marked = markedElementAround(event.target);
325
+ if (marked === void 0) {
326
+ togglePartAt(event.target);
327
+ listeners.onClick(void 0);
328
+ return;
329
+ }
330
+ revealPartAt(event.target);
331
+ const point = { x: event.clientX, y: event.clientY };
332
+ listeners.onClick({ ...marked, point });
333
+ }
334
+ function preventDefault(event) {
335
+ event.preventDefault();
336
+ }
337
+ function followFileDrags(listeners) {
247
338
  let dragGoneTimer;
248
339
  const dragFile = (event) => {
249
340
  if (!carriesFiles(event)) return;
@@ -270,36 +361,15 @@ function listenToPage(document2, listeners) {
270
361
  clearTimeout(dragGoneTimer);
271
362
  dragGoneTimer = setTimeout(listeners.onFileDragEnd, DRAG_GONE_AFTER_MS);
272
363
  };
273
- const documentListeners = [
274
- ["mouseover", hover],
275
- ["click", click],
276
- ["auxclick", preventDefault],
277
- ["submit", preventDefault],
278
- ["dragstart", preventDefault],
279
- ["keydown", listeners.onKeyDown],
280
- ["dragover", dragFile],
281
- ["drop", dropFile],
282
- ["dragleave", leaveWithFile],
283
- ["scroll", listeners.onLayoutChange],
284
- // An opened or closed `<details>` moves what is below it; toggle does not bubble.
285
- ["toggle", listeners.onLayoutChange],
286
- // A flipped switch shows and hides parts of the page.
287
- ["change", listeners.onLayoutChange],
288
- // Images load after the page is shown and move what is below them; load does not bubble.
289
- ["load", listeners.onLayoutChange]
290
- ];
291
- for (const [type, listener] of documentListeners) {
292
- document2.addEventListener(type, listener, true);
293
- }
294
- document2.documentElement.addEventListener("mouseleave", leave);
295
- view?.addEventListener("resize", listeners.onLayoutChange);
296
- return () => {
297
- clearTimeout(dragGoneTimer);
298
- for (const [type, listener] of documentListeners) {
299
- document2.removeEventListener(type, listener, true);
364
+ return {
365
+ listenings: (document2) => [
366
+ onDocument(document2, "dragover", dragFile),
367
+ onDocument(document2, "drop", dropFile),
368
+ onDocument(document2, "dragleave", leaveWithFile)
369
+ ],
370
+ stop: () => {
371
+ clearTimeout(dragGoneTimer);
300
372
  }
301
- document2.documentElement.removeEventListener("mouseleave", leave);
302
- view?.removeEventListener("resize", listeners.onLayoutChange);
303
373
  };
304
374
  }
305
375
  function linkToFollow(target) {
@@ -337,13 +407,16 @@ function measureLayout(document2, link) {
337
407
  }
338
408
  );
339
409
  const root = document2.documentElement;
340
- const linkBox = link === void 0 ? void 0 : boxOf(link.element);
341
410
  return {
342
411
  markers,
343
412
  viewport: { width: root.clientWidth, height: root.clientHeight },
344
- link: link === void 0 || linkBox === void 0 ? void 0 : { href: link.href, box: linkBox }
413
+ link: link === void 0 ? void 0 : linkBoxOf(link)
345
414
  };
346
415
  }
416
+ function linkBoxOf(link) {
417
+ const box = boxOf(link.element);
418
+ return box === void 0 ? void 0 : { href: link.href, box };
419
+ }
347
420
  function createLayoutReports(document2, report) {
348
421
  const view = document2.defaultView;
349
422
  let link;
@@ -389,24 +462,18 @@ function followPresses(document2, onRelease) {
389
462
  const releaseOnMove = (event) => {
390
463
  if (event.buttons === 0) release();
391
464
  };
392
- const listeners = [
393
- ["pointerdown", press],
394
- ["pointerup", release],
395
- ["pointercancel", release],
396
- ["pointermove", releaseOnMove]
397
- ];
398
- for (const [type, listener] of listeners) {
399
- document2.addEventListener(type, listener, true);
400
- }
401
- view?.addEventListener("blur", release);
465
+ const stopListening = listenToAll([
466
+ onDocument(document2, "pointerdown", press),
467
+ onDocument(document2, "pointerup", release),
468
+ onDocument(document2, "pointercancel", release),
469
+ onDocument(document2, "pointermove", releaseOnMove),
470
+ ...onWindow(view, "blur", release)
471
+ ]);
402
472
  return {
403
473
  isPressed: () => isPressed,
404
474
  stop: () => {
405
475
  clearTimeout(releaseTimer);
406
- for (const [type, listener] of listeners) {
407
- document2.removeEventListener(type, listener, true);
408
- }
409
- view?.removeEventListener("blur", release);
476
+ stopListening();
410
477
  }
411
478
  };
412
479
  }
@@ -416,11 +483,15 @@ function createPageRenderer(options) {
416
483
  let request;
417
484
  let heldPage;
418
485
  const show = (html) => {
419
- if (!options.canReplacePage()) return;
486
+ const isPageBusy = options.isPagePressed() || !options.canReplacePage();
487
+ if (isPageBusy) {
488
+ heldPage = html;
489
+ return;
490
+ }
420
491
  replacePageBody(options.document, html);
421
492
  options.onRendered();
422
493
  };
423
- const render2 = (content) => {
494
+ const render = (content) => {
424
495
  request?.abort();
425
496
  heldPage = void 0;
426
497
  const currentRequest = new AbortController();
@@ -429,13 +500,7 @@ function createPageRenderer(options) {
429
500
  pageAddress(options.document),
430
501
  content,
431
502
  currentRequest.signal
432
- ).then((html) => {
433
- if (options.isPagePressed()) {
434
- heldPage = html;
435
- return;
436
- }
437
- show(html);
438
- }).catch((error) => {
503
+ ).then(show).catch((error) => {
439
504
  if (currentRequest.signal.aborted) return;
440
505
  options.onFailed(error);
441
506
  });
@@ -446,7 +511,7 @@ function createPageRenderer(options) {
446
511
  if (html !== void 0) show(html);
447
512
  };
448
513
  return {
449
- render: render2,
514
+ render,
450
515
  showHeldPage,
451
516
  stop: () => {
452
517
  request?.abort();
@@ -482,12 +547,14 @@ function replacePageBody(document2, html) {
482
547
  }
483
548
 
484
549
  // src/bridge/editable-elements.ts
550
+ var EDITABLE_AS_PLAIN_TEXT = "plaintext-only";
551
+ var EDITABLE_AS_RICH_TEXT = "true";
485
552
  var OUTLINE = "outline";
486
553
  var NO_OUTLINE = "none";
487
554
  var IMPORTANT = "important";
488
555
  var keptOutlines = /* @__PURE__ */ new WeakMap();
489
- function makeEditable(element, editable) {
490
- element.setAttribute("contenteditable", editable);
556
+ function makeEditable(element, editableAs) {
557
+ element.setAttribute("contenteditable", editableAs);
491
558
  if (keptOutlines.has(element)) return;
492
559
  const { style } = element;
493
560
  keptOutlines.set(element, {
@@ -569,45 +636,56 @@ function whiteSpaceOf(element) {
569
636
  return view === null ? "normal" : view.getComputedStyle(element).whiteSpace;
570
637
  }
571
638
 
572
- // src/bridge/inline-text-editing.ts
573
- var EDITABLE_AS_PLAIN_TEXT = "plaintext-only";
574
- function startInlineTextEditing(element, options) {
575
- const reportInput = () => {
576
- options.onInput(readElementText(element));
577
- };
639
+ // src/bridge/typing-sessions.ts
640
+ function startTypingSession(element, options) {
578
641
  const endOnKey = (event) => {
579
- const isEnd = event.key === "Enter" && options.isSingleLine;
580
- if (event.key === "Escape") {
581
- event.preventDefault();
582
- end("reverted");
583
- } else if (isEnd) {
584
- event.preventDefault();
585
- end("kept");
586
- }
587
- };
588
- const endOnBlur = () => {
589
- end("kept");
590
- };
591
- const stop = () => {
592
- element.removeEventListener("input", reportInput);
593
- element.removeEventListener("keydown", endOnKey);
594
- element.removeEventListener("blur", endOnBlur);
595
- makeUneditable(element);
642
+ const outcome = outcomeOfKey(event, options.isEndingKey);
643
+ if (outcome === void 0) return;
644
+ event.preventDefault();
645
+ end(outcome);
596
646
  };
597
647
  let hasEnded = false;
598
648
  const end = (outcome) => {
599
649
  if (hasEnded) return;
600
650
  hasEnded = true;
601
- stop();
651
+ stopListening();
652
+ options.onStop();
653
+ makeUneditable(element);
602
654
  options.onEnd(outcome);
603
655
  };
604
- makeEditable(element, EDITABLE_AS_PLAIN_TEXT);
605
- element.addEventListener("input", reportInput);
606
- element.addEventListener("keydown", endOnKey);
607
- element.addEventListener("blur", endOnBlur);
656
+ makeEditable(element, options.editableAs);
657
+ const stopListening = listenToAll([
658
+ onElement(element, "keydown", endOnKey),
659
+ ...options.listeners
660
+ ]);
608
661
  element.focus();
662
+ return { end };
663
+ }
664
+ function outcomeOfKey(event, isEndingKey) {
665
+ if (event.key === "Escape") return "reverted";
666
+ return isEndingKey(event) ? "kept" : void 0;
667
+ }
668
+
669
+ // src/bridge/inline-text-editing.ts
670
+ function startInlineTextEditing(element, options) {
671
+ const reportInput = () => {
672
+ options.onInput(readElementText(element));
673
+ };
674
+ const endOnBlur = () => {
675
+ session.end("kept");
676
+ };
677
+ const session = startTypingSession(element, {
678
+ editableAs: EDITABLE_AS_PLAIN_TEXT,
679
+ isEndingKey: (event) => event.key === "Enter" && options.isSingleLine,
680
+ listeners: [
681
+ onElement(element, "input", reportInput),
682
+ onElement(element, "blur", endOnBlur)
683
+ ],
684
+ onStop: () => void 0,
685
+ onEnd: options.onEnd
686
+ });
609
687
  return () => {
610
- end("kept");
688
+ session.end("kept");
611
689
  };
612
690
  }
613
691
  function placeCaretAt(element, point) {
@@ -637,145 +715,47 @@ function caretRangeAt(document2, point) {
637
715
  return lookup.caretRangeFromPoint?.(point.x, point.y) ?? void 0;
638
716
  }
639
717
 
640
- // src/bridge/rich-text-editing.ts
641
- var EDITABLE_AS_RICH_TEXT = "true";
642
- var PASTED_LINE_BREAKS = /\s*\n\s*/gu;
643
- var SPACE = " ";
644
- function canEditRichTextInPlace(element, value, isInline) {
645
- if (hasMarkedDescendants(element)) return false;
646
- const normalized = readRichText(value) ?? [];
647
- const expected = isInline ? asOneParagraph(normalized) : normalized;
648
- const shown = readTextOf(element, isInline);
649
- return JSON.stringify(shown) === JSON.stringify(expected);
650
- }
651
- function startRichTextEditing(element, options) {
718
+ // src/bridge/typed-selections.ts
719
+ function createSelectionReports(element, readText, onSelect) {
652
720
  const document2 = element.ownerDocument;
653
721
  const view = document2.defaultView;
654
722
  let lastSelection;
655
723
  let reportFrame;
656
- let blurTimer;
657
- const readText = () => readTextOf(element, options.isInline);
658
724
  const liveSelection = () => isSelectionIn(element) ? readSelectionFrom(element) : void 0;
659
- const reportSelection = () => {
725
+ const report = () => {
660
726
  reportFrame = void 0;
661
- options.onSelect(measureSelection(element, readText(), lastSelection));
727
+ onSelect(measureSelection(element, readText(), lastSelection));
662
728
  };
663
- const scheduleReport = () => {
729
+ const schedule = () => {
664
730
  if (reportFrame !== void 0) return;
665
- reportFrame = view?.requestAnimationFrame(reportSelection);
731
+ reportFrame = view?.requestAnimationFrame(report);
666
732
  };
667
- const followSelection = () => {
733
+ const follow = () => {
668
734
  const selection = document2.hasFocus() ? liveSelection() : void 0;
669
735
  if (selection === void 0) return;
670
736
  lastSelection = selection;
671
- scheduleReport();
672
- };
673
- const reportInput = () => {
674
- options.onInput(readText());
675
- followSelection();
737
+ schedule();
676
738
  };
677
- const change = (changed, selection) => {
678
- render(element, changed, options.isInline);
739
+ const keep = (selection) => {
679
740
  lastSelection = selection;
680
741
  if (document2.hasFocus()) selectIn(element, selection);
681
- options.onInput(changed);
682
- scheduleReport();
742
+ schedule();
683
743
  };
684
- const format = (requested) => {
685
- const selection = liveSelection() ?? lastSelection;
686
- const isListInLine = requested === "list" && options.isInline;
687
- if (selection === void 0 || isListInLine) return;
688
- const current = readText();
689
- const changed = requested === "list" ? toggleList(current, selection) : toggleMark(current, selection, requested);
690
- change(changed, selection);
691
- };
692
- const linkTo = (href) => {
693
- const selection = lastSelection;
694
- if (selection === void 0) return;
695
- const current = readText();
696
- const target = isCollapsed(selection) ? linkSelectionAt(current, selection.start) : selection;
697
- if (target === void 0) return;
698
- change(setLink(current, target, href), selection);
699
- };
700
- const pressKey = (event) => {
701
- const isEnd = event.key === "Enter" && options.isInline && !event.shiftKey;
702
- const shortcut = formatShortcutOf(event);
703
- if (event.key === "Escape") {
704
- event.preventDefault();
705
- end("reverted");
706
- } else if (isEnd) {
707
- event.preventDefault();
708
- end("kept");
709
- } else if (shortcut !== void 0) {
710
- event.preventDefault();
711
- if (shortcut !== "none") format(shortcut);
712
- }
713
- };
714
- const pastePlainText = (event) => {
715
- event.preventDefault();
716
- const pasted = event.clipboardData?.getData("text/plain") ?? "";
717
- const words = options.isInline ? pasted.replace(PASTED_LINE_BREAKS, SPACE) : pasted;
718
- insertPlainText(element, words);
719
- reportInput();
720
- };
721
- const refuseDrop = (event) => {
722
- event.preventDefault();
723
- };
724
- const endOnBlur = () => {
725
- clearTimeout(blurTimer);
726
- blurTimer = setTimeout(() => {
727
- const isFocusOnPage = document2.hasFocus();
728
- const isFocusInText = document2.activeElement === element;
729
- if (isFocusOnPage && !isFocusInText) end("kept");
730
- });
731
- };
732
- const stopListening = () => {
733
- clearTimeout(blurTimer);
734
- if (reportFrame !== void 0) view?.cancelAnimationFrame(reportFrame);
735
- element.removeEventListener("input", reportInput);
736
- element.removeEventListener("keydown", pressKey);
737
- element.removeEventListener("paste", pastePlainText);
738
- element.removeEventListener("drop", refuseDrop);
739
- element.removeEventListener("blur", endOnBlur);
740
- document2.removeEventListener("selectionchange", followSelection);
741
- document2.removeEventListener("scroll", scheduleReport, true);
742
- view?.removeEventListener("resize", scheduleReport);
743
- makeUneditable(element);
744
- };
745
- let hasEnded = false;
746
- const end = (outcome) => {
747
- if (hasEnded) return;
748
- hasEnded = true;
749
- stopListening();
750
- options.onSelect(void 0);
751
- options.onEnd(outcome);
752
- };
753
- makeEditable(element, EDITABLE_AS_RICH_TEXT);
754
- element.addEventListener("input", reportInput);
755
- element.addEventListener("keydown", pressKey);
756
- element.addEventListener("paste", pastePlainText);
757
- element.addEventListener("drop", refuseDrop);
758
- element.addEventListener("blur", endOnBlur);
759
- document2.addEventListener("selectionchange", followSelection);
760
- document2.addEventListener("scroll", scheduleReport, true);
761
- view?.addEventListener("resize", scheduleReport);
762
- element.focus();
763
744
  return {
764
- format,
765
- setLink: linkTo,
745
+ listeners: [
746
+ onDocument(document2, "selectionchange", follow),
747
+ onDocument(document2, "scroll", schedule),
748
+ ...onWindow(view, "resize", schedule)
749
+ ],
750
+ current: () => liveSelection() ?? lastSelection,
751
+ last: () => lastSelection,
752
+ follow,
753
+ keep,
766
754
  stop: () => {
767
- end("kept");
755
+ if (reportFrame !== void 0) view?.cancelAnimationFrame(reportFrame);
768
756
  }
769
757
  };
770
758
  }
771
- function readTextOf(element, isInline) {
772
- const read = readRichTextFrom(element);
773
- return isInline ? asOneParagraph(read) : read;
774
- }
775
- function render(element, text, isInline) {
776
- if (isInline) renderInlineRichText(element, text);
777
- else renderRichText(element, text);
778
- }
779
759
  function measureSelection(element, text, selection) {
780
760
  if (selection === void 0) return void 0;
781
761
  const range = isSelectionIn(element) ? element.ownerDocument.getSelection()?.getRangeAt(0) : rangeOf(element, selection);
@@ -789,7 +769,8 @@ function measureSelection(element, text, selection) {
789
769
  }
790
770
  function boxOfRange(range) {
791
771
  const whole = range.getBoundingClientRect();
792
- const rectangle = whole.width > 0 || whole.height > 0 ? whole : range.getClientRects()[0];
772
+ const hasOwnBox = whole.width > 0 || whole.height > 0;
773
+ const rectangle = hasOwnBox ? whole : range.getClientRects()[0];
793
774
  if (rectangle === void 0) return void 0;
794
775
  const isShown = rectangle.width > 0 || rectangle.height > 0;
795
776
  if (!isShown) return void 0;
@@ -806,6 +787,118 @@ function isSelectionIn(element) {
806
787
  const hasRange = (selection?.rangeCount ?? 0) > 0;
807
788
  return hasRange && anchor !== null && element.contains(anchor);
808
789
  }
790
+
791
+ // src/bridge/rich-text-editing.ts
792
+ function canEditRichTextInPlace(element, value, layout) {
793
+ if (hasMarkedDescendants(element)) return false;
794
+ const normalized = readRichText(value) ?? [];
795
+ const expected = layout.shapeOf(normalized);
796
+ const shown = layout.read(element);
797
+ return JSON.stringify(shown) === JSON.stringify(expected);
798
+ }
799
+ function startRichTextEditing(element, options) {
800
+ const { layout } = options;
801
+ const readText = () => layout.read(element);
802
+ const selections = createSelectionReports(
803
+ element,
804
+ readText,
805
+ options.onSelect
806
+ );
807
+ const reportInput = () => {
808
+ options.onInput(readText());
809
+ selections.follow();
810
+ };
811
+ const change = (changed, selection) => {
812
+ layout.render(element, changed);
813
+ selections.keep(selection);
814
+ options.onInput(changed);
815
+ };
816
+ const format = (requested) => {
817
+ const selection = selections.current();
818
+ const isRefusedList = requested === "list" && !layout.canHoldLists;
819
+ if (selection === void 0 || isRefusedList) return;
820
+ change(formatted(readText(), selection, requested), selection);
821
+ };
822
+ const linkTo = (href) => {
823
+ const selection = selections.last();
824
+ const isUnsafe = href !== void 0 && safeHref(href) === void 0;
825
+ if (selection === void 0 || isUnsafe) return;
826
+ const current = readText();
827
+ const target = isCollapsed(selection) ? linkSelectionAt(current, selection.start) : selection;
828
+ if (target === void 0) return;
829
+ change(setLink(current, target, href), selection);
830
+ };
831
+ const blurs = endWhenFocusMovesOnPage(element, () => {
832
+ session.end("kept");
833
+ });
834
+ const session = startTypingSession(element, {
835
+ editableAs: EDITABLE_AS_RICH_TEXT,
836
+ isEndingKey: layout.isEndingKey,
837
+ listeners: [
838
+ onElement(element, "input", reportInput),
839
+ onElement(element, "keydown", (event) => {
840
+ formatOnShortcut(event, format);
841
+ }),
842
+ onElement(element, "paste", (event) => {
843
+ pastePlainText(event, element, layout);
844
+ reportInput();
845
+ }),
846
+ onElement(element, "drop", refuseDrop),
847
+ onElement(element, "blur", blurs.listener),
848
+ ...selections.listeners
849
+ ],
850
+ onStop: () => {
851
+ blurs.stop();
852
+ selections.stop();
853
+ },
854
+ onEnd: (outcome) => {
855
+ options.onSelect(void 0);
856
+ options.onEnd(outcome);
857
+ }
858
+ });
859
+ return {
860
+ format,
861
+ setLink: linkTo,
862
+ stop: () => {
863
+ session.end("kept");
864
+ }
865
+ };
866
+ }
867
+ function formatted(text, selection, format) {
868
+ return format === "list" ? toggleList(text, selection) : toggleMark(text, selection, format);
869
+ }
870
+ function formatOnShortcut(event, format) {
871
+ const shortcut = formatShortcutOf(event);
872
+ if (shortcut === void 0) return;
873
+ event.preventDefault();
874
+ if (shortcut !== "refused") format(shortcut);
875
+ }
876
+ function pastePlainText(event, element, layout) {
877
+ event.preventDefault();
878
+ const pasted = event.clipboardData?.getData("text/plain") ?? "";
879
+ insertPlainText(element, layout.pastedWords(pasted));
880
+ }
881
+ function refuseDrop(event) {
882
+ event.preventDefault();
883
+ }
884
+ function endWhenFocusMovesOnPage(element, end) {
885
+ let blurTimer;
886
+ const listener = () => {
887
+ clearTimeout(blurTimer);
888
+ blurTimer = setTimeout(() => {
889
+ const { ownerDocument } = element;
890
+ const isFocusOnPage = ownerDocument.hasFocus();
891
+ const isFocusInText = ownerDocument.activeElement === element;
892
+ if (isFocusOnPage && !isFocusInText) end();
893
+ });
894
+ };
895
+ return {
896
+ listener,
897
+ stop: () => {
898
+ clearTimeout(blurTimer);
899
+ }
900
+ };
901
+ }
809
902
  function formatShortcutOf(event) {
810
903
  const hasCommandKey = event.metaKey || event.ctrlKey;
811
904
  if (!hasCommandKey || event.altKey || event.shiftKey) return void 0;
@@ -815,12 +908,35 @@ function formatShortcutOf(event) {
815
908
  case "KeyI":
816
909
  return "italic";
817
910
  case "KeyU":
818
- return "none";
911
+ return "refused";
819
912
  default:
820
913
  return void 0;
821
914
  }
822
915
  }
823
916
 
917
+ // src/bridge/rich-text-layouts.ts
918
+ var PASTED_LINE_BREAKS = /\s*\n\s*/gu;
919
+ var SPACE = " ";
920
+ var BLOCKS_LAYOUT = {
921
+ shapeOf: (text) => text,
922
+ read: readRichTextFrom,
923
+ render: renderRichText,
924
+ canHoldLists: true,
925
+ isEndingKey: () => false,
926
+ pastedWords: (pasted) => pasted
927
+ };
928
+ var LINE_LAYOUT = {
929
+ shapeOf: asOneParagraph,
930
+ read: (element) => asOneParagraph(readRichTextFrom(element)),
931
+ render: renderInlineRichText,
932
+ canHoldLists: false,
933
+ isEndingKey: (event) => event.key === "Enter" && !event.shiftKey,
934
+ pastedWords: (pasted) => pasted.replace(PASTED_LINE_BREAKS, SPACE)
935
+ };
936
+ function richTextLayoutOf(isInline) {
937
+ return isInline ? LINE_LAYOUT : BLOCKS_LAYOUT;
938
+ }
939
+
824
940
  // src/bridge/page-typing.ts
825
941
  var PREPARED_TYPING_TIMEOUT_MS = 3e3;
826
942
  var EDITABLE_VALUES = {
@@ -828,93 +944,111 @@ var EDITABLE_VALUES = {
828
944
  rich: EDITABLE_AS_RICH_TEXT
829
945
  };
830
946
  function createPageTyping(options) {
831
- let stopEditing;
832
- let richEditing;
947
+ const { send } = options;
948
+ let typing;
833
949
  let prepared;
834
- const takePrepared = (isTyped) => {
950
+ const isTyping = () => typing !== void 0 || prepared !== void 0;
951
+ const reportIfOver = () => {
952
+ if (!isTyping()) options.onTypingOver();
953
+ };
954
+ const releasePrepared = () => {
835
955
  if (prepared === void 0) return;
836
956
  clearTimeout(prepared.timer);
837
- if (!isTyped && stopEditing === void 0) {
957
+ if (typing === void 0) {
838
958
  prepared.element.blur();
839
959
  makeUneditable(prepared.element);
840
960
  }
841
961
  prepared = void 0;
842
962
  };
963
+ const handOverPrepared = () => {
964
+ if (prepared !== void 0) clearTimeout(prepared.timer);
965
+ prepared = void 0;
966
+ };
843
967
  const prepare = (element, kind) => {
844
968
  const isTypedAlready = element.hasAttribute("contenteditable");
845
969
  if (isTypedAlready) return;
846
- takePrepared(false);
970
+ releasePrepared();
847
971
  makeEditable(element, EDITABLE_VALUES[kind]);
848
972
  element.focus({ preventScroll: true });
849
973
  const timer = setTimeout(() => {
850
- takePrepared(false);
974
+ releasePrepared();
975
+ reportIfOver();
851
976
  }, PREPARED_TYPING_TIMEOUT_MS);
852
977
  prepared = { element, timer };
853
978
  };
854
979
  const stop = () => {
855
- stopEditing?.();
980
+ typing?.stop();
856
981
  };
857
- const ended = (path) => (outcome) => {
858
- stopEditing = void 0;
859
- richEditing = void 0;
860
- options.send({ type: "typing-ended", path, outcome });
982
+ const endedAt = (path) => (outcome) => {
983
+ typing = void 0;
984
+ send({ type: "typing-ended", path, outcome });
985
+ reportIfOver();
861
986
  };
862
- const start = (request) => {
987
+ const begin = (start) => {
863
988
  stop();
864
- const { path } = request;
989
+ const { path } = start;
865
990
  const element = findMarkedElement(options.document, path);
866
- const canType = element !== void 0 && canEditTextInPlace(element, request.text);
867
- if (!canType) {
868
- takePrepared(false);
869
- options.send({ type: "typing-refused", path });
991
+ if (element === void 0 || !start.canTypeIn(element)) {
992
+ releasePrepared();
993
+ send({ type: "typing-refused", path });
994
+ reportIfOver();
870
995
  return;
871
996
  }
872
- takePrepared(true);
873
- options.send({ type: "typing-started", path });
874
- stopEditing = startInlineTextEditing(element, {
875
- isSingleLine: request.isSingleLine,
876
- onInput: (text) => {
877
- options.send({ type: "typed", path, text });
878
- options.onInput();
879
- },
880
- onEnd: ended(path)
997
+ handOverPrepared();
998
+ send({ type: "typing-started", path });
999
+ typing = start.typeIn(element, endedAt(path));
1000
+ placeCaretAt(element, start.point);
1001
+ };
1002
+ const reportTyped = (message) => {
1003
+ send(message);
1004
+ options.onInput();
1005
+ };
1006
+ const typePlainText = (request) => {
1007
+ const { path, text } = request;
1008
+ begin({
1009
+ path,
1010
+ point: request.point,
1011
+ canTypeIn: (element) => canEditTextInPlace(element, text),
1012
+ typeIn: (element, onEnd) => ({
1013
+ stop: startInlineTextEditing(element, {
1014
+ isSingleLine: request.isSingleLine,
1015
+ onInput: (typed) => {
1016
+ reportTyped({ type: "typed", path, text: typed });
1017
+ },
1018
+ onEnd
1019
+ })
1020
+ })
881
1021
  });
882
- placeCaretAt(element, request.point);
883
1022
  };
884
- const startRich = (request) => {
885
- stop();
886
- const { path, isInline } = request;
887
- const element = findMarkedElement(options.document, path);
888
- const canType = element !== void 0 && canEditRichTextInPlace(element, request.text, isInline);
889
- if (!canType) {
890
- takePrepared(false);
891
- options.send({ type: "typing-refused", path });
892
- return;
893
- }
894
- takePrepared(true);
895
- options.send({ type: "typing-started", path });
896
- const editing = startRichTextEditing(element, {
897
- isInline,
898
- onInput: (text) => {
899
- options.send({ type: "rich-typed", path, text });
900
- options.onInput();
901
- },
902
- onSelect: (selection) => {
903
- options.send({ type: "typed-selection", path, selection });
904
- },
905
- onEnd: ended(path)
1023
+ const typeRichText = (request) => {
1024
+ const { path, text } = request;
1025
+ const layout = richTextLayoutOf(request.isInline);
1026
+ begin({
1027
+ path,
1028
+ point: request.point,
1029
+ canTypeIn: (element) => canEditRichTextInPlace(element, text, layout),
1030
+ typeIn: (element, onEnd) => {
1031
+ const editing = startRichTextEditing(element, {
1032
+ layout,
1033
+ onInput: (typed) => {
1034
+ reportTyped({ type: "rich-typed", path, text: typed });
1035
+ },
1036
+ onSelect: (selection) => {
1037
+ send({ type: "typed-selection", path, selection });
1038
+ },
1039
+ onEnd
1040
+ });
1041
+ return { stop: editing.stop, richText: editing };
1042
+ }
906
1043
  });
907
- richEditing = editing;
908
- stopEditing = editing.stop;
909
- placeCaretAt(element, request.point);
910
1044
  };
911
1045
  return {
912
1046
  prepare,
913
- isTyping: () => stopEditing !== void 0 || prepared !== void 0,
914
- start,
915
- startRich,
916
- format: (format) => richEditing?.format(format),
917
- setLink: (href) => richEditing?.setLink(href),
1047
+ isTyping,
1048
+ start: typePlainText,
1049
+ startRich: typeRichText,
1050
+ format: (format) => typing?.richText?.format(format),
1051
+ setLink: (href) => typing?.richText?.setLink(href),
918
1052
  stop
919
1053
  };
920
1054
  }
@@ -950,10 +1084,16 @@ function connectPage(document2, schema, send) {
950
1084
  const layout = createLayoutReports(document2, (measured) => {
951
1085
  send({ type: "layout", layout: measured });
952
1086
  });
953
- const typing = createPageTyping({ document: document2, send, onInput: layout.schedule });
954
- const presses = followPresses(document2, () => {
1087
+ const showHeldPage = () => {
955
1088
  renderer.showHeldPage();
1089
+ };
1090
+ const typing = createPageTyping({
1091
+ document: document2,
1092
+ send,
1093
+ onInput: layout.schedule,
1094
+ onTypingOver: showHeldPage
956
1095
  });
1096
+ const presses = followPresses(document2, showHeldPage);
957
1097
  const renderer = createPageRenderer({
958
1098
  document: document2,
959
1099
  // Replacing the page under a text being typed would throw the typing away.
@@ -968,31 +1108,45 @@ function connectPage(document2, schema, send) {
968
1108
  send({ type: "render-failed" });
969
1109
  }
970
1110
  });
971
- const pressKey = (event) => {
972
- const key = editorKeyOf(event);
973
- const isTypingEscape = key === "escape" && typing.isTyping();
974
- if (key === void 0 || isTypingEscape) return;
975
- event.preventDefault();
976
- typing.stop();
977
- send({ type: "key", key });
1111
+ const page = {
1112
+ document: document2,
1113
+ schema,
1114
+ send,
1115
+ layout,
1116
+ typing,
1117
+ renderer
978
1118
  };
979
- const stopListening = listenToPage(document2, {
1119
+ const stopListening = listenToPage(document2, pageListenersOf(page));
1120
+ layout.reportNow();
1121
+ return {
1122
+ receive: (message) => {
1123
+ receiveEditorMessage(page, message);
1124
+ },
1125
+ stop: () => {
1126
+ typing.stop();
1127
+ renderer.stop();
1128
+ presses.stop();
1129
+ layout.stop();
1130
+ stopListening();
1131
+ }
1132
+ };
1133
+ }
1134
+ function pageListenersOf(page) {
1135
+ const { schema, send, layout } = page;
1136
+ return {
980
1137
  onHover: (path) => {
981
1138
  send({ type: "hover", path });
982
1139
  },
983
1140
  onClick: (pointer) => {
984
- const kind = pointer === void 0 ? void 0 : typedKindAt(schema, pointer.path);
985
- if (pointer !== void 0 && kind !== void 0) {
986
- typing.prepare(pointer.element, kind);
987
- }
988
- const target = pointer === void 0 ? void 0 : { path: pointer.path, point: pointer.point };
989
- send({ type: "click", target });
1141
+ clickOn(page, pointer);
990
1142
  },
991
1143
  onLinkHover: layout.hoverLink,
992
1144
  onLinkClick: (link) => {
993
1145
  send({ type: "link-click", href: link.href });
994
1146
  },
995
- onKeyDown: pressKey,
1147
+ onKeyDown: (event) => {
1148
+ pressKey(page, event);
1149
+ },
996
1150
  onFileDragOver: (path) => {
997
1151
  const isTaken = imageFieldAt(schema, path) !== void 0;
998
1152
  send({ type: "file-drag", path: isTaken ? path : void 0 });
@@ -1006,45 +1160,55 @@ function connectPage(document2, schema, send) {
1006
1160
  if (isTaken) send({ type: "file-drop", path, file });
1007
1161
  },
1008
1162
  onLayoutChange: layout.schedule
1009
- });
1010
- layout.reportNow();
1011
- const receive = (message) => {
1012
- switch (message.type) {
1013
- case "connect":
1014
- break;
1015
- case "render":
1016
- renderer.render(message.content);
1017
- break;
1018
- case "start-typing":
1019
- typing.start(message);
1020
- break;
1021
- case "start-rich-typing":
1022
- typing.startRich(message);
1023
- break;
1024
- case "format-typed":
1025
- typing.format(message.format);
1026
- break;
1027
- case "link-typed":
1028
- typing.setLink(message.href);
1029
- break;
1030
- case "stop-typing":
1031
- typing.stop();
1032
- break;
1033
- case "scroll-to":
1034
- scrollToField(document2, message.path);
1035
- break;
1036
- }
1037
1163
  };
1038
- return {
1039
- receive,
1040
- stop: () => {
1164
+ }
1165
+ function clickOn(page, pointer) {
1166
+ if (pointer === void 0) {
1167
+ page.send({ type: "click", target: void 0 });
1168
+ return;
1169
+ }
1170
+ const kind = typedKindAt(page.schema, pointer.path);
1171
+ if (kind !== void 0) page.typing.prepare(pointer.element, kind);
1172
+ page.send({
1173
+ type: "click",
1174
+ target: { path: pointer.path, point: pointer.point }
1175
+ });
1176
+ }
1177
+ function pressKey(page, event) {
1178
+ const key = editorKeyOf(event);
1179
+ const isTypingEscape = key === "escape" && page.typing.isTyping();
1180
+ if (key === void 0 || isTypingEscape) return;
1181
+ event.preventDefault();
1182
+ page.typing.stop();
1183
+ page.send({ type: "key", key });
1184
+ }
1185
+ function receiveEditorMessage(page, message) {
1186
+ const { typing } = page;
1187
+ switch (message.type) {
1188
+ case "connect":
1189
+ break;
1190
+ case "render":
1191
+ page.renderer.render(message.content);
1192
+ break;
1193
+ case "start-typing":
1194
+ typing.start(message);
1195
+ break;
1196
+ case "start-rich-typing":
1197
+ typing.startRich(message);
1198
+ break;
1199
+ case "format-typed":
1200
+ typing.format(message.format);
1201
+ break;
1202
+ case "link-typed":
1203
+ typing.setLink(message.href);
1204
+ break;
1205
+ case "stop-typing":
1041
1206
  typing.stop();
1042
- renderer.stop();
1043
- presses.stop();
1044
- layout.stop();
1045
- stopListening();
1046
- }
1047
- };
1207
+ break;
1208
+ case "scroll-to":
1209
+ scrollToField(page.document, message.path);
1210
+ break;
1211
+ }
1048
1212
  }
1049
1213
  function scrollToField(document2, path) {
1050
1214
  const element = findClosestMarkedElement(document2, path);