@upstart.gg/vite-plugins 0.1.68 → 0.1.69

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 (39) 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/click-handler.d.ts.map +1 -1
  9. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +30 -16
  10. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  11. package/dist/vite-plugin-upstart-editor/runtime/dom-guard.js +45 -0
  12. package/dist/vite-plugin-upstart-editor/runtime/dom-guard.js.map +1 -0
  13. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts +7 -1
  14. package/dist/vite-plugin-upstart-editor/runtime/error-handler.d.ts.map +1 -1
  15. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js +41 -2
  16. package/dist/vite-plugin-upstart-editor/runtime/error-handler.js.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/image-pick.js +32 -0
  18. package/dist/vite-plugin-upstart-editor/runtime/image-pick.js.map +1 -0
  19. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js +7 -0
  21. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +6 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +9 -0
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/src/tests/dom-guard.test.ts +100 -0
  28. package/src/tests/image-pick.test.ts +50 -0
  29. package/src/tests/upstart-editor-api.test.ts +75 -0
  30. package/src/tests/vite-plugin-upstart-attrs.test.ts +21 -0
  31. package/src/upstart-editor-api.ts +74 -8
  32. package/src/vite-plugin-upstart-attrs.ts +44 -1
  33. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +29 -19
  34. package/src/vite-plugin-upstart-editor/runtime/dom-guard.ts +67 -0
  35. package/src/vite-plugin-upstart-editor/runtime/error-handler.ts +47 -1
  36. package/src/vite-plugin-upstart-editor/runtime/image-pick.ts +40 -0
  37. package/src/vite-plugin-upstart-editor/runtime/index.ts +8 -0
  38. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +14 -0
  39. package/src/vite-plugin-upstart-editor/runtime/types.ts +6 -0
@@ -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":";;AAaA;;;;iBAAgB,mBAAA,CAAA;AAAA,iBA8BA,gBAAA,CAAA"}
@@ -1,15 +1,54 @@
1
1
  import { sendToParent } from "./utils.js";
2
2
  //#region src/vite-plugin-upstart-editor/runtime/error-handler.ts
3
+ /** Errors thrown by React's DOM reconciliation once the editor mutated its nodes. */
4
+ const RECONCILIATION_ERROR_RE = /insertBefore|removeChild|appendChild|Minified React error #(?:418|422|423|425)/;
5
+ const OVERLAY_ID = "upstart-preview-recovery";
6
+ /**
7
+ * Full-screen notice inside the preview asking the user to refresh, shown when the page
8
+ * could not be kept alive (React crashed into the site's error boundary). Reloading the
9
+ * iframe re-syncs the DOM with the source and the editor's registry.
10
+ */
11
+ function showRecoveryOverlay() {
12
+ if (document.getElementById(OVERLAY_ID)) return;
13
+ const overlay = document.createElement("div");
14
+ overlay.id = OVERLAY_ID;
15
+ overlay.setAttribute("role", "alertdialog");
16
+ 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;";
17
+ const card = document.createElement("div");
18
+ card.style.cssText = "max-width: 24rem; margin: 1rem; padding: 1.5rem; border-radius: 0.75rem; background: #fff; box-shadow: 0 20px 50px rgba(0,0,0,0.3); text-align: center;";
19
+ const title = document.createElement("p");
20
+ title.textContent = "The preview needs a refresh";
21
+ title.style.cssText = "margin: 0 0 0.5rem; font-size: 1.1rem; font-weight: 600;";
22
+ const text = document.createElement("p");
23
+ text.textContent = "Your changes are saved. Refresh the preview to keep editing.";
24
+ text.style.cssText = "margin: 0 0 1.25rem; font-size: 0.9rem; color: #555;";
25
+ const button = document.createElement("button");
26
+ button.type = "button";
27
+ button.textContent = "Refresh";
28
+ button.style.cssText = "padding: 0.6rem 1.4rem; border: 0; border-radius: 9999px; background: #7270c6; color: #fff; font-weight: 600; font-size: 0.95rem; cursor: pointer;";
29
+ button.addEventListener("click", () => window.location.reload());
30
+ card.append(title, text, button);
31
+ overlay.append(card);
32
+ (document.body ?? document.documentElement).append(overlay);
33
+ }
3
34
  function initErrorHandler() {
4
35
  window.addEventListener("error", (event) => {
5
36
  console.error("[Upstart Editor] Uncaught error in editor:", event.error);
6
37
  sendToParent({
7
38
  type: "editor-error",
8
- error: event.error
39
+ error: String(event.error?.message ?? event.message ?? event.error)
9
40
  });
41
+ const message = String(event.error?.message ?? event.message ?? "");
42
+ if (RECONCILIATION_ERROR_RE.test(message)) {
43
+ sendToParent({
44
+ type: "editor-desync",
45
+ fatal: true
46
+ });
47
+ showRecoveryOverlay();
48
+ }
10
49
  });
11
50
  }
