livelocale 0.12.1 → 1.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.
@@ -1,17 +1,47 @@
1
- export { W as WrapOptions, d as decodeKey, e as encodeKey, h as hasWatermark, r as removeWatermarks, s as sanitizeZeroWidth, a as stripWatermark, u as unwrap, w as wrapNextIntl, b as wrapTranslationEngine } from '../wrap-COvz8N3U.js';
1
+ export { W as WrapOptions, d as decodeKey, e as encodeKey, h as hasWatermark, r as removeWatermarks, s as sanitizeZeroWidth, a as stripWatermark, u as unwrap, w as wrapNextIntl, b as wrapTranslationEngine } from '../wrap-BbTu7D0Q.js';
2
2
 
3
- type ClientMessageType = 'ELEMENT_SELECTED' | 'READY' | 'ERROR' | 'VISIBLE_KEYS_CHANGED' | 'VISIBLE_TEXTS_CHANGED' | 'LOCATION_CHANGED' | 'LEAVING';
4
- type StudioMessageType = 'APPLY_PREVIEW' | 'COMMIT_PREVIEW' | 'CLEAR_SELECTION' | 'NAVIGATE';
5
- /** A key on the page with the text it shows now; the Studio infers the page locale from these. */
3
+ /**
4
+ * The messages the in-page client sends the Studio. The wire protocol is stable; this
5
+ * type is not, because new messages are added in minor releases.
6
+ *
7
+ * @internal Not part of the public API: semver does not cover it and it may change in any
8
+ * release. See https://livelocale.js.org/docs/reference/stability.html
9
+ */
10
+ type ClientMessageType = 'ELEMENT_SELECTED' | 'READY' | 'ERROR' | 'VISIBLE_KEYS_CHANGED' | 'VISIBLE_TEXTS_CHANGED' | 'LOCATION_CHANGED' | 'LEAVING' | 'RENDERED_KEYS_CHANGED' | 'TAP_UNKEYED';
11
+ /**
12
+ * The messages the Studio sends the in-page client.
13
+ *
14
+ * @internal Not part of the public API: semver does not cover it and it may change in any
15
+ * release. See https://livelocale.js.org/docs/reference/stability.html
16
+ */
17
+ type StudioMessageType = 'APPLY_PREVIEW' | 'COMMIT_PREVIEW' | 'CLEAR_SELECTION' | 'NAVIGATE' | 'SET_EDIT_MODE' | 'SELECT_KEY' | 'REVEAL_KEY';
18
+ /**
19
+ * A key on the page with the text it shows now; the Studio infers the page locale from these.
20
+ *
21
+ * @internal Not part of the public API: semver does not cover it and it may change in any
22
+ * release. See https://livelocale.js.org/docs/reference/stability.html
23
+ */
6
24
  interface VisibleText {
7
25
  key: string;
8
26
  text: string;
9
27
  }
28
+ /**
29
+ * The ELEMENT_SELECTED payload.
30
+ *
31
+ * @internal Not part of the public API: semver does not cover it and it may change in any
32
+ * release. See https://livelocale.js.org/docs/reference/stability.html
33
+ */
10
34
  interface TranslationPayload {
11
35
  key: string;
12
36
  fallbackValue: string;
13
37
  currentValue: string;
14
38
  }
39
+ /**
40
+ * The postMessage transport between the in-page client and the Studio.
41
+ *
42
+ * @internal Not part of the public API: semver does not cover it and it may change in any
43
+ * release. See https://livelocale.js.org/docs/reference/stability.html
44
+ */
15
45
  declare class IframeBridge {
16
46
  private allowedOrigin;
17
47
  constructor(allowedOrigin: string);
@@ -25,21 +55,32 @@ declare class IframeBridge {
25
55
  listenToParent(callback: (type: StudioMessageType, payload: any) => void): () => void;
26
56
  }
27
57
 
58
+ /**
59
+ * The in-page engine the client starts on its own inside the Studio.
60
+ *
61
+ * @internal Not part of the public API: semver does not cover it and it may change in any
62
+ * release. See https://livelocale.js.org/docs/reference/stability.html
63
+ */
28
64
  declare class DOMInterceptor {
29
65
  private onElementSelected;
30
66
  private onVisibleKeysChanged?;
67
+ private onUnkeyedTap?;
31
68
  private hoveredElement;
32
69
  private observer;
33
70
  private inputSanitizersAttached;
34
71
  private overlay;
35
72
  private overlayBadge;
36
73
  private isAltHeld;
74
+ private editMode;
75
+ private active;
76
+ /** The element carrying `PICKED_ATTRIBUTE`, if any. */
77
+ private pickedElement;
37
78
  private broadcastVisibleKeys;
38
79
  constructor(onElementSelected: (payload: {
39
80
  key: string;
40
81
  fallbackValue: string;
41
82
  currentValue: string;
42
- }) => void, onVisibleKeysChanged?: (keys: string[]) => void);
83
+ }) => void, onVisibleKeysChanged?: (keys: string[]) => void, onUnkeyedTap?: () => void);
43
84
  /**
44
85
  * Returns the unique set of translation keys currently visible in the DOM.
45
86
  */
