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/dist/client/index.d.ts
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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()
|
package/dist/client/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
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__ = {
|