@upstart.gg/vite-plugins 0.1.83 → 0.1.85
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/site-theme.d.ts +55 -0
- package/dist/site-theme.d.ts.map +1 -0
- package/dist/site-theme.js +184 -0
- package/dist/site-theme.js.map +1 -0
- package/dist/upstart-editor-api.d.ts +63 -1
- package/dist/upstart-editor-api.d.ts.map +1 -1
- package/dist/upstart-editor-api.js +233 -1
- package/dist/upstart-editor-api.js.map +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts +1 -1
- package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-attrs.js +65 -2
- package/dist/vite-plugin-upstart-attrs.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +2 -24
- package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +7 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js +33 -0
- package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +35 -0
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/labels.js +5 -0
- package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js +434 -0
- package/dist/vite-plugin-upstart-editor/runtime/move-controls.js.map +1 -0
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +9 -3
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/site-theme.ts +205 -0
- package/src/tests/move-controls.test.ts +86 -0
- package/src/tests/site-theme.test.ts +122 -0
- package/src/tests/upstart-editor-api-move-element.test.ts +220 -0
- package/src/tests/vite-plugin-upstart-attrs.test.ts +165 -4
- package/src/upstart-editor-api.ts +294 -0
- package/src/vite-plugin-upstart-attrs.ts +93 -2
- package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +2 -40
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +17 -3
- package/src/vite-plugin-upstart-editor/runtime/controls-ui.ts +48 -0
- package/src/vite-plugin-upstart-editor/runtime/index.ts +46 -0
- package/src/vite-plugin-upstart-editor/runtime/labels.ts +10 -0
- package/src/vite-plugin-upstart-editor/runtime/move-controls.ts +536 -0
- package/src/vite-plugin-upstart-editor/runtime/types.ts +9 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
import { getLabels } from "./labels.js";
|
|
4
|
+
import { baseButton, createControlsLayer, cssAttr, setIcon } from "./controls-ui.js";
|
|
4
5
|
//#region src/vite-plugin-upstart-editor/runtime/array-controls.ts
|
|
5
6
|
const DRAFT_ATTR = "data-upstart-draft-item";
|
|
6
7
|
const BTN_SIZE = 22;
|
|
@@ -130,10 +131,7 @@ function hideArrayControls() {
|
|
|
130
131
|
}
|
|
131
132
|
function ensureLayer() {
|
|
132
133
|
if (layer) return layer;
|
|
133
|
-
layer =
|
|
134
|
-
layer.id = "upstart-array-controls";
|
|
135
|
-
layer.style.cssText = "position: absolute; top: 0; left: 0; width: 0; height: 0; pointer-events: none; z-index: 2147483646;";
|
|
136
|
-
document.body.appendChild(layer);
|
|
134
|
+
layer = createControlsLayer("upstart-array-controls");
|
|
137
135
|
return layer;
|
|
138
136
|
}
|
|
139
137
|
function itemIndex(el) {
|
|
@@ -160,9 +158,6 @@ function isReordered(arrayId) {
|
|
|
160
158
|
}
|
|
161
159
|
return false;
|
|
162
160
|
}
|
|
163
|
-
function cssAttr(value) {
|
|
164
|
-
return value.replace(/["\\]/g, "\\$&");
|
|
165
|
-
}
|
|
166
161
|
function textOf(el) {
|
|
167
162
|
return (el.textContent ?? "").trim();
|
|
168
163
|
}
|
|
@@ -177,23 +172,6 @@ function applyDeletionStyle(item, marked) {
|
|
|
177
172
|
item.style.removeProperty("pointer-events");
|
|
178
173
|
}
|
|
179
174
|
}
|
|
180
|
-
const ICON_PATHS = {
|
|
181
|
-
plus: "<path d=\"M12 5v14M5 12h14\"/>",
|
|
182
|
-
cross: "<path d=\"M6 6 18 18M18 6 6 18\"/>",
|
|
183
|
-
check: "<path d=\"M5 12.5 10 17.5 19 7\"/>",
|
|
184
|
-
restore: "<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>",
|
|
185
|
-
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\"/>"
|
|
186
|
-
};
|
|
187
|
-
function setIcon(btn, name, px) {
|
|
188
|
-
const dots = name === "grip";
|
|
189
|
-
btn.innerHTML = `<svg width="${px}" height="${px}" viewBox="0 0 24 24" fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
|
|
190
|
-
}
|
|
191
|
-
function baseButton(top, left, size, bg) {
|
|
192
|
-
const btn = document.createElement("button");
|
|
193
|
-
btn.type = "button";
|
|
194
|
-
btn.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; padding: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);`;
|
|
195
|
-
return btn;
|
|
196
|
-
}
|
|
197
175
|
function makeItemDeleteButton(arrayId, index, marked, rect) {
|
|
198
176
|
const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, "#ef4444");
|
|
199
177
|
setIcon(btn, marked ? "restore" : "cross", 10);
|
|
@@ -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\";\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
|
+
{"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\";\nimport { baseButton, createControlsLayer, cssAttr, setIcon } from \"./controls-ui.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 = createControlsLayer(\"upstart-array-controls\");\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 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\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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"}
|
|
@@ -157,6 +157,12 @@ function handleSelectClick(event, target) {
|
|
|
157
157
|
}
|
|
158
158
|
function handleClick(event) {
|
|
159
159
|
const mode = getCurrentMode();
|
|
160
|
+
if (mode === "move") {
|
|
161
|
+
if (event.target?.closest("#upstart-move-controls, #upstart-move-bar")) return;
|
|
162
|
+
event.preventDefault();
|
|
163
|
+
event.stopPropagation();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
160
166
|
if (mode !== "edit" && mode !== "select") return;
|
|
161
167
|
console.debug("[Upstart Editor] Click event:", event);
|
|
162
168
|
const target = event.target;
|
|
@@ -172,7 +178,7 @@ function handleClick(event) {
|
|
|
172
178
|
handleSelectClick(event, target);
|
|
173
179
|
return;
|
|
174
180
|
}
|
|
175
|
-
if (target.closest("#upstart-array-controls, [data-upstart-draft-item]")) return;
|
|
181
|
+
if (target.closest("#upstart-array-controls, #upstart-move-controls, #upstart-move-bar, [data-upstart-draft-item]")) return;
|
|
176
182
|
if (target.closest(".upstart-editor-bubble-menu")) {
|
|
177
183
|
console.info("[Upstart Editor] Click ignored: target is inside the bubble menu");
|
|
178
184
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"sourcesContent":["import { getCurrentMode, getSelection, toggleSelection } from \"./state.js\";\nimport { pickImagesForClick, type Point } from \"./image-pick.js\";\nimport { sendToParent } from \"./utils.js\";\nimport type { EditableImageRef, SelectionRef } from \"./types.js\";\n\nlet isInitialized = false;\n\nconst DAISY_VAR_NAMES = [\n \"base-100\",\n \"base-200\",\n \"base-300\",\n \"base-content\",\n \"primary\",\n \"primary-content\",\n \"secondary\",\n \"secondary-content\",\n \"accent\",\n \"accent-content\",\n \"neutral\",\n \"neutral-content\",\n \"info\",\n \"info-content\",\n \"success\",\n \"success-content\",\n \"warning\",\n \"warning-content\",\n \"error\",\n \"error-content\",\n];\n\n/**\n * Whether any ancestor (up to <body>) clips its overflow, so a scaled-up image\n * stays visually contained.\n */\nfunction isClipped(node: HTMLElement): boolean {\n for (let el = node.parentElement; el && el !== document.body; el = el.parentElement) {\n const style = getComputedStyle(el);\n if (style.overflowX !== \"visible\" || style.overflowY !== \"visible\") return true;\n }\n return false;\n}\n\nfunction toImageRef(node: HTMLElement): EditableImageRef {\n const img = node as HTMLImageElement;\n const rect = node.getBoundingClientRect();\n const parent = node.parentElement;\n return {\n id: node.dataset.upstartImageId as string,\n src: img.currentSrc || img.src || \"\",\n alt: img.alt || \"\",\n classNameId: node.dataset.upstartClassnameId || undefined,\n currentClassName: node.dataset.upstartClassnameId ? node.className : undefined,\n width: Math.round(rect.width),\n height: Math.round(rect.height),\n naturalWidth: img.naturalWidth || undefined,\n naturalHeight: img.naturalHeight || undefined,\n clipped: isClipped(node),\n parentClassNameId: parent?.dataset.upstartClassnameId || undefined,\n parentClassName: parent?.dataset.upstartClassnameId ? parent.className : undefined,\n };\n}\n\nfunction editableImagesIn(el: HTMLElement): HTMLElement[] {\n const found = new Set<HTMLElement>();\n if (el.dataset.upstartImageId) found.add(el);\n for (const node of el.querySelectorAll<HTMLElement>(\"[data-upstart-image-id]\")) {\n found.add(node);\n }\n return Array.from(found);\n}\n\n/**\n * Collect the editable images (elements carrying data-upstart-image-id) relevant to a click\n * on `start`: the images inside the clicked element, or, when it has none, an ancestor's image\n * lying under the click point (a hero <img> covered by an overlay). See pickImagesForClick for\n * the exact rules — in particular an ancestor's images are never surfaced when the click is\n * not visually on them, so clicking a title never opens the logo next to it.\n */\nfunction collectImages(start: HTMLElement, point?: Point): EditableImageRef[] {\n const withRect = (node: HTMLElement) => ({ node, rect: node.getBoundingClientRect() });\n const own = editableImagesIn(start).map(withRect);\n const ancestorLevels: { node: HTMLElement; rect: DOMRect }[][] = [];\n if (own.length === 0) {\n let el = start.parentElement;\n for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {\n ancestorLevels.push(editableImagesIn(el).map(withRect));\n }\n }\n return pickImagesForClick(own, ancestorLevels, point).map(({ node }) => toImageRef(node));\n}\n\n/**\n * Initialize click handler for className editing.\n */\nexport function initClickHandler(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing click handler...\");\n document.addEventListener(\"click\", handleClick, true);\n isInitialized = true;\n}\n\n/**\n * Cleanup click handler.\n */\nexport function cleanupClickHandler(): void {\n document.removeEventListener(\"click\", handleClick, true);\n isInitialized = false;\n}\n\n/**\n * Build a SelectionRef from a clicked element, walking up to the nearest\n * component (data-upstart-file) for the source file / component name and\n * collecting i18n keys from the element's subtree.\n */\nfunction buildSelectionRef(element: HTMLElement): SelectionRef | null {\n const hash = element.dataset.upstartHash ?? \"\";\n\n const fileEl = element.closest<HTMLElement>(\"[data-upstart-file]\");\n const filePath = fileEl?.dataset.upstartFile ?? element.dataset.upstartFile ?? \"\";\n const componentName = fileEl?.dataset.upstartComponent ?? element.dataset.upstartComponent;\n\n // A stable identity for toggling. Prefer filePath+hash; fall back so distinct\n // elements without a hash don't collapse onto the same key.\n const key = `${filePath}:${hash}` !== \":\" ? `${filePath}:${hash}` : \"\";\n if (!key) {\n return null;\n }\n\n const i18nKeys = new Set<string>();\n const collectKeys = (el: HTMLElement) => {\n const raw = el.dataset.upstartI18n;\n if (raw) {\n for (const k of raw.split(/[\\s,]+/).filter(Boolean)) i18nKeys.add(k);\n }\n };\n collectKeys(element);\n for (const node of element.querySelectorAll<HTMLElement>(\"[data-upstart-i18n]\")) {\n collectKeys(node);\n }\n\n const text = (element.innerText ?? \"\").trim().slice(0, 200) || undefined;\n\n // Whether THIS element's content is derived from database data (element-level marker).\n // This is what gates content edits — its content can't be edited from the editor, only\n // its presentation. i18n/translated content is editable (it lives in translation files),\n // so i18n keys on the element take precedence and suppress the data-bound flag even if the\n // element was wrongly marked.\n const dataBoundEl = element.closest<HTMLElement>(\"[data-upstart-data-bound]\");\n const dataBound = !!dataBoundEl && dataBoundEl.dataset.upstartDataBound !== \"false\" && i18nKeys.size === 0;\n\n return {\n key,\n hash,\n filePath,\n componentName,\n i18nKeys: i18nKeys.size > 0 ? Array.from(i18nKeys) : undefined,\n text,\n dataBound: dataBound || undefined,\n };\n}\n\n/**\n * \"Select\" mode click: toggle the clicked component in/out of the selection,\n * mark it visually with data-upstart-selected, and report the new selection\n * to the parent. Never edits or navigates.\n */\nfunction handleSelectClick(event: MouseEvent, target: HTMLElement): void {\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n console.info(\"[Upstart Editor] Select click ignored: no data-upstart-hash ancestor\");\n return;\n }\n\n // Bubble up to a rich-panel editable ancestor so selecting an inline child\n // (e.g. <strong>) selects the whole editable region — mirrors edit-mode resolution.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.preventDefault();\n event.stopPropagation();\n\n const ref = buildSelectionRef(element);\n if (!ref) {\n return;\n }\n\n const nowSelected = toggleSelection(ref, element);\n if (nowSelected) {\n element.setAttribute(\"data-upstart-selected\", \"\");\n } else {\n element.removeAttribute(\"data-upstart-selected\");\n }\n\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n console.log(\"[Upstart Editor] Selection toggled:\", ref.key, nowSelected);\n}\n\nfunction handleClick(event: MouseEvent): void {\n const mode = getCurrentMode();\n if (mode !== \"edit\" && mode !== \"select\") {\n return;\n }\n\n console.debug(\"[Upstart Editor] Click event:\", event);\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n console.warn(\"[Upstart Editor] Click target is not an HTMLElement\");\n return;\n }\n // Viewport coordinates of the click, to tell which image (if any) was visually hit.\n const clickPoint: Point = { x: event.clientX, y: event.clientY };\n\n if (mode === \"select\") {\n handleSelectClick(event, target);\n return;\n }\n\n // Editor-only chrome (the array ×/+/✓ controls) and locally-staged draft list\n // items handle their own clicks and must never be treated as element selection.\n if (target.closest(\"#upstart-array-controls, [data-upstart-draft-item]\")) {\n return;\n }\n\n if (target.closest(\".upstart-editor-bubble-menu\")) {\n console.info(\"[Upstart Editor] Click ignored: target is inside the bubble menu\");\n return;\n }\n\n const activeLink = target.closest(\"a[data-upstart-editor-active]\");\n const isFormControlClick = Boolean(\n (target as HTMLElement | null)?.closest(\"input, textarea, select, button\"),\n );\n if (activeLink || !isFormControlClick) {\n event.preventDefault();\n }\n\n // Clicks inside contenteditable (TipTap active) — still notify style panel if applicable\n if (target.closest(\"[contenteditable='true']\")) {\n const editableEl = target.closest<HTMLElement>(\"[data-upstart-editable-text='true']\");\n if (editableEl) {\n let classNameId = editableEl.dataset.upstartClassnameId ?? \"\";\n let currentClassName = editableEl.className;\n if (!classNameId) {\n const parentWithClassname = editableEl.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n if (classNameId) {\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n sendToParent({\n type: \"element-clicked\",\n hash: editableEl.dataset.upstartHash ?? \"\",\n componentName: editableEl.dataset.upstartComponent,\n filePath: editableEl.dataset.upstartFile ?? \"\",\n classNameId,\n currentClassName,\n themeColors,\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n tagName: editableEl.tagName.toLowerCase(),\n images: collectImages(editableEl, clickPoint),\n editableText: true,\n });\n }\n }\n console.info(\"[Upstart Editor] Click inside contenteditable: TipTap handles text editing\");\n return;\n }\n\n // Detect a datasource record under the click. We check this BEFORE the\n // data-upstart-hash gate because a record may be rendered without that\n // attribute (e.g. an <a> from <Link to={...}>) and we still want to open\n // the inline datasource editor instead of letting the browser navigate.\n const datasourceEl = target.closest<HTMLElement>(\"[data-upstart-datasource][data-upstart-record-id]\");\n\n // Find the closest element with data-upstart-hash\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n if (datasourceEl) {\n // Datasource record click without a data-upstart-hash ancestor: stop\n // navigation, surface the datasource event so the editor can open the\n // inline datasource item inspector.\n event.preventDefault();\n event.stopPropagation();\n sendToParent({\n type: \"element-clicked\",\n hash: \"\",\n componentName: undefined,\n filePath: \"\",\n classNameId: \"\",\n currentClassName: \"\",\n datasourceId: datasourceEl.dataset.upstartDatasource,\n recordId: datasourceEl.dataset.upstartRecordId,\n themeColors: {},\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n tagName: datasourceEl.tagName.toLowerCase(),\n images: collectImages(datasourceEl, clickPoint),\n });\n return;\n }\n console.info(\"[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found\");\n return;\n }\n\n // If the clicked element is nested inside a rich-panel editable ancestor, bubble up to it\n // so that clicking any inline child (e.g. <strong>, <em>) edits the whole rich-panel region.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.stopPropagation();\n\n console.log(\"Element clicked dataset:\", element.dataset);\n\n const hash = element.dataset.upstartHash as string;\n const componentName = element.dataset.upstartComponent;\n const filePath = element.dataset.upstartFile ?? \"\";\n\n // If this element has editable-text, focus the TipTap editor (all modes now use inline TipTap)\n if (element.dataset.upstartEditableText === \"true\") {\n const proseMirror = element.querySelector<HTMLElement>(\".ProseMirror\");\n if (proseMirror) {\n proseMirror.focus();\n }\n }\n\n // Find classNameId on the element itself, or walk up to find it on a parent\n let classNameId = element.dataset.upstartClassnameId ?? \"\";\n let currentClassName = element.className;\n if (!classNameId) {\n const parentWithClassname = element.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n\n // Read DaisyUI CSS custom property values from the iframe's document\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n\n const rect = element.getBoundingClientRect();\n const bounds = {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n right: rect.right,\n bottom: rect.bottom,\n };\n\n // datasourceEl was resolved before the data-upstart-hash gate so we could\n // intercept datasource clicks even without a hash ancestor (see above).\n const datasourceId = datasourceEl?.dataset.upstartDatasource;\n const recordId = datasourceEl?.dataset.upstartRecordId;\n\n sendToParent({\n type: \"element-clicked\",\n hash,\n componentName,\n filePath,\n classNameId,\n currentClassName,\n datasourceId,\n recordId,\n themeColors,\n bounds,\n viewportWidth: window.innerWidth,\n tagName: element.tagName.toLowerCase(),\n images: collectImages(element, clickPoint),\n editableText: element.dataset.upstartEditableText === \"true\",\n });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"click-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"sourcesContent":["import { getCurrentMode, getSelection, toggleSelection } from \"./state.js\";\nimport { pickImagesForClick, type Point } from \"./image-pick.js\";\nimport { sendToParent } from \"./utils.js\";\nimport type { EditableImageRef, SelectionRef } from \"./types.js\";\n\nlet isInitialized = false;\n\nconst DAISY_VAR_NAMES = [\n \"base-100\",\n \"base-200\",\n \"base-300\",\n \"base-content\",\n \"primary\",\n \"primary-content\",\n \"secondary\",\n \"secondary-content\",\n \"accent\",\n \"accent-content\",\n \"neutral\",\n \"neutral-content\",\n \"info\",\n \"info-content\",\n \"success\",\n \"success-content\",\n \"warning\",\n \"warning-content\",\n \"error\",\n \"error-content\",\n];\n\n/**\n * Whether any ancestor (up to <body>) clips its overflow, so a scaled-up image\n * stays visually contained.\n */\nfunction isClipped(node: HTMLElement): boolean {\n for (let el = node.parentElement; el && el !== document.body; el = el.parentElement) {\n const style = getComputedStyle(el);\n if (style.overflowX !== \"visible\" || style.overflowY !== \"visible\") return true;\n }\n return false;\n}\n\nfunction toImageRef(node: HTMLElement): EditableImageRef {\n const img = node as HTMLImageElement;\n const rect = node.getBoundingClientRect();\n const parent = node.parentElement;\n return {\n id: node.dataset.upstartImageId as string,\n src: img.currentSrc || img.src || \"\",\n alt: img.alt || \"\",\n classNameId: node.dataset.upstartClassnameId || undefined,\n currentClassName: node.dataset.upstartClassnameId ? node.className : undefined,\n width: Math.round(rect.width),\n height: Math.round(rect.height),\n naturalWidth: img.naturalWidth || undefined,\n naturalHeight: img.naturalHeight || undefined,\n clipped: isClipped(node),\n parentClassNameId: parent?.dataset.upstartClassnameId || undefined,\n parentClassName: parent?.dataset.upstartClassnameId ? parent.className : undefined,\n };\n}\n\nfunction editableImagesIn(el: HTMLElement): HTMLElement[] {\n const found = new Set<HTMLElement>();\n if (el.dataset.upstartImageId) found.add(el);\n for (const node of el.querySelectorAll<HTMLElement>(\"[data-upstart-image-id]\")) {\n found.add(node);\n }\n return Array.from(found);\n}\n\n/**\n * Collect the editable images (elements carrying data-upstart-image-id) relevant to a click\n * on `start`: the images inside the clicked element, or, when it has none, an ancestor's image\n * lying under the click point (a hero <img> covered by an overlay). See pickImagesForClick for\n * the exact rules — in particular an ancestor's images are never surfaced when the click is\n * not visually on them, so clicking a title never opens the logo next to it.\n */\nfunction collectImages(start: HTMLElement, point?: Point): EditableImageRef[] {\n const withRect = (node: HTMLElement) => ({ node, rect: node.getBoundingClientRect() });\n const own = editableImagesIn(start).map(withRect);\n const ancestorLevels: { node: HTMLElement; rect: DOMRect }[][] = [];\n if (own.length === 0) {\n let el = start.parentElement;\n for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {\n ancestorLevels.push(editableImagesIn(el).map(withRect));\n }\n }\n return pickImagesForClick(own, ancestorLevels, point).map(({ node }) => toImageRef(node));\n}\n\n/**\n * Initialize click handler for className editing.\n */\nexport function initClickHandler(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing click handler...\");\n document.addEventListener(\"click\", handleClick, true);\n isInitialized = true;\n}\n\n/**\n * Cleanup click handler.\n */\nexport function cleanupClickHandler(): void {\n document.removeEventListener(\"click\", handleClick, true);\n isInitialized = false;\n}\n\n/**\n * Build a SelectionRef from a clicked element, walking up to the nearest\n * component (data-upstart-file) for the source file / component name and\n * collecting i18n keys from the element's subtree.\n */\nfunction buildSelectionRef(element: HTMLElement): SelectionRef | null {\n const hash = element.dataset.upstartHash ?? \"\";\n\n const fileEl = element.closest<HTMLElement>(\"[data-upstart-file]\");\n const filePath = fileEl?.dataset.upstartFile ?? element.dataset.upstartFile ?? \"\";\n const componentName = fileEl?.dataset.upstartComponent ?? element.dataset.upstartComponent;\n\n // A stable identity for toggling. Prefer filePath+hash; fall back so distinct\n // elements without a hash don't collapse onto the same key.\n const key = `${filePath}:${hash}` !== \":\" ? `${filePath}:${hash}` : \"\";\n if (!key) {\n return null;\n }\n\n const i18nKeys = new Set<string>();\n const collectKeys = (el: HTMLElement) => {\n const raw = el.dataset.upstartI18n;\n if (raw) {\n for (const k of raw.split(/[\\s,]+/).filter(Boolean)) i18nKeys.add(k);\n }\n };\n collectKeys(element);\n for (const node of element.querySelectorAll<HTMLElement>(\"[data-upstart-i18n]\")) {\n collectKeys(node);\n }\n\n const text = (element.innerText ?? \"\").trim().slice(0, 200) || undefined;\n\n // Whether THIS element's content is derived from database data (element-level marker).\n // This is what gates content edits — its content can't be edited from the editor, only\n // its presentation. i18n/translated content is editable (it lives in translation files),\n // so i18n keys on the element take precedence and suppress the data-bound flag even if the\n // element was wrongly marked.\n const dataBoundEl = element.closest<HTMLElement>(\"[data-upstart-data-bound]\");\n const dataBound = !!dataBoundEl && dataBoundEl.dataset.upstartDataBound !== \"false\" && i18nKeys.size === 0;\n\n return {\n key,\n hash,\n filePath,\n componentName,\n i18nKeys: i18nKeys.size > 0 ? Array.from(i18nKeys) : undefined,\n text,\n dataBound: dataBound || undefined,\n };\n}\n\n/**\n * \"Select\" mode click: toggle the clicked component in/out of the selection,\n * mark it visually with data-upstart-selected, and report the new selection\n * to the parent. Never edits or navigates.\n */\nfunction handleSelectClick(event: MouseEvent, target: HTMLElement): void {\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n console.info(\"[Upstart Editor] Select click ignored: no data-upstart-hash ancestor\");\n return;\n }\n\n // Bubble up to a rich-panel editable ancestor so selecting an inline child\n // (e.g. <strong>) selects the whole editable region — mirrors edit-mode resolution.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.preventDefault();\n event.stopPropagation();\n\n const ref = buildSelectionRef(element);\n if (!ref) {\n return;\n }\n\n const nowSelected = toggleSelection(ref, element);\n if (nowSelected) {\n element.setAttribute(\"data-upstart-selected\", \"\");\n } else {\n element.removeAttribute(\"data-upstart-selected\");\n }\n\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n console.log(\"[Upstart Editor] Selection toggled:\", ref.key, nowSelected);\n}\n\nfunction handleClick(event: MouseEvent): void {\n const mode = getCurrentMode();\n if (mode === \"move\") {\n // The page is inert while moving elements: following a link or submitting a form would\n // reload the page and drop the staged moves. The move controls handle their own clicks.\n const target = event.target as HTMLElement | null;\n if (target?.closest(\"#upstart-move-controls, #upstart-move-bar\")) return;\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n if (mode !== \"edit\" && mode !== \"select\") {\n return;\n }\n\n console.debug(\"[Upstart Editor] Click event:\", event);\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n console.warn(\"[Upstart Editor] Click target is not an HTMLElement\");\n return;\n }\n // Viewport coordinates of the click, to tell which image (if any) was visually hit.\n const clickPoint: Point = { x: event.clientX, y: event.clientY };\n\n if (mode === \"select\") {\n handleSelectClick(event, target);\n return;\n }\n\n // Editor-only chrome (the array ×/+/✓ controls, the move handles and pending-moves bar)\n // and locally-staged draft list items handle their own clicks and must never be treated\n // as element selection.\n if (\n target.closest(\n \"#upstart-array-controls, #upstart-move-controls, #upstart-move-bar, [data-upstart-draft-item]\",\n )\n ) {\n return;\n }\n\n if (target.closest(\".upstart-editor-bubble-menu\")) {\n console.info(\"[Upstart Editor] Click ignored: target is inside the bubble menu\");\n return;\n }\n\n const activeLink = target.closest(\"a[data-upstart-editor-active]\");\n const isFormControlClick = Boolean(\n (target as HTMLElement | null)?.closest(\"input, textarea, select, button\"),\n );\n if (activeLink || !isFormControlClick) {\n event.preventDefault();\n }\n\n // Clicks inside contenteditable (TipTap active) — still notify style panel if applicable\n if (target.closest(\"[contenteditable='true']\")) {\n const editableEl = target.closest<HTMLElement>(\"[data-upstart-editable-text='true']\");\n if (editableEl) {\n let classNameId = editableEl.dataset.upstartClassnameId ?? \"\";\n let currentClassName = editableEl.className;\n if (!classNameId) {\n const parentWithClassname = editableEl.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n if (classNameId) {\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n sendToParent({\n type: \"element-clicked\",\n hash: editableEl.dataset.upstartHash ?? \"\",\n componentName: editableEl.dataset.upstartComponent,\n filePath: editableEl.dataset.upstartFile ?? \"\",\n classNameId,\n currentClassName,\n themeColors,\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n tagName: editableEl.tagName.toLowerCase(),\n images: collectImages(editableEl, clickPoint),\n editableText: true,\n });\n }\n }\n console.info(\"[Upstart Editor] Click inside contenteditable: TipTap handles text editing\");\n return;\n }\n\n // Detect a datasource record under the click. We check this BEFORE the\n // data-upstart-hash gate because a record may be rendered without that\n // attribute (e.g. an <a> from <Link to={...}>) and we still want to open\n // the inline datasource editor instead of letting the browser navigate.\n const datasourceEl = target.closest<HTMLElement>(\"[data-upstart-datasource][data-upstart-record-id]\");\n\n // Find the closest element with data-upstart-hash\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n if (datasourceEl) {\n // Datasource record click without a data-upstart-hash ancestor: stop\n // navigation, surface the datasource event so the editor can open the\n // inline datasource item inspector.\n event.preventDefault();\n event.stopPropagation();\n sendToParent({\n type: \"element-clicked\",\n hash: \"\",\n componentName: undefined,\n filePath: \"\",\n classNameId: \"\",\n currentClassName: \"\",\n datasourceId: datasourceEl.dataset.upstartDatasource,\n recordId: datasourceEl.dataset.upstartRecordId,\n themeColors: {},\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n tagName: datasourceEl.tagName.toLowerCase(),\n images: collectImages(datasourceEl, clickPoint),\n });\n return;\n }\n console.info(\"[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found\");\n return;\n }\n\n // If the clicked element is nested inside a rich-panel editable ancestor, bubble up to it\n // so that clicking any inline child (e.g. <strong>, <em>) edits the whole rich-panel region.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.stopPropagation();\n\n console.log(\"Element clicked dataset:\", element.dataset);\n\n const hash = element.dataset.upstartHash as string;\n const componentName = element.dataset.upstartComponent;\n const filePath = element.dataset.upstartFile ?? \"\";\n\n // If this element has editable-text, focus the TipTap editor (all modes now use inline TipTap)\n if (element.dataset.upstartEditableText === \"true\") {\n const proseMirror = element.querySelector<HTMLElement>(\".ProseMirror\");\n if (proseMirror) {\n proseMirror.focus();\n }\n }\n\n // Find classNameId on the element itself, or walk up to find it on a parent\n let classNameId = element.dataset.upstartClassnameId ?? \"\";\n let currentClassName = element.className;\n if (!classNameId) {\n const parentWithClassname = element.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n\n // Read DaisyUI CSS custom property values from the iframe's document\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n\n const rect = element.getBoundingClientRect();\n const bounds = {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n right: rect.right,\n bottom: rect.bottom,\n };\n\n // datasourceEl was resolved before the data-upstart-hash gate so we could\n // intercept datasource clicks even without a hash ancestor (see above).\n const datasourceId = datasourceEl?.dataset.upstartDatasource;\n const recordId = datasourceEl?.dataset.upstartRecordId;\n\n sendToParent({\n type: \"element-clicked\",\n hash,\n componentName,\n filePath,\n classNameId,\n currentClassName,\n datasourceId,\n recordId,\n themeColors,\n bounds,\n viewportWidth: window.innerWidth,\n tagName: element.tagName.toLowerCase(),\n images: collectImages(element, clickPoint),\n editableText: element.dataset.upstartEditableText === \"true\",\n });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/vite-plugin-upstart-editor/runtime/controls-ui.ts
|
|
2
|
+
const ICON_PATHS = {
|
|
3
|
+
plus: "<path d=\"M12 5v14M5 12h14\"/>",
|
|
4
|
+
cross: "<path d=\"M6 6 18 18M18 6 6 18\"/>",
|
|
5
|
+
check: "<path d=\"M5 12.5 10 17.5 19 7\"/>",
|
|
6
|
+
restore: "<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>",
|
|
7
|
+
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\"/>"
|
|
8
|
+
};
|
|
9
|
+
function setIcon(btn, name, px) {
|
|
10
|
+
const dots = name === "grip";
|
|
11
|
+
btn.innerHTML = `<svg width="${px}" height="${px}" viewBox="0 0 24 24" fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
|
|
12
|
+
}
|
|
13
|
+
function baseButton(top, left, size, bg) {
|
|
14
|
+
const btn = document.createElement("button");
|
|
15
|
+
btn.type = "button";
|
|
16
|
+
btn.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; padding: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);`;
|
|
17
|
+
return btn;
|
|
18
|
+
}
|
|
19
|
+
/** Absolutely positioned, click-through layer appended to <body> that hosts editor controls. */
|
|
20
|
+
function createControlsLayer(id) {
|
|
21
|
+
const layer = document.createElement("div");
|
|
22
|
+
layer.id = id;
|
|
23
|
+
layer.style.cssText = "position: absolute; top: 0; left: 0; width: 0; height: 0; pointer-events: none; z-index: 2147483646;";
|
|
24
|
+
document.body.appendChild(layer);
|
|
25
|
+
return layer;
|
|
26
|
+
}
|
|
27
|
+
function cssAttr(value) {
|
|
28
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { baseButton, createControlsLayer, cssAttr, setIcon };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=controls-ui.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controls-ui.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/controls-ui.ts"],"sourcesContent":["// Shared building blocks of the editor-only controls (array list buttons, move handles).\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 = {\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} as const;\n\nexport type IconName = keyof typeof ICON_PATHS;\n\nexport function setIcon(btn: HTMLElement, name: IconName, 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\nexport function 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\n/** Absolutely positioned, click-through layer appended to <body> that hosts editor controls. */\nexport function createControlsLayer(id: string): HTMLDivElement {\n const layer = document.createElement(\"div\");\n layer.id = id;\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\nexport function cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n"],"mappings":";AAIA,MAAM,aAAa;CACjB,MAAM;CACN,OAAO;CACP,OAAO;CACP,SAAS;CACT,MAAM;CACP;AAID,SAAgB,QAAQ,KAAkB,MAAgB,IAAkB;CAC1E,MAAM,OAAO,SAAS;CACtB,IAAI,YACF,eAAe,GAAG,YAAY,GAAG,8BACxB,OAAO,iBAAiB,OAAO,YAAY,OAAO,SAAS,eAAe,gHAEtC,WAAW,MAAM;;AAGlE,SAAgB,WAAW,KAAa,MAAc,MAAc,IAA+B;CACjG,MAAM,MAAM,SAAS,cAAc,SAAS;CAC5C,IAAI,OAAO;CACX,IAAI,MAAM,UACR,4BAA4B,IAAI,YAAY,KAAK,aAAa,KAAK,cAAc,KAAK,2FAEvE,GAAG;CAEpB,OAAO;;;AAIT,SAAgB,oBAAoB,IAA4B;CAC9D,MAAM,QAAQ,SAAS,cAAc,MAAM;CAC3C,MAAM,KAAK;CACX,MAAM,MAAM,UACV;CAEF,SAAS,KAAK,YAAY,MAAM;CAChC,OAAO;;AAGT,SAAgB,QAAQ,OAAuB;CAC7C,OAAO,MAAM,QAAQ,UAAU,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":";;;;;;;;;;;;iBAyBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAsBd,gBAAA,CAAiB,QAAA;;AAtBjC;;iBA8FgB,iBAAA,CAAA"}
|
|
@@ -7,6 +7,7 @@ import { initErrorHandler } from "./error-handler.js";
|
|
|
7
7
|
import { installDomGuard } from "./dom-guard.js";
|
|
8
8
|
import { activateAllEditors, destroyAllActiveEditors, initTextEditor } from "./text-editor.js";
|
|
9
9
|
import { hideArrayControls, initArrayControls, refreshArrayControls } from "./array-controls.js";
|
|
10
|
+
import { hideMoveControls, initMoveControls, onMovesApplyFailed, refreshMoveControls } from "./move-controls.js";
|
|
10
11
|
import { initFormGuard } from "./form-guard.js";
|
|
11
12
|
//#region src/vite-plugin-upstart-editor/runtime/index.ts
|
|
12
13
|
let isInitialized = false;
|
|
@@ -18,13 +19,20 @@ function setMode(mode) {
|
|
|
18
19
|
console.log(`[Upstart Editor] Setting mode to: ${mode}`);
|
|
19
20
|
if (mode === "edit") {
|
|
20
21
|
disableSelectMode();
|
|
22
|
+
disableMoveMode();
|
|
21
23
|
enableEditMode();
|
|
22
24
|
} else if (mode === "select") {
|
|
23
25
|
disableEditMode();
|
|
26
|
+
disableMoveMode();
|
|
24
27
|
enableSelectMode();
|
|
28
|
+
} else if (mode === "move") {
|
|
29
|
+
disableEditMode();
|
|
30
|
+
disableSelectMode();
|
|
31
|
+
enableMoveMode();
|
|
25
32
|
} else {
|
|
26
33
|
disableEditMode();
|
|
27
34
|
disableSelectMode();
|
|
35
|
+
disableMoveMode();
|
|
28
36
|
}
|
|
29
37
|
}
|
|
30
38
|
function waitForHydration(callback) {
|
|
@@ -131,6 +139,7 @@ function initUpstartEditor() {
|
|
|
131
139
|
initClickHandler();
|
|
132
140
|
initHoverOverlay();
|
|
133
141
|
initArrayControls();
|
|
142
|
+
initMoveControls();
|
|
134
143
|
initFormGuard();
|
|
135
144
|
initErrorHandler();
|
|
136
145
|
initRouteReporter();
|
|
@@ -196,6 +205,18 @@ function handleParentMessage(event) {
|
|
|
196
205
|
}
|
|
197
206
|
styleEl.textContent = css;
|
|
198
207
|
} else if (styleEl) styleEl.remove();
|
|
208
|
+
} else if (message.type === "preview-theme") {
|
|
209
|
+
const STYLE_ID = "upstart-preview-theme";
|
|
210
|
+
let styleEl = document.getElementById(STYLE_ID);
|
|
211
|
+
const css = typeof message.css === "string" ? message.css : "";
|
|
212
|
+
if (css) {
|
|
213
|
+
if (!styleEl) {
|
|
214
|
+
styleEl = document.createElement("style");
|
|
215
|
+
styleEl.id = STYLE_ID;
|
|
216
|
+
}
|
|
217
|
+
styleEl.textContent = css;
|
|
218
|
+
document.head.appendChild(styleEl);
|
|
219
|
+
} else if (styleEl) styleEl.remove();
|
|
199
220
|
} else if (message.type === "request-scroll-position") sendToParent({
|
|
200
221
|
type: "scroll-position",
|
|
201
222
|
x: window.scrollX,
|
|
@@ -219,6 +240,7 @@ function handleParentMessage(event) {
|
|
|
219
240
|
selection: getSelection()
|
|
220
241
|
});
|
|
221
242
|
} else if (message.type === "set-labels") setLabels(message.labels);
|
|
243
|
+
else if (message.type === "moves-apply-failed") onMovesApplyFailed();
|
|
222
244
|
}
|
|
223
245
|
/**
|
|
224
246
|
* Drop the whole "select" mode selection: clear the in-memory set and remove the
|
|
@@ -245,6 +267,19 @@ function enableSelectMode() {
|
|
|
245
267
|
console.log("[Upstart Editor] Select mode enabled");
|
|
246
268
|
document.documentElement.setAttribute("data-upstart-select-mode", "");
|
|
247
269
|
}
|
|
270
|
+
function enableMoveMode() {
|
|
271
|
+
console.log("[Upstart Editor] Move mode enabled");
|
|
272
|
+
document.documentElement.setAttribute("data-upstart-move-mode", "");
|
|
273
|
+
refreshMoveControls();
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Staged moves are kept when leaving the mode (the moved elements stay where they were
|
|
277
|
+
* dropped); the pending-moves bar comes back with the mode.
|
|
278
|
+
*/
|
|
279
|
+
function disableMoveMode() {
|
|
280
|
+
document.documentElement.removeAttribute("data-upstart-move-mode");
|
|
281
|
+
hideMoveControls();
|
|
282
|
+
}
|
|
248
283
|
function disableSelectMode() {
|
|
249
284
|
document.documentElement.removeAttribute("data-upstart-select-mode");
|
|
250
285
|
hideOverlays();
|
|
@@ -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 { 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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"}
|
|
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 {\n initMoveControls,\n refreshMoveControls,\n hideMoveControls,\n onMovesApplyFailed,\n} from \"./move-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 disableMoveMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n disableMoveMode();\n enableSelectMode();\n } else if (mode === \"move\") {\n disableEditMode();\n disableSelectMode();\n enableMoveMode();\n } else {\n disableEditMode();\n disableSelectMode();\n disableMoveMode();\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 initMoveControls();\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 === \"preview-theme\") {\n // Appended last in <head> so it wins over the theme's own variables until it is cleared.\n const STYLE_ID = \"upstart-preview-theme\";\n let styleEl = document.getElementById(STYLE_ID) 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 = STYLE_ID;\n }\n styleEl.textContent = css;\n document.head.appendChild(styleEl);\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 } else if (message.type === \"moves-apply-failed\") {\n onMovesApplyFailed();\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 enableMoveMode(): void {\n console.log(\"[Upstart Editor] Move mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-move-mode\", \"\");\n refreshMoveControls();\n}\n\n/**\n * Staged moves are kept when leaving the mode (the moved elements stay where they were\n * dropped); the pending-moves bar comes back with the mode.\n */\nfunction disableMoveMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-move-mode\");\n hideMoveControls();\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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"}
|
|
@@ -12,6 +12,11 @@ interface RuntimeLabels {
|
|
|
12
12
|
addItem: string;
|
|
13
13
|
applyChanges: string;
|
|
14
14
|
dragToReorder: string;
|
|
15
|
+
dragToMove: string;
|
|
16
|
+
pendingMoves: string;
|
|
17
|
+
undoMove: string;
|
|
18
|
+
discardMoves: string;
|
|
19
|
+
applyMoves: string;
|
|
15
20
|
clickToEdit: string;
|
|
16
21
|
startTyping: string;
|
|
17
22
|
recoveryTitle: string;
|
|
@@ -1 +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"}
|
|
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,UAAA;EACA,YAAA;EACA,QAAA;EACA,YAAA;EACA,UAAA;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"}
|
|
@@ -6,6 +6,11 @@ const DEFAULT_LABELS = Object.freeze({
|
|
|
6
6
|
addItem: "Add item",
|
|
7
7
|
applyChanges: "Apply changes",
|
|
8
8
|
dragToReorder: "Drag to reorder",
|
|
9
|
+
dragToMove: "Drag to move",
|
|
10
|
+
pendingMoves: "Moves not saved yet:",
|
|
11
|
+
undoMove: "Undo",
|
|
12
|
+
discardMoves: "Discard all",
|
|
13
|
+
applyMoves: "Apply",
|
|
9
14
|
clickToEdit: "Click to edit...",
|
|
10
15
|
startTyping: "Start typing...",
|
|
11
16
|
recoveryTitle: "The preview needs a refresh",
|
|
@@ -1 +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":";
|
|
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 dragToMove: string;\n pendingMoves: string;\n undoMove: string;\n discardMoves: string;\n applyMoves: 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 dragToMove: \"Drag to move\",\n pendingMoves: \"Moves not saved yet:\",\n undoMove: \"Undo\",\n discardMoves: \"Discard all\",\n applyMoves: \"Apply\",\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":";AAkCA,MAAa,iBAA0C,OAAO,OAAO;CACnE,SAAS;CACT,UAAU;CACV,YAAY;CACZ,SAAS;CACT,cAAc;CACd,eAAe;CACf,YAAY;CACZ,cAAc;CACd,UAAU;CACV,cAAc;CACd,YAAY;CACZ,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"}
|