@upstart.gg/vite-plugins 0.1.68 → 0.1.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/upstart-editor-api.d.ts +2 -0
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +67 -9
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  6. package/dist/vite-plugin-upstart-attrs.js +18 -1
  7. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  8. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +7 -7
  9. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
  10. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +30 -16
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  13. package/dist/vite-plugin-upstart-editor/runtime/dom-guard.js +45 -0
  14. package/dist/vite-plugin-upstart-editor/runtime/dom-guard.js.map +1 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +7 -1
  16. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +42 -2
  18. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
  19. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js +2 -2
  20. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js.map +1 -1
  21. package/dist/vite-plugin-upstart-editor/runtime/image-pick.js +32 -0
  22. package/dist/vite-plugin-upstart-editor/runtime/image-pick.js.map +1 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/runtime/index.js +9 -1
  25. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +32 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/labels.js +45 -0
  29. package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -0
  30. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  31. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +26 -14
  32. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  33. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +14 -1
  34. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  35. package/package.json +3 -3
  36. package/src/tests/dom-guard.test.ts +100 -0
  37. package/src/tests/editor-labels.test.ts +40 -0
  38. package/src/tests/image-pick.test.ts +50 -0
  39. package/src/tests/upstart-editor-api.test.ts +75 -0
  40. package/src/tests/vite-plugin-upstart-attrs.test.ts +21 -0
  41. package/src/upstart-editor-api.ts +74 -8
  42. package/src/vite-plugin-upstart-attrs.ts +44 -1
  43. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +7 -7
  44. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +29 -19
  45. package/src/vite-plugin-upstart-editor/runtime/dom-guard.ts +67 -0
  46. package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +48 -1
  47. package/src/vite-plugin-upstart-editor/runtime/form-guard.ts +3 -2
  48. package/src/vite-plugin-upstart-editor/runtime/image-pick.ts +40 -0
  49. package/src/vite-plugin-upstart-editor/runtime/index.ts +11 -0
  50. package/src/vite-plugin-upstart-editor/runtime/labels.ts +77 -0
  51. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +39 -15
  52. package/src/vite-plugin-upstart-editor/runtime/types.ts +12 -1