12
51
  //#endregion
13
- export { initErrorHandler };
52
+ export { initErrorHandler, showRecoveryOverlay };
14
53
 
15
54
  //# 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\";\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 = \"The preview needs a refresh\";\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 = \"Your changes are saved. Refresh the preview to keep editing.\";\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 = \"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":";;;AAGA,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;CACpB,MAAM,MAAM,UAAU;CACtB,MAAM,OAAO,SAAS,cAAc,IAAI;CACxC,KAAK,cAAc;CACnB,KAAK,MAAM,UAAU;CACrB,MAAM,SAAS,SAAS,cAAc,SAAS;CAC/C,OAAO,OAAO;CACd,OAAO,cAAc;CACrB,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"}
@@ -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":";;;;;;;;;;;;iBAkBgB,OAAA,CAAQ,IAAA,EAAM,UAAA;AAAA,iBAed,gBAAA,CAAiB,QAAA;;AAfjC;;iBAuFgB,iBAAA,CAAA"}
@@ -3,6 +3,7 @@ import { sendToParent } from "./utils.js";
3
3
  import { initClickHandler } from "./click-handler.js";
4
4
  import { hideOverlays, initHoverOverlay } from "./hover-overlay.js";
5
5
  import { initErrorHandler } from "./error-handler.js";
6
+ import { installDomGuard } from "./dom-guard.js";
6
7
  import { activateAllEditors, destroyAllActiveEditors, initTextEditor } from "./text-editor.js";
7
8
  import { hideArrayControls, initArrayControls, refreshArrayControls } from "./array-controls.js";
8
9
  import { initFormGuard } from "./form-guard.js";
@@ -120,6 +121,11 @@ function initUpstartEditor() {
120
121
  };
121
122
  const i18next = globalThis.__i18next;
122
123
  const getRawI18nTemplate = i18next ? (namespace, key) => i18next.getResource(i18next.language, namespace, key) : void 0;
124
+ document.documentElement.setAttribute("data-upstart-editor", "");
125
+ installDomGuard(Node.prototype, () => sendToParent({
126
+ type: "editor-desync",
127
+ fatal: false
128
+ }));
123
129
  initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});
124
130
  initClickHandler();
125
131
  initHoverOverlay();
