@seamlet/astro 0.11.2 → 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;
@@ -372,30 +445,77 @@ function createLayoutReports(document2, report) {
372
445
  };
373
446
  }
374
447
 
448
+ // src/bridge/page-presses.ts
449
+ function followPresses(document2, onRelease) {
450
+ const view = document2.defaultView;
451
+ let isPressed = false;
452
+ let releaseTimer;
453
+ const press = () => {
454
+ clearTimeout(releaseTimer);
455
+ isPressed = true;
456
+ };
457
+ const release = () => {
458
+ if (!isPressed) return;
459
+ isPressed = false;
460
+ releaseTimer = setTimeout(onRelease, 0);
461
+ };
462
+ const releaseOnMove = (event) => {
463
+ if (event.buttons === 0) release();
464
+ };
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
+ ]);
472
+ return {
473
+ isPressed: () => isPressed,
474
+ stop: () => {
475
+ clearTimeout(releaseTimer);
476
+ stopListening();
477
+ }
478
+ };
479
+ }
480
+
375
481
  // src/bridge/page-rendering.ts
376
482
  function createPageRenderer(options) {
377
483
  let request;
378
- const render2 = (content) => {
484
+ let heldPage;
485
+ const show = (html) => {
486
+ const isPageBusy = options.isPagePressed() || !options.canReplacePage();
487
+ if (isPageBusy) {
488
+ heldPage = html;
489
+ return;
490
+ }
491
+ replacePageBody(options.document, html);
492
+ options.onRendered();
493
+ };
494
+ const render = (content) => {
379
495
  request?.abort();
496
+ heldPage = void 0;
380
497
  const currentRequest = new AbortController();
381
498
  request = currentRequest;
382
499
  fetchRenderedPage(
383
500
  pageAddress(options.document),
384
501
  content,
385
502
  currentRequest.signal
386
- ).then((html) => {
387
- if (!options.canReplacePage()) return;
388
- replacePageBody(options.document, html);
389
- options.onRendered();
390
- }).catch((error) => {
503
+ ).then(show).catch((error) => {
391
504
  if (currentRequest.signal.aborted) return;
392
505
  options.onFailed(error);
393
506
  });
394
507
  };
508
+ const showHeldPage = () => {
509
+ const html = heldPage;
510
+ heldPage = void 0;
511
+ if (html !== void 0) show(html);
512
+ };
395
513
  return {
396
- render: render2,
514
+ render,
515
+ showHeldPage,
397
516
  stop: () => {
398
517
  request?.abort();
518
+ heldPage = void 0;
399
519
  }
400
520
  };
401
521
  }
@@ -427,12 +547,14 @@ function replacePageBody(document2, html) {
427
547
  }
428
548
 
429
549
  // src/bridge/editable-elements.ts
550
+ var EDITABLE_AS_PLAIN_TEXT = "plaintext-only";
551
+ var EDITABLE_AS_RICH_TEXT = "true";
430
552
  var OUTLINE = "outline";
431
553
  var NO_OUTLINE = "none";
432
554
  var IMPORTANT = "important";
433
555
  var keptOutlines = /* @__PURE__ */ new WeakMap();
434
- function makeEditable(element, editable) {
435
- element.setAttribute("contenteditable", editable);
556
+ function makeEditable(element, editableAs) {
557
+ element.setAttribute("contenteditable", editableAs);
436
558
  if (keptOutlines.has(element)) return;
437
559
  const { style } = element;
438
560
  keptOutlines.set(element, {
@@ -514,45 +636,56 @@ function whiteSpaceOf(element) {
514
636
  return view === null ? "normal" : view.getComputedStyle(element).whiteSpace;
515
637
  }
516
638
 
517
- // src/bridge/inline-text-editing.ts
518
- var EDITABLE_AS_PLAIN_TEXT = "plaintext-only";
519
- function startInlineTextEditing(element, options) {
520
- const reportInput = () => {
521
- options.onInput(readElementText(element));
522
- };
639
+ // src/bridge/typing-sessions.ts
640
+ function startTypingSession(element, options) {
523
641
  const endOnKey = (event) => {
524
- const isEnd = event.key === "Enter" && options.isSingleLine;
525
- if (event.key === "Escape") {
526
- event.preventDefault();
527
- end("reverted");
528
- } else if (isEnd) {
529
- event.preventDefault();
530
- end("kept");
531
- }
532
- };
533
- const endOnBlur = () => {
534
- end("kept");
535
- };
536
- const stop = () => {
537
- element.removeEventListener("input", reportInput);
538
- element.removeEventListener("keydown", endOnKey);
539
- element.removeEventListener("blur", endOnBlur);
540
- makeUneditable(element);
642
+ const outcome = outcomeOfKey(event, options.isEndingKey);
643
+ if (outcome === void 0) return;
644
+ event.preventDefault();
645
+ end(outcome);
541
646
  };
542
647
  let hasEnded = false;
543
648
  const end = (outcome) => {
544
649
  if (hasEnded) return;
545
650
  hasEnded = true;
546
- stop();
651
+ stopListening();
652
+ options.onStop();
653
+ makeUneditable(element);
547
654
  options.onEnd(outcome);
548
655
  };
549
- makeEditable(element, EDITABLE_AS_PLAIN_TEXT);
550
- element.addEventListener("input", reportInput);
551
- element.addEventListener("keydown", endOnKey);
552
- element.addEventListener("blur", endOnBlur);
656
+ makeEditable(element, options.editableAs);
657
+ const stopListening = listenToAll([
658
+ onElement(element, "keydown", endOnKey),
659
+ ...options.listeners
660
+ ]);
553
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
+ });
554
687
  return () => {
555
- end("kept");
688
+ session.end("kept");
556
689
  };
557
690
  }
558
691
  function placeCaretAt(element, point) {
@@ -582,145 +715,47 @@ function caretRangeAt(document2, point) {
582
715
  return lookup.caretRangeFromPoint?.(point.x, point.y) ?? void 0;
583
716
  }
584
717
 
585
- // src/bridge/rich-text-editing.ts
586
- var EDITABLE_AS_RICH_TEXT = "true";
587
- var PASTED_LINE_BREAKS = /\s*\n\s*/gu;
588
- var SPACE = " ";
589
- function canEditRichTextInPlace(element, value, isInline) {
590
- if (hasMarkedDescendants(element)) return false;
591
- const normalized = readRichText(value) ?? [];
592
- const expected = isInline ? asOneParagraph(normalized) : normalized;
593
- const shown = readTextOf(element, isInline);
594
- return JSON.stringify(shown) === JSON.stringify(expected);
595
- }
596
- function startRichTextEditing(element, options) {
718
+ // src/bridge/typed-selections.ts
719
+ function createSelectionReports(element, readText, onSelect) {
597
720
  const document2 = element.ownerDocument;
598
721
  const view = document2.defaultView;
599
722
  let lastSelection;
600
723
  let reportFrame;
601
- let blurTimer;
602
- const readText = () => readTextOf(element, options.isInline);
603
724
  const liveSelection = () => isSelectionIn(element) ? readSelectionFrom(element) : void 0;
604
- const reportSelection = () => {
725
+ const report = () => {
605
726
  reportFrame = void 0;
606
- options.onSelect(measureSelection(element, readText(), lastSelection));
727
+ onSelect(measureSelection(element, readText(), lastSelection));
607
728
  };
608
- const scheduleReport = () => {
729
+ const schedule = () => {
609
730
  if (reportFrame !== void 0) return;
610
- reportFrame = view?.requestAnimationFrame(reportSelection);
731
+ reportFrame = view?.requestAnimationFrame(report);
611
732
  };
612
- const followSelection = () => {
733
+ const follow = () => {
613
734
  const selection = document2.hasFocus() ? liveSelection() : void 0;
614
735
  if (selection === void 0) return;
615
736
  lastSelection = selection;
616
- scheduleReport();
737
+ schedule();
617
738
  };
618
- const reportInput = () => {
619
- options.onInput(readText());
620
- followSelection();
621
- };
622
- const change = (changed, selection) => {
623
- render(element, changed, options.isInline);
739
+ const keep = (selection) => {
624
740
  lastSelection = selection;
625
741
  if (document2.hasFocus()) selectIn(element, selection);
626
- options.onInput(changed);
627
- scheduleReport();
628
- };
629
- const format = (requested) => {
630
- const selection = liveSelection() ?? lastSelection;
631
- const isListInLine = requested === "list" && options.isInline;
632
- if (selection === void 0 || isListInLine) return;
633
- const current = readText();
634
- const changed = requested === "list" ? toggleList(current, selection) : toggleMark(current, selection, requested);
635
- change(changed, selection);
742
+ schedule();
636
743
  };
637
- const linkTo = (href) => {
638
- const selection = lastSelection;
639
- if (selection === void 0) return;
640
- const current = readText();
641
- const target = isCollapsed(selection) ? linkSelectionAt(current, selection.start) : selection;
642
- if (target === void 0) return;
643
- change(setLink(current, target, href), selection);
644
- };
645
- const pressKey = (event) => {
646
- const isEnd = event.key === "Enter" && options.isInline && !event.shiftKey;
647
- const shortcut = formatShortcutOf(event);
648
- if (event.key === "Escape") {
649
- event.preventDefault();
650
- end("reverted");
651
- } else if (isEnd) {
652
- event.preventDefault();
653
- end("kept");
654
- } else if (shortcut !== void 0) {
655
- event.preventDefault();
656
- if (shortcut !== "none") format(shortcut);
657
- }
658
- };
659
- const pastePlainText = (event) => {
660
- event.preventDefault();
661
- const pasted = event.clipboardData?.getData("text/plain") ?? "";
662
- const words = options.isInline ? pasted.replace(PASTED_LINE_BREAKS, SPACE) : pasted;
663
- insertPlainText(element, words);
664
- reportInput();
665
- };
666
- const refuseDrop = (event) => {
667
- event.preventDefault();
668
- };
669
- const endOnBlur = () => {
670
- clearTimeout(blurTimer);
671
- blurTimer = setTimeout(() => {
672
- const isFocusOnPage = document2.hasFocus();
673
- const isFocusInText = document2.activeElement === element;
674
- if (isFocusOnPage && !isFocusInText) end("kept");
675
- });
676
- };
677
- const stopListening = () => {
678
- clearTimeout(blurTimer);
679
- if (reportFrame !== void 0) view?.cancelAnimationFrame(reportFrame);
680
- element.removeEventListener("input", reportInput);
681
- element.removeEventListener("keydown", pressKey);
682
- element.removeEventListener("paste", pastePlainText);
683
- element.removeEventListener("drop", refuseDrop);
684
- element.removeEventListener("blur", endOnBlur);
685
- document2.removeEventListener("selectionchange", followSelection);
686
- document2.removeEventListener("scroll", scheduleReport, true);
687
- view?.removeEventListener("resize", scheduleReport);
688
- makeUneditable(element);
689
- };
690
- let hasEnded = false;
691
- const end = (outcome) => {
692
- if (hasEnded) return;
693
- hasEnded = true;
694
- stopListening();
695
- options.onSelect(void 0);
696
- options.onEnd(outcome);
697
- };
698
- makeEditable(element, EDITABLE_AS_RICH_TEXT);
699
- element.addEventListener("input", reportInput);
700
- element.addEventListener("keydown", pressKey);
701
- element.addEventListener("paste", pastePlainText);
702
- element.addEventListener("drop", refuseDrop);
703
- element.addEventListener("blur", endOnBlur);
704
- document2.addEventListener("selectionchange", followSelection);
705
- document2.addEventListener("scroll", scheduleReport, true);
706
- view?.addEventListener("resize", scheduleReport);
707
- element.focus();
708
744
  return {
709
- format,
710
- 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,
711
754
  stop: () => {
712
- end("kept");
755
+ if (reportFrame !== void 0) view?.cancelAnimationFrame(reportFrame);
713
756
  }
714
757
  };
715
758
  }
716
- function readTextOf(element, isInline) {
717
- const read = readRichTextFrom(element);
718
- return isInline ? asOneParagraph(read) : read;
719
- }
720
- function render(element, text, isInline) {
721
- if (isInline) renderInlineRichText(element, text);
722
- else renderRichText(element, text);
723
- }
724
759
  function measureSelection(element, text, selection) {
725
760
  if (selection === void 0) return void 0;
726
761
  const range = isSelectionIn(element) ? element.ownerDocument.getSelection()?.getRangeAt(0) : rangeOf(element, selection);
@@ -734,7 +769,8 @@ function measureSelection(element, text, selection) {
734
769
  }
735
770
  function boxOfRange(range) {
736
771
  const whole = range.getBoundingClientRect();
737
- 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];
738
774
  if (rectangle === void 0) return void 0;
739
775
  const isShown = rectangle.width > 0 || rectangle.height > 0;
740
776
  if (!isShown) return void 0;
@@ -751,6 +787,118 @@ function isSelectionIn(element) {
751
787
  const hasRange = (selection?.rangeCount ?? 0) > 0;
752
788
  return hasRange && anchor !== null && element.contains(anchor);
753
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
+ }
754
902
  function formatShortcutOf(event) {
755
903
  const hasCommandKey = event.metaKey || event.ctrlKey;
756
904
  if (!hasCommandKey || event.altKey || event.shiftKey) return void 0;
@@ -760,12 +908,35 @@ function formatShortcutOf(event) {
760
908
  case "KeyI":
761
909
  return "italic";
762
910
  case "KeyU":
763
- return "none";
911
+ return "refused";
764
912
  default:
765
913
  return void 0;
766
914
  }
767
915
  }
768
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
+
769
940
  // src/bridge/page-typing.ts
770
941
  var PREPARED_TYPING_TIMEOUT_MS = 3e3;
771
942
  var EDITABLE_VALUES = {
@@ -773,93 +944,111 @@ var EDITABLE_VALUES = {
773
944
  rich: EDITABLE_AS_RICH_TEXT
774
945
  };
775
946
  function createPageTyping(options) {
776
- let stopEditing;
777
- let richEditing;
947
+ const { send } = options;
948
+ let typing;
778
949
  let prepared;
779
- const takePrepared = (isTyped) => {
950
+ const isTyping = () => typing !== void 0 || prepared !== void 0;
951
+ const reportIfOver = () => {
952
+ if (!isTyping()) options.onTypingOver();
953
+ };
954
+ const releasePrepared = () => {
780
955
  if (prepared === void 0) return;
781
956
  clearTimeout(prepared.timer);
782
- if (!isTyped && stopEditing === void 0) {
957
+ if (typing === void 0) {
783
958
  prepared.element.blur();
784
959
  makeUneditable(prepared.element);
785
960
  }
786
961
  prepared = void 0;
787
962
  };
963
+ const handOverPrepared = () => {
964
+ if (prepared !== void 0) clearTimeout(prepared.timer);
965
+ prepared = void 0;
966
+ };
788
967
  const prepare = (element, kind) => {
789
968
  const isTypedAlready = element.hasAttribute("contenteditable");
790
969
  if (isTypedAlready) return;
791
- takePrepared(false);
970
+ releasePrepared();
792
971
  makeEditable(element, EDITABLE_VALUES[kind]);
793
972
  element.focus({ preventScroll: true });
794
973
  const timer = setTimeout(() => {
795
- takePrepared(false);
974
+ releasePrepared();
975
+ reportIfOver();
796
976
  }, PREPARED_TYPING_TIMEOUT_MS);
797
977
  prepared = { element, timer };
798
978
  };
799
979
  const stop = () => {
800
- stopEditing?.();
980
+ typing?.stop();
801
981
  };
802
- const ended = (path) => (outcome) => {
803
- stopEditing = void 0;
804
- richEditing = void 0;
805
- 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();
806
986
  };
807
- const start = (request) => {
987
+ const begin = (start) => {
808
988
  stop();
809
- const { path } = request;
989
+ const { path } = start;
810
990
  const element = findMarkedElement(options.document, path);
811
- const canType = element !== void 0 && canEditTextInPlace(element, request.text);
812
- if (!canType) {
813
- takePrepared(false);
814
- options.send({ type: "typing-refused", path });
991
+ if (element === void 0 || !start.canTypeIn(element)) {
992
+ releasePrepared();
993
+ send({ type: "typing-refused", path });
994
+ reportIfOver();
815
995
  return;
816
996
  }
817
- takePrepared(true);
818
- options.send({ type: "typing-started", path });
819
- stopEditing = startInlineTextEditing(element, {
820
- isSingleLine: request.isSingleLine,
821
- onInput: (text) => {
822
- options.send({ type: "typed", path, text });
823
- options.onInput();
824
- },
825
- 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
+ })
826
1021
  });
827
- placeCaretAt(element, request.point);
828
1022
  };
829
- const startRich = (request) => {
830
- stop();
831
- const { path, isInline } = request;
832
- const element = findMarkedElement(options.document, path);
833
- const canType = element !== void 0 && canEditRichTextInPlace(element, request.text, isInline);
834
- if (!canType) {
835
- takePrepared(false);
836
- options.send({ type: "typing-refused", path });
837
- return;
838
- }
839
- takePrepared(true);
840
- options.send({ type: "typing-started", path });
841
- const editing = startRichTextEditing(element, {
842
- isInline,
843
- onInput: (text) => {
844
- options.send({ type: "rich-typed", path, text });
845
- options.onInput();
846
- },
847
- onSelect: (selection) => {
848
- options.send({ type: "typed-selection", path, selection });
849
- },
850
- 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
+ }
851
1043
  });
852
- richEditing = editing;
853
- stopEditing = editing.stop;
854
- placeCaretAt(element, request.point);
855
1044
  };
856
1045
  return {
857
1046
  prepare,
858
- isTyping: () => stopEditing !== void 0,
859
- start,
860
- startRich,
861
- format: (format) => richEditing?.format(format),
862
- 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),
863
1052
  stop
864
1053
  };
865
1054
  }
@@ -895,11 +1084,21 @@ function connectPage(document2, schema, send) {
895
1084
  const layout = createLayoutReports(document2, (measured) => {
896
1085
  send({ type: "layout", layout: measured });
897
1086
  });
898
- const typing = createPageTyping({ document: document2, send, onInput: layout.schedule });
1087
+ const showHeldPage = () => {
1088
+ renderer.showHeldPage();
1089
+ };
1090
+ const typing = createPageTyping({
1091
+ document: document2,
1092
+ send,
1093
+ onInput: layout.schedule,
1094
+ onTypingOver: showHeldPage
1095
+ });
1096
+ const presses = followPresses(document2, showHeldPage);
899
1097
  const renderer = createPageRenderer({
900
1098
  document: document2,
901
1099
  // Replacing the page under a text being typed would throw the typing away.
902
1100
  canReplacePage: () => !typing.isTyping(),
1101
+ isPagePressed: presses.isPressed,
903
1102
  onRendered: () => {
904
1103
  layout.reportNow();
905
1104
  send({ type: "rendered" });
@@ -909,31 +1108,45 @@ function connectPage(document2, schema, send) {
909
1108
  send({ type: "render-failed" });
910
1109
  }
911
1110
  });
912
- const pressKey = (event) => {
913
- const key = editorKeyOf(event);
914
- const isTypingEscape = key === "escape" && typing.isTyping();
915
- if (key === void 0 || isTypingEscape) return;
916
- event.preventDefault();
917
- typing.stop();
918
- send({ type: "key", key });
1111
+ const page = {
1112
+ document: document2,
1113
+ schema,
1114
+ send,
1115
+ layout,
1116
+ typing,
1117
+ renderer
919
1118
  };
920
- 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 {
921
1137
  onHover: (path) => {
922
1138
  send({ type: "hover", path });
923
1139
  },
924
1140
  onClick: (pointer) => {
925
- const kind = pointer === void 0 ? void 0 : typedKindAt(schema, pointer.path);
926
- if (pointer !== void 0 && kind !== void 0) {
927
- typing.prepare(pointer.element, kind);
928
- }
929
- const target = pointer === void 0 ? void 0 : { path: pointer.path, point: pointer.point };
930
- send({ type: "click", target });
1141
+ clickOn(page, pointer);
931
1142
  },
932
1143
  onLinkHover: layout.hoverLink,
933
1144
  onLinkClick: (link) => {
934
1145
  send({ type: "link-click", href: link.href });
935
1146
  },
936
- onKeyDown: pressKey,
1147
+ onKeyDown: (event) => {
1148
+ pressKey(page, event);
1149
+ },
937
1150
  onFileDragOver: (path) => {
938
1151
  const isTaken = imageFieldAt(schema, path) !== void 0;
939
1152
  send({ type: "file-drag", path: isTaken ? path : void 0 });
@@ -947,44 +1160,55 @@ function connectPage(document2, schema, send) {
947
1160
  if (isTaken) send({ type: "file-drop", path, file });
948
1161
  },
949
1162
  onLayoutChange: layout.schedule
950
- });
951
- layout.reportNow();
952
- const receive = (message) => {
953
- switch (message.type) {
954
- case "connect":
955
- break;
956
- case "render":
957
- renderer.render(message.content);
958
- break;
959
- case "start-typing":
960
- typing.start(message);
961
- break;
962
- case "start-rich-typing":
963
- typing.startRich(message);
964
- break;
965
- case "format-typed":
966
- typing.format(message.format);
967
- break;
968
- case "link-typed":
969
- typing.setLink(message.href);
970
- break;
971
- case "stop-typing":
972
- typing.stop();
973
- break;
974
- case "scroll-to":
975
- scrollToField(document2, message.path);
976
- break;
977
- }
978
1163
  };
979
- return {
980
- receive,
981
- 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":
982
1206
  typing.stop();
983
- renderer.stop();
984
- layout.stop();
985
- stopListening();
986
- }
987
- };
1207
+ break;
1208
+ case "scroll-to":
1209
+ scrollToField(page.document, message.path);
1210
+ break;
1211
+ }
988
1212
  }
989
1213
  function scrollToField(document2, path) {
990
1214
  const element = findClosestMarkedElement(document2, path);