@@ -1 +1 @@
1
- {"version":3,"file":"array-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/array-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\n// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY\n// (no server round-trip per action): \"×\" marks an item for removal, \"+\" inserts an\n// editable draft pill, and a green \"✓\" below the \"+\" commits the whole batch at once\n// via a single `arraySet` edit + rebuild. Per-item text editing stays live; arraySet\n// reads the current item texts from the DOM so live edits are preserved.\n\nconst DEFAULT_NEW_ITEM = \"New item\";\nconst DRAFT_ATTR = \"data-upstart-draft-item\";\n\n// Layout of the \"+\"/\"✓\" stack placed to the right of a list.\nconst BTN_SIZE = 22;\nconst STACK_GAP = 6; // vertical gap between \"+\" and \"✓\"\nconst BUTTON_MARGIN = 12; // left margin between the item and the buttons\n\nlet layer: HTMLDivElement | null = null;\nlet observer: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nlet isInitialized = false;\nlet isMutatingDom = false;\n\n// Items observed for size changes (e.g. text edits widen a pill) so the \"×\" stays\n// anchored to the item's corner. WeakSet keeps observe() calls idempotent.\nconst observedForResize = new WeakSet<Element>();\n\n// arrayId -> set of original item indices currently marked for deletion.\nconst pendingDeletes = new Map<string, Set<number>>();\n\n// In-progress drag (reorder). Tracked at document level so it survives overlay\n// rebuilds; the overlay is frozen while a drag is active.\nlet drag: { arrayId: string; node: HTMLElement } | null = null;\n\n// The list item currently hovered — its per-item \"×\"/\"⠿\" controls are the only\n// ones shown (others stay hidden until hovered). buttonOwner lets a button keep its\n// item \"hovered\" while the pointer sits on the button (which overlays the corner).\nlet hoveredItem: HTMLElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nexport function initArrayControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n\n observer = new MutationObserver((mutations) => {\n if (isMutatingDom) return;\n for (const m of mutations) {\n const t = m.target as Node;\n if (layer && (t === layer || layer.contains(t))) continue;\n scheduleRefresh();\n return;\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => scheduleRefresh());\n }\n}\n\n// Append a per-item button and record which item it belongs to, so hovering the\n// button (which overlays the item's corner) keeps that item considered hovered.\nfunction appendOwned(root: HTMLElement, btn: HTMLElement, owner: HTMLElement): void {\n buttonOwner.set(btn, owner);\n root.appendChild(btn);\n}\n\n// Track the hovered list item; refresh when it changes so its controls toggle.\nfunction onHoverMove(e: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The list item under the pointer: either the item itself (or a descendant), or a\n// per-item control button that belongs to one.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n const item = target.closest<HTMLElement>(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);\n if (item) return item;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n if (owner && document.contains(owner)) return owner;\n }\n return null;\n}\n\n// Observe an item once so the overlay re-anchors when the item resizes.\nfunction observeResize(el: Element): void {\n if (resizeObserver && !observedForResize.has(el)) {\n observedForResize.add(el);\n resizeObserver.observe(el);\n }\n}\n\nexport function refreshArrayControls(): void {\n if (typeof document === \"undefined\") return;\n\n if (getCurrentMode() !== \"edit\") {\n hideArrayControls();\n return;\n }\n\n // While dragging we move list nodes live; keep the overlay frozen and rebuild it\n // once on drop (the drag is driven by document-level listeners, not the overlay).\n if (drag) return;\n\n isMutatingDom = true;\n try {\n const root = ensureLayer();\n root.replaceChildren();\n\n // Group original (non-draft) items by array id, ordered by loop index.\n const groups = new Map<string, HTMLElement[]>();\n for (const el of document.querySelectorAll<HTMLElement>(\"[data-upstart-array-id]\")) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const id = el.dataset.upstartArrayId;\n if (!id) continue;\n (groups.get(id) ?? groups.set(id, []).get(id)!).push(el);\n }\n\n for (const [arrayId, items] of groups) {\n items.sort((a, b) => itemIndex(a) - itemIndex(b));\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const drafts = draftPillsFor(arrayId);\n const ordered = orderedItemsFor(arrayId);\n // Reordering only makes sense with more than one item.\n const draggable = ordered.length > 1;\n\n // Original items: per-item controls show only on hover; a marked item always\n // keeps its restore \"×\" (it has pointer-events:none, so it can't be hovered).\n for (const item of items) {\n observeResize(item);\n const idx = itemIndex(item);\n const marked = deletes.has(idx);\n applyDeletionStyle(item, marked);\n if (!marked && item !== hoveredItem) continue;\n const rect = item.getBoundingClientRect();\n if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);\n appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);\n }\n\n // Draft (added) pills: controls show on hover; each has its own \"×\".\n for (const pill of drafts) {\n observeResize(pill);\n if (pill !== hoveredItem) continue;\n const rect = pill.getBoundingClientRect();\n if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);\n appendOwned(root, makeDraftRemoveButton(pill, rect), pill);\n }\n\n // \"+\" (and \"✓\" when there are pending changes) after the last item in DOM\n // order, as a vertically-centred stack with a small left margin.\n const anchor = ordered[ordered.length - 1];\n if (!anchor) continue;\n const a = anchor.getBoundingClientRect();\n\n const finalCount = items.length - deletes.size + drafts.length;\n const dirty = deletes.size > 0 || drafts.length > 0 || isReordered(arrayId);\n const showApply = dirty && finalCount >= 1;\n\n const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;\n const left = a.right + window.scrollX + BUTTON_MARGIN;\n const top = a.top + window.scrollY + (a.height - stackHeight) / 2;\n\n root.appendChild(makeAddButton(arrayId, top, left));\n if (showApply) {\n root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));\n }\n }\n } finally {\n isMutatingDom = false;\n }\n}\n\nexport function hideArrayControls(): void {\n if (layer) layer.replaceChildren();\n}\n\n// ---------------------------------------------------------------------------\n// DOM helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLayer(): HTMLDivElement {\n if (layer) return layer;\n layer = document.createElement(\"div\");\n layer.id = \"upstart-array-controls\";\n layer.style.cssText =\n \"position: absolute; top: 0; left: 0; width: 0; height: 0; \" +\n \"pointer-events: none; z-index: 2147483646;\";\n document.body.appendChild(layer);\n return layer;\n}\n\nfunction itemIndex(el: HTMLElement): number {\n const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : Number.NaN;\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction originalItemsFor(arrayId: string): HTMLElement[] {\n return [\n ...document.querySelectorAll<HTMLElement>(\n `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}])`,\n ),\n ].sort((a, b) => itemIndex(a) - itemIndex(b));\n}\n\nfunction draftPillsFor(arrayId: string): HTMLElement[] {\n return [...document.querySelectorAll<HTMLElement>(`[${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`)];\n}\n\n// All items of an array (originals + drafts) in DOM order — this is the order the\n// list is rendered in and the order applied to the source on ✓.\nfunction orderedItemsFor(arrayId: string): HTMLElement[] {\n const sel = `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`;\n return [...document.querySelectorAll<HTMLElement>(sel)];\n}\n\n// True when the original items no longer appear in their source order (i.e. the\n// user has dragged at least one item), so the ✓ apply button should be offered.\nfunction isReordered(arrayId: string): boolean {\n let prev = -1;\n for (const el of orderedItemsFor(arrayId)) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const idx = itemIndex(el);\n if (idx < prev) return true;\n prev = idx;\n }\n return false;\n}\n\nfunction cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction textOf(el: HTMLElement): string {\n return (el.textContent ?? \"\").trim();\n}\n\nfunction applyDeletionStyle(item: HTMLElement, marked: boolean): void {\n if (marked) {\n item.style.opacity = \"0.4\";\n item.style.textDecoration = \"line-through\";\n item.style.pointerEvents = \"none\";\n } else {\n item.style.removeProperty(\"opacity\");\n item.style.removeProperty(\"text-decoration\");\n item.style.removeProperty(\"pointer-events\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Buttons\n// ---------------------------------------------------------------------------\n\n// Geometric SVG icons — centred by the flex container + viewBox, so they don't\n// depend on the host page's font metrics (which left text glyphs mis-aligned).\nconst ICON_PATHS: Record<string, string> = {\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n cross: '<path d=\"M6 6 18 18M18 6 6 18\"/>',\n check: '<path d=\"M5 12.5 10 17.5 19 7\"/>',\n restore: '<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>',\n grip: '<circle cx=\"9\" cy=\"6\" r=\"1.5\"/><circle cx=\"15\" cy=\"6\" r=\"1.5\"/><circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/><circle cx=\"9\" cy=\"18\" r=\"1.5\"/><circle cx=\"15\" cy=\"18\" r=\"1.5\"/>',\n};\n\nfunction setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {\n const dots = name === \"grip\";\n btn.innerHTML =\n `<svg width=\"${px}\" height=\"${px}\" viewBox=\"0 0 24 24\" ` +\n `fill=\"${dots ? \"currentColor\" : \"none\"}\" stroke=\"${dots ? \"none\" : \"currentColor\"}\" ` +\n 'stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" ' +\n `style=\"display:block;pointer-events:none\">${ICON_PATHS[name]}</svg>`;\n}\n\nfunction baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.style.cssText =\n `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +\n \"padding: 0; display: flex; align-items: center; justify-content: center; \" +\n `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +\n \"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);\";\n return btn;\n}\n\nfunction makeItemDeleteButton(\n arrayId: string,\n index: number,\n marked: boolean,\n rect: DOMRect,\n): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, marked ? \"restore\" : \"cross\", 10);\n btn.title = marked ? \"Keep item\" : \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const set = pendingDeletes.get(arrayId) ?? new Set<number>();\n if (set.has(index)) set.delete(index);\n else set.add(index);\n if (set.size) pendingDeletes.set(arrayId, set);\n else pendingDeletes.delete(arrayId);\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeDraftRemoveButton(pill: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, \"cross\", 10);\n btn.title = \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n pill.remove();\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeAddButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#7270c6\");\n setIcon(btn, \"plus\", 14);\n btn.title = \"Add item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n addDraftPill(arrayId);\n });\n return btn;\n}\n\nfunction makeApplyButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#16a34a\");\n setIcon(btn, \"check\", 13);\n btn.title = \"Apply changes\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n applyArray(arrayId);\n });\n return btn;\n}\n\n// Grip centred on the item's top edge (away from the neighbour's \"×\" corners);\n// press and drag to reorder.\nfunction makeDragHandle(arrayId: string, node: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const top = rect.top + window.scrollY - 8;\n const left = rect.left + window.scrollX + rect.width / 2 - 8;\n const btn = baseButton(top, left, 16, \"#6b7280\");\n setIcon(btn, \"grip\", 12);\n btn.title = \"Drag to reorder\";\n btn.style.cursor = \"grab\";\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n startDrag(arrayId, node);\n });\n return btn;\n}\n\n// ---------------------------------------------------------------------------\n// Drag to reorder\n// ---------------------------------------------------------------------------\n\nfunction startDrag(arrayId: string, node: HTMLElement): void {\n drag = { arrayId, node };\n node.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n // Drive the drag from document level so it survives overlay rebuilds.\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragEnd, { once: true });\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);\n if (!target) return;\n const r = target.getBoundingClientRect();\n // Insert before/after based on the pointer vs the target's horizontal midpoint.\n if (e.clientX > r.left + r.width / 2) target.after(drag.node);\n else target.before(drag.node);\n}\n\nfunction onDragEnd(): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.body.style.removeProperty(\"user-select\");\n if (drag) {\n drag.node.style.removeProperty(\"opacity\");\n drag = null;\n }\n refreshArrayControls();\n}\n\n// The array item (original or draft) whose box contains the pointer, excluding the\n// node being dragged.\nfunction itemUnderPointer(arrayId: string, x: number, y: number, exclude: HTMLElement): HTMLElement | null {\n for (const el of orderedItemsFor(arrayId)) {\n if (el === exclude) continue;\n const r = el.getBoundingClientRect();\n if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction addDraftPill(arrayId: string): void {\n const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];\n if (!template?.parentElement) return;\n\n const pill = template.cloneNode(false) as HTMLElement;\n // Clone keeps the pill styling (class); strip identity/editor attrs so it is not\n // picked up by the text editor, click handler, hover overlay or the array query.\n for (const attr of [...pill.attributes]) {\n if (attr.name.startsWith(\"data-upstart-\") || attr.name === \"contenteditable\") {\n pill.removeAttribute(attr.name);\n }\n }\n pill.removeAttribute(\"style\");\n pill.setAttribute(DRAFT_ATTR, arrayId);\n pill.setAttribute(\"contenteditable\", \"true\");\n pill.textContent = DEFAULT_NEW_ITEM;\n\n // Insert after the current last pill of this array (drafts go at the end).\n const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();\n if (last && last.parentElement === template.parentElement) {\n last.after(pill);\n } else {\n template.parentElement.appendChild(pill);\n }\n\n // Focus + select so the user can type the name immediately.\n pill.focus();\n const range = document.createRange();\n range.selectNodeContents(pill);\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n\n refreshArrayControls();\n}\n\nfunction applyArray(arrayId: string): void {\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const items: string[] = [];\n\n // DOM order = the order the user sees (incl. any drag reordering).\n for (const el of orderedItemsFor(arrayId)) {\n if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;\n items.push(textOf(el));\n }\n\n if (items.length === 0) return;\n\n // The server rewrites the whole array, rebuilds + swaps, then the editor reloads\n // the iframe — which discards the local draft state, so just clear our marks.\n pendingDeletes.delete(arrayId);\n sendToParent({ type: \"text-edit\", payload: { action: \"arraySet\", arrayId, items } });\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshArrayControls();\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"array-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/array-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getLabels } from \"./labels.js\";\n\n// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY\n// (no server round-trip per action): \"×\" marks an item for removal, \"+\" inserts an\n// editable draft pill, and a green \"✓\" below the \"+\" commits the whole batch at once\n// via a single `arraySet` edit + rebuild. Per-item text editing stays live; arraySet\n// reads the current item texts from the DOM so live edits are preserved.\n\nconst DRAFT_ATTR = \"data-upstart-draft-item\";\n\n// Layout of the \"+\"/\"✓\" stack placed to the right of a list.\nconst BTN_SIZE = 22;\nconst STACK_GAP = 6; // vertical gap between \"+\" and \"✓\"\nconst BUTTON_MARGIN = 12; // left margin between the item and the buttons\n\nlet layer: HTMLDivElement | null = null;\nlet observer: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nlet isInitialized = false;\nlet isMutatingDom = false;\n\n// Items observed for size changes (e.g. text edits widen a pill) so the \"×\" stays\n// anchored to the item's corner. WeakSet keeps observe() calls idempotent.\nconst observedForResize = new WeakSet<Element>();\n\n// arrayId -> set of original item indices currently marked for deletion.\nconst pendingDeletes = new Map<string, Set<number>>();\n\n// In-progress drag (reorder). Tracked at document level so it survives overlay\n// rebuilds; the overlay is frozen while a drag is active.\nlet drag: { arrayId: string; node: HTMLElement } | null = null;\n\n// The list item currently hovered — its per-item \"×\"/\"⠿\" controls are the only\n// ones shown (others stay hidden until hovered). buttonOwner lets a button keep its\n// item \"hovered\" while the pointer sits on the button (which overlays the corner).\nlet hoveredItem: HTMLElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nexport function initArrayControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n\n observer = new MutationObserver((mutations) => {\n if (isMutatingDom) return;\n for (const m of mutations) {\n const t = m.target as Node;\n if (layer && (t === layer || layer.contains(t))) continue;\n scheduleRefresh();\n return;\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => scheduleRefresh());\n }\n}\n\n// Append a per-item button and record which item it belongs to, so hovering the\n// button (which overlays the item's corner) keeps that item considered hovered.\nfunction appendOwned(root: HTMLElement, btn: HTMLElement, owner: HTMLElement): void {\n buttonOwner.set(btn, owner);\n root.appendChild(btn);\n}\n\n// Track the hovered list item; refresh when it changes so its controls toggle.\nfunction onHoverMove(e: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The list item under the pointer: either the item itself (or a descendant), or a\n// per-item control button that belongs to one.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n const item = target.closest<HTMLElement>(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);\n if (item) return item;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n if (owner && document.contains(owner)) return owner;\n }\n return null;\n}\n\n// Observe an item once so the overlay re-anchors when the item resizes.\nfunction observeResize(el: Element): void {\n if (resizeObserver && !observedForResize.has(el)) {\n observedForResize.add(el);\n resizeObserver.observe(el);\n }\n}\n\nexport function refreshArrayControls(): void {\n if (typeof document === \"undefined\") return;\n\n if (getCurrentMode() !== \"edit\") {\n hideArrayControls();\n return;\n }\n\n // While dragging we move list nodes live; keep the overlay frozen and rebuild it\n // once on drop (the drag is driven by document-level listeners, not the overlay).\n if (drag) return;\n\n isMutatingDom = true;\n try {\n const root = ensureLayer();\n root.replaceChildren();\n\n // Group original (non-draft) items by array id, ordered by loop index.\n const groups = new Map<string, HTMLElement[]>();\n for (const el of document.querySelectorAll<HTMLElement>(\"[data-upstart-array-id]\")) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const id = el.dataset.upstartArrayId;\n if (!id) continue;\n (groups.get(id) ?? groups.set(id, []).get(id)!).push(el);\n }\n\n for (const [arrayId, items] of groups) {\n items.sort((a, b) => itemIndex(a) - itemIndex(b));\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const drafts = draftPillsFor(arrayId);\n const ordered = orderedItemsFor(arrayId);\n // Reordering only makes sense with more than one item.\n const draggable = ordered.length > 1;\n\n // Original items: per-item controls show only on hover; a marked item always\n // keeps its restore \"×\" (it has pointer-events:none, so it can't be hovered).\n for (const item of items) {\n observeResize(item);\n const idx = itemIndex(item);\n const marked = deletes.has(idx);\n applyDeletionStyle(item, marked);\n if (!marked && item !== hoveredItem) continue;\n const rect = item.getBoundingClientRect();\n if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);\n appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);\n }\n\n // Draft (added) pills: controls show on hover; each has its own \"×\".\n for (const pill of drafts) {\n observeResize(pill);\n if (pill !== hoveredItem) continue;\n const rect = pill.getBoundingClientRect();\n if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);\n appendOwned(root, makeDraftRemoveButton(pill, rect), pill);\n }\n\n // \"+\" (and \"✓\" when there are pending changes) after the last item in DOM\n // order, as a vertically-centred stack with a small left margin.\n const anchor = ordered[ordered.length - 1];\n if (!anchor) continue;\n const a = anchor.getBoundingClientRect();\n\n const finalCount = items.length - deletes.size + drafts.length;\n const dirty = deletes.size > 0 || drafts.length > 0 || isReordered(arrayId);\n const showApply = dirty && finalCount >= 1;\n\n const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;\n const left = a.right + window.scrollX + BUTTON_MARGIN;\n const top = a.top + window.scrollY + (a.height - stackHeight) / 2;\n\n root.appendChild(makeAddButton(arrayId, top, left));\n if (showApply) {\n root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));\n }\n }\n } finally {\n isMutatingDom = false;\n }\n}\n\nexport function hideArrayControls(): void {\n if (layer) layer.replaceChildren();\n}\n\n// ---------------------------------------------------------------------------\n// DOM helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLayer(): HTMLDivElement {\n if (layer) return layer;\n layer = document.createElement(\"div\");\n layer.id = \"upstart-array-controls\";\n layer.style.cssText =\n \"position: absolute; top: 0; left: 0; width: 0; height: 0; \" +\n \"pointer-events: none; z-index: 2147483646;\";\n document.body.appendChild(layer);\n return layer;\n}\n\nfunction itemIndex(el: HTMLElement): number {\n const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : Number.NaN;\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction originalItemsFor(arrayId: string): HTMLElement[] {\n return [\n ...document.querySelectorAll<HTMLElement>(\n `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}])`,\n ),\n ].sort((a, b) => itemIndex(a) - itemIndex(b));\n}\n\nfunction draftPillsFor(arrayId: string): HTMLElement[] {\n return [...document.querySelectorAll<HTMLElement>(`[${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`)];\n}\n\n// All items of an array (originals + drafts) in DOM order — this is the order the\n// list is rendered in and the order applied to the source on ✓.\nfunction orderedItemsFor(arrayId: string): HTMLElement[] {\n const sel = `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`;\n return [...document.querySelectorAll<HTMLElement>(sel)];\n}\n\n// True when the original items no longer appear in their source order (i.e. the\n// user has dragged at least one item), so the ✓ apply button should be offered.\nfunction isReordered(arrayId: string): boolean {\n let prev = -1;\n for (const el of orderedItemsFor(arrayId)) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const idx = itemIndex(el);\n if (idx < prev) return true;\n prev = idx;\n }\n return false;\n}\n\nfunction cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction textOf(el: HTMLElement): string {\n return (el.textContent ?? \"\").trim();\n}\n\nfunction applyDeletionStyle(item: HTMLElement, marked: boolean): void {\n if (marked) {\n item.style.opacity = \"0.4\";\n item.style.textDecoration = \"line-through\";\n item.style.pointerEvents = \"none\";\n } else {\n item.style.removeProperty(\"opacity\");\n item.style.removeProperty(\"text-decoration\");\n item.style.removeProperty(\"pointer-events\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Buttons\n// ---------------------------------------------------------------------------\n\n// Geometric SVG icons — centred by the flex container + viewBox, so they don't\n// depend on the host page's font metrics (which left text glyphs mis-aligned).\nconst ICON_PATHS: Record<string, string> = {\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n cross: '<path d=\"M6 6 18 18M18 6 6 18\"/>',\n check: '<path d=\"M5 12.5 10 17.5 19 7\"/>',\n restore: '<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>',\n grip: '<circle cx=\"9\" cy=\"6\" r=\"1.5\"/><circle cx=\"15\" cy=\"6\" r=\"1.5\"/><circle cx=\"9\" cy=\"12\" r=\"1.5\"/><circle cx=\"15\" cy=\"12\" r=\"1.5\"/><circle cx=\"9\" cy=\"18\" r=\"1.5\"/><circle cx=\"15\" cy=\"18\" r=\"1.5\"/>',\n};\n\nfunction setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {\n const dots = name === \"grip\";\n btn.innerHTML =\n `<svg width=\"${px}\" height=\"${px}\" viewBox=\"0 0 24 24\" ` +\n `fill=\"${dots ? \"currentColor\" : \"none\"}\" stroke=\"${dots ? \"none\" : \"currentColor\"}\" ` +\n 'stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" ' +\n `style=\"display:block;pointer-events:none\">${ICON_PATHS[name]}</svg>`;\n}\n\nfunction baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.style.cssText =\n `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +\n \"padding: 0; display: flex; align-items: center; justify-content: center; \" +\n `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +\n \"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);\";\n return btn;\n}\n\nfunction makeItemDeleteButton(\n arrayId: string,\n index: number,\n marked: boolean,\n rect: DOMRect,\n): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, marked ? \"restore\" : \"cross\", 10);\n btn.title = marked ? getLabels().keepItem : getLabels().removeItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const set = pendingDeletes.get(arrayId) ?? new Set<number>();\n if (set.has(index)) set.delete(index);\n else set.add(index);\n if (set.size) pendingDeletes.set(arrayId, set);\n else pendingDeletes.delete(arrayId);\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeDraftRemoveButton(pill: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, \"cross\", 10);\n btn.title = getLabels().removeItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n pill.remove();\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeAddButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#7270c6\");\n setIcon(btn, \"plus\", 14);\n btn.title = getLabels().addItem;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n addDraftPill(arrayId);\n });\n return btn;\n}\n\nfunction makeApplyButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#16a34a\");\n setIcon(btn, \"check\", 13);\n btn.title = getLabels().applyChanges;\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n applyArray(arrayId);\n });\n return btn;\n}\n\n// Grip centred on the item's top edge (away from the neighbour's \"×\" corners);\n// press and drag to reorder.\nfunction makeDragHandle(arrayId: string, node: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const top = rect.top + window.scrollY - 8;\n const left = rect.left + window.scrollX + rect.width / 2 - 8;\n const btn = baseButton(top, left, 16, \"#6b7280\");\n setIcon(btn, \"grip\", 12);\n btn.title = getLabels().dragToReorder;\n btn.style.cursor = \"grab\";\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n startDrag(arrayId, node);\n });\n return btn;\n}\n\n// ---------------------------------------------------------------------------\n// Drag to reorder\n// ---------------------------------------------------------------------------\n\nfunction startDrag(arrayId: string, node: HTMLElement): void {\n drag = { arrayId, node };\n node.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n // Drive the drag from document level so it survives overlay rebuilds.\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragEnd, { once: true });\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);\n if (!target) return;\n const r = target.getBoundingClientRect();\n // Insert before/after based on the pointer vs the target's horizontal midpoint.\n if (e.clientX > r.left + r.width / 2) target.after(drag.node);\n else target.before(drag.node);\n}\n\nfunction onDragEnd(): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.body.style.removeProperty(\"user-select\");\n if (drag) {\n drag.node.style.removeProperty(\"opacity\");\n drag = null;\n }\n refreshArrayControls();\n}\n\n// The array item (original or draft) whose box contains the pointer, excluding the\n// node being dragged.\nfunction itemUnderPointer(arrayId: string, x: number, y: number, exclude: HTMLElement): HTMLElement | null {\n for (const el of orderedItemsFor(arrayId)) {\n if (el === exclude) continue;\n const r = el.getBoundingClientRect();\n if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction addDraftPill(arrayId: string): void {\n const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];\n if (!template?.parentElement) return;\n\n const pill = template.cloneNode(false) as HTMLElement;\n // Clone keeps the pill styling (class); strip identity/editor attrs so it is not\n // picked up by the text editor, click handler, hover overlay or the array query.\n for (const attr of [...pill.attributes]) {\n if (attr.name.startsWith(\"data-upstart-\") || attr.name === \"contenteditable\") {\n pill.removeAttribute(attr.name);\n }\n }\n pill.removeAttribute(\"style\");\n pill.setAttribute(DRAFT_ATTR, arrayId);\n pill.setAttribute(\"contenteditable\", \"true\");\n pill.textContent = getLabels().newItem;\n\n // Insert after the current last pill of this array (drafts go at the end).\n const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();\n if (last && last.parentElement === template.parentElement) {\n last.after(pill);\n } else {\n template.parentElement.appendChild(pill);\n }\n\n // Focus + select so the user can type the name immediately.\n pill.focus();\n const range = document.createRange();\n range.selectNodeContents(pill);\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n\n refreshArrayControls();\n}\n\nfunction applyArray(arrayId: string): void {\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const items: string[] = [];\n\n // DOM order = the order the user sees (incl. any drag reordering).\n for (const el of orderedItemsFor(arrayId)) {\n if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;\n items.push(textOf(el));\n }\n\n if (items.length === 0) return;\n\n // The server rewrites the whole array, rebuilds + swaps, then the editor reloads\n // the iframe — which discards the local draft state, so just clear our marks.\n pendingDeletes.delete(arrayId);\n sendToParent({ type: \"text-edit\", payload: { action: \"arraySet\", arrayId, items } });\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshArrayControls();\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;AAsFA;;iBAAgB,gBAAA,CAAA;;;AAiBhB;iBAAgB,mBAAA,CAAA"}
1
+ {"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;AA8FA;;iBAAgB,gBAAA,CAAA;;;AAiBhB;iBAAgB,mBAAA,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { getCurrentMode, getSelection, toggleSelection } from "./state.js";
2
+ import { pickImagesForClick } from "./image-pick.js";
2
3
  import { sendToParent } from "./utils.js";
3
4
  //#region src/vite-plugin-upstart-editor/runtime/click-handler.ts
4
5
  let isInitialized = false;
@@ -54,22 +55,31 @@ function toImageRef(node) {
54
55
  parentClassName: parent?.dataset.upstartClassnameId ? parent.className : void 0
55
56
  };
56
57
  }
58
+ function editableImagesIn(el) {
59
+ const found = /* @__PURE__ */ new Set();
60
+ if (el.dataset.upstartImageId) found.add(el);
61
+ for (const node of el.querySelectorAll("[data-upstart-image-id]")) found.add(node);
62
+ return Array.from(found);
63
+ }
57
64
  /**
58
- * Collect editable images (elements carrying data-upstart-image-id) from the
59
- * clicked element's nearest subtree. Starting at `start`, we climb ancestors
60
- * until we find a subtree that contains at least one editable image. This lets
61
- * us surface a background image (e.g. a hero <img> covered by overlays) even
62
- * when the click lands on a sibling overlay rather than the image itself.
65
+ * Collect the editable images (elements carrying data-upstart-image-id) relevant to a click
66
+ * on `start`: the images inside the clicked element, or, when it has none, an ancestor's image
67
+ * lying under the click point (a hero <img> covered by an overlay). See pickImagesForClick for
68
+ * the exact rules — in particular an ancestor's images are never surfaced when the click is
69
+ * not visually on them, so clicking a title never opens the logo next to it.
63
70
  */
64
- function collectImages(start) {
65
- let el = start;
66
- for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {
67
- const found = /* @__PURE__ */ new Set();
68
- if (el.dataset.upstartImageId) found.add(el);
69
- for (const node of el.querySelectorAll("[data-upstart-image-id]")) found.add(node);
70
- if (found.size > 0) return Array.from(found).map(toImageRef);
71
+ function collectImages(start, point) {
72
+ const withRect = (node) => ({
73
+ node,
74
+ rect: node.getBoundingClientRect()
75
+ });
76
+ const own = editableImagesIn(start).map(withRect);
77
+ const ancestorLevels = [];
78
+ if (own.length === 0) {
79
+ let el = start.parentElement;
80
+ for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) ancestorLevels.push(editableImagesIn(el).map(withRect));
71
81
  }
72
- return [];
82
+ return pickImagesForClick(own, ancestorLevels, point).map(({ node }) => toImageRef(node));
73
83
  }
74
84
  /**
75
85
  * Initialize click handler for className editing.
@@ -154,6 +164,10 @@ function handleClick(event) {
154
164
  console.warn("[Upstart Editor] Click target is not an HTMLElement");
155
165
  return;
156
166
  }
167
+ const clickPoint = {
168
+ x: event.clientX,
169
+ y: event.clientY
170
+ };
157
171
  if (mode === "select") {
158
172
  handleSelectClick(event, target);
159
173
  return;
@@ -202,7 +216,7 @@ function handleClick(event) {
202
216
  bottom: 0
203
217
  },
204
218
  tagName: editableEl.tagName.toLowerCase(),
205
- images: collectImages(editableEl)
219
+ images: collectImages(editableEl, clickPoint)
206
220
  });
207
221
  }
208
222
  }
@@ -234,7 +248,7 @@ function handleClick(event) {
234
248
  bottom: 0
235
249
  },
236
250
  tagName: datasourceEl.tagName.toLowerCase(),
237
- images: collectImages(datasourceEl)
251
+ images: collectImages(datasourceEl, clickPoint)
238
252
  });
239
253
  return;
240
254
  }
@@ -290,7 +304,7 @@ function handleClick(event) {
290
304
  bounds,
291
305
  viewportWidth: window.innerWidth,
292
306
  tagName: element.tagName.toLowerCase(),
293
- images: collectImages(element)
307
+ images: collectImages(element, clickPoint)
294
308
  });
295
309
  console.log("[Upstart Editor] Element clicked:", componentName, hash);
296
310
  }
@@ -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 { 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\n/**\n * Collect editable images (elements carrying data-upstart-image-id) from the\n * clicked element's nearest subtree. Starting at `start`, we climb ancestors\n * until we find a subtree that contains at least one editable image. This lets\n * us surface a background image (e.g. a hero <img> covered by overlays) even\n * when the click lands on a sibling overlay rather than the image itself.\n */\nfunction collectImages(start: HTMLElement): EditableImageRef[] {\n let el: HTMLElement | null = start;\n for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {\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 if (found.size > 0) {\n return Array.from(found).map(toImageRef);\n }\n }\n return [];\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\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),\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),\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),\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 !== \"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 });\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 });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ //#region src/vite-plugin-upstart-editor/runtime/dom-guard.ts
2
+ function describeNode(node) {
3
+ if (!node) return "null";
4
+ if (node.nodeType === 3) return `#text "${(node.textContent ?? "").slice(0, 40)}"`;
5
+ const el = node;
6
+ if (!el.tagName) return node.nodeName;
7
+ const id = el.id ? `#${el.id}` : "";
8
+ const cls = typeof el.className === "string" && el.className ? `.${el.className.split(/\s+/).slice(0, 3).join(".")}` : "";
9
+ const hash = el.dataset?.upstartHash ? ` [hash=${el.dataset.upstartHash}]` : "";
10
+ return `<${el.tagName.toLowerCase()}${id}${cls}>${hash}`;
11
+ }
12
+ function installDomGuard(proto, onDesync) {
13
+ const originalInsertBefore = proto.insertBefore;
14
+ const originalRemoveChild = proto.removeChild;
15
+ let reported = false;
16
+ const report = (operation, parent, node, child) => {
17
+ console.warn(`[Upstart Editor] DOM out of sync: ${operation} on ${describeNode(parent)} with ${describeNode(node)}` + (child ? ` before ${describeNode(child)} (no longer a child)` : " (no longer a child)"), {
18
+ parent,
19
+ node,
20
+ child,
21
+ stack: (/* @__PURE__ */ new Error("DOM out of sync")).stack
22
+ });
23
+ if (reported) return;
24
+ reported = true;
25
+ onDesync();
26
+ };
27
+ proto.insertBefore = function(node, child) {
28
+ if (child && child.parentNode !== this) {
29
+ report("insertBefore", this, node, child);
30
+ return originalInsertBefore.call(this, node, null);
31
+ }
32
+ return originalInsertBefore.call(this, node, child);
33
+ };
34
+ proto.removeChild = function(child) {
35
+ if (child.parentNode !== this) {
36
+ report("removeChild", this, child, null);
37
+ return child;
38
+ }
39
+ return originalRemoveChild.call(this, child);
40
+ };
41
+ }
42
+ //#endregion
43
+ export { installDomGuard };
44
+
45
+ //# sourceMappingURL=dom-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dom-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/dom-guard.ts"],"sourcesContent":["/**\n * Makes React's DOM reconciliation survive the editor's own DOM mutations.\n *\n * In edit mode the text editor replaces the children of React-managed elements with a\n * TipTap view. When React later re-renders one of those subtrees (an interaction of the\n * site, a revalidation, a language switch…) it references child nodes that are gone and\n * `insertBefore` / `removeChild` throw `NotFoundError`, which unmounts the whole page into\n * the site's error boundary. Tolerating those two calls keeps the page alive: an insert\n * whose reference node is gone becomes an append, and removing a node that is no longer a\n * child is a no-op. The first time it happens `onDesync` is called so the editor can offer\n * a refresh, as the preview may now differ slightly from the source.\n */\nexport interface DomLike {\n insertBefore: <T extends Node>(this: Node, node: T, child: Node | null) => T;\n removeChild: <T extends Node>(this: Node, child: T) => T;\n}\n\nfunction describeNode(node: Node | null): string {\n if (!node) return \"null\";\n if (node.nodeType === 3) return `#text \"${(node.textContent ?? \"\").slice(0, 40)}\"`;\n const el = node as Element;\n if (!el.tagName) return node.nodeName;\n const id = el.id ? `#${el.id}` : \"\";\n const cls =\n typeof el.className === \"string\" && el.className\n ? `.${el.className.split(/\\s+/).slice(0, 3).join(\".\")}`\n : \"\";\n const hash = (el as HTMLElement).dataset?.upstartHash\n ? ` [hash=${(el as HTMLElement).dataset.upstartHash}]`\n : \"\";\n return `<${el.tagName.toLowerCase()}${id}${cls}>${hash}`;\n}\n\nexport function installDomGuard(proto: DomLike, onDesync: () => void): void {\n const originalInsertBefore = proto.insertBefore;\n const originalRemoveChild = proto.removeChild;\n let reported = false;\n // Every tolerated call is logged with its stack: that is what tells which update (React\n // reconciliation, a library…) touched an element the editor had modified. Open the\n // preview iframe's console context in the devtools to read it.\n const report = (operation: string, parent: Node, node: Node, child: Node | null) => {\n console.warn(\n `[Upstart Editor] DOM out of sync: ${operation} on ${describeNode(parent)} with ${describeNode(node)}` +\n (child ? ` before ${describeNode(child)} (no longer a child)` : \" (no longer a child)\"),\n { parent, node, child, stack: new Error(\"DOM out of sync\").stack },\n );\n if (reported) return;\n reported = true;\n onDesync();\n };\n\n proto.insertBefore = function <T extends Node>(this: Node, node: T, child: Node | null): T {\n if (child && child.parentNode !== this) {\n report(\"insertBefore\", this, node, child);\n return originalInsertBefore.call(this, node, null) as T;\n }\n return originalInsertBefore.call(this, node, child) as T;\n };\n\n proto.removeChild = function <T extends Node>(this: Node, child: T): T {\n if (child.parentNode !== this) {\n report(\"removeChild\", this, child, null);\n return child;\n }\n return originalRemoveChild.call(this, child) as T;\n };\n}\n"],"mappings":";AAiBA,SAAS,aAAa,MAA2B;CAC/C,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,aAAa,GAAG,OAAO,WAAW,KAAK,eAAe,IAAI,MAAM,GAAG,GAAG,CAAC;CAChF,MAAM,KAAK;CACX,IAAI,CAAC,GAAG,SAAS,OAAO,KAAK;CAC7B,MAAM,KAAK,GAAG,KAAK,IAAI,GAAG,OAAO;CACjC,MAAM,MACJ,OAAO,GAAG,cAAc,YAAY,GAAG,YACnC,IAAI,GAAG,UAAU,MAAM,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,KACnD;CACN,MAAM,OAAQ,GAAmB,SAAS,cACtC,UAAW,GAAmB,QAAQ,YAAY,KAClD;CACJ,OAAO,IAAI,GAAG,QAAQ,aAAa,GAAG,KAAK,IAAI,GAAG;;AAGpD,SAAgB,gBAAgB,OAAgB,UAA4B;CAC1E,MAAM,uBAAuB,MAAM;CACnC,MAAM,sBAAsB,MAAM;CAClC,IAAI,WAAW;CAIf,MAAM,UAAU,WAAmB,QAAc,MAAY,UAAuB;EAClF,QAAQ,KACN,qCAAqC,UAAU,MAAM,aAAa,OAAO,CAAC,QAAQ,aAAa,KAAK,MACjG,QAAQ,WAAW,aAAa,MAAM,CAAC,wBAAwB,yBAClE;GAAE;GAAQ;GAAM;GAAO,wBAAO,IAAI,MAAM,kBAAkB,EAAC;GAAO,CACnE;EACD,IAAI,UAAU;EACd,WAAW;EACX,UAAU;;CAGZ,MAAM,eAAe,SAAsC,MAAS,OAAuB;EACzF,IAAI,SAAS,MAAM,eAAe,MAAM;GACtC,OAAO,gBAAgB,MAAM,MAAM,MAAM;GACzC,OAAO,qBAAqB,KAAK,MAAM,MAAM,KAAK;;EAEpD,OAAO,qBAAqB,KAAK,MAAM,MAAM,MAAM;;CAGrD,MAAM,cAAc,SAAsC,OAAa;EACrE,IAAI,MAAM,eAAe,MAAM;GAC7B,OAAO,eAAe,MAAM,OAAO,KAAK;GACxC,OAAO;;EAET,OAAO,oBAAoB,KAAK,MAAM,MAAM"}
@@ -1,5 +1,11 @@
1
1
  //#region src/vite-plugin-upstart-editor/runtime/error-handler.d.ts
