@upstart.gg/vite-plugins 0.1.69 → 0.1.70
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/vite-plugin-upstart-editor/runtime/array-controls.js +7 -7
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +4 -3
- package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/form-guard.js +2 -2
- package/dist/vite-plugin-upstart-editor/runtime/form-guard.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +2 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +32 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.js +45 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +20 -14
- package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +5 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/tests/editor-labels.test.ts +40 -0
- package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +7 -7
- package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +4 -3
- package/src/vite-plugin-upstart-editor/runtime/form-guard.ts +3 -2
- package/src/vite-plugin-upstart-editor/runtime/index.ts +3 -0
- package/src/vite-plugin-upstart-editor/runtime/labels.ts +77 -0
- package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +25 -15
- package/src/vite-plugin-upstart-editor/runtime/types.ts +6 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
|
+
import { getLabels } from "./labels.js";
|
|
3
4
|
//#region src/vite-plugin-upstart-editor/runtime/array-controls.ts
|
|
4
|
-
const DEFAULT_NEW_ITEM = "New item";
|
|
5
5
|
const DRAFT_ATTR = "data-upstart-draft-item";
|
|
6
6
|
const BTN_SIZE = 22;
|
|
7
7
|
const STACK_GAP = 6;
|
|
@@ -197,7 +197,7 @@ function baseButton(top, left, size, bg) {
|
|
|
197
197
|
function makeItemDeleteButton(arrayId, index, marked, rect) {
|
|
198
198
|
const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, "#ef4444");
|
|
199
199
|
setIcon(btn, marked ? "restore" : "cross", 10);
|
|
200
|
-
btn.title = marked ?
|
|
200
|
+
btn.title = marked ? getLabels().keepItem : getLabels().removeItem;
|
|
201
201
|
btn.addEventListener("click", (e) => {
|
|
202
202
|
e.preventDefault();
|
|
203
203
|
e.stopPropagation();
|
|
@@ -213,7 +213,7 @@ function makeItemDeleteButton(arrayId, index, marked, rect) {
|
|
|
213
213
|
function makeDraftRemoveButton(pill, rect) {
|
|
214
214
|
const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, "#ef4444");
|
|
215
215
|
setIcon(btn, "cross", 10);
|
|
216
|
-
btn.title =
|
|
216
|
+
btn.title = getLabels().removeItem;
|
|
217
217
|
btn.addEventListener("click", (e) => {
|
|
218
218
|
e.preventDefault();
|
|
219
219
|
e.stopPropagation();
|
|
@@ -225,7 +225,7 @@ function makeDraftRemoveButton(pill, rect) {
|
|
|
225
225
|
function makeAddButton(arrayId, top, left) {
|
|
226
226
|
const btn = baseButton(top, left, 22, "#7270c6");
|
|
227
227
|
setIcon(btn, "plus", 14);
|
|
228
|
-
btn.title =
|
|
228
|
+
btn.title = getLabels().addItem;
|
|
229
229
|
btn.addEventListener("click", (e) => {
|
|
230
230
|
e.preventDefault();
|
|
231
231
|
e.stopPropagation();
|
|
@@ -236,7 +236,7 @@ function makeAddButton(arrayId, top, left) {
|
|
|
236
236
|
function makeApplyButton(arrayId, top, left) {
|
|
237
237
|
const btn = baseButton(top, left, 22, "#16a34a");
|
|
238
238
|
setIcon(btn, "check", 13);
|
|
239
|
-
btn.title =
|
|
239
|
+
btn.title = getLabels().applyChanges;
|
|
240
240
|
btn.addEventListener("click", (e) => {
|
|
241
241
|
e.preventDefault();
|
|
242
242
|
e.stopPropagation();
|
|
@@ -247,7 +247,7 @@ function makeApplyButton(arrayId, top, left) {
|
|
|
247
247
|
function makeDragHandle(arrayId, node, rect) {
|
|
248
248
|
const btn = baseButton(rect.top + window.scrollY - 8, rect.left + window.scrollX + rect.width / 2 - 8, 16, "#6b7280");
|
|
249
249
|
setIcon(btn, "grip", 12);
|
|
250
|
-
btn.title =
|
|
250
|
+
btn.title = getLabels().dragToReorder;
|
|
251
251
|
btn.style.cursor = "grab";
|
|
252
252
|
btn.addEventListener("pointerdown", (e) => {
|
|
253
253
|
e.preventDefault();
|
|
@@ -299,7 +299,7 @@ function addDraftPill(arrayId) {
|
|
|
299
299
|
pill.removeAttribute("style");
|
|
300
300
|
pill.setAttribute(DRAFT_ATTR, arrayId);
|
|
301
301
|
pill.setAttribute("contenteditable", "true");
|
|
302
|
-
pill.textContent =
|
|
302
|
+
pill.textContent = getLabels().newItem;
|
|
303
303
|
const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();
|
|
304
304
|
if (last && last.parentElement === template.parentElement) last.after(pill);
|
|
305
305
|
else template.parentElement.appendChild(pill);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"array-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/array-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\n// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY\n// (no server round-trip per action): \"×\" marks an item for removal, \"+\" inserts an\n// editable draft pill, and a green \"✓\" below the \"+\" commits the whole batch at once\n// via a single `arraySet` edit + rebuild. Per-item text editing stays live; arraySet\n// reads the current item texts from the DOM so live edits are preserved.\n\nconst DEFAULT_NEW_ITEM = \"New item\";\nconst DRAFT_ATTR = \"data-upstart-draft-item\";\n\n// Layout of the \"+\"/\"✓\" stack placed to the right of a list.\nconst BTN_SIZE = 22;\nconst STACK_GAP = 6; // vertical gap between \"+\" and \"✓\"\nconst BUTTON_MARGIN = 12; // left margin between the item and the buttons\n\nlet layer: HTMLDivElement | null = null;\nlet observer: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nlet isInitialized = false;\nlet isMutatingDom = false;\n\n// Items observed for size changes (e.g. text edits widen a pill) so the \"×\" stays\n// anchored to the item's corner. WeakSet keeps observe() calls idempotent.\nconst observedForResize = new WeakSet<Element>();\n\n// arrayId -> set of original item indices currently marked for deletion.\nconst pendingDeletes = new Map<string, Set<number>>();\n\n// In-progress drag (reorder). Tracked at document level so it survives overlay\n// rebuilds; the overlay is frozen while a drag is active.\nlet drag: { arrayId: string; node: HTMLElement } | null = null;\n\n// The list item currently hovered — its per-item \"×\"/\"⠿\" controls are the only\n// ones shown (others stay hidden until hovered). buttonOwner lets a button keep its\n// item \"hovered\" while the pointer sits on the button (which overlays the corner).\nlet hoveredItem: HTMLElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nexport function initArrayControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n\n observer = new MutationObserver((mutations) => {\n if (isMutatingDom) return;\n for (const m of mutations) {\n const t = m.target as Node;\n if (layer && (t === layer || layer.contains(t))) continue;\n scheduleRefresh();\n return;\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => scheduleRefresh());\n }\n}\n\n// Append a per-item button and record which item it belongs to, so hovering the\n// button (which overlays the item's corner) keeps that item considered hovered.\nfunction appendOwned(root: HTMLElement, btn: HTMLElement, owner: HTMLElement): void {\n buttonOwner.set(btn, owner);\n root.appendChild(btn);\n}\n\n// Track the hovered list item; refresh when it changes so its controls toggle.\nfunction onHoverMove(e: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The list item under the pointer: either the item itself (or a descendant), or a\n// per-item control button that belongs to one.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n const item = target.closest<HTMLElement>(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);\n if (item) return item;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n if (owner && document.contains(owner)) return owner;\n }\n return null;\n}\n\n// Observe an item once so the overlay re-anchors when the item resizes.\nfunction observeResize(el: Element): void {\n if (resizeObserver && !observedForResize.has(el)) {\n observedForResize.add(el);\n resizeObserver.observe(el);\n }\n}\n\nexport function refreshArrayControls(): void {\n if (typeof document === \"undefined\") return;\n\n if (getCurrentMode() !== \"edit\") {\n hideArrayControls();\n return;\n }\n\n // While dragging we move list nodes live; keep the overlay frozen and rebuild it\n // once on drop (the drag is driven by document-level listeners, not the overlay).\n if (drag) return;\n\n isMutatingDom = true;\n try {\n const root = ensureLayer();\n root.replaceChildren();\n\n // Group original (non-draft) items by array id, ordered by loop index.\n const groups = new Map<string, HTMLElement[]>();\n for (const el of document.querySelectorAll<HTMLElement>(\"[data-upstart-array-id]\")) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const id = el.dataset.upstartArrayId;\n if (!id) continue;\n (groups.get(id) ?? groups.set(id, []).get(id)!).push(el);\n }\n\n for (const [arrayId, items] of groups) {\n items.sort((a, b) => itemIndex(a) - itemIndex(b));\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const drafts = draftPillsFor(arrayId);\n const ordered = orderedItemsFor(arrayId);\n // Reordering only makes sense with more than one item.\n const draggable = ordered.length > 1;\n\n // Original items: per-item controls show only on hover; a marked item always\n // keeps its restore \"×\" (it has pointer-events:none, so it can't be hovered).\n for (const item of items) {\n observeResize(item);\n const idx = itemIndex(item);\n const marked = deletes.has(idx);\n applyDeletionStyle(item, marked);\n if (!marked && item !== hoveredItem) continue;\n const rect = item.getBoundingClientRect();\n if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);\n appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);\n }\n\n // Draft (added) pills: controls show on hover; each has its own \"×\".\n for (const pill of drafts) {\n observeResize(pill);\n if (pill !== hoveredItem) continue;\n const rect = pill.getBoundingClientRect();\n if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);\n appendOwned(root, makeDraftRemoveButton(pill, rect), pill);\n }\n\n // \"+\" (and \"✓\" when there are pending changes) after the last item in DOM\n // order, as a vertically-centred stack with a small left margin.\n const anchor = ordered[ordered.length - 1];\n if (!anchor) continue;\n const a = anchor.getBoundingClientRect();\n\n const finalCount = items.length - deletes.size + drafts.length;\n const dirty = deletes.size > 0 || drafts.length > 0 || isReordered(arrayId);\n const showApply = dirty && finalCount >= 1;\n\n const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;\n const left = a.right + window.scrollX + BUTTON_MARGIN;\n const top = a.top + window.scrollY + (a.height - stackHeight) / 2;\n\n root.appendChild(makeAddButton(arrayId, top, left));\n if (showApply) {\n root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));\n }\n }\n } finally {\n isMutatingDom = false;\n }\n}\n\nexport function hideArrayControls(): void {\n if (layer) layer.replaceChildren();\n}\n\n// ---------------------------------------------------------------------------\n// DOM helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLayer(): HTMLDivElement {\n if (layer) return layer;\n layer = document.createElement(\"div\");\n layer.id = \"upstart-array-controls\";\n layer.style.cssText =\n \"position: absolute; top: 0; left: 0; width: 0; height: 0; \" +\n \"pointer-events: none; z-index: 2147483646;\";\n document.body.appendChild(layer);\n return layer;\n}\n\nfunction itemIndex(el: HTMLElement): number {\n const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : Number.NaN;\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction originalItemsFor(arrayId: string): HTMLElement[] {\n return [\n ...document.querySelectorAll<HTMLElement>(\n `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}])`,\n ),\n ].sort((a, b) => itemIndex(a) - itemIndex(b));\n}\n\nfunction draftPillsFor(arrayId: string): HTMLElement[] {\n return [...document.querySelectorAll<HTMLElement>(`[${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`)];\n}\n\n// All items of an array (originals + drafts) in DOM order — this is the order the\n// list is rendered in and the order applied to the source on ✓.\nfunction orderedItemsFor(arrayId: string): HTMLElement[] {\n const sel = `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`;\n return [...document.querySelectorAll<HTMLElement>(sel)];\n}\n\n// True when the original items no longer appear in their source order (i.e. the\n// user has dragged at least one item), so the ✓ apply button should be offered.\nfunction isReordered(arrayId: string): boolean {\n let prev = -1;\n for (const el of orderedItemsFor(arrayId)) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const idx = itemIndex(el);\n if (idx < prev) return true;\n prev = idx;\n }\n return false;\n}\n\nfunction cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction textOf(el: HTMLElement): string {\n return (el.textContent ?? \"\").trim();\n}\n\nfunction applyDeletionStyle(item: HTMLElement, marked: boolean): void {\n if (marked) {\n item.style.opacity = \"0.4\";\n item.style.textDecoration = \"line-through\";\n item.style.pointerEvents = \"none\";\n } else {\n item.style.removeProperty(\"opacity\");\n item.style.removeProperty(\"text-decoration\");\n item.style.removeProperty(\"pointer-events\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Buttons\n// ---------------------------------------------------------------------------\n\n// Geometric SVG icons — centred by the flex container + viewBox, so they don't\n// depend on the host page's font metrics (which left text glyphs mis-aligned).\nconst ICON_PATHS: Record<string, string> = {\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n cross: '<path d=\"M6 6 18 18M18 6 6 18\"/>',\n check: '<path d=\"M5 12.5 10 17.5 19 7\"/>',\n restore: '<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>',\n grip: '<circle cx=\"9\" cy=\"6\" r=\"1.5\"/><circle cx=\"15\" cy=\"6\" r=\"1.5\"/><circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/><circle cx=\"9\" cy=\"18\" r=\"1.5\"/><circle cx=\"15\" cy=\"18\" r=\"1.5\"/>',\n};\n\nfunction setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {\n const dots = name === \"grip\";\n btn.innerHTML =\n `<svg width=\"${px}\" height=\"${px}\" viewBox=\"0 0 24 24\" ` +\n `fill=\"${dots ? \"currentColor\" : \"none\"}\" stroke=\"${dots ? \"none\" : \"currentColor\"}\" ` +\n 'stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" ' +\n `style=\"display:block;pointer-events:none\">${ICON_PATHS[name]}</svg>`;\n}\n\nfunction baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.style.cssText =\n `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +\n \"padding: 0; display: flex; align-items: center; justify-content: center; \" +\n `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +\n \"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);\";\n return btn;\n}\n\nfunction makeItemDeleteButton(\n arrayId: string,\n index: number,\n marked: boolean,\n rect: DOMRect,\n): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, marked ? \"restore\" : \"cross\", 10);\n btn.title = marked ? \"Keep item\" : \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const set = pendingDeletes.get(arrayId) ?? new Set<number>();\n if (set.has(index)) set.delete(index);\n else set.add(index);\n if (set.size) pendingDeletes.set(arrayId, set);\n else pendingDeletes.delete(arrayId);\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeDraftRemoveButton(pill: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, \"cross\", 10);\n btn.title = \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n pill.remove();\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeAddButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#7270c6\");\n setIcon(btn, \"plus\", 14);\n btn.title = \"Add item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n addDraftPill(arrayId);\n });\n return btn;\n}\n\nfunction makeApplyButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#16a34a\");\n setIcon(btn, \"check\", 13);\n btn.title = \"Apply changes\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n applyArray(arrayId);\n });\n return btn;\n}\n\n// Grip centred on the item's top edge (away from the neighbour's \"×\" corners);\n// press and drag to reorder.\nfunction makeDragHandle(arrayId: string, node: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const top = rect.top + window.scrollY - 8;\n const left = rect.left + window.scrollX + rect.width / 2 - 8;\n const btn = baseButton(top, left, 16, \"#6b7280\");\n setIcon(btn, \"grip\", 12);\n btn.title = \"Drag to reorder\";\n btn.style.cursor = \"grab\";\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n startDrag(arrayId, node);\n });\n return btn;\n}\n\n// ---------------------------------------------------------------------------\n// Drag to reorder\n// ---------------------------------------------------------------------------\n\nfunction startDrag(arrayId: string, node: HTMLElement): void {\n drag = { arrayId, node };\n node.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n // Drive the drag from document level so it survives overlay rebuilds.\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragEnd, { once: true });\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);\n if (!target) return;\n const r = target.getBoundingClientRect();\n // Insert before/after based on the pointer vs the target's horizontal midpoint.\n if (e.clientX > r.left + r.width / 2) target.after(drag.node);\n else target.before(drag.node);\n}\n\nfunction onDragEnd(): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.body.style.removeProperty(\"user-select\");\n if (drag) {\n drag.node.style.removeProperty(\"opacity\");\n drag = null;\n }\n refreshArrayControls();\n}\n\n// The array item (original or draft) whose box contains the pointer, excluding the\n// node being dragged.\nfunction itemUnderPointer(arrayId: string, x: number, y: number, exclude: HTMLElement): HTMLElement | null {\n for (const el of orderedItemsFor(arrayId)) {\n if (el === exclude) continue;\n const r = el.getBoundingClientRect();\n if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction addDraftPill(arrayId: string): void {\n const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];\n if (!template?.parentElement) return;\n\n const pill = template.cloneNode(false) as HTMLElement;\n // Clone keeps the pill styling (class); strip identity/editor attrs so it is not\n // picked up by the text editor, click handler, hover overlay or the array query.\n for (const attr of [...pill.attributes]) {\n if (attr.name.startsWith(\"data-upstart-\") || attr.name === \"contenteditable\") {\n pill.removeAttribute(attr.name);\n }\n }\n pill.removeAttribute(\"style\");\n pill.setAttribute(DRAFT_ATTR, arrayId);\n pill.setAttribute(\"contenteditable\", \"true\");\n pill.textContent = DEFAULT_NEW_ITEM;\n\n // Insert after the current last pill of this array (drafts go at the end).\n const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();\n if (last && last.parentElement === template.parentElement) {\n last.after(pill);\n } else {\n template.parentElement.appendChild(pill);\n }\n\n // Focus + select so the user can type the name immediately.\n pill.focus();\n const range = document.createRange();\n range.selectNodeContents(pill);\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n\n refreshArrayControls();\n}\n\nfunction applyArray(arrayId: string): void {\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const items: string[] = [];\n\n // DOM order = the order the user sees (incl. any drag reordering).\n for (const el of orderedItemsFor(arrayId)) {\n if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;\n items.push(textOf(el));\n }\n\n if (items.length === 0) return;\n\n // The server rewrites the whole array, rebuilds + swaps, then the editor reloads\n // the iframe — which discards the local draft state, so just clear our marks.\n pendingDeletes.delete(arrayId);\n sendToParent({ type: \"text-edit\", payload: { action: \"arraySet\", arrayId, items } });\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshArrayControls();\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"array-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/array-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getLabels } from \"./labels.js\";\n\n// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY\n// (no server round-trip per action): \"×\" marks an item for removal, \"+\" inserts an\n// editable draft pill, and a green \"✓\" below the \"+\" commits the whole batch at once\n// via a single `arraySet` edit + rebuild. Per-item text editing stays live; arraySet\n// reads the current item texts from the DOM so live edits are preserved.\n\nconst DRAFT_ATTR = \"data-upstart-draft-item\";\n\n// Layout of the \"+\"/\"✓\" stack placed to the right of a list.\nconst BTN_SIZE = 22;\nconst STACK_GAP = 6; // vertical gap between \"+\" and \"✓\"\nconst BUTTON_MARGIN = 12; // left margin between the item and the buttons\n\nlet layer: HTMLDivElement | null = null;\nlet observer: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nlet isInitialized = false;\nlet isMutatingDom = false;\n\n// Items observed for size changes (e.g. text edits widen a pill) so the \"×\" stays\n// anchored to the item's corner. WeakSet keeps observe() calls idempotent.\nconst observedForResize = new WeakSet<Element>();\n\n// arrayId -> set of original item indices currently marked for deletion.\nconst pendingDeletes = new Map<string, Set<number>>();\n\n// In-progress drag (reorder). Tracked at document level so it survives overlay\n// rebuilds; the overlay is frozen while a drag is active.\nlet drag: { arrayId: string; node: HTMLElement } | null = null;\n\n// The list item currently hovered — its per-item \"×\"/\"⠿\" controls are the only\n// ones shown (others stay hidden until hovered). buttonOwner lets a button keep its\n// item \"hovered\" while the pointer sits on the button (which overlays the corner).\nlet hoveredItem: HTMLElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nexport function initArrayControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n\n observer = new MutationObserver((mutations) => {\n if (isMutatingDom) return;\n for (const m of mutations) {\n const t = m.target as Node;\n if (layer && (t === layer || layer.contains(t))) continue;\n scheduleRefresh();\n return;\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => scheduleRefresh());\n }\n}\n\n// Append a per-item button and record which item it belongs to, so hovering the\n// button (which overlays the item's corner) keeps that item considered hovered.\nfunction appendOwned(root: HTMLElement, btn: HTMLElement, owner: HTMLElement): void {\n buttonOwner.set(btn, owner);\n root.appendChild(btn);\n}\n\n// Track the hovered list item; refresh when it changes so its controls toggle.\nfunction onHoverMove(e: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The list item under the pointer: either the item itself (or a descendant), or a\n// per-item control button that belongs to one.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n const item = target.closest<HTMLElement>(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);\n if (item) return item;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n if (owner && document.contains(owner)) return owner;\n }\n return null;\n}\n\n// Observe an item once so the overlay re-anchors when the item resizes.\nfunction observeResize(el: Element): void {\n if (resizeObserver && !observedForResize.has(el)) {\n observedForResize.add(el);\n resizeObserver.observe(el);\n }\n}\n\nexport function refreshArrayControls(): void {\n if (typeof document === \"undefined\") return;\n\n if (getCurrentMode() !== \"edit\") {\n hideArrayControls();\n return;\n }\n\n // While dragging we move list nodes live; keep the overlay frozen and rebuild it\n // once on drop (the drag is driven by document-level listeners, not the overlay).\n if (drag) return;\n\n isMutatingDom = true;\n try {\n const root = ensureLayer();\n root.replaceChildren();\n\n // Group original (non-draft) items by array id, ordered by loop index.\n const groups = new Map<string, HTMLElement[]>();\n for (const el of document.querySelectorAll<HTMLElement>(\"[data-upstart-array-id]\")) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const id = el.dataset.upstartArrayId;\n if (!id) continue;\n (groups.get(id) ?? groups.set(id, []).get(id)!).push(el);\n }\n\n for (const [arrayId, items] of groups) {\n items.sort((a, b) => itemIndex(a) - itemIndex(b));\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const drafts = draftPillsFor(arrayId);\n const ordered = orderedItemsFor(arrayId);\n // Reordering only makes sense with more than one item.\n const draggable = ordered.length > 1;\n\n // Original items: per-item controls show only on hover; a marked item always\n // keeps its restore \"×\" (it has pointer-events:none, so it can't be hovered).\n for (const item of items) {\n observeResize(item);\n const idx = itemIndex(item);\n const marked = deletes.has(idx);\n applyDeletionStyle(item, marked);\n if (!marked && item !== hoveredItem) continue;\n const rect = item.getBoundingClientRect();\n if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);\n appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);\n }\n\n // Draft (added) pills: controls show on hover; each has its own \"×\".\n for (const pill of drafts) {\n observeResize(pill);\n if (pill !== hoveredItem) continue;\n const rect = pill.getBoundingClientRect();\n if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);\n appendOwned(root, makeDraftRemoveButton(pill, rect), pill);\n }\n\n // \"+\" (and \"✓\" when there are pending changes) after the last item in DOM\n // order, as a vertically-centred stack with a small left margin.\n const anchor = ordered[ordered.length - 1];\n if (!anchor) continue;\n const a = anchor.getBoundingClientRect();\n\n const finalCount = items.length - deletes.size + drafts.length;\n const dirty = deletes.size > 0 || drafts.length > 0 || isReordered(arrayId);\n const showApply = dirty && finalCount >= 1;\n\n const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;\n const left = a.right + window.scrollX + BUTTON_MARGIN;\n const top = a.top + window.scrollY + (a.height - stackHeight) / 2;\n\n root.appendChild(makeAddButton(arrayId, top, left));\n if (showApply) {\n root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));\n }\n }\n } finally {\n isMutatingDom = false;\n }\n}\n\nexport function hideArrayControls(): void {\n if (layer) layer.replaceChildren();\n}\n\n// ---------------------------------------------------------------------------\n// DOM helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLayer(): HTMLDivElement {\n if (layer) return layer;\n layer = document.createElement(\"div\");\n layer.id = \"upstart-array-controls\";\n layer.style.cssText =\n \"position: absolute; top: 0; left: 0; width: 0; height: 0; \" +\n \"pointer-events: none; z-index: 2147483646;\";\n document.body.appendChild(layer);\n return layer;\n}\n\nfunction itemIndex(el: HTMLElement): number {\n const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : Number.NaN;\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction originalItemsFor(arrayId: string): HTMLElement[] {\n return [\n ...document.querySelectorAll<HTMLElement>(\n `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}])`,\n ),\n ].sort((a, b) => itemIndex(a) - itemIndex(b));\n}\n\nfunction draftPillsFor(arrayId: string): HTMLElement[] {\n return [...document.querySelectorAll<HTMLElement>(`[${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`)];\n}\n\n// All items of an array (originals + drafts) in DOM order — this is the order the\n// list is rendered in and the order applied to the source on ✓.\nfunction orderedItemsFor(arrayId: string): HTMLElement[] {\n const sel = `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`;\n return [...document.querySelectorAll<HTMLElement>(sel)];\n}\n\n// True when the original items no longer appear in their source order (i.e. the\n// user has dragged at least one item), so the ✓ apply button should be offered.\nfunction isReordered(arrayId: string): boolean {\n let prev = -1;\n for (const el of orderedItemsFor(arrayId)) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const idx = itemIndex(el);\n if (idx < prev) return true;\n prev = idx;\n }\n return false;\n}\n\nfunction cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction textOf(el: HTMLElement): string {\n return (el.textContent ?? \"\").trim();\n}\n\nfunction applyDeletionStyle(item: HTMLElement, marked: boolean): void {\n if (marked) {\n item.style.opacity = \"0.4\";\n item.style.textDecoration = \"line-through\";\n item.style.pointerEvents = \"none\";\n } else {\n item.style.removeProperty(\"opacity\");\n item.style.removeProperty(\"text-decoration\");\n item.style.removeProperty(\"pointer-events\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Buttons\n// ---------------------------------------------------------------------------\n\n// Geometric SVG icons — centred by the flex container + viewBox, so they don't\n// depend on the host page's font metrics (which left text glyphs mis-aligned).\nconst ICON_PATHS: Record<string, string> = {\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n cross: '<path d=\"M6 6 18 18M18 6 6 18\"/>',\n check: '<path d=\"M5 12.5 10 17.5 19 7\"/>',\n restore: '<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>',\n grip: '<circle cx=\"9\" cy=\"6\" r=\"1.5\"/><circle cx=\"15\" cy=\"6\" r=\"1.5\"/><circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/><circle cx=\"9\" cy=\"18\" r=\"1.5\"/><circle cx=\"15\" cy=\"18\" r=\"1.5\"/>',\n};\n\nfunction setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {\n const dots = name === \"grip\";\n btn.innerHTML =\n `<svg width=\"${px}\" height=\"${px}\" viewBox=\"0 0 24 24\" ` +\n `fill=\"${dots ? \"currentColor\" : \"none\"}\" stroke=\"${dots ? \"none\" : \"currentColor\"}\" ` +\n 'stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" ' +\n `style=\"display:block;pointer-events:none\">${ICON_PATHS[name]}</svg>`;\n}\n\nfunction baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.style.cssText =\n `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +\n \"padding: 0; display: flex; align-items: center; justify-content: center; \" +\n `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +\n \"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);\";\n return btn;\n}\n\nfunction makeItemDeleteButton(\n arrayId: string,\n index: number,\n marked: boolean,\n rect: DOMRect,\n): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, marked ? \"restore\" : \"cross\", 10);\n btn.title = marked ? getLabels().keepItem : getLabels().removeItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const set = pendingDeletes.get(arrayId) ?? new Set<number>();\n if (set.has(index)) set.delete(index);\n else set.add(index);\n if (set.size) pendingDeletes.set(arrayId, set);\n else pendingDeletes.delete(arrayId);\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeDraftRemoveButton(pill: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, \"cross\", 10);\n btn.title = getLabels().removeItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n pill.remove();\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeAddButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#7270c6\");\n setIcon(btn, \"plus\", 14);\n btn.title = getLabels().addItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n addDraftPill(arrayId);\n });\n return btn;\n}\n\nfunction makeApplyButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#16a34a\");\n setIcon(btn, \"check\", 13);\n btn.title = getLabels().applyChanges;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n applyArray(arrayId);\n });\n return btn;\n}\n\n// Grip centred on the item's top edge (away from the neighbour's \"×\" corners);\n// press and drag to reorder.\nfunction makeDragHandle(arrayId: string, node: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const top = rect.top + window.scrollY - 8;\n const left = rect.left + window.scrollX + rect.width / 2 - 8;\n const btn = baseButton(top, left, 16, \"#6b7280\");\n setIcon(btn, \"grip\", 12);\n btn.title = getLabels().dragToReorder;\n btn.style.cursor = \"grab\";\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n startDrag(arrayId, node);\n });\n return btn;\n}\n\n// ---------------------------------------------------------------------------\n// Drag to reorder\n// ---------------------------------------------------------------------------\n\nfunction startDrag(arrayId: string, node: HTMLElement): void {\n drag = { arrayId, node };\n node.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n // Drive the drag from document level so it survives overlay rebuilds.\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragEnd, { once: true });\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);\n if (!target) return;\n const r = target.getBoundingClientRect();\n // Insert before/after based on the pointer vs the target's horizontal midpoint.\n if (e.clientX > r.left + r.width / 2) target.after(drag.node);\n else target.before(drag.node);\n}\n\nfunction onDragEnd(): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.body.style.removeProperty(\"user-select\");\n if (drag) {\n drag.node.style.removeProperty(\"opacity\");\n drag = null;\n }\n refreshArrayControls();\n}\n\n// The array item (original or draft) whose box contains the pointer, excluding the\n// node being dragged.\nfunction itemUnderPointer(arrayId: string, x: number, y: number, exclude: HTMLElement): HTMLElement | null {\n for (const el of orderedItemsFor(arrayId)) {\n if (el === exclude) continue;\n const r = el.getBoundingClientRect();\n if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction addDraftPill(arrayId: string): void {\n const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];\n if (!template?.parentElement) return;\n\n const pill = template.cloneNode(false) as HTMLElement;\n // Clone keeps the pill styling (class); strip identity/editor attrs so it is not\n // picked up by the text editor, click handler, hover overlay or the array query.\n for (const attr of [...pill.attributes]) {\n if (attr.name.startsWith(\"data-upstart-\") || attr.name === \"contenteditable\") {\n pill.removeAttribute(attr.name);\n }\n }\n pill.removeAttribute(\"style\");\n pill.setAttribute(DRAFT_ATTR, arrayId);\n pill.setAttribute(\"contenteditable\", \"true\");\n pill.textContent = getLabels().newItem;\n\n // Insert after the current last pill of this array (drafts go at the end).\n const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();\n if (last && last.parentElement === template.parentElement) {\n last.after(pill);\n } else {\n template.parentElement.appendChild(pill);\n }\n\n // Focus + select so the user can type the name immediately.\n pill.focus();\n const range = document.createRange();\n range.selectNodeContents(pill);\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n\n refreshArrayControls();\n}\n\nfunction applyArray(arrayId: string): void {\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const items: string[] = [];\n\n // DOM order = the order the user sees (incl. any drag reordering).\n for (const el of orderedItemsFor(arrayId)) {\n if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;\n items.push(textOf(el));\n }\n\n if (items.length === 0) return;\n\n // The server rewrites the whole array, rebuilds + swaps, then the editor reloads\n // the iframe — which discards the local draft state, so just clear our marks.\n pendingDeletes.delete(arrayId);\n sendToParent({ type: \"text-edit\", payload: { action: \"arraySet\", arrayId, items } });\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshArrayControls();\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"error-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"mappings":";;AAcA;;;;iBAAgB,mBAAA,CAAA;AAAA,iBA8BA,gBAAA,CAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sendToParent } from "./utils.js";
|
|
2
|
+
import { getLabels } from "./labels.js";
|
|
2
3
|
//#region src/vite-plugin-upstart-editor/runtime/error-handler.ts
|
|
3
4
|
/** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */
|
|
4
5
|
const RECONCILIATION_ERROR_RE = /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;
|
|
@@ -17,14 +18,14 @@ function showRecoveryOverlay() {
|
|
|
17
18
|
const card = document.createElement("div");
|
|
18
19
|
card.style.cssText = "max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;";
|
|
19
20
|
const title = document.createElement("p");
|
|
20
|
-
title.textContent =
|
|
21
|
+
title.textContent = getLabels().recoveryTitle;
|
|
21
22
|
title.style.cssText = "margin: 0 0 0.5rem; font-size: 1.1rem; font-weight: 600;";
|
|
22
23
|
const text = document.createElement("p");
|
|
23
|
-
text.textContent =
|
|
24
|
+
text.textContent = getLabels().recoveryText;
|
|
24
25
|
text.style.cssText = "margin: 0 0 1.25rem; font-size: 0.9rem; color: #555;";
|
|
25
26
|
const button = document.createElement("button");
|
|
26
27
|
button.type = "button";
|
|
27
|
-
button.textContent =
|
|
28
|
+
button.textContent = getLabels().refresh;
|
|
28
29
|
button.style.cssText = "padding: 0.6rem 1.4rem; border: 0; border-radius: 9999px; background: #7270c6; color: #fff; font-weight: 600; font-size: 0.95rem; cursor: pointer;";
|
|
29
30
|
button.addEventListener("click", () => window.location.reload());
|
|
30
31
|
card.append(title, text, button);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"sourcesContent":["import { sendToParent } from \"./utils.js\";\n\n/** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */\nconst RECONCILIATION_ERROR_RE =\n /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;\n\nconst OVERLAY_ID = \"upstart-preview-recovery\";\n\n/**\n * Full-screen notice inside the preview asking the user to refresh, shown when the page\n * could not be kept alive (React crashed into the site's error boundary). Reloading the\n * iframe re-syncs the DOM with the source and the editor's registry.\n */\nexport function showRecoveryOverlay(): void {\n if (document.getElementById(OVERLAY_ID)) return;\n const overlay = document.createElement(\"div\");\n overlay.id = OVERLAY_ID;\n overlay.setAttribute(\"role\", \"alertdialog\");\n overlay.style.cssText =\n \"position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; \" +\n \"background: rgba(17, 17, 27, 0.72); font-family: system-ui, sans-serif; color: #111;\";\n const card = document.createElement(\"div\");\n card.style.cssText =\n \"max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; \" +\n \"box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;\";\n const title = document.createElement(\"p\");\n title.textContent =
|
|
1
|
+
{"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"sourcesContent":["import { sendToParent } from \"./utils.js\";\nimport { getLabels } from \"./labels.js\";\n\n/** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */\nconst RECONCILIATION_ERROR_RE =\n /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;\n\nconst OVERLAY_ID = \"upstart-preview-recovery\";\n\n/**\n * Full-screen notice inside the preview asking the user to refresh, shown when the page\n * could not be kept alive (React crashed into the site's error boundary). Reloading the\n * iframe re-syncs the DOM with the source and the editor's registry.\n */\nexport function showRecoveryOverlay(): void {\n if (document.getElementById(OVERLAY_ID)) return;\n const overlay = document.createElement(\"div\");\n overlay.id = OVERLAY_ID;\n overlay.setAttribute(\"role\", \"alertdialog\");\n overlay.style.cssText =\n \"position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; \" +\n \"background: rgba(17, 17, 27, 0.72); font-family: system-ui, sans-serif; color: #111;\";\n const card = document.createElement(\"div\");\n card.style.cssText =\n \"max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; \" +\n \"box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;\";\n const title = document.createElement(\"p\");\n title.textContent = getLabels().recoveryTitle;\n title.style.cssText = \"margin: 0 0 0.5rem; font-size: 1.1rem; font-weight: 600;\";\n const text = document.createElement(\"p\");\n text.textContent = getLabels().recoveryText;\n text.style.cssText = \"margin: 0 0 1.25rem; font-size: 0.9rem; color: #555;\";\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.textContent = getLabels().refresh;\n button.style.cssText =\n \"padding: 0.6rem 1.4rem; border: 0; border-radius: 9999px; background: #7270c6; color: #fff; \" +\n \"font-weight: 600; font-size: 0.95rem; cursor: pointer;\";\n button.addEventListener(\"click\", () => window.location.reload());\n card.append(title, text, button);\n overlay.append(card);\n (document.body ?? document.documentElement).append(overlay);\n}\n\nexport function initErrorHandler() {\n // Global error handler for uncaught errors in the editor\n window.addEventListener(\"error\", (event) => {\n console.error(\"[Upstart Editor] Uncaught error in editor:\", event.error);\n sendToParent({\n type: \"editor-error\",\n error: String(event.error?.message ?? event.message ?? event.error),\n });\n const message = String(event.error?.message ?? event.message ?? \"\");\n if (RECONCILIATION_ERROR_RE.test(message)) {\n sendToParent({ type: \"editor-desync\", fatal: true });\n showRecoveryOverlay();\n }\n });\n}\n"],"mappings":";;;;AAIA,MAAM,0BACJ;AAEF,MAAM,aAAa;;;;;;AAOnB,SAAgB,sBAA4B;CAC1C,IAAI,SAAS,eAAe,WAAW,EAAE;CACzC,MAAM,UAAU,SAAS,cAAc,MAAM;CAC7C,QAAQ,KAAK;CACb,QAAQ,aAAa,QAAQ,cAAc;CAC3C,QAAQ,MAAM,UACZ;CAEF,MAAM,OAAO,SAAS,cAAc,MAAM;CAC1C,KAAK,MAAM,UACT;CAEF,MAAM,QAAQ,SAAS,cAAc,IAAI;CACzC,MAAM,cAAc,WAAW,CAAC;CAChC,MAAM,MAAM,UAAU;CACtB,MAAM,OAAO,SAAS,cAAc,IAAI;CACxC,KAAK,cAAc,WAAW,CAAC;CAC/B,KAAK,MAAM,UAAU;CACrB,MAAM,SAAS,SAAS,cAAc,SAAS;CAC/C,OAAO,OAAO;CACd,OAAO,cAAc,WAAW,CAAC;CACjC,OAAO,MAAM,UACX;CAEF,OAAO,iBAAiB,eAAe,OAAO,SAAS,QAAQ,CAAC;CAChE,KAAK,OAAO,OAAO,MAAM,OAAO;CAChC,QAAQ,OAAO,KAAK;CACpB,CAAC,SAAS,QAAQ,SAAS,iBAAiB,OAAO,QAAQ;;AAG7D,SAAgB,mBAAmB;CAEjC,OAAO,iBAAiB,UAAU,UAAU;EAC1C,QAAQ,MAAM,8CAA8C,MAAM,MAAM;EACxE,aAAa;GACX,MAAM;GACN,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,WAAW,MAAM,MAAM;GACpE,CAAC;EACF,MAAM,UAAU,OAAO,MAAM,OAAO,WAAW,MAAM,WAAW,GAAG;EACnE,IAAI,wBAAwB,KAAK,QAAQ,EAAE;GACzC,aAAa;IAAE,MAAM;IAAiB,OAAO;IAAM,CAAC;GACpD,qBAAqB;;GAEvB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { getLabels } from "./labels.js";
|
|
1
2
|
//#region src/vite-plugin-upstart-editor/runtime/form-guard.ts
|
|
2
3
|
let isInitialized = false;
|
|
3
4
|
const SAFE_METHODS = new Set(["get", "dialog"]);
|
|
4
|
-
const TOOLTIP_MESSAGE = "This action isn’t available in the editor preview";
|
|
5
5
|
const TOOLTIP_DURATION_MS = 2500;
|
|
6
6
|
const SUPPORTS_POPOVER = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
|
|
7
7
|
let tooltip = null;
|
|
@@ -64,7 +64,7 @@ function ensureTooltip() {
|
|
|
64
64
|
if (tooltip) return tooltip;
|
|
65
65
|
tooltip = document.createElement("div");
|
|
66
66
|
tooltip.id = "upstart-form-guard-tooltip";
|
|
67
|
-
tooltip.textContent =
|
|
67
|
+
tooltip.textContent = getLabels().formGuard;
|
|
68
68
|
let css = "background: #111827; color: #fff; font-size: 12px; line-height: 1.3; padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; transform: translate(-50%, -100%); z-index: 2147483647;";
|
|
69
69
|
if (SUPPORTS_POPOVER) {
|
|
70
70
|
tooltip.setAttribute("popover", "manual");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/form-guard.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"form-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/form-guard.ts"],"sourcesContent":["import { getLabels } from \"./labels.js\";\n\nlet isInitialized = false;\n\n// Methods that don't mutate data and must keep working: GET forms (search /\n// navigation) and method=\"dialog\" (e.g. a modal's close/backdrop buttons).\nconst SAFE_METHODS = new Set([\"get\", \"dialog\"]);\n\nconst TOOLTIP_DURATION_MS = 2500;\n\n// Popover API renders in the browser top layer, so the tooltip sits above modal\n// <dialog>s opened with showModal() (which also live in the top layer and beat any\n// z-index). Fall back to absolute positioning where it isn't supported.\nconst SUPPORTS_POPOVER = typeof HTMLElement !== \"undefined\" && \"showPopover\" in HTMLElement.prototype;\n\nlet tooltip: HTMLDivElement | null = null;\nlet tooltipTimer: number | null = null;\n\n/**\n * Block mutating form submissions inside the previewed site so the user can't\n * trigger data changes (which only hit the ephemeral preview DB and never persist)\n * from the editor. This matters most in PREVIEW mode, where the site's buttons are\n * live — in EDIT mode a click edits the button's text instead of submitting.\n *\n * We block the `submit` event rather than disabling buttons: one place covers native\n * POST forms, React Router <Form>, button clicks and Enter-key submits. The\n * capture-phase listener + stopImmediatePropagation runs before React's delegated\n * handler, so React Router never processes the submission; preventDefault blocks a\n * plain form's POST. GET and dialog submissions are left untouched. When a mutating\n * submission is blocked, a tooltip on the triggering button explains why.\n */\nexport function initFormGuard(): void {\n if (typeof document === \"undefined\" || isInitialized) {\n return;\n }\n isInitialized = true;\n\n document.addEventListener(\n \"submit\",\n (event: SubmitEvent) => {\n const form = event.target as HTMLFormElement | null;\n const method = (\n event.submitter?.getAttribute(\"formmethod\") ||\n form?.getAttribute(\"method\") ||\n \"get\"\n ).toLowerCase();\n\n if (SAFE_METHODS.has(method)) {\n return;\n }\n\n event.preventDefault();\n event.stopImmediatePropagation();\n console.debug(\"[Upstart Editor] Blocked a mutating form submission in the editor:\", method);\n\n const anchor = (event.submitter as HTMLElement | null) ?? form;\n if (anchor) showBlockedTooltip(anchor);\n },\n true,\n );\n}\n\nfunction showBlockedTooltip(anchor: HTMLElement): void {\n const tip = ensureTooltip();\n const rect = anchor.getBoundingClientRect();\n\n if (SUPPORTS_POPOVER) {\n // Popover is position:fixed → viewport coordinates (no scroll offset).\n tip.style.left = `${rect.left + rect.width / 2}px`;\n tip.style.top = `${rect.top - 8}px`;\n try {\n tip.hidePopover();\n } catch {}\n try {\n tip.showPopover();\n } catch {}\n } else {\n tip.style.left = `${rect.left + window.scrollX + rect.width / 2}px`;\n tip.style.top = `${rect.top + window.scrollY - 8}px`;\n tip.style.opacity = \"1\";\n }\n\n if (tooltipTimer !== null) {\n clearTimeout(tooltipTimer);\n }\n tooltipTimer = window.setTimeout(() => {\n tooltipTimer = null;\n if (!tooltip) return;\n if (SUPPORTS_POPOVER) {\n try {\n tooltip.hidePopover();\n } catch {}\n } else {\n tooltip.style.opacity = \"0\";\n }\n }, TOOLTIP_DURATION_MS);\n}\n\nfunction ensureTooltip(): HTMLDivElement {\n if (tooltip) return tooltip;\n tooltip = document.createElement(\"div\");\n tooltip.id = \"upstart-form-guard-tooltip\";\n tooltip.textContent = getLabels().formGuard;\n\n let css =\n \"background: #111827; color: #fff; font-size: 12px; line-height: 1.3; \" +\n \"padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; \" +\n \"box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; \" +\n \"transform: translate(-50%, -100%); z-index: 2147483647;\";\n\n if (SUPPORTS_POPOVER) {\n tooltip.setAttribute(\"popover\", \"manual\");\n // Neutralise the UA popover centering so our top/left apply.\n css += \"position: fixed; margin: 0; inset: auto; overflow: visible; border: 0;\";\n } else {\n css += \"position: absolute; opacity: 0; transition: opacity 0.15s ease;\";\n }\n\n tooltip.style.cssText = css;\n document.body.appendChild(tooltip);\n return tooltip;\n}\n"],"mappings":";;AAEA,IAAI,gBAAgB;AAIpB,MAAM,eAAe,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AAE/C,MAAM,sBAAsB;AAK5B,MAAM,mBAAmB,OAAO,gBAAgB,eAAe,iBAAiB,YAAY;AAE5F,IAAI,UAAiC;AACrC,IAAI,eAA8B;;;;;;;;;;;;;;AAelC,SAAgB,gBAAsB;CACpC,IAAI,OAAO,aAAa,eAAe,eACrC;CAEF,gBAAgB;CAEhB,SAAS,iBACP,WACC,UAAuB;EACtB,MAAM,OAAO,MAAM;EACnB,MAAM,UACJ,MAAM,WAAW,aAAa,aAAa,IAC3C,MAAM,aAAa,SAAS,IAC5B,OACA,aAAa;EAEf,IAAI,aAAa,IAAI,OAAO,EAC1B;EAGF,MAAM,gBAAgB;EACtB,MAAM,0BAA0B;EAChC,QAAQ,MAAM,sEAAsE,OAAO;EAE3F,MAAM,SAAU,MAAM,aAAoC;EAC1D,IAAI,QAAQ,mBAAmB,OAAO;IAExC,KACD;;AAGH,SAAS,mBAAmB,QAA2B;CACrD,MAAM,MAAM,eAAe;CAC3B,MAAM,OAAO,OAAO,uBAAuB;CAE3C,IAAI,kBAAkB;EAEpB,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,KAAK,QAAQ,EAAE;EAC/C,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;EAChC,IAAI;GACF,IAAI,aAAa;UACX;EACR,IAAI;GACF,IAAI,aAAa;UACX;QACH;EACL,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,OAAO,UAAU,KAAK,QAAQ,EAAE;EAChE,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,OAAO,UAAU,EAAE;EACjD,IAAI,MAAM,UAAU;;CAGtB,IAAI,iBAAiB,MACnB,aAAa,aAAa;CAE5B,eAAe,OAAO,iBAAiB;EACrC,eAAe;EACf,IAAI,CAAC,SAAS;EACd,IAAI,kBACF,IAAI;GACF,QAAQ,aAAa;UACf;OAER,QAAQ,MAAM,UAAU;IAEzB,oBAAoB;;AAGzB,SAAS,gBAAgC;CACvC,IAAI,SAAS,OAAO;CACpB,UAAU,SAAS,cAAc,MAAM;CACvC,QAAQ,KAAK;CACb,QAAQ,cAAc,WAAW,CAAC;CAElC,IAAI,MACF;CAKF,IAAI,kBAAkB;EACpB,QAAQ,aAAa,WAAW,SAAS;EAEzC,OAAO;QAEP,OAAO;CAGT,QAAQ,MAAM,UAAU;CACxB,SAAS,KAAK,YAAY,QAAQ;CAClC,OAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;iBAmBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAuFgB,iBAAA,CAAA"}
|
|
@@ -2,6 +2,7 @@ import { clearSelection, getCurrentMode, getSelection, removeSelection, setCurre
|
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
import { initClickHandler } from "./click-handler.js";
|
|
4
4
|
import { hideOverlays, initHoverOverlay } from "./hover-overlay.js";
|
|
5
|
+
import { setLabels } from "./labels.js";
|
|
5
6
|
import { initErrorHandler } from "./error-handler.js";
|
|
6
7
|
import { installDomGuard } from "./dom-guard.js";
|
|
7
8
|
import { activateAllEditors, destroyAllActiveEditors, initTextEditor } from "./text-editor.js";
|
|
@@ -217,7 +218,7 @@ function handleParentMessage(event) {
|
|
|
217
218
|
type: "selection-changed",
|
|
218
219
|
selection: getSelection()
|
|
219
220
|
});
|
|
220
|
-
}
|
|
221
|
+
} else if (message.type === "set-labels") setLabels(message.labels);
|
|
221
222
|
}
|
|
222
223
|
/**
|
|
223
224
|
* Drop the whole "select" mode selection: clear the in-memory set and remove the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { installDomGuard } from \"./dom-guard.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Report the react-router id of the rendered route to the parent editor, so it can edit\n * that route's `meta` export (page title, description…).\n *\n * The id is read from react-router's own data router rather than from `history.pushState`:\n * the router pushes the new URL BEFORE it commits the new matches, so a route id read at\n * pushState time would still be the previous page's. Subscribing gives us the id once the\n * navigation is complete. The router global only appears during hydration, hence the retry.\n */\nfunction initRouteReporter(): void {\n let lastReported: string | undefined;\n\n const report = () => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const matches = (window as any).__reactRouterDataRouter?.state?.matches;\n const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : undefined;\n if (typeof routeId !== \"string\" || routeId === lastReported) return;\n lastReported = routeId;\n sendToParent({ type: \"editor-route\", routeId });\n };\n\n const attach = (): boolean => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const router = (window as any).__reactRouterDataRouter;\n if (typeof router?.subscribe !== \"function\") return false;\n router.subscribe(() => report());\n report();\n return true;\n };\n\n if (attach()) return;\n let attempts = 0;\n const timer = setInterval(() => {\n if (attach() || ++attempts > 25) clearInterval(timer);\n }, 200);\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n // Marks the document as running inside the editor preview (the site's error boundary\n // uses it to offer a refresh instead of a raw error).\n document.documentElement.setAttribute(\"data-upstart-editor\", \"\");\n installDomGuard(Node.prototype, () => sendToParent({ type: \"editor-desync\", fatal: false }));\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n initRouteReporter();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n // No `previewCSS` at all means \"patch the className only\": the editor uses it after a save\n // to sync the DOM with the file without touching the current CSS preview.\n if (message.previewCSS === undefined) return;\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"preview-image-style\") {\n // One <style> per image so previewing an image and its container at the same\n // time never overwrite each other (the className preview uses its own element).\n const styleId = `upstart-preview-image-style-${String(message.imageId).replace(/[^a-zA-Z0-9_-]/g, \"_\")}`;\n let styleEl = document.getElementById(styleId) as HTMLStyleElement | null;\n const css = typeof message.css === \"string\" ? message.css : \"\";\n if (css) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = styleId;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = css;\n } else if (styleEl) {\n styleEl.remove();\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { installDomGuard } from \"./dom-guard.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport { setLabels } from \"./labels.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Report the react-router id of the rendered route to the parent editor, so it can edit\n * that route's `meta` export (page title, description…).\n *\n * The id is read from react-router's own data router rather than from `history.pushState`:\n * the router pushes the new URL BEFORE it commits the new matches, so a route id read at\n * pushState time would still be the previous page's. Subscribing gives us the id once the\n * navigation is complete. The router global only appears during hydration, hence the retry.\n */\nfunction initRouteReporter(): void {\n let lastReported: string | undefined;\n\n const report = () => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const matches = (window as any).__reactRouterDataRouter?.state?.matches;\n const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : undefined;\n if (typeof routeId !== \"string\" || routeId === lastReported) return;\n lastReported = routeId;\n sendToParent({ type: \"editor-route\", routeId });\n };\n\n const attach = (): boolean => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const router = (window as any).__reactRouterDataRouter;\n if (typeof router?.subscribe !== \"function\") return false;\n router.subscribe(() => report());\n report();\n return true;\n };\n\n if (attach()) return;\n let attempts = 0;\n const timer = setInterval(() => {\n if (attach() || ++attempts > 25) clearInterval(timer);\n }, 200);\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n // Marks the document as running inside the editor preview (the site's error boundary\n // uses it to offer a refresh instead of a raw error).\n document.documentElement.setAttribute(\"data-upstart-editor\", \"\");\n installDomGuard(Node.prototype, () => sendToParent({ type: \"editor-desync\", fatal: false }));\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n initRouteReporter();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n // No `previewCSS` at all means \"patch the className only\": the editor uses it after a save\n // to sync the DOM with the file without touching the current CSS preview.\n if (message.previewCSS === undefined) return;\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"preview-image-style\") {\n // One <style> per image so previewing an image and its container at the same\n // time never overwrite each other (the className preview uses its own element).\n const styleId = `upstart-preview-image-style-${String(message.imageId).replace(/[^a-zA-Z0-9_-]/g, \"_\")}`;\n let styleEl = document.getElementById(styleId) as HTMLStyleElement | null;\n const css = typeof message.css === \"string\" ? message.css : \"\";\n if (css) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = styleId;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = css;\n } else if (styleEl) {\n styleEl.remove();\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"set-labels\") {\n setLabels(message.labels);\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/vite-plugin-upstart-editor/runtime/labels.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* User-facing labels of the editor runtime. The runtime runs inside the previewed site,
|
|
4
|
+
* which has no access to the dashboard's i18n catalogs: the editor sends the translated
|
|
5
|
+
* labels through a `set-labels` message once the runtime is ready, and everything the
|
|
6
|
+
* runtime renders reads them lazily via {@link getLabels}. English is the fallback.
|
|
7
|
+
*/
|
|
8
|
+
interface RuntimeLabels {
|
|
9
|
+
newItem: string;
|
|
10
|
+
keepItem: string;
|
|
11
|
+
removeItem: string;
|
|
12
|
+
addItem: string;
|
|
13
|
+
applyChanges: string;
|
|
14
|
+
dragToReorder: string;
|
|
15
|
+
clickToEdit: string;
|
|
16
|
+
startTyping: string;
|
|
17
|
+
recoveryTitle: string;
|
|
18
|
+
recoveryText: string;
|
|
19
|
+
refresh: string;
|
|
20
|
+
formGuard: string;
|
|
21
|
+
bold: string;
|
|
22
|
+
italic: string;
|
|
23
|
+
strikethrough: string;
|
|
24
|
+
inlineCode: string;
|
|
25
|
+
heading: string;
|
|
26
|
+
blockquote: string;
|
|
27
|
+
bulletList: string;
|
|
28
|
+
orderedList: string;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { RuntimeLabels };
|
|
32
|
+
//# sourceMappingURL=labels.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/labels.ts"],"mappings":";;AAMA;;;;;UAAiB,aAAA;EACf,OAAA;EACA,QAAA;EACA,UAAA;EACA,OAAA;EACA,YAAA;EACA,aAAA;EACA,WAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,OAAA;EACA,SAAA;EACA,IAAA;EACA,MAAA;EACA,aAAA;EACA,UAAA;EACA,OAAA;EACA,UAAA;EACA,UAAA;EACA,WAAA;AAAA"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/vite-plugin-upstart-editor/runtime/labels.ts
|
|
2
|
+
const DEFAULT_LABELS = Object.freeze({
|
|
3
|
+
newItem: "New item",
|
|
4
|
+
keepItem: "Keep item",
|
|
5
|
+
removeItem: "Remove item",
|
|
6
|
+
addItem: "Add item",
|
|
7
|
+
applyChanges: "Apply changes",
|
|
8
|
+
dragToReorder: "Drag to reorder",
|
|
9
|
+
clickToEdit: "Click to edit...",
|
|
10
|
+
startTyping: "Start typing...",
|
|
11
|
+
recoveryTitle: "The preview needs a refresh",
|
|
12
|
+
recoveryText: "Your changes are saved. Refresh the preview to keep editing.",
|
|
13
|
+
refresh: "Refresh",
|
|
14
|
+
formGuard: "This action isn’t available in the editor preview",
|
|
15
|
+
bold: "Bold",
|
|
16
|
+
italic: "Italic",
|
|
17
|
+
strikethrough: "Strikethrough",
|
|
18
|
+
inlineCode: "Inline Code",
|
|
19
|
+
heading: "Heading",
|
|
20
|
+
blockquote: "Blockquote",
|
|
21
|
+
bulletList: "Bullet List",
|
|
22
|
+
orderedList: "Ordered List"
|
|
23
|
+
});
|
|
24
|
+
let labels = { ...DEFAULT_LABELS };
|
|
25
|
+
/** Current labels (translated once the editor sent them, English before that). */
|
|
26
|
+
function getLabels() {
|
|
27
|
+
return labels;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Override labels with the ones received from the editor. Unknown keys and non-string
|
|
31
|
+
* values are ignored so a malformed message can never blank a control.
|
|
32
|
+
*/
|
|
33
|
+
function setLabels(input) {
|
|
34
|
+
if (typeof input !== "object" || input === null) return;
|
|
35
|
+
const next = { ...labels };
|
|
36
|
+
for (const key of Object.keys(DEFAULT_LABELS)) {
|
|
37
|
+
const value = input[key];
|
|
38
|
+
if (typeof value === "string" && value.length > 0) next[key] = value;
|
|
39
|
+
}
|
|
40
|
+
labels = next;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { getLabels, setLabels };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"labels.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/labels.ts"],"sourcesContent":["/**\n * User-facing labels of the editor runtime. The runtime runs inside the previewed site,\n * which has no access to the dashboard's i18n catalogs: the editor sends the translated\n * labels through a `set-labels` message once the runtime is ready, and everything the\n * runtime renders reads them lazily via {@link getLabels}. English is the fallback.\n */\nexport interface RuntimeLabels {\n newItem: string;\n keepItem: string;\n removeItem: string;\n addItem: string;\n applyChanges: string;\n dragToReorder: string;\n clickToEdit: string;\n startTyping: string;\n recoveryTitle: string;\n recoveryText: string;\n refresh: string;\n formGuard: string;\n bold: string;\n italic: string;\n strikethrough: string;\n inlineCode: string;\n heading: string;\n blockquote: string;\n bulletList: string;\n orderedList: string;\n}\n\nexport const DEFAULT_LABELS: Readonly<RuntimeLabels> = Object.freeze({\n newItem: \"New item\",\n keepItem: \"Keep item\",\n removeItem: \"Remove item\",\n addItem: \"Add item\",\n applyChanges: \"Apply changes\",\n dragToReorder: \"Drag to reorder\",\n clickToEdit: \"Click to edit...\",\n startTyping: \"Start typing...\",\n recoveryTitle: \"The preview needs a refresh\",\n recoveryText: \"Your changes are saved. Refresh the preview to keep editing.\",\n refresh: \"Refresh\",\n formGuard: \"This action isn’t available in the editor preview\",\n bold: \"Bold\",\n italic: \"Italic\",\n strikethrough: \"Strikethrough\",\n inlineCode: \"Inline Code\",\n heading: \"Heading\",\n blockquote: \"Blockquote\",\n bulletList: \"Bullet List\",\n orderedList: \"Ordered List\",\n});\n\nlet labels: RuntimeLabels = { ...DEFAULT_LABELS };\n\n/** Current labels (translated once the editor sent them, English before that). */\nexport function getLabels(): Readonly<RuntimeLabels> {\n return labels;\n}\n\n/**\n * Override labels with the ones received from the editor. Unknown keys and non-string\n * values are ignored so a malformed message can never blank a control.\n */\nexport function setLabels(input: unknown): void {\n if (typeof input !== \"object\" || input === null) return;\n const next: RuntimeLabels = { ...labels };\n for (const key of Object.keys(DEFAULT_LABELS) as (keyof RuntimeLabels)[]) {\n const value = (input as Record<string, unknown>)[key];\n if (typeof value === \"string\" && value.length > 0) next[key] = value;\n }\n labels = next;\n}\n\n/** Back to the English defaults (used by tests). */\nexport function resetLabels(): void {\n labels = { ...DEFAULT_LABELS };\n}\n"],"mappings":";AA6BA,MAAa,iBAA0C,OAAO,OAAO;CACnE,SAAS;CACT,UAAU;CACV,YAAY;CACZ,SAAS;CACT,cAAc;CACd,eAAe;CACf,aAAa;CACb,aAAa;CACb,eAAe;CACf,cAAc;CACd,SAAS;CACT,WAAW;CACX,MAAM;CACN,QAAQ;CACR,eAAe;CACf,YAAY;CACZ,SAAS;CACT,YAAY;CACZ,YAAY;CACZ,aAAa;CACd,CAAC;AAEF,IAAI,SAAwB,EAAE,GAAG,gBAAgB;;AAGjD,SAAgB,YAAqC;CACnD,OAAO;;;;;;AAOT,SAAgB,UAAU,OAAsB;CAC9C,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;CACjD,MAAM,OAAsB,EAAE,GAAG,QAAQ;CACzC,KAAK,MAAM,OAAO,OAAO,KAAK,eAAe,EAA6B;EACxE,MAAM,QAAS,MAAkC;EACjD,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAAG,KAAK,OAAO;;CAEjE,SAAS"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-editor.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/text-editor.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"text-editor.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/text-editor.ts"],"mappings":";;;;;AAuQA;;iBAAgB,cAAA,CAAe,OAAA,GAAS,oBAAA;;;AAyDxC;iBAAgB,kBAAA,CAAA;;;;iBAgBA,uBAAA,CAAA"}
|