@@ -163,6 +169,7 @@ function handleParentMessage(event) {
163
169
  } else if (message.type === "preview-classname") {
164
170
  const el = document.querySelector(`[data-upstart-classname-id="${message.classNameId}"]`);
165
171
  if (el) el.className = message.className;
172
+ if (message.previewCSS === void 0) return;
166
173
  const STYLE_ID = "upstart-preview-style";
167
174
  let styleEl = document.getElementById(STYLE_ID);
168
175
  if (message.previewCSS) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Report the react-router id of the rendered route to the parent editor, so it can edit\n * that route's `meta` export (page title, description…).\n *\n * The id is read from react-router's own data router rather than from `history.pushState`:\n * the router pushes the new URL BEFORE it commits the new matches, so a route id read at\n * pushState time would still be the previous page's. Subscribing gives us the id once the\n * navigation is complete. The router global only appears during hydration, hence the retry.\n */\nfunction initRouteReporter(): void {\n let lastReported: string | undefined;\n\n const report = () => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const matches = (window as any).__reactRouterDataRouter?.state?.matches;\n const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : undefined;\n if (typeof routeId !== \"string\" || routeId === lastReported) return;\n lastReported = routeId;\n sendToParent({ type: \"editor-route\", routeId });\n };\n\n const attach = (): boolean => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const router = (window as any).__reactRouterDataRouter;\n if (typeof router?.subscribe !== \"function\") return false;\n router.subscribe(() => report());\n report();\n return true;\n };\n\n if (attach()) return;\n let attempts = 0;\n const timer = setInterval(() => {\n if (attach() || ++attempts > 25) clearInterval(timer);\n }, 200);\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n initRouteReporter();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"preview-image-style\") {\n // One <style> per image so previewing an image and its container at the same\n // time never overwrite each other (the className preview uses its own element).\n const styleId = `upstart-preview-image-style-${String(message.imageId).replace(/[^a-zA-Z0-9_-]/g, \"_\")}`;\n let styleEl = document.getElementById(styleId) as HTMLStyleElement | null;\n const css = typeof message.css === \"string\" ? message.css : \"\";\n if (css) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = styleId;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = css;\n } else if (styleEl) {\n styleEl.remove();\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/index.ts"],"sourcesContent":["import { initClickHandler } from \"./click-handler.js\";\nimport { initHoverOverlay, hideOverlays } from \"./hover-overlay.js\";\nimport { initErrorHandler } from \"./error-handler.js\";\nimport { installDomGuard } from \"./dom-guard.js\";\nimport { initTextEditor, activateAllEditors, destroyAllActiveEditors } from \"./text-editor.js\";\nimport { initArrayControls, refreshArrayControls, hideArrayControls } from \"./array-controls.js\";\nimport { initFormGuard } from \"./form-guard.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from \"./state.js\";\nimport type { EditorMode, UpstartParentMessage } from \"./types.js\";\n\nlet isInitialized = false;\n\nexport { getCurrentMode };\n\n/**\n * Set the current editor mode.\n */\nexport function setMode(mode: EditorMode): void {\n setCurrentMode(mode);\n console.log(`[Upstart Editor] Setting mode to: ${mode}`);\n if (mode === \"edit\") {\n disableSelectMode();\n enableEditMode();\n } else if (mode === \"select\") {\n disableEditMode();\n enableSelectMode();\n } else {\n disableEditMode();\n disableSelectMode();\n }\n}\n\nexport function waitForHydration(callback: () => void): void {\n // Remix/React 18 wraps hydrateRoot in startTransition, making hydration a\n // concurrent (low-priority) operation that can span many frames after the\n // load event. Instead of guessing a delay, we wait for the DOM to stabilise:\n // once 200 ms pass without any childList mutations, hydration is done.\n const STABILITY_MS = 200;\n\n const onReady = () => {\n let timer: number | null = null;\n\n const settle = () => {\n if (timer) clearTimeout(timer);\n timer = window.setTimeout(() => {\n observer.disconnect();\n callback();\n }, STABILITY_MS);\n };\n\n const observer = new MutationObserver(settle);\n observer.observe(document.documentElement, { childList: true, subtree: true });\n\n // Kick off the first timer (covers case where no mutations occur after load)\n settle();\n };\n\n if (document.readyState === \"complete\") {\n onReady();\n } else {\n window.addEventListener(\"load\", onReady, { once: true });\n }\n}\n\n/**\n * Report the react-router id of the rendered route to the parent editor, so it can edit\n * that route's `meta` export (page title, description…).\n *\n * The id is read from react-router's own data router rather than from `history.pushState`:\n * the router pushes the new URL BEFORE it commits the new matches, so a route id read at\n * pushState time would still be the previous page's. Subscribing gives us the id once the\n * navigation is complete. The router global only appears during hydration, hence the retry.\n */\nfunction initRouteReporter(): void {\n let lastReported: string | undefined;\n\n const report = () => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const matches = (window as any).__reactRouterDataRouter?.state?.matches;\n const routeId = Array.isArray(matches) ? matches[matches.length - 1]?.route?.id : undefined;\n if (typeof routeId !== \"string\" || routeId === lastReported) return;\n lastReported = routeId;\n sendToParent({ type: \"editor-route\", routeId });\n };\n\n const attach = (): boolean => {\n // biome-ignore lint/suspicious/noExplicitAny: react-router does not type its window globals\n const router = (window as any).__reactRouterDataRouter;\n if (typeof router?.subscribe !== \"function\") return false;\n router.subscribe(() => report());\n report();\n return true;\n };\n\n if (attach()) return;\n let attempts = 0;\n const timer = setInterval(() => {\n if (attach() || ++attempts > 25) clearInterval(timer);\n }, 200);\n}\n\n/**\n * Initialize the Upstart editor runtime.\n */\nexport function initUpstartEditor(): void {\n if (isInitialized) {\n console.log(\"[Upstart Editor] Editor is already initialized\");\n return;\n }\n\n try {\n console.log(\"[Upstart Editor] Initializing...\");\n\n isInitialized = true;\n\n window.addEventListener(\"message\", handleParentMessage);\n\n // Current path (pathname + search), reported to the parent so it can reload\n // the iframe on the page being viewed instead of always the home page.\n const currentPath = () => location.pathname + location.search;\n\n // Notify parent on SPA navigation so it can resend the current editMode\n // and track the current page for the next reload.\n window.addEventListener(\"popstate\", () => {\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\n resetSelection();\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n\n // i18next integration: if the app exposes it on window.__i18next (set before hydration),\n // wire it up so template variables (e.g. {{year}}) render as non-editable atoms.\n const i18next = (globalThis as any).__i18next;\n const getRawI18nTemplate = i18next\n ? (namespace: string, key: string) =>\n i18next.getResource(i18next.language, namespace, key) as string | undefined\n : undefined;\n\n // Marks the document as running inside the editor preview (the site's error boundary\n // uses it to offer a refresh instead of a raw error).\n document.documentElement.setAttribute(\"data-upstart-editor\", \"\");\n installDomGuard(Node.prototype, () => sendToParent({ type: \"editor-desync\", fatal: false }));\n initTextEditor(getRawI18nTemplate ? { getRawI18nTemplate } : {});\n initClickHandler();\n initHoverOverlay();\n initArrayControls();\n initFormGuard();\n initErrorHandler();\n initRouteReporter();\n\n sendToParent({ type: \"editor-ready\", path: currentPath() });\n } catch (error) {\n console.error(\"[Upstart Editor] Initialization failed:\", error);\n sendToParent({\n type: \"editor-error\",\n error: error instanceof Error ? error.message : \"Unknown error\",\n });\n }\n}\n\nconst ALLOWED_ORIGINS = [\"http://localhost:8080\", /upstart.gg$/];\n\nconst matchAllowedOrigins = (origin: string) => {\n return ALLOWED_ORIGINS.some((allowedOrigin) => {\n if (typeof allowedOrigin === \"string\") {\n return origin === allowedOrigin;\n } else if (allowedOrigin instanceof RegExp) {\n return allowedOrigin.test(origin);\n }\n return false;\n });\n};\n\nfunction handleParentMessage(event: MessageEvent): void {\n const message = event.data as UpstartParentMessage | undefined;\n\n console.log(\"[Upstart Editor] Received message from parent:\", { event, message });\n\n if (!message || !matchAllowedOrigins(event.origin)) {\n console.warn(\"[Upstart Editor] Ignoring message from unknown source:\", event.origin);\n return;\n }\n\n if (message.type === \"set-mode\") {\n console.log(\"Setting editor mode to:\", message.mode);\n setMode(message.mode);\n } else if (message.type === \"preview-classname\") {\n const el = document.querySelector<HTMLElement>(`[data-upstart-classname-id=\"${message.classNameId}\"]`);\n if (el) {\n el.className = message.className;\n }\n // No `previewCSS` at all means \"patch the className only\": the editor uses it after a save\n // to sync the DOM with the file without touching the current CSS preview.\n if (message.previewCSS === undefined) return;\n const STYLE_ID = \"upstart-preview-style\";\n let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;\n if (message.previewCSS) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = STYLE_ID;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = message.previewCSS;\n } else if (styleEl) {\n styleEl.textContent = \"\";\n }\n } else if (message.type === \"preview-image\") {\n const el = document.querySelector<HTMLImageElement>(`[data-upstart-image-id=\"${message.imageId}\"]`);\n if (el) {\n el.src = message.src;\n }\n } else if (message.type === \"preview-image-style\") {\n // One <style> per image so previewing an image and its container at the same\n // time never overwrite each other (the className preview uses its own element).\n const styleId = `upstart-preview-image-style-${String(message.imageId).replace(/[^a-zA-Z0-9_-]/g, \"_\")}`;\n let styleEl = document.getElementById(styleId) as HTMLStyleElement | null;\n const css = typeof message.css === \"string\" ? message.css : \"\";\n if (css) {\n if (!styleEl) {\n styleEl = document.createElement(\"style\");\n styleEl.id = styleId;\n document.head.appendChild(styleEl);\n }\n styleEl.textContent = css;\n } else if (styleEl) {\n styleEl.remove();\n }\n } else if (message.type === \"request-scroll-position\") {\n sendToParent({ type: \"scroll-position\", x: window.scrollX, y: window.scrollY });\n } else if (message.type === \"restore-scroll-position\") {\n window.scrollTo({ left: message.x, top: message.y, behavior: \"auto\" });\n } else if (message.type === \"clear-selection\") {\n resetSelection();\n // Echo the now-empty selection so the parent store (chips / counter) updates.\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n } else if (message.type === \"deselect\") {\n const el = removeSelection(message.key);\n el?.removeAttribute(\"data-upstart-selected\");\n sendToParent({ type: \"selection-changed\", selection: getSelection() });\n }\n}\n\n/**\n * Drop the whole \"select\" mode selection: clear the in-memory set and remove the\n * visual marker from any element still carrying it.\n */\nfunction resetSelection(): void {\n clearSelection();\n for (const el of document.querySelectorAll(\"[data-upstart-selected]\")) {\n el.removeAttribute(\"data-upstart-selected\");\n }\n}\n\nfunction enableEditMode(): void {\n console.log(\"[Upstart Editor] Edit mode enabled\");\n document.documentElement.setAttribute(\"data-upstart-edit-mode\", \"\");\n activateAllEditors();\n refreshArrayControls();\n}\n\nfunction disableEditMode(): void {\n console.log(\"[Upstart Editor] Preview mode enabled\");\n document.documentElement.removeAttribute(\"data-upstart-edit-mode\");\n destroyAllActiveEditors();\n hideOverlays();\n hideArrayControls();\n}\n\nfunction enableSelectMode(): void {\n console.log(\"[Upstart Editor] Select mode enabled\");\n // Selectable hover (hover-overlay) and the persistent selected outline are\n // gated on this attribute. The selection Map / data-upstart-selected attrs are\n // kept across mode switches, so toggling back to select restores the outlines.\n document.documentElement.setAttribute(\"data-upstart-select-mode\", \"\");\n}\n\nfunction disableSelectMode(): void {\n document.documentElement.removeAttribute(\"data-upstart-select-mode\");\n hideOverlays();\n}\n\nexport { initTextEditor } from \"./text-editor.js\";\nexport { initClickHandler } from \"./click-handler.js\";\nexport { initErrorHandler } from \"./error-handler.js\";\nexport { initHoverOverlay } from \"./hover-overlay.js\";\nexport { sendToParent } from \"./utils.js\";\nexport type { EditorMessage, UpstartEditorMessage } from \"./types.js\";\n"],"mappings":"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"}
@@ -323,9 +323,15 @@ function destroyAllActiveEditors() {
323
323
  stopDomObserver();
324
324
  for (const element of activeEditors.keys()) destroyEditor(element);
325
325
  }
326
+ /** Descendants the plain editor cannot reproduce: taking over the element would destroy them. */
327
+ const PLAIN_EDITOR_FOREIGN_CHILDREN = "svg, img, picture, video, iframe, canvas";
326
328
  function setupEditableElement(element, options) {
327
329
  const hash = getEditableHash(element);
328
330
  if (!hash || activeEditors.has(element)) return;
331
+ if (getEditorMode(element, options) === "plain" && element.querySelector(PLAIN_EDITOR_FOREIGN_CHILDREN)) {
332
+ console.warn("[Upstart Editor] Skipping text editor on an element that also holds media/icons:", element.dataset.upstartHash, element);
333
+ return;
334
+ }
329
335
  cacheStyles(element);
330
336
  activateEditor(element, hash, options);
331
337
  }