2
+ /**
3
+ * Full-screen notice inside the preview asking the user to refresh, shown when the page
4
+ * could not be kept alive (React crashed into the site's error boundary). Reloading the
5
+ * iframe re-syncs the DOM with the source and the editor's registry.
6
+ */
7
+ declare function showRecoveryOverlay(): void;
2
8
  declare function initErrorHandler(): void;
3
9
  //#endregion
4
- export { initErrorHandler };
10
+ export { initErrorHandler, showRecoveryOverlay };
5
11
  //# sourceMappingURL=error-handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"mappings":";iBAEgB,gBAAA,CAAA"}
1
+ {"version":3,"file":"error-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"mappings":";;AAcA;;;;iBAAgB,mBAAA,CAAA;AAAA,iBA8BA,gBAAA,CAAA"}
@@ -1,15 +1,55 @@
1
1
  import { sendToParent } from "./utils.js";
2
+ import { getLabels } from "./labels.js";
2
3
  //#region src/vite-plugin-upstart-editor/runtime/error-handler.ts
4
+ /** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */
5
+ const RECONCILIATION_ERROR_RE = /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;
6
+ const OVERLAY_ID = "upstart-preview-recovery";
7
+ /**
8
+ * Full-screen notice inside the preview asking the user to refresh, shown when the page
9
+ * could not be kept alive (React crashed into the site's error boundary). Reloading the
10
+ * iframe re-syncs the DOM with the source and the editor's registry.
11
+ */
12
+ function showRecoveryOverlay() {
13
+ if (document.getElementById(OVERLAY_ID)) return;
14
+ const overlay = document.createElement("div");
15
+ overlay.id = OVERLAY_ID;
16
+ overlay.setAttribute("role", "alertdialog");
17
+ overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; background: rgba(17, 17, 27, 0.72); font-family: system-ui, sans-serif; color: #111;";
18
+ const card = document.createElement("div");
19
+ card.style.cssText = "max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;";
20
+ const title = document.createElement("p");
21
+ title.textContent = getLabels().recoveryTitle;
22
+ title.style.cssText = "margin: 0 0 0.5rem; font-size: 1.1rem; font-weight: 600;";
23
+ const text = document.createElement("p");
24
+ text.textContent = getLabels().recoveryText;
25
+ text.style.cssText = "margin: 0 0 1.25rem; font-size: 0.9rem; color: #555;";
26
+ const button = document.createElement("button");
27
+ button.type = "button";
28
+ button.textContent = getLabels().refresh;
29
+ button.style.cssText = "padding: 0.6rem 1.4rem; border: 0; border-radius: 9999px; background: #7270c6; color: #fff; font-weight: 600; font-size: 0.95rem; cursor: pointer;";
30
+ button.addEventListener("click", () => window.location.reload());
31
+ card.append(title, text, button);
32
+ overlay.append(card);
33
+ (document.body ?? document.documentElement).append(overlay);
34
+ }
3
35
  function initErrorHandler() {
4
36
  window.addEventListener("error", (event) => {
5
37
  console.error("[Upstart Editor] Uncaught error in editor:", event.error);
6
38
  sendToParent({
7
39
  type: "editor-error",
8
- error: event.error
40
+ error: String(event.error?.message ?? event.message ?? event.error)
9
41
  });
42
+ const message = String(event.error?.message ?? event.message ?? "");
43
+ if (RECONCILIATION_ERROR_RE.test(message)) {
44
+ sendToParent({
45
+ type: "editor-desync",
46
+ fatal: true
47
+ });
48
+ showRecoveryOverlay();
49
+ }
10
50
  });
11
51
  }
12
52
  //#endregion
13
- export { initErrorHandler };
53
+ export { initErrorHandler, showRecoveryOverlay };
14
54
 
15
55
  //# sourceMappingURL=error-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"sourcesContent":["import { sendToParent } from \"./utils.js\";\n\nexport function initErrorHandler() {\n // Global error handler for uncaught errors in the editor\n window.addEventListener(\"error\", (event) => {\n console.error(\"[Upstart Editor] Uncaught error in editor:\", event.error);\n sendToParent({\n type: \"editor-error\",\n error: event.error,\n });\n });\n}\n"],"mappings":";;AAEA,SAAgB,mBAAmB;CAEjC,OAAO,iBAAiB,UAAU,UAAU;EAC1C,QAAQ,MAAM,8CAA8C,MAAM,MAAM;EACxE,aAAa;GACX,MAAM;GACN,OAAO,MAAM;GACd,CAAC;GACF"}
1
+ {"version":3,"file":"error-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/error-handler.ts"],"sourcesContent":["import { sendToParent } from \"./utils.js\";\nimport { getLabels } from \"./labels.js\";\n\n/** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */\nconst RECONCILIATION_ERROR_RE =\n /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;\n\nconst OVERLAY_ID = \"upstart-preview-recovery\";\n\n/**\n * Full-screen notice inside the preview asking the user to refresh, shown when the page\n * could not be kept alive (React crashed into the site's error boundary). Reloading the\n * iframe re-syncs the DOM with the source and the editor's registry.\n */\nexport function showRecoveryOverlay(): void {\n if (document.getElementById(OVERLAY_ID)) return;\n const overlay = document.createElement(\"div\");\n overlay.id = OVERLAY_ID;\n overlay.setAttribute(\"role\", \"alertdialog\");\n overlay.style.cssText =\n \"position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; \" +\n \"background: rgba(17, 17, 27, 0.72); font-family: system-ui, sans-serif; color: #111;\";\n const card = document.createElement(\"div\");\n card.style.cssText =\n \"max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; \" +\n \"box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;\";\n const title = document.createElement(\"p\");\n title.textContent = getLabels().recoveryTitle;\n title.style.cssText = \"margin: 0 0 0.5rem; font-size: 1.1rem; font-weight: 600;\";\n const text = document.createElement(\"p\");\n text.textContent = getLabels().recoveryText;\n text.style.cssText = \"margin: 0 0 1.25rem; font-size: 0.9rem; color: #555;\";\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.textContent = getLabels().refresh;\n button.style.cssText =\n \"padding: 0.6rem 1.4rem; border: 0; border-radius: 9999px; background: #7270c6; color: #fff; \" +\n \"font-weight: 600; font-size: 0.95rem; cursor: pointer;\";\n button.addEventListener(\"click\", () => window.location.reload());\n card.append(title, text, button);\n overlay.append(card);\n (document.body ?? document.documentElement).append(overlay);\n}\n\nexport function initErrorHandler() {\n // Global error handler for uncaught errors in the editor\n window.addEventListener(\"error\", (event) => {\n console.error(\"[Upstart Editor] Uncaught error in editor:\", event.error);\n sendToParent({\n type: \"editor-error\",\n error: String(event.error?.message ?? event.message ?? event.error),\n });\n const message = String(event.error?.message ?? event.message ?? \"\");\n if (RECONCILIATION_ERROR_RE.test(message)) {\n sendToParent({ type: \"editor-desync\", fatal: true });\n showRecoveryOverlay();\n }\n });\n}\n"],"mappings":";;;;AAIA,MAAM,0BACJ;AAEF,MAAM,aAAa;;;;;;AAOnB,SAAgB,sBAA4B;CAC1C,IAAI,SAAS,eAAe,WAAW,EAAE;CACzC,MAAM,UAAU,SAAS,cAAc,MAAM;CAC7C,QAAQ,KAAK;CACb,QAAQ,aAAa,QAAQ,cAAc;CAC3C,QAAQ,MAAM,UACZ;CAEF,MAAM,OAAO,SAAS,cAAc,MAAM;CAC1C,KAAK,MAAM,UACT;CAEF,MAAM,QAAQ,SAAS,cAAc,IAAI;CACzC,MAAM,cAAc,WAAW,CAAC;CAChC,MAAM,MAAM,UAAU;CACtB,MAAM,OAAO,SAAS,cAAc,IAAI;CACxC,KAAK,cAAc,WAAW,CAAC;CAC/B,KAAK,MAAM,UAAU;CACrB,MAAM,SAAS,SAAS,cAAc,SAAS;CAC/C,OAAO,OAAO;CACd,OAAO,cAAc,WAAW,CAAC;CACjC,OAAO,MAAM,UACX;CAEF,OAAO,iBAAiB,eAAe,OAAO,SAAS,QAAQ,CAAC;CAChE,KAAK,OAAO,OAAO,MAAM,OAAO;CAChC,QAAQ,OAAO,KAAK;CACpB,CAAC,SAAS,QAAQ,SAAS,iBAAiB,OAAO,QAAQ;;AAG7D,SAAgB,mBAAmB;CAEjC,OAAO,iBAAiB,UAAU,UAAU;EAC1C,QAAQ,MAAM,8CAA8C,MAAM,MAAM;EACxE,aAAa;GACX,MAAM;GACN,OAAO,OAAO,MAAM,OAAO,WAAW,MAAM,WAAW,MAAM,MAAM;GACpE,CAAC;EACF,MAAM,UAAU,OAAO,MAAM,OAAO,WAAW,MAAM,WAAW,GAAG;EACnE,IAAI,wBAAwB,KAAK,QAAQ,EAAE;GACzC,aAAa;IAAE,MAAM;IAAiB,OAAO;IAAM,CAAC;GACpD,qBAAqB;;GAEvB"}
@@ -1,7 +1,7 @@
1
+ import { getLabels } from "./labels.js";
1
2
  //#region src/vite-plugin-upstart-editor/runtime/form-guard.ts
