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.
- package/README.md +27 -2
- package/dist/client/index.cjs +220 -14
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +91 -5
- package/dist/client/index.d.ts +91 -5
- package/dist/client/index.js +220 -14
- package/dist/client/index.js.map +1 -1
- package/dist/codemod/index.cjs +576 -439
- package/dist/codemod/index.cjs.map +1 -1
- package/dist/codemod/index.js +576 -439
- package/dist/codemod/index.js.map +1 -1
- package/dist/react/index.cjs +222 -16
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.js +222 -16
- package/dist/react/index.js.map +1 -1
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +71 -7
- package/dist/server/index.d.ts +71 -7
- package/dist/server/index.js.map +1 -1
- package/dist/studio/assets/index-CwvUCfuq.js +14 -0
- package/dist/studio/assets/index-DrqnTE2K.css +1 -0
- package/dist/studio/index.html +2 -2
- package/dist/vite/index.cjs.map +1 -1
- package/dist/vite/index.d.cts +3 -18
- package/dist/vite/index.d.ts +3 -18
- package/dist/vite/index.js.map +1 -1
- package/dist/{wrap-COvz8N3U.d.cts → wrap-BbTu7D0Q.d.cts} +22 -4
- package/dist/{wrap-COvz8N3U.d.ts → wrap-BbTu7D0Q.d.ts} +22 -4
- package/package.json +9 -2
- package/dist/studio/assets/index-B9qNpq5Q.js +0 -14
- package/dist/studio/assets/index-KRNgb_rB.css +0 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<img src="brand/logo.svg" alt="livelocale" height="64">
|
|
6
6
|
</picture>
|
|
7
7
|
</h1>
|
|
8
|
-
<p><strong>
|
|
9
|
-
<p>
|
|
8
|
+
<p><strong>The editing and review layer on top of next-intl or react-i18next.</strong></p>
|
|
9
|
+
<p>Your content people fix text on the real site, and you get each fix as an edit to your JSON locale files in git, or as a pull request. For Next.js and React + Vite: no account, no server, no browser extension.</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/Araby99/livelocale/actions/workflows/ci.yml?query=branch%3Amain"><img src="https://github.com/Araby99/livelocale/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI Status" /></a>
|
|
@@ -18,6 +18,16 @@
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
**It is not an i18n library.** next-intl or react-i18next keeps doing routing, locale detection,
|
|
22
|
+
date and number formatting and message rendering; livelocale only edits and reviews the text.
|
|
23
|
+
|
|
24
|
+
- **Freelancing?** Your client fixes their own copy through a
|
|
25
|
+
[share link](https://livelocale.js.org/docs/guides/share-links.html), and you review a pull
|
|
26
|
+
request instead of collecting screenshots.
|
|
27
|
+
- **Only want CI?** [`npx livelocale check --hardcoded`](https://livelocale.js.org/docs/guides/ci-check.html)
|
|
28
|
+
works without the Studio. Translating on your own? The CI check alone catches missing keys and
|
|
29
|
+
hard-coded text.
|
|
30
|
+
|
|
21
31
|
Run your app beside the livelocale Studio, hold Alt and click a line of text, and fix it. The save
|
|
22
32
|
goes straight into your locale JSON file as an ordinary edit you commit. In development every
|
|
23
33
|
translated string carries an invisible watermark, so the Studio knows which key you clicked; in
|
|
@@ -87,6 +97,20 @@ npx livelocale studio
|
|
|
87
97
|
Open `http://localhost:3010`, hold Alt (Option on a Mac), click a line of text and press
|
|
88
98
|
**Save Changes**. `git diff locales/` shows the fix.
|
|
89
99
|
|
|
100
|
+
## Supported versions
|
|
101
|
+
|
|
102
|
+
| Needs | Tested in CI |
|
|
103
|
+
| ----------------------------------------- | ------------------------------------------ |
|
|
104
|
+
| Node.js 18 or later | Node.js 20 and 24; the built package on 18 |
|
|
105
|
+
| Next.js 13 or later | Next.js 16 |
|
|
106
|
+
| React 18 or later | React 18 |
|
|
107
|
+
| Vite 5 or later | Vite 8 |
|
|
108
|
+
| TypeScript 4.7 to 6 (`check --hardcoded`) | TypeScript 5 |
|
|
109
|
+
|
|
110
|
+
From 1.0 on, livelocale follows semantic versioning, and share links people already sent keep
|
|
111
|
+
working in newer versions. [Versioning and stability](https://livelocale.js.org/docs/reference/stability.html)
|
|
112
|
+
lists what that covers and what is internal.
|
|
113
|
+
|
|
90
114
|
## Docs
|
|
91
115
|
|
|
92
116
|
- [Get started](https://livelocale.js.org/docs/): Next.js, Vite + React, or any static host.
|
|
@@ -94,6 +118,7 @@ Open `http://localhost:3010`, hold Alt (Option on a Mac), click a line of text a
|
|
|
94
118
|
- [Share links](https://livelocale.js.org/docs/guides/share-links.html): fixes from the deployed site, no account and no install.
|
|
95
119
|
- [Catch untranslated text in CI](https://livelocale.js.org/docs/guides/ci-check.html): missing keys, placeholder drift and hardcoded text.
|
|
96
120
|
- [CLI reference](https://livelocale.js.org/docs/reference/cli/index.html): every command and flag.
|
|
121
|
+
- [Versioning and stability](https://livelocale.js.org/docs/reference/stability.html): what semver covers, and the versions CI tests.
|
|
97
122
|
|
|
98
123
|
## Contributing
|
|
99
124
|
|
package/dist/client/index.cjs
CHANGED
|
@@ -194,6 +194,7 @@ function isLiveLocaleEnabled() {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
// src/client/iframe-bridge.ts
|
|
197
|
+
var CLIENT_FEATURES = ["edit-mode"];
|
|
197
198
|
var DEFAULT_STUDIO_ORIGIN = "http://localhost:3010";
|
|
198
199
|
var STUDIO_ORIGIN_STORAGE_KEY = "livelocale-studio-origin";
|
|
199
200
|
function toHttpOrigin(value) {
|
|
@@ -237,6 +238,8 @@ function resolveStudioOrigin(search, storage, sameOriginParent) {
|
|
|
237
238
|
var MAX_VISIBLE_TEXTS = 100;
|
|
238
239
|
var MAX_VISIBLE_TEXT_LENGTH = 200;
|
|
239
240
|
var KEY_PATTERN = /^[a-zA-Z0-9._-]+$/;
|
|
241
|
+
var FEATURE_PATTERN = /^[a-z0-9-]+$/;
|
|
242
|
+
var MAX_FEATURES = 16;
|
|
240
243
|
var MAX_LOCATION_LENGTH = 2048;
|
|
241
244
|
var LOCATION_CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/;
|
|
242
245
|
var LOCATION_PROBE_ORIGIN = "http://livelocale.invalid";
|
|
@@ -291,7 +294,7 @@ var IframeBridge = class {
|
|
|
291
294
|
return;
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
|
-
if (type === "VISIBLE_KEYS_CHANGED") {
|
|
297
|
+
if (type === "VISIBLE_KEYS_CHANGED" || type === "RENDERED_KEYS_CHANGED") {
|
|
295
298
|
if (!Array.isArray(payload)) return;
|
|
296
299
|
const validKeys = payload.filter(
|
|
297
300
|
(k) => typeof k === "string" && /^[a-zA-Z0-9._-]+$/.test(k)
|
|
@@ -314,7 +317,15 @@ var IframeBridge = class {
|
|
|
314
317
|
}
|
|
315
318
|
if (type === "READY" || type === "LOCATION_CHANGED") {
|
|
316
319
|
const url = toLocationUrl(payload);
|
|
317
|
-
|
|
320
|
+
const next = url === null ? {} : { url };
|
|
321
|
+
const features = payload?.features;
|
|
322
|
+
if (type === "READY" && Array.isArray(features)) {
|
|
323
|
+
next.features = features.filter((f) => typeof f === "string" && FEATURE_PATTERN.test(f)).slice(0, MAX_FEATURES);
|
|
324
|
+
}
|
|
325
|
+
payload = next;
|
|
326
|
+
}
|
|
327
|
+
if (type === "TAP_UNKEYED") {
|
|
328
|
+
payload = void 0;
|
|
318
329
|
}
|
|
319
330
|
const message = {
|
|
320
331
|
source: "livelocale-client",
|
|
@@ -366,9 +377,27 @@ function debounce(fn, wait) {
|
|
|
366
377
|
};
|
|
367
378
|
return debounced;
|
|
368
379
|
}
|
|
380
|
+
var EDITABLE_SELECTOR = 'input, textarea, select, [contenteditable]:not([contenteditable="false"])';
|
|
381
|
+
var EDIT_STYLE_ID = "livelocale-edit-style";
|
|
382
|
+
var EDIT_ATTRIBUTE = "data-livelocale-edit";
|
|
383
|
+
var PICKED_ATTRIBUTE = "data-livelocale-picked";
|
|
384
|
+
var REVEAL_MARGIN = 16;
|
|
385
|
+
function isRendered(el) {
|
|
386
|
+
const visible = typeof el.checkVisibility === "function" ? el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true }) : el.getClientRects().length > 0;
|
|
387
|
+
if (!visible) return false;
|
|
388
|
+
const rect = el.getBoundingClientRect();
|
|
389
|
+
return rect.width > 0 && rect.height > 0;
|
|
390
|
+
}
|
|
391
|
+
function hasOwnText(el) {
|
|
392
|
+
for (const child of el.childNodes) {
|
|
393
|
+
if (child.nodeType === Node.TEXT_NODE && (child.nodeValue ?? "").trim() !== "") return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
369
397
|
var DOMInterceptor = class {
|
|
370
398
|
onElementSelected;
|
|
371
399
|
onVisibleKeysChanged;
|
|
400
|
+
onUnkeyedTap;
|
|
372
401
|
hoveredElement = null;
|
|
373
402
|
observer = null;
|
|
374
403
|
inputSanitizersAttached = false;
|
|
@@ -377,11 +406,19 @@ var DOMInterceptor = class {
|
|
|
377
406
|
overlayBadge = null;
|
|
378
407
|
// ─── Alt key state ───────────────────────────────────────────────────────
|
|
379
408
|
isAltHeld = false;
|
|
409
|
+
// ─── Edit mode (touch) ───────────────────────────────────────────────────
|
|
410
|
+
// Kept across destroy()/init() so the Studio's CLEAR_SELECTION, which
|
|
411
|
+
// re-inits the interceptor, never unfreezes the site.
|
|
412
|
+
editMode = false;
|
|
413
|
+
active = false;
|
|
414
|
+
/** The element carrying `PICKED_ATTRIBUTE`, if any. */
|
|
415
|
+
pickedElement = null;
|
|
380
416
|
// Debounced key-broadcast to avoid flooding the parent on rapid DOM mutations
|
|
381
417
|
broadcastVisibleKeys;
|
|
382
|
-
constructor(onElementSelected, onVisibleKeysChanged) {
|
|
418
|
+
constructor(onElementSelected, onVisibleKeysChanged, onUnkeyedTap) {
|
|
383
419
|
this.onElementSelected = onElementSelected;
|
|
384
420
|
this.onVisibleKeysChanged = onVisibleKeysChanged;
|
|
421
|
+
this.onUnkeyedTap = onUnkeyedTap;
|
|
385
422
|
this.broadcastVisibleKeys = debounce(() => {
|
|
386
423
|
if (this.onVisibleKeysChanged) {
|
|
387
424
|
this.onVisibleKeysChanged(this.getActiveKeys());
|
|
@@ -429,6 +466,75 @@ var DOMInterceptor = class {
|
|
|
429
466
|
}
|
|
430
467
|
return texts;
|
|
431
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Returns the unique keys that have at least one rendered, visible element
|
|
471
|
+
* (see `isRendered`), in document order. Keys only inside closed menus,
|
|
472
|
+
* hidden slides or a layout hidden at this width are left out.
|
|
473
|
+
*/
|
|
474
|
+
getRenderedKeys() {
|
|
475
|
+
const keys = /* @__PURE__ */ new Set();
|
|
476
|
+
document.querySelectorAll("[data-i18n-key]").forEach((el) => {
|
|
477
|
+
const key = el.getAttribute("data-i18n-key");
|
|
478
|
+
if (key && !keys.has(key) && isRendered(el)) keys.add(key);
|
|
479
|
+
});
|
|
480
|
+
return Array.from(keys);
|
|
481
|
+
}
|
|
482
|
+
/** The first rendered, visible element carrying `key`, else `null`. */
|
|
483
|
+
findRenderedElement(key) {
|
|
484
|
+
for (const el of document.querySelectorAll("[data-i18n-key]")) {
|
|
485
|
+
if (el.getAttribute("data-i18n-key") === key && isRendered(el)) return el;
|
|
486
|
+
}
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Scrolls the first visible element with `key` to the centre and selects
|
|
491
|
+
* it as a tap would. Returns false, selecting nothing, when no element with
|
|
492
|
+
* that key is visible or the interceptor is not running. A key that is no
|
|
493
|
+
* longer visible (a carousel moved on with no DOM change the observer sees)
|
|
494
|
+
* reports the keys again, so the Studio's next try uses a fresh list.
|
|
495
|
+
*/
|
|
496
|
+
selectKey(key) {
|
|
497
|
+
if (!this.active) return false;
|
|
498
|
+
const el = this.findRenderedElement(key);
|
|
499
|
+
if (!el) {
|
|
500
|
+
this.broadcastVisibleKeys();
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
if (typeof el.scrollIntoView === "function") el.scrollIntoView({ block: "center" });
|
|
504
|
+
this.select(el, key);
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Scrolls so the first visible element with `key` sits fully between the
|
|
509
|
+
* frame's top and `bottomInset` CSS px above its bottom (where the Studio's
|
|
510
|
+
* sheet starts), without selecting it. Does nothing when it already does,
|
|
511
|
+
* or when no element with that key is visible. A tall element keeps its top
|
|
512
|
+
* in view rather than its bottom.
|
|
513
|
+
*/
|
|
514
|
+
revealKey(key, bottomInset) {
|
|
515
|
+
if (!this.active) return;
|
|
516
|
+
const el = this.findRenderedElement(key);
|
|
517
|
+
if (!el) return;
|
|
518
|
+
const rect = el.getBoundingClientRect();
|
|
519
|
+
const limit = window.innerHeight - bottomInset;
|
|
520
|
+
if (rect.top >= 0 && rect.bottom <= limit) return;
|
|
521
|
+
const delta = Math.min(rect.bottom - limit + REVEAL_MARGIN, rect.top - REVEAL_MARGIN);
|
|
522
|
+
window.scrollBy(0, delta);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Turns Edit mode on or off. In Edit mode no tap reaches the site: a tap on
|
|
526
|
+
* keyed text selects it, a tap on other text calls `onUnkeyedTap`, and
|
|
527
|
+
* anything else does nothing. Scrolling still works. The mode is kept on
|
|
528
|
+
* the instance and re-applied by `init()`.
|
|
529
|
+
*/
|
|
530
|
+
setEditMode(on) {
|
|
531
|
+
const changed = this.editMode !== on;
|
|
532
|
+
this.editMode = on;
|
|
533
|
+
if (!this.active) return;
|
|
534
|
+
if (on) this.attachEditMode();
|
|
535
|
+
else this.detachEditMode();
|
|
536
|
+
if (changed) this.broadcastVisibleKeys();
|
|
537
|
+
}
|
|
432
538
|
/**
|
|
433
539
|
* Initializes the DOM event interceptors in development mode.
|
|
434
540
|
* Also launches the watermark scanner and MutationObserver.
|
|
@@ -453,6 +559,8 @@ var DOMInterceptor = class {
|
|
|
453
559
|
this.scanSubtree(document.body);
|
|
454
560
|
this.attachMutationObserver();
|
|
455
561
|
this.attachInputSanitizers();
|
|
562
|
+
this.active = true;
|
|
563
|
+
if (this.editMode) this.attachEditMode();
|
|
456
564
|
this.broadcastVisibleKeys();
|
|
457
565
|
}
|
|
458
566
|
/**
|
|
@@ -473,6 +581,8 @@ var DOMInterceptor = class {
|
|
|
473
581
|
window.removeEventListener("keydown", this.handleKeyDown, true);
|
|
474
582
|
window.removeEventListener("keyup", this.handleKeyUp, true);
|
|
475
583
|
window.removeEventListener("beforeinput", this.handleBeforeInput, true);
|
|
584
|
+
this.detachEditMode();
|
|
585
|
+
this.active = false;
|
|
476
586
|
document.removeEventListener("submit", this.handleFormSubmit, true);
|
|
477
587
|
if (this.observer) {
|
|
478
588
|
this.observer.disconnect();
|
|
@@ -487,6 +597,87 @@ var DOMInterceptor = class {
|
|
|
487
597
|
this.hoveredElement = null;
|
|
488
598
|
this.broadcastVisibleKeys.cancel();
|
|
489
599
|
}
|
|
600
|
+
// ─── Edit Mode ───────────────────────────────────────────────────────────
|
|
601
|
+
attachEditMode() {
|
|
602
|
+
window.addEventListener("pointerdown", this.stopFromSite, true);
|
|
603
|
+
window.addEventListener("pointerup", this.stopFromSite, true);
|
|
604
|
+
window.addEventListener("mousedown", this.stopFromSite, true);
|
|
605
|
+
window.addEventListener("mouseup", this.stopFromSite, true);
|
|
606
|
+
window.addEventListener("touchstart", this.stopFromSite, { capture: true, passive: true });
|
|
607
|
+
window.addEventListener("touchend", this.stopFromSite, { capture: true, passive: true });
|
|
608
|
+
window.addEventListener("focusin", this.handleEditFocus, true);
|
|
609
|
+
if (!document.getElementById(EDIT_STYLE_ID)) {
|
|
610
|
+
const style = document.createElement("style");
|
|
611
|
+
style.id = EDIT_STYLE_ID;
|
|
612
|
+
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}`;
|
|
613
|
+
document.head.appendChild(style);
|
|
614
|
+
}
|
|
615
|
+
document.documentElement.setAttribute(EDIT_ATTRIBUTE, "");
|
|
616
|
+
this.hideOverlay();
|
|
617
|
+
this.hoveredElement = null;
|
|
618
|
+
}
|
|
619
|
+
detachEditMode() {
|
|
620
|
+
window.removeEventListener("pointerdown", this.stopFromSite, true);
|
|
621
|
+
window.removeEventListener("pointerup", this.stopFromSite, true);
|
|
622
|
+
window.removeEventListener("mousedown", this.stopFromSite, true);
|
|
623
|
+
window.removeEventListener("mouseup", this.stopFromSite, true);
|
|
624
|
+
window.removeEventListener("touchstart", this.stopFromSite, true);
|
|
625
|
+
window.removeEventListener("touchend", this.stopFromSite, true);
|
|
626
|
+
window.removeEventListener("focusin", this.handleEditFocus, true);
|
|
627
|
+
document.documentElement.removeAttribute(EDIT_ATTRIBUTE);
|
|
628
|
+
document.getElementById(EDIT_STYLE_ID)?.remove();
|
|
629
|
+
this.clearPicked();
|
|
630
|
+
}
|
|
631
|
+
/** Moves the picked-text marker to `element`. */
|
|
632
|
+
markPicked(element) {
|
|
633
|
+
this.clearPicked();
|
|
634
|
+
element.setAttribute(PICKED_ATTRIBUTE, "");
|
|
635
|
+
this.pickedElement = element;
|
|
636
|
+
}
|
|
637
|
+
clearPicked() {
|
|
638
|
+
this.pickedElement?.removeAttribute(PICKED_ATTRIBUTE);
|
|
639
|
+
this.pickedElement = null;
|
|
640
|
+
}
|
|
641
|
+
// Sites open menus on press events; in Edit mode none of them reaches the
|
|
642
|
+
// site. Never preventDefault here: on touchstart it would stop scrolling.
|
|
643
|
+
stopFromSite = (event) => {
|
|
644
|
+
event.stopImmediatePropagation();
|
|
645
|
+
};
|
|
646
|
+
handleEditFocus = (event) => {
|
|
647
|
+
const target = event.target;
|
|
648
|
+
if (!target || typeof target.matches !== "function") return;
|
|
649
|
+
if (target.matches(EDITABLE_SELECTOR) || target.isContentEditable) target.blur();
|
|
650
|
+
};
|
|
651
|
+
/** Edit-mode click: cancelled whatever the target, then a selection or the notice. */
|
|
652
|
+
handleEditClick(event) {
|
|
653
|
+
event.preventDefault();
|
|
654
|
+
event.stopImmediatePropagation();
|
|
655
|
+
const target = event.target;
|
|
656
|
+
if (!target || typeof target.closest !== "function") return;
|
|
657
|
+
const keyedElement = target.closest("[data-i18n-key]");
|
|
658
|
+
const key = keyedElement?.getAttribute("data-i18n-key");
|
|
659
|
+
if (keyedElement && key) {
|
|
660
|
+
this.select(keyedElement, key);
|
|
661
|
+
} else if (hasOwnText(target)) {
|
|
662
|
+
this.onUnkeyedTap?.();
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Reports `element`, which carries `key`, as selected with its clean text. In Edit mode it
|
|
667
|
+
* also gets the picked-text marker; an Alt+Click pick on a desktop does not.
|
|
668
|
+
*/
|
|
669
|
+
select(element, key) {
|
|
670
|
+
if (this.editMode) this.markPicked(element);
|
|
671
|
+
const rawText = element.innerText || "";
|
|
672
|
+
const cleanText = stripWatermark(rawText);
|
|
673
|
+
const fallbackValue = element.getAttribute("data-i18n-template") || cleanText;
|
|
674
|
+
const currentValue = cleanText;
|
|
675
|
+
this.onElementSelected({
|
|
676
|
+
key,
|
|
677
|
+
fallbackValue,
|
|
678
|
+
currentValue
|
|
679
|
+
});
|
|
680
|
+
}
|
|
490
681
|
// ─── Floating Overlay ────────────────────────────────────────────────────
|
|
491
682
|
/**
|
|
492
683
|
* Creates and injects a single floating overlay div into the document body.
|
|
@@ -730,6 +921,7 @@ var DOMInterceptor = class {
|
|
|
730
921
|
};
|
|
731
922
|
// ─── Mouse / Click Handlers ───────────────────────────────────────────────
|
|
732
923
|
handleMouseOver = (event) => {
|
|
924
|
+
if (this.editMode) return;
|
|
733
925
|
const target = event.target;
|
|
734
926
|
if (!target || typeof target.closest !== "function") return;
|
|
735
927
|
const keyedElement = target.closest("[data-i18n-key]");
|
|
@@ -758,6 +950,10 @@ var DOMInterceptor = class {
|
|
|
758
950
|
this.hoveredElement = null;
|
|
759
951
|
};
|
|
760
952
|
handleClick = (event) => {
|
|
953
|
+
if (this.editMode) {
|
|
954
|
+
this.handleEditClick(event);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
761
957
|
if (!event.altKey) return;
|
|
762
958
|
const target = event.target;
|
|
763
959
|
if (!target || typeof target.closest !== "function") return;
|
|
@@ -767,15 +963,7 @@ var DOMInterceptor = class {
|
|
|
767
963
|
if (key === null) return;
|
|
768
964
|
event.preventDefault();
|
|
769
965
|
event.stopPropagation();
|
|
770
|
-
|
|
771
|
-
const cleanText = stripWatermark(rawText);
|
|
772
|
-
const fallbackValue = keyedElement.getAttribute("data-i18n-template") || cleanText;
|
|
773
|
-
const currentValue = cleanText;
|
|
774
|
-
this.onElementSelected({
|
|
775
|
-
key,
|
|
776
|
-
fallbackValue,
|
|
777
|
-
currentValue
|
|
778
|
-
});
|
|
966
|
+
this.select(keyedElement, key);
|
|
779
967
|
};
|
|
780
968
|
handleCopy = (event) => {
|
|
781
969
|
const selection = window.getSelection()?.toString() || "";
|
|
@@ -1036,16 +1224,22 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
|
|
|
1036
1224
|
},
|
|
1037
1225
|
(keys) => {
|
|
1038
1226
|
bridge.sendToParent("VISIBLE_KEYS_CHANGED", keys);
|
|
1227
|
+
bridge.sendToParent("RENDERED_KEYS_CHANGED", interceptor.getRenderedKeys());
|
|
1039
1228
|
bridge.sendToParent("VISIBLE_TEXTS_CHANGED", interceptor.getVisibleTexts());
|
|
1229
|
+
},
|
|
1230
|
+
() => {
|
|
1231
|
+
bridge.sendToParent("TAP_UNKEYED");
|
|
1040
1232
|
}
|
|
1041
1233
|
);
|
|
1042
1234
|
interceptor.init();
|
|
1043
1235
|
const locationUrl = () => window.location.pathname + window.location.search + window.location.hash;
|
|
1044
|
-
bridge.sendToParent("READY", { url: locationUrl() });
|
|
1236
|
+
bridge.sendToParent("READY", { url: locationUrl(), features: CLIENT_FEATURES });
|
|
1045
1237
|
watchLocation(window, (url) => bridge.sendToParent("LOCATION_CHANGED", { url }));
|
|
1046
1238
|
window.addEventListener("pagehide", () => bridge.sendToParent("LEAVING"));
|
|
1047
1239
|
window.addEventListener("pageshow", (event) => {
|
|
1048
|
-
if (event.persisted)
|
|
1240
|
+
if (event.persisted) {
|
|
1241
|
+
bridge.sendToParent("READY", { url: locationUrl(), features: CLIENT_FEATURES });
|
|
1242
|
+
}
|
|
1049
1243
|
});
|
|
1050
1244
|
bridge.listenToParent((type, payload) => {
|
|
1051
1245
|
if (type === "APPLY_PREVIEW") {
|
|
@@ -1063,6 +1257,18 @@ if (typeof window !== "undefined" && isLiveLocaleEnabled() && window.parent !==
|
|
|
1063
1257
|
} else if (type === "NAVIGATE") {
|
|
1064
1258
|
const href = navigationHref(payload, window.location.origin);
|
|
1065
1259
|
if (href !== null) followNavigation(window.location, href);
|
|
1260
|
+
} else if (type === "SET_EDIT_MODE") {
|
|
1261
|
+
if (payload && typeof payload.on === "boolean") {
|
|
1262
|
+
interceptor.setEditMode(payload.on);
|
|
1263
|
+
}
|
|
1264
|
+
} else if (type === "SELECT_KEY") {
|
|
1265
|
+
if (payload && typeof payload.key === "string") {
|
|
1266
|
+
interceptor.selectKey(payload.key);
|
|
1267
|
+
}
|
|
1268
|
+
} else if (type === "REVEAL_KEY") {
|
|
1269
|
+
if (payload && typeof payload.key === "string" && typeof payload.bottomInset === "number" && Number.isFinite(payload.bottomInset) && payload.bottomInset >= 0) {
|
|
1270
|
+
interceptor.revealKey(payload.key, payload.bottomInset);
|
|
1271
|
+
}
|
|
1066
1272
|
}
|
|
1067
1273
|
});
|
|
1068
1274
|
window.__LIVELOCALE_CLIENT__ = {
|