@@ -53,6 +94,37 @@ declare class DOMInterceptor {
53
94
  * `limit` entries are returned.
54
95
  */
55
96
  getVisibleTexts(limit?: number): VisibleText[];
97
+ /**
98
+ * Returns the unique keys that have at least one rendered, visible element
99
+ * (see `isRendered`), in document order. Keys only inside closed menus,
100
+ * hidden slides or a layout hidden at this width are left out.
101
+ */
102
+ getRenderedKeys(): string[];
103
+ /** The first rendered, visible element carrying `key`, else `null`. */
104
+ private findRenderedElement;
105
+ /**
106
+ * Scrolls the first visible element with `key` to the centre and selects
107
+ * it as a tap would. Returns false, selecting nothing, when no element with
108
+ * that key is visible or the interceptor is not running. A key that is no
109
+ * longer visible (a carousel moved on with no DOM change the observer sees)
110
+ * reports the keys again, so the Studio's next try uses a fresh list.
111
+ */
112
+ selectKey(key: string): boolean;
113
+ /**
114
+ * Scrolls so the first visible element with `key` sits fully between the
115
+ * frame's top and `bottomInset` CSS px above its bottom (where the Studio's
116
+ * sheet starts), without selecting it. Does nothing when it already does,
117
+ * or when no element with that key is visible. A tall element keeps its top
118
+ * in view rather than its bottom.
119
+ */
120
+ revealKey(key: string, bottomInset: number): void;
121
+ /**
122
+ * Turns Edit mode on or off. In Edit mode no tap reaches the site: a tap on
123
+ * keyed text selects it, a tap on other text calls `onUnkeyedTap`, and
124
+ * anything else does nothing. Scrolling still works. The mode is kept on
125
+ * the instance and re-applied by `init()`.
126
+ */
127
+ setEditMode(on: boolean): void;
56
128
  /**
57
129
  * Initializes the DOM event interceptors in development mode.
58
130
  * Also launches the watermark scanner and MutationObserver.
@@ -63,6 +135,20 @@ declare class DOMInterceptor {
63
135
  * the floating overlay, and the form-sanitization listeners.
64
136
  */
65
137
  destroy(): void;
138
+ private attachEditMode;
139
+ private detachEditMode;
140
+ /** Moves the picked-text marker to `element`. */
141
+ private markPicked;
142
+ private clearPicked;
143
+ private stopFromSite;
144
+ private handleEditFocus;
145
+ /** Edit-mode click: cancelled whatever the target, then a selection or the notice. */
146
+ private handleEditClick;
147
+ /**
148
+ * Reports `element`, which carries `key`, as selected with its clean text. In Edit mode it
149
+ * also gets the picked-text marker; an Alt+Click pick on a desktop does not.
150
+ */
151
+ private select;
66
152
  /**
67
153
  * Creates and injects a single floating overlay div into the document body.
68
154
  * The overlay is positioned using fixed coordinates from getBoundingClientRect()
@@ -159,6 +159,7 @@ function isLiveLocaleEnabled() {
159
159
  }
160
160
 
161
161
  // src/client/iframe-bridge.ts
162
+ var CLIENT_FEATURES = ["edit-mode"];
162
163
  var DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
163
164
  var STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
164
165
  function toHttpOrigin(value) {
@@ -202,6 +203,8 @@ function resolveStudioOrigin(search, storage, sameOriginParent) {
202
203
  var MAX_VISIBLE_TEXTS = 100;
203
204
  var MAX_VISIBLE_TEXT_LENGTH = 200;
204
205
  var KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
206
+ var FEATURE_PATTERN = /^[a-z0-9-]+$/;
207
+ var MAX_FEATURES = 16;
205
208
  var MAX_LOCATION_LENGTH = 2048;
206
209
  var LOCATION_CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/;
207
210
  var LOCATION_PROBE_ORIGIN = "http://livelocale.invalid";
@@ -256,7 +259,7 @@ var IframeBridge = class {
256
259
  return;
257
260
  }
258
261
  }
259
- if (type === "VISIBLE_KEYS_CHANGED") {
262
+ if (type === "VISIBLE_KEYS_CHANGED" || type === "RENDERED_KEYS_CHANGED") {
260
263
  if (!Array.isArray(payload)) return;
261
264
  const validKeys = payload.filter(
262
265
  (k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
@@ -279,7 +282,15 @@ var IframeBridge = class {
279
282
  }
280
283
  if (type === "READY" || type === "LOCATION_CHANGED") {
281
284
  const url = toLocationUrl(payload);
282
- payload = url === null ? {} : { url };
285
+ const next = url === null ? {} : { url };
286
+ const features = payload?.features;
287
+ if (type === "READY" && Array.isArray(features)) {
288
+ next.features = features.filter((f) => typeof f === "string" && FEATURE_PATTERN.test(f)).slice(0, MAX_FEATURES);
289
+ }
290
+ payload = next;
291
+ }
292
+ if (type === "TAP_UNKEYED") {
293
+ payload = void 0;
283
294
  }
284
295
  const message = {
285
296
  source: "livelocale-client",
@@ -331,9 +342,27 @@ function debounce(fn, wait) {
331
342
  };
332
343
  return debounced;
333
344
  }
345
+ var EDITABLE_SELECTOR = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
346
+ var EDIT_STYLE_ID = "livelocale-edit-style";
347
+ var EDIT_ATTRIBUTE = "data-livelocale-edit";
348
+ var PICKED_ATTRIBUTE = "data-livelocale-picked";
349
+ var REVEAL_MARGIN = 16;
350
+ function isRendered(el) {
351
+ const visible = typeof el.checkVisibility === "function" ? el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) : el.getClientRects().length > 0;
352
+ if (!visible) return false;
353
+ const rect = el.getBoundingClientRect();
354
+ return rect.width > 0 && rect.height > 0;
355
+ }
356
+ function hasOwnText(el) {
357
+ for (const child of el.childNodes) {
358
+ if (child.nodeType === Node.TEXT_NODE && (child.nodeValue ?? "").trim() !== "") return true;
359
+ }
360
+ return false;
361
+ }
334
362
  var DOMInterceptor = class {
335
363
  onElementSelected;
336
364
  onVisibleKeysChanged;
365
+ onUnkeyedTap;
337
366
  hoveredElement = null;
338
367
  observer = null;
339
368
  inputSanitizersAttached = false;
@@ -342,11 +371,19 @@ var DOMInterceptor = class {
342
371
  overlayBadge = null;
343
372
  // ─── Alt key state ───────────────────────────────────────────────────────
344
373
  isAltHeld = false;
374
+ // ─── Edit mode (touch) ───────────────────────────────────────────────────
375
+ // Kept across destroy()/init() so the Studio's CLEAR_SELECTION, which
376
+ // re-inits the interceptor, never unfreezes the site.
377
+ editMode = false;
378
+ active = false;
379
+ /** The element carrying `PICKED_ATTRIBUTE`, if any. */
380
+ pickedElement = null;
345
381
  // Debounced key-broadcast to avoid flooding the parent on rapid DOM mutations
346
382
  broadcastVisibleKeys;
347
- constructor(onElementSelected, onVisibleKeysChanged) {
383
+ constructor(onElementSelected, onVisibleKeysChanged, onUnkeyedTap) {
348
384
  this.onElementSelected = onElementSelected;
349
385
  this.onVisibleKeysChanged = onVisibleKeysChanged;
386
+ this.onUnkeyedTap = onUnkeyedTap;
350
387
  this.broadcastVisibleKeys = debounce(() => {
351
388
  if (this.onVisibleKeysChanged) {
352
389
  this.onVisibleKeysChanged(this.getActiveKeys());
@@ -394,6 +431,75 @@ var DOMInterceptor = class {
394
431
  }
395
432
  return texts;
396
433
  }
434
+ /**
435
+ * Returns the unique keys that have at least one rendered, visible element
436
+ * (see `isRendered`), in document order. Keys only inside closed menus,
437
+ * hidden slides or a layout hidden at this width are left out.
438
+ */
439
+ getRenderedKeys() {
440
+ const keys = /* @__PURE__ */ new Set();
441
+ document.querySelectorAll("[data-i18n-key]").forEach((el) => {
442
+ const key = el.getAttribute("data-i18n-key");
443
+ if (key && !keys.has(key) && isRendered(el)) keys.add(key);
444
+ });
445
+ return Array.from(keys);
446
+ }
447
+ /** The first rendered, visible element carrying `key`, else `null`. */
448
+ findRenderedElement(key) {
449
+ for (const el of document.querySelectorAll("[data-i18n-key]")) {
450
+ if (el.getAttribute("data-i18n-key") === key && isRendered(el)) return el;
451
+ }
452
+ return null;
453
+ }
454
+ /**
455
+ * Scrolls the first visible element with `key` to the centre and selects
456
+ * it as a tap would. Returns false, selecting nothing, when no element with
457
+ * that key is visible or the interceptor is not running. A key that is no
458
+ * longer visible (a carousel moved on with no DOM change the observer sees)
459
+ * reports the keys again, so the Studio's next try uses a fresh list.
460
+ */
461
+ selectKey(key) {
462
+ if (!this.active) return false;
463
+ const el = this.findRenderedElement(key);
464
+ if (!el) {
465
+ this.broadcastVisibleKeys();
466
+ return false;
467
+ }
468
+ if (typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" });
469
+ this.select(el, key);
470
+ return true;
471
+ }
472
+ /**
473
+ * Scrolls so the first visible element with `key` sits fully between the
474
+ * frame's top and `bottomInset` CSS px above its bottom (where the Studio's
475
+ * sheet starts), without selecting it. Does nothing when it already does,
476
+ * or when no element with that key is visible. A tall element keeps its top
477
+ * in view rather than its bottom.
478
+ */
479
+ revealKey(key, bottomInset) {
480
+ if (!this.active) return;
481
+ const el = this.findRenderedElement(key);
482
+ if (!el) return;
483
+ const rect = el.getBoundingClientRect();
484
+ const limit = window.innerHeight - bottomInset;
485
+ if (rect.top >= 0 && rect.bottom <= limit) return;
486
+ const delta = Math.min(rect.bottom - limit + REVEAL_MARGIN, rect.top - REVEAL_MARGIN);
487
+ window.scrollBy(0, delta);
488
+ }
489
+ /**
490
+ * Turns Edit mode on or off. In Edit mode no tap reaches the site: a tap on
491
+ * keyed text selects it, a tap on other text calls `onUnkeyedTap`, and
492
+ * anything else does nothing. Scrolling still works. The mode is kept on
493
+ * the instance and re-applied by `init()`.
494
+ */
495
+ setEditMode(on) {
496
+ const changed = this.editMode !== on;
497
+ this.editMode = on;
498
+ if (!this.active) return;
499
+ if (on) this.attachEditMode();
500
+ else this.detachEditMode();
501
+ if (changed) this.broadcastVisibleKeys();
502
+ }
397
503
  /**
398
504
  * Initializes the DOM event interceptors in development mode.
399
505
  * Also launches the watermark scanner and MutationObserver.
@@ -418,6 +524,8 @@ var DOMInterceptor = class {
418
524
  this.scanSubtree(document.body);
419
525
  this.attachMutationObserver();
420
526
  this.attachInputSanitizers();
527
+ this.active = true;
528
+ if (this.editMode) this.attachEditMode();
421
529
  this.broadcastVisibleKeys();
422
530
  }
423
531
  /**
@@ -438,6 +546,8 @@ var DOMInterceptor = class {
438
546
  window.removeEventListener("keydown", this.handleKeyDown, true);
439
547
  window.removeEventListener("keyup", this.handleKeyUp, true);
440
548
  window.removeEventListener("beforeinput", this.handleBeforeInput, true);
549
+ this.detachEditMode();
550
+ this.active = false;
441
551
  document.removeEventListener("submit", this.handleFormSubmit, true);
442
552
  if (this.observer) {
443
553
  this.observer.disconnect();
@@ -452,6 +562,87 @@ var DOMInterceptor = class {
452
562
  this.hoveredElement = null;
453
563
  this.broadcastVisibleKeys.cancel();
454
564
  }
565
+ // ─── Edit Mode ───────────────────────────────────────────────────────────
566
+ attachEditMode() {
567
+ window.addEventListener("pointerdown", this.stopFromSite, true);
568
+ window.addEventListener("pointerup", this.stopFromSite, true);
569
+ window.addEventListener("mousedown", this.stopFromSite, true);
570
+ window.addEventListener("mouseup", this.stopFromSite, true);
571
+ window.addEventListener("touchstart", this.stopFromSite, { capture: true, passive: true });
572
+ window.addEventListener("touchend", this.stopFromSite, { capture: true, passive: true });
573
+ window.addEventListener("focusin", this.handleEditFocus, true);
574
+ if (!document.getElementById(EDIT_STYLE_ID)) {
575
+ const style = document.createElement("style");
576
+ style.id = EDIT_STYLE_ID;
577
+ style.textContent = `html[${EDIT_ATTRIBUTE}] [data-i18n-key]{outline:1px dashed rgba(79,195,220,.7);outline-offset:2px}html[${EDIT_ATTRIBUTE}] [data-i18n-key][${PICKED_ATTRIBUTE}]{outline:2px solid #FFB224;outline-offset:3px}`;
578
+ document.head.appendChild(style);
579
+ }
580
+ document.documentElement.setAttribute(EDIT_ATTRIBUTE, "");
581
+ this.hideOverlay();
582
+ this.hoveredElement = null;
583
+ }
584
+ detachEditMode() {
585
+ window.removeEventListener("pointerdown", this.stopFromSite, true);
586
+ window.removeEventListener("pointerup", this.stopFromSite, true);
587
+ window.removeEventListener("mousedown", this.stopFromSite, true);
588
+ window.removeEventListener("mouseup", this.stopFromSite, true);
589
+ window.removeEventListener("touchstart", this.stopFromSite, true);
590
+ window.removeEventListener("touchend", this.stopFromSite, true);
591
+ window.removeEventListener("focusin", this.handleEditFocus, true);
592
+ document.documentElement.removeAttribute(EDIT_ATTRIBUTE);
593
+ document.getElementById(EDIT_STYLE_ID)?.remove();
594
+ this.clearPicked();
595
+ }
596
+ /** Moves the picked-text marker to `element`. */
597
+ markPicked(element) {
598
+ this.clearPicked();
599
+ element.setAttribute(PICKED_ATTRIBUTE, "");
600
+ this.pickedElement = element;
601
+ }
602
+ clearPicked() {
603
+ this.pickedElement?.removeAttribute(PICKED_ATTRIBUTE);
604
+ this.pickedElement = null;
605
+ }
606
+ // Sites open menus on press events; in Edit mode none of them reaches the
607
+ // site. Never preventDefault here: on touchstart it would stop scrolling.
608
+ stopFromSite = (event) => {
609
+ event.stopImmediatePropagation();
610
+ };
611
+ handleEditFocus = (event) => {
612
+ const target = event.target;
613
+ if (!target || typeof target.matches !== "function") return;
614
+ if (target.matches(EDITABLE_SELECTOR) || target.isContentEditable) target.blur();
615
+ };
616
+ /** Edit-mode click: cancelled whatever the target, then a selection or the notice. */
617
+ handleEditClick(event) {
618
+ event.preventDefault();
619
+ event.stopImmediatePropagation();
620
+ const target = event.target;
621
+ if (!target || typeof target.closest !== "function") return;
622
+ const keyedElement = target.closest("[data-i18n-key]");
623
+ const key = keyedElement?.getAttribute("data-i18n-key");
624
+ if (keyedElement && key) {
625
+ this.select(keyedElement, key);
626
+ } else if (hasOwnText(target)) {
627
+ this.onUnkeyedTap?.();
628
+ }
629
+ }
630
+ /**
631
+ * Reports `element`, which carries `key`, as selected with its clean text. In Edit mode it
632
+ * also gets the picked-text marker; an Alt+Click pick on a desktop does not.
633
+ */
634
+ select(element, key) {
635
+ if (this.editMode) this.markPicked(element);
636
+ const rawText = element.innerText || "";
637
+ const cleanText = stripWatermark(rawText);
638
+ const fallbackValue = element.getAttribute("data-i18n-template") || cleanText;
639
+ const currentValue = cleanText;
640
+ this.onElementSelected({
641
+ key,
642
+ fallbackValue,
643
+ currentValue
644
+ });
645
+ }
455
646
  // ─── Floating Overlay ────────────────────────────────────────────────────
456
647
  /**
457
648
  * Creates and injects a single floating overlay div into the document body.
@@ -695,6 +886,7 @@ var DOMInterceptor = class {
695
886
  };
696
887
  // ─── Mouse / Click Handlers ───────────────────────────────────────────────
697
888
  handleMouseOver = (event) => {
889
+ if (this.editMode) return;
698
890
  const target = event.target;
699
891
  if (!target || typeof target.closest !== "function") return;
700
892
  const keyedElement = target.closest("[data-i18n-key]");
@@ -723,6 +915,10 @@ var DOMInterceptor = class {
723
915
  this.hoveredElement = null;
724
916
  };
725
917
  handleClick = (event) => {
918
+ if (this.editMode) {
919
+ this.handleEditClick(event);
920
+ return;
921
+ }
726
922
  if (!event.altKey) return;
727
923
  const target = event.target;
728
924
  if (!target || typeof target.closest !== "function") return;
@@ -732,15 +928,7 @@ var DOMInterceptor = class {
732
928
  if (key === null) return;
733
929
  event.preventDefault();
734
930
  event.stopPropagation();
735
- const rawText = keyedElement.innerText || "";
736
- const cleanText = stripWatermark(rawText);
737
- const fallbackValue = keyedElement.getAttribute("data-i18n-template") || cleanText;
738
- const currentValue = cleanText;
739
- this.onElementSelected({
740
- key,
741
- fallbackValue,
742
- currentValue
743
- });
931
+ this.select(keyedElement, key);
744
932
  };
745
933
  handleCopy = (event) => {
746
934
  const selection = window.getSelection()?.toString() || "";
@@ -1001,16 +1189,22 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
1001
1189
  },
1002
1190
  (keys) => {
1003
1191
  bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
1192
+ bridge.sendToParent("RENDERED_KEYS_CHANGED", interceptor.getRenderedKeys());
1004
1193
  bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
1194
+ },
1195
+ () => {
1196
+ bridge.sendToParent("TAP_UNKEYED");
1005
1197
  }
1006
1198
  );
1007
1199
  interceptor.init();
1008
1200
  const locationUrl = () => window.location.pathname + window.location.search + window.location.hash;
1009
- bridge.sendToParent("READY", { url: locationUrl() });
1201
+ bridge.sendToParent("READY", { url: locationUrl(), features: CLIENT_FEATURES });
1010
1202
  watchLocation(window, (url) => bridge.sendToParent("LOCATION_CHANGED", { url }));
1011
1203
  window.addEventListener("pagehide", () => bridge.sendToParent("LEAVING"));
1012
1204
  window.addEventListener("pageshow", (event) => {
1013
- if (event.persisted) bridge.sendToParent("READY", { url: locationUrl() });
1205
+ if (event.persisted) {
1206
+ bridge.sendToParent("READY", { url: locationUrl(), features: CLIENT_FEATURES });
1207
+ }
1014
1208
  });
1015
1209
  bridge.listenToParent((type, payload) => {
1016
1210
  if (type === "APPLY_PREVIEW") {
@@ -1028,6 +1222,18 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
1028
1222
  } else if (type === "NAVIGATE") {
1029
1223
  const href = navigationHref(payload, window.location.origin);
1030
1224
  if (href !== null) followNavigation(window.location, href);
1225
+ } else if (type === "SET_EDIT_MODE") {
1226
+ if (payload && typeof payload.on === "boolean") {
1227
+ interceptor.setEditMode(payload.on);
1228
+ }
1229
+ } else if (type === "SELECT_KEY") {
1230
+ if (payload && typeof payload.key === "string") {
1231
+ interceptor.selectKey(payload.key);
1232
+ }
1233
+ } else if (type === "REVEAL_KEY") {
1234
+ if (payload && typeof payload.key === "string" && typeof payload.bottomInset === "number" && Number.isFinite(payload.bottomInset) && payload.bottomInset >= 0) {
1235
+ interceptor.revealKey(payload.key, payload.bottomInset);
1236
+ }
1031
1237
  }
1032
1238
  });
1033
1239
  window.__LIVELOCALE_CLIENT__ = {