2
3
  let isInitialized = false;
3
4
  const SAFE_METHODS = new Set(["get", "dialog"]);
4
- const TOOLTIP_MESSAGE = "This action isn’t available in the editor preview";
5
5
  const TOOLTIP_DURATION_MS = 2500;
6
6
  const SUPPORTS_POPOVER = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
7
7
  let tooltip = null;
@@ -64,7 +64,7 @@ function ensureTooltip() {
64
64
  if (tooltip) return tooltip;
65
65
  tooltip = document.createElement("div");
66
66
  tooltip.id = "upstart-form-guard-tooltip";
67
- tooltip.textContent = TOOLTIP_MESSAGE;
67
+ tooltip.textContent = getLabels().formGuard;
68
68
  let css = "background: #111827; color: #fff; font-size: 12px; line-height: 1.3; padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; transform: translate(-50%, -100%); z-index: 2147483647;";
69
69
  if (SUPPORTS_POPOVER) {
70
70
  tooltip.setAttribute("popover", "manual");
@@ -1 +1 @@
1
- {"version":3,"file":"form-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/form-guard.ts"],"sourcesContent":["let isInitialized = false;\n\n// Methods that don't mutate data and must keep working: GET forms (search /\n// navigation) and method=\"dialog\" (e.g. a modal's close/backdrop buttons).\nconst SAFE_METHODS = new Set([\"get\", \"dialog\"]);\n\nconst TOOLTIP_MESSAGE = \"This action isn’t available in the editor preview\";\nconst TOOLTIP_DURATION_MS = 2500;\n\n// Popover API renders in the browser top layer, so the tooltip sits above modal\n// <dialog>s opened with showModal() (which also live in the top layer and beat any\n// z-index). Fall back to absolute positioning where it isn't supported.\nconst SUPPORTS_POPOVER = typeof HTMLElement !== \"undefined\" && \"showPopover\" in HTMLElement.prototype;\n\nlet tooltip: HTMLDivElement | null = null;\nlet tooltipTimer: number | null = null;\n\n/**\n * Block mutating form submissions inside the previewed site so the user can't\n * trigger data changes (which only hit the ephemeral preview DB and never persist)\n * from the editor. This matters most in PREVIEW mode, where the site's buttons are\n * live — in EDIT mode a click edits the button's text instead of submitting.\n *\n * We block the `submit` event rather than disabling buttons: one place covers native\n * POST forms, React Router <Form>, button clicks and Enter-key submits. The\n * capture-phase listener + stopImmediatePropagation runs before React's delegated\n * handler, so React Router never processes the submission; preventDefault blocks a\n * plain form's POST. GET and dialog submissions are left untouched. When a mutating\n * submission is blocked, a tooltip on the triggering button explains why.\n */\nexport function initFormGuard(): void {\n if (typeof document === \"undefined\" || isInitialized) {\n return;\n }\n isInitialized = true;\n\n document.addEventListener(\n \"submit\",\n (event: SubmitEvent) => {\n const form = event.target as HTMLFormElement | null;\n const method = (\n event.submitter?.getAttribute(\"formmethod\") ||\n form?.getAttribute(\"method\") ||\n \"get\"\n ).toLowerCase();\n\n if (SAFE_METHODS.has(method)) {\n return;\n }\n\n event.preventDefault();\n event.stopImmediatePropagation();\n console.debug(\"[Upstart Editor] Blocked a mutating form submission in the editor:\", method);\n\n const anchor = (event.submitter as HTMLElement | null) ?? form;\n if (anchor) showBlockedTooltip(anchor);\n },\n true,\n );\n}\n\nfunction showBlockedTooltip(anchor: HTMLElement): void {\n const tip = ensureTooltip();\n const rect = anchor.getBoundingClientRect();\n\n if (SUPPORTS_POPOVER) {\n // Popover is position:fixed → viewport coordinates (no scroll offset).\n tip.style.left = `${rect.left + rect.width / 2}px`;\n tip.style.top = `${rect.top - 8}px`;\n try {\n tip.hidePopover();\n } catch {}\n try {\n tip.showPopover();\n } catch {}\n } else {\n tip.style.left = `${rect.left + window.scrollX + rect.width / 2}px`;\n tip.style.top = `${rect.top + window.scrollY - 8}px`;\n tip.style.opacity = \"1\";\n }\n\n if (tooltipTimer !== null) {\n clearTimeout(tooltipTimer);\n }\n tooltipTimer = window.setTimeout(() => {\n tooltipTimer = null;\n if (!tooltip) return;\n if (SUPPORTS_POPOVER) {\n try {\n tooltip.hidePopover();\n } catch {}\n } else {\n tooltip.style.opacity = \"0\";\n }\n }, TOOLTIP_DURATION_MS);\n}\n\nfunction ensureTooltip(): HTMLDivElement {\n if (tooltip) return tooltip;\n tooltip = document.createElement(\"div\");\n tooltip.id = \"upstart-form-guard-tooltip\";\n tooltip.textContent = TOOLTIP_MESSAGE;\n\n let css =\n \"background: #111827; color: #fff; font-size: 12px; line-height: 1.3; \" +\n \"padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; \" +\n \"box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; \" +\n \"transform: translate(-50%, -100%); z-index: 2147483647;\";\n\n if (SUPPORTS_POPOVER) {\n tooltip.setAttribute(\"popover\", \"manual\");\n // Neutralise the UA popover centering so our top/left apply.\n css += \"position: fixed; margin: 0; inset: auto; overflow: visible; border: 0;\";\n } else {\n css += \"position: absolute; opacity: 0; transition: opacity 0.15s ease;\";\n }\n\n tooltip.style.cssText = css;\n document.body.appendChild(tooltip);\n return tooltip;\n}\n"],"mappings":";AAAA,IAAI,gBAAgB;AAIpB,MAAM,eAAe,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AAE/C,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;AAK5B,MAAM,mBAAmB,OAAO,gBAAgB,eAAe,iBAAiB,YAAY;AAE5F,IAAI,UAAiC;AACrC,IAAI,eAA8B;;;;;;;;;;;;;;AAelC,SAAgB,gBAAsB;CACpC,IAAI,OAAO,aAAa,eAAe,eACrC;CAEF,gBAAgB;CAEhB,SAAS,iBACP,WACC,UAAuB;EACtB,MAAM,OAAO,MAAM;EACnB,MAAM,UACJ,MAAM,WAAW,aAAa,aAAa,IAC3C,MAAM,aAAa,SAAS,IAC5B,OACA,aAAa;EAEf,IAAI,aAAa,IAAI,OAAO,EAC1B;EAGF,MAAM,gBAAgB;EACtB,MAAM,0BAA0B;EAChC,QAAQ,MAAM,sEAAsE,OAAO;EAE3F,MAAM,SAAU,MAAM,aAAoC;EAC1D,IAAI,QAAQ,mBAAmB,OAAO;IAExC,KACD;;AAGH,SAAS,mBAAmB,QAA2B;CACrD,MAAM,MAAM,eAAe;CAC3B,MAAM,OAAO,OAAO,uBAAuB;CAE3C,IAAI,kBAAkB;EAEpB,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,KAAK,QAAQ,EAAE;EAC/C,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;EAChC,IAAI;GACF,IAAI,aAAa;UACX;EACR,IAAI;GACF,IAAI,aAAa;UACX;QACH;EACL,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,OAAO,UAAU,KAAK,QAAQ,EAAE;EAChE,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,OAAO,UAAU,EAAE;EACjD,IAAI,MAAM,UAAU;;CAGtB,IAAI,iBAAiB,MACnB,aAAa,aAAa;CAE5B,eAAe,OAAO,iBAAiB;EACrC,eAAe;EACf,IAAI,CAAC,SAAS;EACd,IAAI,kBACF,IAAI;GACF,QAAQ,aAAa;UACf;OAER,QAAQ,MAAM,UAAU;IAEzB,oBAAoB;;AAGzB,SAAS,gBAAgC;CACvC,IAAI,SAAS,OAAO;CACpB,UAAU,SAAS,cAAc,MAAM;CACvC,QAAQ,KAAK;CACb,QAAQ,cAAc;CAEtB,IAAI,MACF;CAKF,IAAI,kBAAkB;EACpB,QAAQ,aAAa,WAAW,SAAS;EAEzC,OAAO;QAEP,OAAO;CAGT,QAAQ,MAAM,UAAU;CACxB,SAAS,KAAK,YAAY,QAAQ;CAClC,OAAO"}
1
+ {"version":3,"file":"form-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/form-guard.ts"],"sourcesContent":["import { getLabels } from \"./labels.js\";\n\nlet isInitialized = false;\n\n// Methods that don't mutate data and must keep working: GET forms (search /\n// navigation) and method=\"dialog\" (e.g. a modal's close/backdrop buttons).\nconst SAFE_METHODS = new Set([\"get\", \"dialog\"]);\n\nconst TOOLTIP_DURATION_MS = 2500;\n\n// Popover API renders in the browser top layer, so the tooltip sits above modal\n// <dialog>s opened with showModal() (which also live in the top layer and beat any\n// z-index). Fall back to absolute positioning where it isn't supported.\nconst SUPPORTS_POPOVER = typeof HTMLElement !== \"undefined\" && \"showPopover\" in HTMLElement.prototype;\n\nlet tooltip: HTMLDivElement | null = null;\nlet tooltipTimer: number | null = null;\n\n/**\n * Block mutating form submissions inside the previewed site so the user can't\n * trigger data changes (which only hit the ephemeral preview DB and never persist)\n * from the editor. This matters most in PREVIEW mode, where the site's buttons are\n * live — in EDIT mode a click edits the button's text instead of submitting.\n *\n * We block the `submit` event rather than disabling buttons: one place covers native\n * POST forms, React Router <Form>, button clicks and Enter-key submits. The\n * capture-phase listener + stopImmediatePropagation runs before React's delegated\n * handler, so React Router never processes the submission; preventDefault blocks a\n * plain form's POST. GET and dialog submissions are left untouched. When a mutating\n * submission is blocked, a tooltip on the triggering button explains why.\n */\nexport function initFormGuard(): void {\n if (typeof document === \"undefined\" || isInitialized) {\n return;\n }\n isInitialized = true;\n\n document.addEventListener(\n \"submit\",\n (event: SubmitEvent) => {\n const form = event.target as HTMLFormElement | null;\n const method = (\n event.submitter?.getAttribute(\"formmethod\") ||\n form?.getAttribute(\"method\") ||\n \"get\"\n ).toLowerCase();\n\n if (SAFE_METHODS.has(method)) {\n return;\n }\n\n event.preventDefault();\n event.stopImmediatePropagation();\n console.debug(\"[Upstart Editor] Blocked a mutating form submission in the editor:\", method);\n\n const anchor = (event.submitter as HTMLElement | null) ?? form;\n if (anchor) showBlockedTooltip(anchor);\n },\n true,\n );\n}\n\nfunction showBlockedTooltip(anchor: HTMLElement): void {\n const tip = ensureTooltip();\n const rect = anchor.getBoundingClientRect();\n\n if (SUPPORTS_POPOVER) {\n // Popover is position:fixed → viewport coordinates (no scroll offset).\n tip.style.left = `${rect.left + rect.width / 2}px`;\n tip.style.top = `${rect.top - 8}px`;\n try {\n tip.hidePopover();\n } catch {}\n try {\n tip.showPopover();\n } catch {}\n } else {\n tip.style.left = `${rect.left + window.scrollX + rect.width / 2}px`;\n tip.style.top = `${rect.top + window.scrollY - 8}px`;\n tip.style.opacity = \"1\";\n }\n\n if (tooltipTimer !== null) {\n clearTimeout(tooltipTimer);\n }\n tooltipTimer = window.setTimeout(() => {\n tooltipTimer = null;\n if (!tooltip) return;\n if (SUPPORTS_POPOVER) {\n try {\n tooltip.hidePopover();\n } catch {}\n } else {\n tooltip.style.opacity = \"0\";\n }\n }, TOOLTIP_DURATION_MS);\n}\n\nfunction ensureTooltip(): HTMLDivElement {\n if (tooltip) return tooltip;\n tooltip = document.createElement(\"div\");\n tooltip.id = \"upstart-form-guard-tooltip\";\n tooltip.textContent = getLabels().formGuard;\n\n let css =\n \"background: #111827; color: #fff; font-size: 12px; line-height: 1.3; \" +\n \"padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; \" +\n \"box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; \" +\n \"transform: translate(-50%, -100%); z-index: 2147483647;\";\n\n if (SUPPORTS_POPOVER) {\n tooltip.setAttribute(\"popover\", \"manual\");\n // Neutralise the UA popover centering so our top/left apply.\n css += \"position: fixed; margin: 0; inset: auto; overflow: visible; border: 0;\";\n } else {\n css += \"position: absolute; opacity: 0; transition: opacity 0.15s ease;\";\n }\n\n tooltip.style.cssText = css;\n document.body.appendChild(tooltip);\n return tooltip;\n}\n"],"mappings":";;AAEA,IAAI,gBAAgB;AAIpB,MAAM,eAAe,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AAE/C,MAAM,sBAAsB;AAK5B,MAAM,mBAAmB,OAAO,gBAAgB,eAAe,iBAAiB,YAAY;AAE5F,IAAI,UAAiC;AACrC,IAAI,eAA8B;;;;;;;;;;;;;;AAelC,SAAgB,gBAAsB;CACpC,IAAI,OAAO,aAAa,eAAe,eACrC;CAEF,gBAAgB;CAEhB,SAAS,iBACP,WACC,UAAuB;EACtB,MAAM,OAAO,MAAM;EACnB,MAAM,UACJ,MAAM,WAAW,aAAa,aAAa,IAC3C,MAAM,aAAa,SAAS,IAC5B,OACA,aAAa;EAEf,IAAI,aAAa,IAAI,OAAO,EAC1B;EAGF,MAAM,gBAAgB;EACtB,MAAM,0BAA0B;EAChC,QAAQ,MAAM,sEAAsE,OAAO;EAE3F,MAAM,SAAU,MAAM,aAAoC;EAC1D,IAAI,QAAQ,mBAAmB,OAAO;IAExC,KACD;;AAGH,SAAS,mBAAmB,QAA2B;CACrD,MAAM,MAAM,eAAe;CAC3B,MAAM,OAAO,OAAO,uBAAuB;CAE3C,IAAI,kBAAkB;EAEpB,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,KAAK,QAAQ,EAAE;EAC/C,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;EAChC,IAAI;GACF,IAAI,aAAa;UACX;EACR,IAAI;GACF,IAAI,aAAa;UACX;QACH;EACL,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,OAAO,UAAU,KAAK,QAAQ,EAAE;EAChE,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,OAAO,UAAU,EAAE;EACjD,IAAI,MAAM,UAAU;;CAGtB,IAAI,iBAAiB,MACnB,aAAa,aAAa;CAE5B,eAAe,OAAO,iBAAiB;EACrC,eAAe;EACf,IAAI,CAAC,SAAS;EACd,IAAI,kBACF,IAAI;GACF,QAAQ,aAAa;UACf;OAER,QAAQ,MAAM,UAAU;IAEzB,oBAAoB;;AAGzB,SAAS,gBAAgC;CACvC,IAAI,SAAS,OAAO;CACpB,UAAU,SAAS,cAAc,MAAM;CACvC,QAAQ,KAAK;CACb,QAAQ,cAAc,WAAW,CAAC;CAElC,IAAI,MACF;CAKF,IAAI,kBAAkB;EACpB,QAAQ,aAAa,WAAW,SAAS;EAEzC,OAAO;QAEP,OAAO;CAGT,QAAQ,MAAM,UAAU;CACxB,SAAS,KAAK,YAAY,QAAQ;CAClC,OAAO"}
@@ -0,0 +1,32 @@
1
+ //#region src/vite-plugin-upstart-editor/runtime/image-pick.ts
2
+ function rectContainsPoint(rect, point) {
3
+ return point.x >= rect.left && point.x <= rect.left + rect.width && point.y >= rect.top && point.y <= rect.top + rect.height;
4
+ }
5
+ /**
6
+ * Images to surface in the inspector for a click. Pure so it can be unit-tested without a DOM.
7
+ *
8
+ * - `own` are the editable images inside the clicked element (the element itself included):
9
+ * they belong to what was clicked, so they are all candidates. When the click lands on one
10
+ * of them, only that one is kept, so a card with several images doesn't open them all when
11
+ * the user clearly targeted one.
12
+ * - `ancestorLevels` are the images of each successive ancestor (nearest first), used only for
13
+ * the "hero covered by an overlay" case: the click is on a sibling overlay, but visually on
14
+ * the image. Only images under the click point qualify, otherwise clicking a navbar title
15
+ * would surface the logo, and clicking any element would surface every image on the page.
16
+ */
17
+ function pickImagesForClick(own, ancestorLevels, point) {
18
+ if (own.length > 0) {
19
+ const underPoint = point ? own.filter((img) => rectContainsPoint(img.rect, point)) : [];
20
+ return underPoint.length > 0 ? underPoint : own;
21
+ }
22
+ if (!point) return [];
23
+ for (const level of ancestorLevels) {
24
+ const underPoint = level.filter((img) => rectContainsPoint(img.rect, point));
25
+ if (underPoint.length > 0) return underPoint;
26
+ }
27
+ return [];
28
+ }
29
+ //#endregion
30
+ export { pickImagesForClick };
31
+
32
+ //# sourceMappingURL=image-pick.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-pick.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/image-pick.ts"],"sourcesContent":["export type Point = { x: number; y: number };\nexport type Rect = { top: number; left: number; width: number; height: number };\n\nexport function rectContainsPoint(rect: Rect, point: Point): boolean {\n return (\n point.x >= rect.left &&\n point.x <= rect.left + rect.width &&\n point.y >= rect.top &&\n point.y <= rect.top + rect.height\n );\n}\n\n/**\n * Images to surface in the inspector for a click. Pure so it can be unit-tested without a DOM.\n *\n * - `own` are the editable images inside the clicked element (the element itself included):\n * they belong to what was clicked, so they are all candidates. When the click lands on one\n * of them, only that one is kept, so a card with several images doesn't open them all when\n * the user clearly targeted one.\n * - `ancestorLevels` are the images of each successive ancestor (nearest first), used only for\n * the \"hero covered by an overlay\" case: the click is on a sibling overlay, but visually on\n * the image. Only images under the click point qualify, otherwise clicking a navbar title\n * would surface the logo, and clicking any element would surface every image on the page.\n */\nexport function pickImagesForClick<T extends { rect: Rect }>(\n own: T[],\n ancestorLevels: T[][],\n point: Point | undefined,\n): T[] {\n if (own.length > 0) {\n const underPoint = point ? own.filter((img) => rectContainsPoint(img.rect, point)) : [];\n return underPoint.length > 0 ? underPoint : own;\n }\n if (!point) return [];\n for (const level of ancestorLevels) {\n const underPoint = level.filter((img) => rectContainsPoint(img.rect, point));\n if (underPoint.length > 0) return underPoint;\n }\n return [];\n}\n"],"mappings":";AAGA,SAAgB,kBAAkB,MAAY,OAAuB;CACnE,OACE,MAAM,KAAK,KAAK,QAChB,MAAM,KAAK,KAAK,OAAO,KAAK,SAC5B,MAAM,KAAK,KAAK,OAChB,MAAM,KAAK,KAAK,MAAM,KAAK;;;;;;;;;;;;;;AAgB/B,SAAgB,mBACd,KACA,gBACA,OACK;CACL,IAAI,IAAI,SAAS,GAAG;EAClB,MAAM,aAAa,QAAQ,IAAI,QAAQ,QAAQ,kBAAkB,IAAI,MAAM,MAAM,CAAC,GAAG,EAAE;EACvF,OAAO,WAAW,SAAS,IAAI,aAAa;;CAE9C,IAAI,CAAC,OAAO,OAAO,EAAE;CACrB,KAAK,MAAM,SAAS,gBAAgB;EAClC,MAAM,aAAa,MAAM,QAAQ,QAAQ,kBAAkB,IAAI,MAAM,MAAM,CAAC;EAC5E,IAAI,WAAW,SAAS,GAAG,OAAO;;CAEpC,OAAO,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;iBAiBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAuFgB,iBAAA,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"mappings":";;;;;;;;;;;;iBAmBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAuFgB,iBAAA,CAAA"}