@upstart.gg/vite-plugins 0.1.42 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +64 -2
- package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +13 -4
- package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/index.js +43 -3
- package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/state.js +42 -1
- package/dist/vite-plugin-upstart-editor/runtime/state.js.map +1 -1
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +32 -2
- package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +99 -2
- package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +16 -4
- package/src/vite-plugin-upstart-editor/runtime/index.ts +41 -1
- package/src/vite-plugin-upstart-editor/runtime/state.ts +47 -1
- package/src/vite-plugin-upstart-editor/runtime/types.ts +28 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;AAmEA;;iBAAgB,gBAAA,CAAA;;;AAiBhB;iBAAgB,mBAAA,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCurrentMode } from "./state.js";
|
|
1
|
+
import { getCurrentMode, getSelection, toggleSelection } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
//#region src/vite-plugin-upstart-editor/runtime/click-handler.ts
|
|
4
4
|
let isInitialized = false;
|
|
@@ -65,14 +65,76 @@ function cleanupClickHandler() {
|
|
|
65
65
|
document.removeEventListener("click", handleClick, true);
|
|
66
66
|
isInitialized = false;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Build a SelectionRef from a clicked element, walking up to the nearest
|
|
70
|
+
* component (data-upstart-file) for the source file / component name and
|
|
71
|
+
* collecting i18n keys from the element's subtree.
|
|
72
|
+
*/
|
|
73
|
+
function buildSelectionRef(element) {
|
|
74
|
+
const hash = element.dataset.upstartHash ?? "";
|
|
75
|
+
const fileEl = element.closest("[data-upstart-file]");
|
|
76
|
+
const filePath = fileEl?.dataset.upstartFile ?? element.dataset.upstartFile ?? "";
|
|
77
|
+
const componentName = fileEl?.dataset.upstartComponent ?? element.dataset.upstartComponent;
|
|
78
|
+
const key = `${filePath}:${hash}` !== ":" ? `${filePath}:${hash}` : "";
|
|
79
|
+
if (!key) return null;
|
|
80
|
+
const i18nKeys = /* @__PURE__ */ new Set();
|
|
81
|
+
const collectKeys = (el) => {
|
|
82
|
+
const raw = el.dataset.upstartI18n;
|
|
83
|
+
if (raw) for (const k of raw.split(/[\s,]+/).filter(Boolean)) i18nKeys.add(k);
|
|
84
|
+
};
|
|
85
|
+
collectKeys(element);
|
|
86
|
+
for (const node of element.querySelectorAll("[data-upstart-i18n]")) collectKeys(node);
|
|
87
|
+
const text = (element.innerText ?? "").trim().slice(0, 200) || void 0;
|
|
88
|
+
const dataBoundEl = element.closest("[data-upstart-data-bound]");
|
|
89
|
+
const dataBound = !!dataBoundEl && dataBoundEl.dataset.upstartDataBound !== "false" && i18nKeys.size === 0;
|
|
90
|
+
return {
|
|
91
|
+
key,
|
|
92
|
+
hash,
|
|
93
|
+
filePath,
|
|
94
|
+
componentName,
|
|
95
|
+
i18nKeys: i18nKeys.size > 0 ? Array.from(i18nKeys) : void 0,
|
|
96
|
+
text,
|
|
97
|
+
dataBound: dataBound || void 0
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* "Select" mode click: toggle the clicked component in/out of the selection,
|
|
102
|
+
* mark it visually with data-upstart-selected, and report the new selection
|
|
103
|
+
* to the parent. Never edits or navigates.
|
|
104
|
+
*/
|
|
105
|
+
function handleSelectClick(event, target) {
|
|
106
|
+
const rawElement = target.closest("[data-upstart-hash]");
|
|
107
|
+
if (!rawElement) {
|
|
108
|
+
console.info("[Upstart Editor] Select click ignored: no data-upstart-hash ancestor");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const element = rawElement.parentElement?.closest("[data-upstart-editable-text-mode=\"rich-panel\"]") ?? rawElement;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
const ref = buildSelectionRef(element);
|
|
115
|
+
if (!ref) return;
|
|
116
|
+
const nowSelected = toggleSelection(ref, element);
|
|
117
|
+
if (nowSelected) element.setAttribute("data-upstart-selected", "");
|
|
118
|
+
else element.removeAttribute("data-upstart-selected");
|
|
119
|
+
sendToParent({
|
|
120
|
+
type: "selection-changed",
|
|
121
|
+
selection: getSelection()
|
|
122
|
+
});
|
|
123
|
+
console.log("[Upstart Editor] Selection toggled:", ref.key, nowSelected);
|
|
124
|
+
}
|
|
68
125
|
function handleClick(event) {
|
|
69
|
-
|
|
126
|
+
const mode = getCurrentMode();
|
|
127
|
+
if (mode !== "edit" && mode !== "select") return;
|
|
70
128
|
console.debug("[Upstart Editor] Click event:", event);
|
|
71
129
|
const target = event.target;
|
|
72
130
|
if (!target) {
|
|
73
131
|
console.warn("[Upstart Editor] Click target is not an HTMLElement");
|
|
74
132
|
return;
|
|
75
133
|
}
|
|
134
|
+
if (mode === "select") {
|
|
135
|
+
handleSelectClick(event, target);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
76
138
|
if (target.closest("#upstart-array-controls, [data-upstart-draft-item]")) return;
|
|
77
139
|
if (target.closest(".upstart-editor-bubble-menu")) {
|
|
78
140
|
console.info("[Upstart Editor] Click ignored: target is inside the bubble menu");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"click-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.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\ninterface EditableImageRef {\n id: string;\n src: string;\n alt: string;\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((node) => {\n const img = node as HTMLImageElement;\n return {\n id: node.dataset.upstartImageId as string,\n src: img.currentSrc || img.src || \"\",\n alt: img.alt || \"\",\n };\n });\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\nfunction handleClick(event: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") {\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 // 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 });\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 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 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 { sendToParent } from \"./utils.js\";\nimport type { 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\ninterface EditableImageRef {\n id: string;\n src: string;\n alt: string;\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((node) => {\n const img = node as HTMLImageElement;\n return {\n id: node.dataset.upstartImageId as string,\n src: img.currentSrc || img.src || \"\",\n alt: img.alt || \"\",\n };\n });\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 });\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 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 images: collectImages(element),\n });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover-overlay.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"mappings":";;AAYA;;iBAAgB,gBAAA,CAAA;;;
|
|
1
|
+
{"version":3,"file":"hover-overlay.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"mappings":";;AAYA;;iBAAgB,gBAAA,CAAA;;;AA4DhB;iBAAgB,YAAA,CAAA"}
|
|
@@ -21,7 +21,8 @@ function initHoverOverlay() {
|
|
|
21
21
|
":root[data-upstart-edit-mode] [data-upstart-editor-active] {",
|
|
22
22
|
" transition: outline 150ms, outline-offset 150ms;",
|
|
23
23
|
"}",
|
|
24
|
-
":root[data-upstart-edit-mode] [data-upstart-hovered]
|
|
24
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered],",
|
|
25
|
+
":root[data-upstart-select-mode] [data-upstart-hovered] {",
|
|
25
26
|
" outline: 2px solid rgba(114, 112, 198, 0.6);",
|
|
26
27
|
" outline-offset: 4px;",
|
|
27
28
|
" cursor: pointer;",
|
|
@@ -31,9 +32,15 @@ function initHoverOverlay() {
|
|
|
31
32
|
" outline-offset: 4px;",
|
|
32
33
|
" cursor: text;",
|
|
33
34
|
"}",
|
|
34
|
-
":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor]
|
|
35
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor],",
|
|
36
|
+
":root[data-upstart-select-mode] [data-upstart-hovered-ancestor] {",
|
|
35
37
|
" outline: 1px dashed rgba(114, 112, 198, 0.6);",
|
|
36
38
|
" cursor: pointer;",
|
|
39
|
+
"}",
|
|
40
|
+
":root[data-upstart-select-mode] [data-upstart-selected] {",
|
|
41
|
+
" outline: 2px dashed rgba(114, 112, 198, 0.95) !important;",
|
|
42
|
+
" outline-offset: 2px;",
|
|
43
|
+
" background-color: rgba(114, 112, 198, 0.08);",
|
|
37
44
|
"}"
|
|
38
45
|
].join("\n");
|
|
39
46
|
document.head.appendChild(style);
|
|
@@ -90,7 +97,8 @@ function applyEditableHover(target, withAncestors) {
|
|
|
90
97
|
}
|
|
91
98
|
}
|
|
92
99
|
function handleMouseOver(event) {
|
|
93
|
-
|
|
100
|
+
const mode = getCurrentMode();
|
|
101
|
+
if (mode !== "edit" && mode !== "select") return;
|
|
94
102
|
const target = event.target;
|
|
95
103
|
if (!target) return;
|
|
96
104
|
const textEl = target.closest("[data-upstart-editor-active]");
|
|
@@ -117,7 +125,8 @@ function handleMouseOut(event) {
|
|
|
117
125
|
hideOverlays();
|
|
118
126
|
}
|
|
119
127
|
function handleKeyChange(event) {
|
|
120
|
-
|
|
128
|
+
const mode = getCurrentMode();
|
|
129
|
+
if (mode !== "edit" && mode !== "select" || !hoveredEl) return;
|
|
121
130
|
if (event.key !== "Meta" && event.key !== "Control") return;
|
|
122
131
|
applyEditableHover(hoveredEl, event.type === "keydown" && (event.metaKey || event.ctrlKey));
|
|
123
132
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hover-overlay.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\nlet overlay: HTMLDivElement | null = null;\nlet currentTarget: HTMLElement | null = null;\nlet hoveredEl: HTMLElement | null = null;\nlet isInitialized = false;\nlet rafId: number | null = null;\n\n/**\n * Initialize hover overlay.\n */\nexport function initHoverOverlay(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing hover overlay...\");\n\n const style = document.createElement(\"style\");\n style.id = \"upstart-hover-edit-styles\";\n style.textContent = [\n \":root[data-upstart-edit-mode] [data-upstart-classname-id]:not([data-upstart-editable-text='true']),\",\n \":root[data-upstart-edit-mode] [data-upstart-image-id],\",\n \":root[data-upstart-edit-mode] [data-upstart-datasource][data-upstart-record-id]:not([data-upstart-editable-text='true']),\",\n \":root[data-upstart-edit-mode] [data-upstart-editor-active] {\",\n \" transition: outline 150ms, outline-offset 150ms;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-hovered] {\",\n \" outline: 2px solid rgba(114, 112, 198, 0.6);\",\n \" outline-offset: 4px;\",\n \" cursor: pointer;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-editor-active][data-upstart-hovered] {\",\n \" outline: 2px solid rgba(114, 112, 198, 0.6);\",\n \" outline-offset: 4px;\",\n \" cursor: text;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor] {\",\n \" outline: 1px dashed rgba(114, 112, 198, 0.6);\",\n \" cursor: pointer;\",\n \"}\",\n ].join(\"\\n\");\n document.head.appendChild(style);\n\n document.addEventListener(\"mouseover\", handleMouseOver);\n document.addEventListener(\"mouseout\", handleMouseOut);\n document.addEventListener(\"keydown\", handleKeyChange);\n document.addEventListener(\"keyup\", handleKeyChange);\n window.addEventListener(\"scroll\", scheduleOverlayUpdate, { passive: true });\n window.addEventListener(\"resize\", scheduleOverlayUpdate, { passive: true });\n\n isInitialized = true;\n}\n\n/**\n * Hide all overlays.\n */\nexport function hideOverlays(): void {\n if (overlay) {\n overlay.style.display = \"none\";\n currentTarget = null;\n }\n clearEditableHover();\n}\n\nfunction isEligible(el: HTMLElement): boolean {\n if (el.dataset.upstartEditableText === \"true\") return false;\n return !!(\n el.dataset.upstartClassnameId ||\n el.dataset.upstartImageId ||\n (el.dataset.upstartDatasource && el.dataset.upstartRecordId)\n );\n}\n\nfunction clearEditableHover(): void {\n for (const el of document.querySelectorAll(\"[data-upstart-hovered],[data-upstart-hovered-ancestor]\")) {\n (el as HTMLElement).removeAttribute(\"data-upstart-hovered\");\n (el as HTMLElement).removeAttribute(\"data-upstart-hovered-ancestor\");\n }\n hoveredEl = null;\n}\n\nfunction applyTextHover(el: HTMLElement): void {\n clearEditableHover();\n el.setAttribute(\"data-upstart-hovered\", \"\");\n hoveredEl = el;\n}\n\nfunction applyEditableHover(target: HTMLElement, withAncestors: boolean): void {\n clearEditableHover();\n\n // Find closest eligible element\n let el: HTMLElement | null = target;\n while (el && el !== document.documentElement) {\n if (isEligible(el)) break;\n el = el.parentElement;\n }\n if (!el || el === document.documentElement) return;\n\n el.setAttribute(\"data-upstart-hovered\", \"\");\n hoveredEl = el;\n\n if (withAncestors) {\n let ancestor = el.parentElement;\n while (ancestor && ancestor !== document.documentElement) {\n if (isEligible(ancestor)) {\n ancestor.setAttribute(\"data-upstart-hovered-ancestor\", \"\");\n }\n ancestor = ancestor.parentElement;\n }\n }\n}\n\nfunction handleMouseOver(event: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") {\n return;\n }\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n return;\n }\n\n // If hovering inside an active text editor, highlight only that element\n // and suppress the component overlay box so parents don't get hover effects.\n const textEl = target.closest<HTMLElement>(\"[data-upstart-editor-active]\");\n if (textEl) {\n applyTextHover(textEl);\n if (overlay) overlay.style.display = \"none\";\n currentTarget = null;\n return;\n }\n\n applyEditableHover(target, event.metaKey || event.ctrlKey);\n\n const component = target.closest<HTMLElement>(\"[data-upstart-component]\");\n if (!component || component.dataset.upstartEditorActive) {\n return;\n }\n\n if (!overlay) {\n createOverlay();\n }\n\n currentTarget = component;\n positionOverlay(component);\n}\n\nfunction handleMouseOut(event: MouseEvent): void {\n const target = event.target as HTMLElement | null;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n\n if (!target) {\n return;\n }\n\n const component = target.closest<HTMLElement>(\"[data-upstart-component]\");\n if (!component) {\n return;\n }\n\n if (relatedTarget && component.contains(relatedTarget)) {\n return;\n }\n\n hideOverlays();\n}\n\nfunction handleKeyChange(event: KeyboardEvent): void {\n if (getCurrentMode() !== \"edit\" || !hoveredEl) return;\n if (event.key !== \"Meta\" && event.key !== \"Control\") return;\n applyEditableHover(hoveredEl, event.type === \"keydown\" && (event.metaKey || event.ctrlKey));\n}\n\nfunction createOverlay(): void {\n overlay = document.createElement(\"div\");\n overlay.id = \"upstart-hover-overlay\";\n overlay.style.cssText =\n \"position: absolute; pointer-events: none; border: 2px solid rgba(114, 112, 198, 0.6); \" +\n \"background: rgba(114, 112, 198, 0.05); border-radius: 4px; z-index: 9999; \" +\n \"transition: all 0.1s ease; display: none;\";\n document.body.appendChild(overlay);\n}\n\nfunction positionOverlay(element: HTMLElement): void {\n if (!overlay) {\n return;\n }\n\n const rect = element.getBoundingClientRect();\n overlay.style.top = `${rect.top + window.scrollY}px`;\n overlay.style.left = `${rect.left + window.scrollX}px`;\n overlay.style.width = `${rect.width}px`;\n overlay.style.height = `${rect.height}px`;\n overlay.style.display = \"block\";\n}\n\nfunction scheduleOverlayUpdate(): void {\n if (rafId !== null) {\n return;\n }\n\n rafId = requestAnimationFrame(() => {\n rafId = null;\n if (currentTarget && overlay && overlay.style.display === \"block\") {\n positionOverlay(currentTarget);\n }\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"hover-overlay.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\nlet overlay: HTMLDivElement | null = null;\nlet currentTarget: HTMLElement | null = null;\nlet hoveredEl: HTMLElement | null = null;\nlet isInitialized = false;\nlet rafId: number | null = null;\n\n/**\n * Initialize hover overlay.\n */\nexport function initHoverOverlay(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing hover overlay...\");\n\n const style = document.createElement(\"style\");\n style.id = \"upstart-hover-edit-styles\";\n style.textContent = [\n \":root[data-upstart-edit-mode] [data-upstart-classname-id]:not([data-upstart-editable-text='true']),\",\n \":root[data-upstart-edit-mode] [data-upstart-image-id],\",\n \":root[data-upstart-edit-mode] [data-upstart-datasource][data-upstart-record-id]:not([data-upstart-editable-text='true']),\",\n \":root[data-upstart-edit-mode] [data-upstart-editor-active] {\",\n \" transition: outline 150ms, outline-offset 150ms;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-hovered],\",\n \":root[data-upstart-select-mode] [data-upstart-hovered] {\",\n \" outline: 2px solid rgba(114, 112, 198, 0.6);\",\n \" outline-offset: 4px;\",\n \" cursor: pointer;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-editor-active][data-upstart-hovered] {\",\n \" outline: 2px solid rgba(114, 112, 198, 0.6);\",\n \" outline-offset: 4px;\",\n \" cursor: text;\",\n \"}\",\n \":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor],\",\n \":root[data-upstart-select-mode] [data-upstart-hovered-ancestor] {\",\n \" outline: 1px dashed rgba(114, 112, 198, 0.6);\",\n \" cursor: pointer;\",\n \"}\",\n // Persistent outline for elements picked in select mode. Solid + filled so\n // they stand out from the dashed/translucent hover state. Only visible while\n // in select mode (the selection set itself is kept across mode switches).\n \":root[data-upstart-select-mode] [data-upstart-selected] {\",\n \" outline: 2px dashed rgba(114, 112, 198, 0.95) !important;\",\n \" outline-offset: 2px;\",\n \" background-color: rgba(114, 112, 198, 0.08);\",\n \"}\",\n ].join(\"\\n\");\n document.head.appendChild(style);\n\n document.addEventListener(\"mouseover\", handleMouseOver);\n document.addEventListener(\"mouseout\", handleMouseOut);\n document.addEventListener(\"keydown\", handleKeyChange);\n document.addEventListener(\"keyup\", handleKeyChange);\n window.addEventListener(\"scroll\", scheduleOverlayUpdate, { passive: true });\n window.addEventListener(\"resize\", scheduleOverlayUpdate, { passive: true });\n\n isInitialized = true;\n}\n\n/**\n * Hide all overlays.\n */\nexport function hideOverlays(): void {\n if (overlay) {\n overlay.style.display = \"none\";\n currentTarget = null;\n }\n clearEditableHover();\n}\n\nfunction isEligible(el: HTMLElement): boolean {\n if (el.dataset.upstartEditableText === \"true\") return false;\n return !!(\n el.dataset.upstartClassnameId ||\n el.dataset.upstartImageId ||\n (el.dataset.upstartDatasource && el.dataset.upstartRecordId)\n );\n}\n\nfunction clearEditableHover(): void {\n for (const el of document.querySelectorAll(\"[data-upstart-hovered],[data-upstart-hovered-ancestor]\")) {\n (el as HTMLElement).removeAttribute(\"data-upstart-hovered\");\n (el as HTMLElement).removeAttribute(\"data-upstart-hovered-ancestor\");\n }\n hoveredEl = null;\n}\n\nfunction applyTextHover(el: HTMLElement): void {\n clearEditableHover();\n el.setAttribute(\"data-upstart-hovered\", \"\");\n hoveredEl = el;\n}\n\nfunction applyEditableHover(target: HTMLElement, withAncestors: boolean): void {\n clearEditableHover();\n\n // Find closest eligible element\n let el: HTMLElement | null = target;\n while (el && el !== document.documentElement) {\n if (isEligible(el)) break;\n el = el.parentElement;\n }\n if (!el || el === document.documentElement) return;\n\n el.setAttribute(\"data-upstart-hovered\", \"\");\n hoveredEl = el;\n\n if (withAncestors) {\n let ancestor = el.parentElement;\n while (ancestor && ancestor !== document.documentElement) {\n if (isEligible(ancestor)) {\n ancestor.setAttribute(\"data-upstart-hovered-ancestor\", \"\");\n }\n ancestor = ancestor.parentElement;\n }\n }\n}\n\nfunction handleMouseOver(event: MouseEvent): void {\n const mode = getCurrentMode();\n if (mode !== \"edit\" && mode !== \"select\") {\n return;\n }\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n return;\n }\n\n // If hovering inside an active text editor, highlight only that element\n // and suppress the component overlay box so parents don't get hover effects.\n const textEl = target.closest<HTMLElement>(\"[data-upstart-editor-active]\");\n if (textEl) {\n applyTextHover(textEl);\n if (overlay) overlay.style.display = \"none\";\n currentTarget = null;\n return;\n }\n\n applyEditableHover(target, event.metaKey || event.ctrlKey);\n\n const component = target.closest<HTMLElement>(\"[data-upstart-component]\");\n if (!component || component.dataset.upstartEditorActive) {\n return;\n }\n\n if (!overlay) {\n createOverlay();\n }\n\n currentTarget = component;\n positionOverlay(component);\n}\n\nfunction handleMouseOut(event: MouseEvent): void {\n const target = event.target as HTMLElement | null;\n const relatedTarget = event.relatedTarget as HTMLElement | null;\n\n if (!target) {\n return;\n }\n\n const component = target.closest<HTMLElement>(\"[data-upstart-component]\");\n if (!component) {\n return;\n }\n\n if (relatedTarget && component.contains(relatedTarget)) {\n return;\n }\n\n hideOverlays();\n}\n\nfunction handleKeyChange(event: KeyboardEvent): void {\n const mode = getCurrentMode();\n if ((mode !== \"edit\" && mode !== \"select\") || !hoveredEl) return;\n if (event.key !== \"Meta\" && event.key !== \"Control\") return;\n applyEditableHover(hoveredEl, event.type === \"keydown\" && (event.metaKey || event.ctrlKey));\n}\n\nfunction createOverlay(): void {\n overlay = document.createElement(\"div\");\n overlay.id = \"upstart-hover-overlay\";\n overlay.style.cssText =\n \"position: absolute; pointer-events: none; border: 2px solid rgba(114, 112, 198, 0.6); \" +\n \"background: rgba(114, 112, 198, 0.05); border-radius: 4px; z-index: 9999; \" +\n \"transition: all 0.1s ease; display: none;\";\n document.body.appendChild(overlay);\n}\n\nfunction positionOverlay(element: HTMLElement): void {\n if (!overlay) {\n return;\n }\n\n const rect = element.getBoundingClientRect();\n overlay.style.top = `${rect.top + window.scrollY}px`;\n overlay.style.left = `${rect.left + window.scrollX}px`;\n overlay.style.width = `${rect.width}px`;\n overlay.style.height = `${rect.height}px`;\n overlay.style.display = \"block\";\n}\n\nfunction scheduleOverlayUpdate(): void {\n if (rafId !== null) {\n return;\n }\n\n rafId = requestAnimationFrame(() => {\n rafId = null;\n if (currentTarget && overlay && overlay.style.display === \"block\") {\n positionOverlay(currentTarget);\n }\n });\n}\n"],"mappings":"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"}
|
|
@@ -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,
|
|
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;;iBAkDgB,iBAAA,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCurrentMode, setCurrentMode } from "./state.js";
|
|
1
|
+
import { clearSelection, getCurrentMode, getSelection, removeSelection, setCurrentMode } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
3
|
import { initClickHandler } from "./click-handler.js";
|
|
4
4
|
import { hideOverlays, initHoverOverlay } from "./hover-overlay.js";
|
|
@@ -14,8 +14,16 @@ let isInitialized = false;
|
|
|
14
14
|
function setMode(mode) {
|
|
15
15
|
setCurrentMode(mode);
|
|
16
16
|
console.log(`[Upstart Editor] Setting mode to: ${mode}`);
|
|
17
|
-
if (mode === "edit")
|
|
18
|
-
|
|
17
|
+
if (mode === "edit") {
|
|
18
|
+
disableSelectMode();
|
|
19
|
+
enableEditMode();
|
|
20
|
+
} else if (mode === "select") {
|
|
21
|
+
disableEditMode();
|
|
22
|
+
enableSelectMode();
|
|
23
|
+
} else {
|
|
24
|
+
disableEditMode();
|
|
25
|
+
disableSelectMode();
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
function waitForHydration(callback) {
|
|
21
29
|
const STABILITY_MS = 200;
|
|
@@ -52,6 +60,7 @@ function initUpstartEditor() {
|
|
|
52
60
|
window.addEventListener("message", handleParentMessage);
|
|
53
61
|
const currentPath = () => location.pathname + location.search;
|
|
54
62
|
window.addEventListener("popstate", () => {
|
|
63
|
+
resetSelection();
|
|
55
64
|
sendToParent({
|
|
56
65
|
type: "editor-navigated",
|
|
57
66
|
path: currentPath()
|
|
@@ -60,6 +69,7 @@ function initUpstartEditor() {
|
|
|
60
69
|
const originalPushState = history.pushState.bind(history);
|
|
61
70
|
history.pushState = (...args) => {
|
|
62
71
|
originalPushState(...args);
|
|
72
|
+
resetSelection();
|
|
63
73
|
sendToParent({
|
|
64
74
|
type: "editor-navigated",
|
|
65
75
|
path: currentPath()
|
|
@@ -68,6 +78,7 @@ function initUpstartEditor() {
|
|
|
68
78
|
const originalReplaceState = history.replaceState.bind(history);
|
|
69
79
|
history.replaceState = (...args) => {
|
|
70
80
|
originalReplaceState(...args);
|
|
81
|
+
resetSelection();
|
|
71
82
|
sendToParent({
|
|
72
83
|
type: "editor-navigated",
|
|
73
84
|
path: currentPath()
|
|
@@ -140,6 +151,27 @@ function handleParentMessage(event) {
|
|
|
140
151
|
top: message.y,
|
|
141
152
|
behavior: "auto"
|
|
142
153
|
});
|
|
154
|
+
else if (message.type === "clear-selection") {
|
|
155
|
+
resetSelection();
|
|
156
|
+
sendToParent({
|
|
157
|
+
type: "selection-changed",
|
|
158
|
+
selection: getSelection()
|
|
159
|
+
});
|
|
160
|
+
} else if (message.type === "deselect") {
|
|
161
|
+
removeSelection(message.key)?.removeAttribute("data-upstart-selected");
|
|
162
|
+
sendToParent({
|
|
163
|
+
type: "selection-changed",
|
|
164
|
+
selection: getSelection()
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Drop the whole "select" mode selection: clear the in-memory set and remove the
|
|
170
|
+
* visual marker from any element still carrying it.
|
|
171
|
+
*/
|
|
172
|
+
function resetSelection() {
|
|
173
|
+
clearSelection();
|
|
174
|
+
for (const el of document.querySelectorAll("[data-upstart-selected]")) el.removeAttribute("data-upstart-selected");
|
|
143
175
|
}
|
|
144
176
|
function enableEditMode() {
|
|
145
177
|
console.log("[Upstart Editor] Edit mode enabled");
|
|
@@ -154,6 +186,14 @@ function disableEditMode() {
|
|
|
154
186
|
hideOverlays();
|
|
155
187
|
hideArrayControls();
|
|
156
188
|
}
|
|
189
|
+
function enableSelectMode() {
|
|
190
|
+
console.log("[Upstart Editor] Select mode enabled");
|
|
191
|
+
document.documentElement.setAttribute("data-upstart-select-mode", "");
|
|
192
|
+
}
|
|
193
|
+
function disableSelectMode() {
|
|
194
|
+
document.documentElement.removeAttribute("data-upstart-select-mode");
|
|
195
|
+
hideOverlays();
|
|
196
|
+
}
|
|
157
197
|
//#endregion
|
|
158
198
|
export { getCurrentMode, initClickHandler, initErrorHandler, initHoverOverlay, initTextEditor, initUpstartEditor, sendToParent, setMode, waitForHydration };
|
|
159
199
|
|
|
@@ -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 } 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 enableEditMode();\n } else {\n disableEditMode();\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 * 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 sendToParent({ type: \"editor-navigated\", path: currentPath() });\n });\n const originalPushState = history.pushState.bind(history);\n history.pushState = (...args) => {\n originalPushState(...args);\n sendToParent({ type: \"editor-navigated\", path: currentPath() });\n };\n const originalReplaceState = history.replaceState.bind(history);\n history.replaceState = (...args) => {\n originalReplaceState(...args);\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\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 === \"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 }\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\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":";;;;;;;;;AAUA,IAAI,gBAAgB;;;;AAOpB,SAAgB,QAAQ,MAAwB;CAC9C,eAAe,KAAK;CACpB,QAAQ,IAAI,qCAAqC,OAAO;CACxD,IAAI,SAAS,QACX,gBAAgB;MAEhB,iBAAiB;;AAIrB,SAAgB,iBAAiB,UAA4B;CAK3D,MAAM,eAAe;CAErB,MAAM,gBAAgB;EACpB,IAAI,QAAuB;EAE3B,MAAM,eAAe;GACnB,IAAI,OAAO,aAAa,MAAM;GAC9B,QAAQ,OAAO,iBAAiB;IAC9B,SAAS,YAAY;IACrB,UAAU;MACT,aAAa;;EAGlB,MAAM,WAAW,IAAI,iBAAiB,OAAO;EAC7C,SAAS,QAAQ,SAAS,iBAAiB;GAAE,WAAW;GAAM,SAAS;GAAM,CAAC;EAG9E,QAAQ;;CAGV,IAAI,SAAS,eAAe,YAC1B,SAAS;MAET,OAAO,iBAAiB,QAAQ,SAAS,EAAE,MAAM,MAAM,CAAC;;;;;AAO5D,SAAgB,oBAA0B;CACxC,IAAI,eAAe;EACjB,QAAQ,IAAI,iDAAiD;EAC7D;;CAGF,IAAI;EACF,QAAQ,IAAI,mCAAmC;EAE/C,gBAAgB;EAEhB,OAAO,iBAAiB,WAAW,oBAAoB;EAIvD,MAAM,oBAAoB,SAAS,WAAW,SAAS;EAIvD,OAAO,iBAAiB,kBAAkB;GACxC,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;IAC/D;EACF,MAAM,oBAAoB,QAAQ,UAAU,KAAK,QAAQ;EACzD,QAAQ,aAAa,GAAG,SAAS;GAC/B,kBAAkB,GAAG,KAAK;GAC1B,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;;EAEjE,MAAM,uBAAuB,QAAQ,aAAa,KAAK,QAAQ;EAC/D,QAAQ,gBAAgB,GAAG,SAAS;GAClC,qBAAqB,GAAG,KAAK;GAC7B,aAAa;IAAE,MAAM;IAAoB,MAAM,aAAa;IAAE,CAAC;;EAKjE,MAAM,UAAW,WAAmB;EACpC,MAAM,qBAAqB,WACtB,WAAmB,QAClB,QAAQ,YAAY,QAAQ,UAAU,WAAW,IAAI,GACvD,KAAA;EAEJ,eAAe,qBAAqB,EAAE,oBAAoB,GAAG,EAAE,CAAC;EAChE,kBAAkB;EAClB,kBAAkB;EAClB,mBAAmB;EACnB,eAAe;EACf,kBAAkB;EAElB,aAAa;GAAE,MAAM;GAAgB,MAAM,aAAa;GAAE,CAAC;UACpD,OAAO;EACd,QAAQ,MAAM,2CAA2C,MAAM;EAC/D,aAAa;GACX,MAAM;GACN,OAAO,iBAAiB,QAAQ,MAAM,UAAU;GACjD,CAAC;;;AAIN,MAAM,kBAAkB,CAAC,yBAAyB,cAAc;AAEhE,MAAM,uBAAuB,WAAmB;CAC9C,OAAO,gBAAgB,MAAM,kBAAkB;EAC7C,IAAI,OAAO,kBAAkB,UAC3B,OAAO,WAAW;OACb,IAAI,yBAAyB,QAClC,OAAO,cAAc,KAAK,OAAO;EAEnC,OAAO;GACP;;AAGJ,SAAS,oBAAoB,OAA2B;CACtD,MAAM,UAAU,MAAM;CAEtB,QAAQ,IAAI,kDAAkD;EAAE;EAAO;EAAS,CAAC;CAEjF,IAAI,CAAC,WAAW,CAAC,oBAAoB,MAAM,OAAO,EAAE;EAClD,QAAQ,KAAK,0DAA0D,MAAM,OAAO;EACpF;;CAGF,IAAI,QAAQ,SAAS,YAAY;EAC/B,QAAQ,IAAI,2BAA2B,QAAQ,KAAK;EACpD,QAAQ,QAAQ,KAAK;QAChB,IAAI,QAAQ,SAAS,qBAAqB;EAC/C,MAAM,KAAK,SAAS,cAA2B,+BAA+B,QAAQ,YAAY,IAAI;EACtG,IAAI,IACF,GAAG,YAAY,QAAQ;EAEzB,MAAM,WAAW;EACjB,IAAI,UAAU,SAAS,eAAe,SAAS;EAC/C,IAAI,QAAQ,YAAY;GACtB,IAAI,CAAC,SAAS;IACZ,UAAU,SAAS,cAAc,QAAQ;IACzC,QAAQ,KAAK;IACb,SAAS,KAAK,YAAY,QAAQ;;GAEpC,QAAQ,cAAc,QAAQ;SACzB,IAAI,SACT,QAAQ,cAAc;QAEnB,IAAI,QAAQ,SAAS,iBAAiB;EAC3C,MAAM,KAAK,SAAS,cAAgC,2BAA2B,QAAQ,QAAQ,IAAI;EACnG,IAAI,IACF,GAAG,MAAM,QAAQ;QAEd,IAAI,QAAQ,SAAS,2BAC1B,aAAa;EAAE,MAAM;EAAmB,GAAG,OAAO;EAAS,GAAG,OAAO;EAAS,CAAC;MAC1E,IAAI,QAAQ,SAAS,2BAC1B,OAAO,SAAS;EAAE,MAAM,QAAQ;EAAG,KAAK,QAAQ;EAAG,UAAU;EAAQ,CAAC;;AAI1E,SAAS,iBAAuB;CAC9B,QAAQ,IAAI,qCAAqC;CACjD,SAAS,gBAAgB,aAAa,0BAA0B,GAAG;CACnE,oBAAoB;CACpB,sBAAsB;;AAGxB,SAAS,kBAAwB;CAC/B,QAAQ,IAAI,wCAAwC;CACpD,SAAS,gBAAgB,gBAAgB,yBAAyB;CAClE,yBAAyB;CACzB,cAAc;CACd,mBAAmB"}
|
|
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 * 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\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 === \"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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"}
|
|
@@ -12,7 +12,48 @@ function getCurrentMode() {
|
|
|
12
12
|
function setCurrentMode(mode) {
|
|
13
13
|
currentMode = mode;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Selection set for "select" mode. Keyed by SelectionRef.key (stable across
|
|
17
|
+
* the session for a given element), insertion order preserved for the chips UI.
|
|
18
|
+
* `selectionEls` holds the matching DOM nodes (not serializable, so kept apart
|
|
19
|
+
* from the ref data sent to the parent) to clear their visual marker on removal.
|
|
20
|
+
*/
|
|
21
|
+
const selection = /* @__PURE__ */ new Map();
|
|
22
|
+
const selectionEls = /* @__PURE__ */ new Map();
|
|
23
|
+
/** Current selection as an array, in insertion order. */
|
|
24
|
+
function getSelection() {
|
|
25
|
+
return Array.from(selection.values());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Toggle an element in/out of the selection. Returns true if the element is now
|
|
29
|
+
* selected, false if it was removed.
|
|
30
|
+
*/
|
|
31
|
+
function toggleSelection(ref, el) {
|
|
32
|
+
if (selection.has(ref.key)) {
|
|
33
|
+
selection.delete(ref.key);
|
|
34
|
+
selectionEls.delete(ref.key);
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
selection.set(ref.key, ref);
|
|
38
|
+
selectionEls.set(ref.key, el);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Remove a single element from the selection by key. Returns the matching DOM
|
|
43
|
+
* node (if known) so the caller can clear its visual marker.
|
|
44
|
+
*/
|
|
45
|
+
function removeSelection(key) {
|
|
46
|
+
selection.delete(key);
|
|
47
|
+
const el = selectionEls.get(key);
|
|
48
|
+
selectionEls.delete(key);
|
|
49
|
+
return el;
|
|
50
|
+
}
|
|
51
|
+
/** Clear the whole selection. */
|
|
52
|
+
function clearSelection() {
|
|
53
|
+
selection.clear();
|
|
54
|
+
selectionEls.clear();
|
|
55
|
+
}
|
|
15
56
|
//#endregion
|
|
16
|
-
export { getCurrentMode, setCurrentMode };
|
|
57
|
+
export { clearSelection, getCurrentMode, getSelection, removeSelection, setCurrentMode, toggleSelection };
|
|
17
58
|
|
|
18
59
|
//# sourceMappingURL=state.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"state.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/state.ts"],"sourcesContent":["import type { EditorMode } from \"./types.js\";\n\nlet currentMode: EditorMode = \"preview\";\n\n/**\n * Get the current editor mode.\n */\nexport function getCurrentMode(): EditorMode {\n return currentMode;\n}\n\n/**\n * Set the current editor mode (internal use only).\n */\nexport function setCurrentMode(mode: EditorMode): void {\n currentMode = mode;\n}\n"],"mappings":";AAEA,IAAI,cAA0B;;;;AAK9B,SAAgB,iBAA6B;CAC3C,OAAO;;;;;AAMT,SAAgB,eAAe,MAAwB;CACrD,cAAc"}
|
|
1
|
+
{"version":3,"file":"state.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/state.ts"],"sourcesContent":["import type { EditorMode, SelectionRef } from \"./types.js\";\n\nlet currentMode: EditorMode = \"preview\";\n\n/**\n * Get the current editor mode.\n */\nexport function getCurrentMode(): EditorMode {\n return currentMode;\n}\n\n/**\n * Set the current editor mode (internal use only).\n */\nexport function setCurrentMode(mode: EditorMode): void {\n currentMode = mode;\n}\n\n/**\n * Selection set for \"select\" mode. Keyed by SelectionRef.key (stable across\n * the session for a given element), insertion order preserved for the chips UI.\n * `selectionEls` holds the matching DOM nodes (not serializable, so kept apart\n * from the ref data sent to the parent) to clear their visual marker on removal.\n */\nconst selection = new Map<string, SelectionRef>();\nconst selectionEls = new Map<string, HTMLElement>();\n\n/** Current selection as an array, in insertion order. */\nexport function getSelection(): SelectionRef[] {\n return Array.from(selection.values());\n}\n\n/**\n * Toggle an element in/out of the selection. Returns true if the element is now\n * selected, false if it was removed.\n */\nexport function toggleSelection(ref: SelectionRef, el: HTMLElement): boolean {\n if (selection.has(ref.key)) {\n selection.delete(ref.key);\n selectionEls.delete(ref.key);\n return false;\n }\n selection.set(ref.key, ref);\n selectionEls.set(ref.key, el);\n return true;\n}\n\n/**\n * Remove a single element from the selection by key. Returns the matching DOM\n * node (if known) so the caller can clear its visual marker.\n */\nexport function removeSelection(key: string): HTMLElement | undefined {\n selection.delete(key);\n const el = selectionEls.get(key);\n selectionEls.delete(key);\n return el;\n}\n\n/** Clear the whole selection. */\nexport function clearSelection(): void {\n selection.clear();\n selectionEls.clear();\n}\n"],"mappings":";AAEA,IAAI,cAA0B;;;;AAK9B,SAAgB,iBAA6B;CAC3C,OAAO;;;;;AAMT,SAAgB,eAAe,MAAwB;CACrD,cAAc;;;;;;;;AAShB,MAAM,4BAAY,IAAI,KAA2B;AACjD,MAAM,+BAAe,IAAI,KAA0B;;AAGnD,SAAgB,eAA+B;CAC7C,OAAO,MAAM,KAAK,UAAU,QAAQ,CAAC;;;;;;AAOvC,SAAgB,gBAAgB,KAAmB,IAA0B;CAC3E,IAAI,UAAU,IAAI,IAAI,IAAI,EAAE;EAC1B,UAAU,OAAO,IAAI,IAAI;EACzB,aAAa,OAAO,IAAI,IAAI;EAC5B,OAAO;;CAET,UAAU,IAAI,IAAI,KAAK,IAAI;CAC3B,aAAa,IAAI,IAAI,KAAK,GAAG;CAC7B,OAAO;;;;;;AAOT,SAAgB,gBAAgB,KAAsC;CACpE,UAAU,OAAO,IAAI;CACrB,MAAM,KAAK,aAAa,IAAI,IAAI;CAChC,aAAa,OAAO,IAAI;CACxB,OAAO;;;AAIT,SAAgB,iBAAuB;CACrC,UAAU,OAAO;CACjB,aAAa,OAAO"}
|
|
@@ -4,8 +4,30 @@ import { Editor } from "@tiptap/core";
|
|
|
4
4
|
//#region src/vite-plugin-upstart-editor/runtime/types.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Editor mode
|
|
7
|
+
* - preview: navigate the site, no editing
|
|
8
|
+
* - edit: click an element to edit it (inspector / inline text)
|
|
9
|
+
* - select: pick multiple elements to attach to the next chat message
|
|
7
10
|
*/
|
|
8
|
-
type EditorMode = "preview" | "edit";
|
|
11
|
+
type EditorMode = "preview" | "edit" | "select";
|
|
12
|
+
/**
|
|
13
|
+
* A reference to an element picked in "select" mode. Built from the data-upstart-*
|
|
14
|
+
* attributes injected by vite-plugin-upstart-attrs. `key` (e.g. `filePath:hash`) is
|
|
15
|
+
* the stable identity used to toggle the element in/out of the selection.
|
|
16
|
+
*/
|
|
17
|
+
interface SelectionRef {
|
|
18
|
+
key: string;
|
|
19
|
+
hash: string;
|
|
20
|
+
filePath: string;
|
|
21
|
+
componentName?: string;
|
|
22
|
+
i18nKeys?: string[];
|
|
23
|
+
text?: string;
|
|
24
|
+
/**
|
|
25
|
+
* True when the element's visible content is derived from datasource (database) data
|
|
26
|
+
* (element carries / is inside `data-upstart-data-bound`). Its CONTENT is not editable
|
|
27
|
+
* from the editor — only its presentation is.
|
|
28
|
+
*/
|
|
29
|
+
dataBound?: boolean;
|
|
30
|
+
}
|
|
9
31
|
/**
|
|
10
32
|
* Text editor mode: determines schema constraints and bubble menu options.
|
|
11
33
|
* - block-rich: inline TipTap rich text (block-level elements: p, div, article…)
|
|
@@ -63,6 +85,9 @@ type EditorMessage = {
|
|
|
63
85
|
} | {
|
|
64
86
|
type: "editor-error";
|
|
65
87
|
error: string;
|
|
88
|
+
} | {
|
|
89
|
+
type: "selection-changed";
|
|
90
|
+
selection: SelectionRef[];
|
|
66
91
|
} | {
|
|
67
92
|
type: "element-clicked";
|
|
68
93
|
hash: string;
|
|
@@ -98,6 +123,11 @@ type ParentMessage = {
|
|
|
98
123
|
type: "restore-scroll-position";
|
|
99
124
|
x: number;
|
|
100
125
|
y: number;
|
|
126
|
+
} | {
|
|
127
|
+
type: "clear-selection";
|
|
128
|
+
} | {
|
|
129
|
+
type: "deselect";
|
|
130
|
+
key: string;
|
|
101
131
|
};
|
|
102
132
|
/**
|
|
103
133
|
* Message wrapper sent via postMessage from iframe.
|
|
@@ -173,5 +203,5 @@ interface UpstartEditorPluginOptions {
|
|
|
173
203
|
autoInject?: boolean;
|
|
174
204
|
}
|
|
175
205
|
//#endregion
|
|
176
|
-
export { EditableImageRef, EditorInstance, EditorMessage, EditorMode, ElementBounds, ParentMessage, TextEditorMode, UpstartEditorMessage, UpstartEditorOptions, UpstartEditorPluginOptions, UpstartParentMessage };
|
|
206
|
+
export { EditableImageRef, EditorInstance, EditorMessage, EditorMode, ElementBounds, ParentMessage, SelectionRef, TextEditorMode, UpstartEditorMessage, UpstartEditorOptions, UpstartEditorPluginOptions, UpstartParentMessage };
|
|
177
207
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;AAeA;;;;KAAY,UAAA;AAOZ;;;;;AAAA,UAAiB,YAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,IAAA;EAMS;AAWX;;;;EAXE,SAAA;AAAA;;;;;;;;;KAWU,cAAA;;;AAiBZ;UAZiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMe,cAAA;EACf,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,WAAA;EACT,IAAA;EACA,IAAA,EAAM,cAAA;AAAA;;;;;UAOS,gBAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;AAAA;;;;KAMU,aAAA;EAEN,IAAA;EACA,OAAA,EACI,eAAA,GACA,qBAAA,GACA,mBAAA,GACA,sBAAA,GACA,eAAA;AAAA;EAEJ,IAAA;EAAsB,IAAA;AAAA;EACtB,IAAA;EAA0B,IAAA;AAAA;EAC1B,IAAA;EAAyB,CAAA;EAAW,CAAA;AAAA;EACpC,IAAA;EAAsB,KAAA;AAAA;EACtB,IAAA;EAA2B,SAAA,EAAW,YAAA;AAAA;EAEtC,IAAA;EACA,IAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA,GAAc,MAAA;EACd,MAAA,EAAQ,aAAA;EACR,aAAA,WARA;EAUA,MAAA,GAAS,gBAAA;AAAA;;;;KAMH,aAAA;EACN,IAAA;EAAkB,IAAA,EAAM,UAAA;AAAA;EACxB,IAAA;EAA2B,WAAA;EAAqB,SAAA;EAAmB,UAAA;AAAA;EACnE,IAAA;EAAuB,OAAA;EAAiB,GAAA;AAAA;EACxC,IAAA;AAAA;EACA,IAAA;EAAiC,CAAA;EAAW,CAAA;AAAA;EAC5C,IAAA;AAAA;EACA,IAAA;EAAkB,GAAA;AAAA;;;;UAKP,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;AAHH;;;AAAA,UASiB,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;AAHH;UASiB,oBAAA;;;;;EAKf,gBAAA;EAXC;;;AAMH;;EAYE,sBAAA;EAZmC;;;;EAkBnC,iBAAA;EAYA;;;;EANA,UAAA;EAmBoD;;AAMtD;;EAnBE,WAAA;EAwBA;;;;EAlBA,aAAA;;;;;;EAOA,kBAAA,IAAsB,SAAA,UAAmB,GAAA;AAAA;;;;UAM1B,0BAAA;;;;;EAKf,OAAA;;;;;EAMA,UAAA;AAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@upstart.gg/vite-plugins",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"oxc-parser": "0.101.0",
|
|
24
24
|
"unplugin": "^2.3.11",
|
|
25
25
|
"zimmerframe": "^1.1.4",
|
|
26
|
-
"@upstart.gg/sdk": "^0.1.
|
|
26
|
+
"@upstart.gg/sdk": "^0.1.44"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@rolldown/binding-linux-arm64-gnu": "1.0.0",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
"peerDependencies": {
|
|
115
115
|
"zod": "4.3.6",
|
|
116
|
-
"@upstart.gg/sdk": "^0.1.
|
|
116
|
+
"@upstart.gg/sdk": "^0.1.44"
|
|
117
117
|
},
|
|
118
118
|
"author": "Upstart",
|
|
119
119
|
"publishConfig": {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getCurrentMode } from "./state.js";
|
|
1
|
+
import { getCurrentMode, getSelection, toggleSelection } from "./state.js";
|
|
2
2
|
import { sendToParent } from "./utils.js";
|
|
3
|
+
import type { SelectionRef } from "./types.js";
|
|
3
4
|
|
|
4
5
|
let isInitialized = false;
|
|
5
6
|
|
|
@@ -86,8 +87,99 @@ export function cleanupClickHandler(): void {
|
|
|
86
87
|
isInitialized = false;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Build a SelectionRef from a clicked element, walking up to the nearest
|
|
92
|
+
* component (data-upstart-file) for the source file / component name and
|
|
93
|
+
* collecting i18n keys from the element's subtree.
|
|
94
|
+
*/
|
|
95
|
+
function buildSelectionRef(element: HTMLElement): SelectionRef | null {
|
|
96
|
+
const hash = element.dataset.upstartHash ?? "";
|
|
97
|
+
|
|
98
|
+
const fileEl = element.closest<HTMLElement>("[data-upstart-file]");
|
|
99
|
+
const filePath = fileEl?.dataset.upstartFile ?? element.dataset.upstartFile ?? "";
|
|
100
|
+
const componentName = fileEl?.dataset.upstartComponent ?? element.dataset.upstartComponent;
|
|
101
|
+
|
|
102
|
+
// A stable identity for toggling. Prefer filePath+hash; fall back so distinct
|
|
103
|
+
// elements without a hash don't collapse onto the same key.
|
|
104
|
+
const key = `${filePath}:${hash}` !== ":" ? `${filePath}:${hash}` : "";
|
|
105
|
+
if (!key) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const i18nKeys = new Set<string>();
|
|
110
|
+
const collectKeys = (el: HTMLElement) => {
|
|
111
|
+
const raw = el.dataset.upstartI18n;
|
|
112
|
+
if (raw) {
|
|
113
|
+
for (const k of raw.split(/[\s,]+/).filter(Boolean)) i18nKeys.add(k);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
collectKeys(element);
|
|
117
|
+
for (const node of element.querySelectorAll<HTMLElement>("[data-upstart-i18n]")) {
|
|
118
|
+
collectKeys(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const text = (element.innerText ?? "").trim().slice(0, 200) || undefined;
|
|
122
|
+
|
|
123
|
+
// Whether THIS element's content is derived from database data (element-level marker).
|
|
124
|
+
// This is what gates content edits — its content can't be edited from the editor, only
|
|
125
|
+
// its presentation. i18n/translated content is editable (it lives in translation files),
|
|
126
|
+
// so i18n keys on the element take precedence and suppress the data-bound flag even if the
|
|
127
|
+
// element was wrongly marked.
|
|
128
|
+
const dataBoundEl = element.closest<HTMLElement>("[data-upstart-data-bound]");
|
|
129
|
+
const dataBound = !!dataBoundEl && dataBoundEl.dataset.upstartDataBound !== "false" && i18nKeys.size === 0;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
key,
|
|
133
|
+
hash,
|
|
134
|
+
filePath,
|
|
135
|
+
componentName,
|
|
136
|
+
i18nKeys: i18nKeys.size > 0 ? Array.from(i18nKeys) : undefined,
|
|
137
|
+
text,
|
|
138
|
+
dataBound: dataBound || undefined,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* "Select" mode click: toggle the clicked component in/out of the selection,
|
|
144
|
+
* mark it visually with data-upstart-selected, and report the new selection
|
|
145
|
+
* to the parent. Never edits or navigates.
|
|
146
|
+
*/
|
|
147
|
+
function handleSelectClick(event: MouseEvent, target: HTMLElement): void {
|
|
148
|
+
const rawElement = target.closest<HTMLElement>("[data-upstart-hash]");
|
|
149
|
+
if (!rawElement) {
|
|
150
|
+
console.info("[Upstart Editor] Select click ignored: no data-upstart-hash ancestor");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Bubble up to a rich-panel editable ancestor so selecting an inline child
|
|
155
|
+
// (e.g. <strong>) selects the whole editable region — mirrors edit-mode resolution.
|
|
156
|
+
const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(
|
|
157
|
+
'[data-upstart-editable-text-mode="rich-panel"]',
|
|
158
|
+
);
|
|
159
|
+
const element = richPanelAncestor ?? rawElement;
|
|
160
|
+
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
event.stopPropagation();
|
|
163
|
+
|
|
164
|
+
const ref = buildSelectionRef(element);
|
|
165
|
+
if (!ref) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const nowSelected = toggleSelection(ref, element);
|
|
170
|
+
if (nowSelected) {
|
|
171
|
+
element.setAttribute("data-upstart-selected", "");
|
|
172
|
+
} else {
|
|
173
|
+
element.removeAttribute("data-upstart-selected");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
sendToParent({ type: "selection-changed", selection: getSelection() });
|
|
177
|
+
console.log("[Upstart Editor] Selection toggled:", ref.key, nowSelected);
|
|
178
|
+
}
|
|
179
|
+
|
|
89
180
|
function handleClick(event: MouseEvent): void {
|
|
90
|
-
|
|
181
|
+
const mode = getCurrentMode();
|
|
182
|
+
if (mode !== "edit" && mode !== "select") {
|
|
91
183
|
return;
|
|
92
184
|
}
|
|
93
185
|
|
|
@@ -99,6 +191,11 @@ function handleClick(event: MouseEvent): void {
|
|
|
99
191
|
return;
|
|
100
192
|
}
|
|
101
193
|
|
|
194
|
+
if (mode === "select") {
|
|
195
|
+
handleSelectClick(event, target);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
102
199
|
// Editor-only chrome (the array ×/+/✓ controls) and locally-staged draft list
|
|
103
200
|
// items handle their own clicks and must never be treated as element selection.
|
|
104
201
|
if (target.closest("#upstart-array-controls, [data-upstart-draft-item]")) {
|
|
@@ -30,7 +30,8 @@ export function initHoverOverlay(): void {
|
|
|
30
30
|
":root[data-upstart-edit-mode] [data-upstart-editor-active] {",
|
|
31
31
|
" transition: outline 150ms, outline-offset 150ms;",
|
|
32
32
|
"}",
|
|
33
|
-
":root[data-upstart-edit-mode] [data-upstart-hovered]
|
|
33
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered],",
|
|
34
|
+
":root[data-upstart-select-mode] [data-upstart-hovered] {",
|
|
34
35
|
" outline: 2px solid rgba(114, 112, 198, 0.6);",
|
|
35
36
|
" outline-offset: 4px;",
|
|
36
37
|
" cursor: pointer;",
|
|
@@ -40,10 +41,19 @@ export function initHoverOverlay(): void {
|
|
|
40
41
|
" outline-offset: 4px;",
|
|
41
42
|
" cursor: text;",
|
|
42
43
|
"}",
|
|
43
|
-
":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor]
|
|
44
|
+
":root[data-upstart-edit-mode] [data-upstart-hovered-ancestor],",
|
|
45
|
+
":root[data-upstart-select-mode] [data-upstart-hovered-ancestor] {",
|
|
44
46
|
" outline: 1px dashed rgba(114, 112, 198, 0.6);",
|
|
45
47
|
" cursor: pointer;",
|
|
46
48
|
"}",
|
|
49
|
+
// Persistent outline for elements picked in select mode. Solid + filled so
|
|
50
|
+
// they stand out from the dashed/translucent hover state. Only visible while
|
|
51
|
+
// in select mode (the selection set itself is kept across mode switches).
|
|
52
|
+
":root[data-upstart-select-mode] [data-upstart-selected] {",
|
|
53
|
+
" outline: 2px dashed rgba(114, 112, 198, 0.95) !important;",
|
|
54
|
+
" outline-offset: 2px;",
|
|
55
|
+
" background-color: rgba(114, 112, 198, 0.08);",
|
|
56
|
+
"}",
|
|
47
57
|
].join("\n");
|
|
48
58
|
document.head.appendChild(style);
|
|
49
59
|
|
|
@@ -117,7 +127,8 @@ function applyEditableHover(target: HTMLElement, withAncestors: boolean): void {
|
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
function handleMouseOver(event: MouseEvent): void {
|
|
120
|
-
|
|
130
|
+
const mode = getCurrentMode();
|
|
131
|
+
if (mode !== "edit" && mode !== "select") {
|
|
121
132
|
return;
|
|
122
133
|
}
|
|
123
134
|
|
|
@@ -172,7 +183,8 @@ function handleMouseOut(event: MouseEvent): void {
|
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
function handleKeyChange(event: KeyboardEvent): void {
|
|
175
|
-
|
|
186
|
+
const mode = getCurrentMode();
|
|
187
|
+
if ((mode !== "edit" && mode !== "select") || !hoveredEl) return;
|
|
176
188
|
if (event.key !== "Meta" && event.key !== "Control") return;
|
|
177
189
|
applyEditableHover(hoveredEl, event.type === "keydown" && (event.metaKey || event.ctrlKey));
|
|
178
190
|
}
|
|
@@ -5,7 +5,7 @@ import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./t
|
|
|
5
5
|
import { initArrayControls, refreshArrayControls, hideArrayControls } from "./array-controls.js";
|
|
6
6
|
import { initFormGuard } from "./form-guard.js";
|
|
7
7
|
import { sendToParent } from "./utils.js";
|
|
8
|
-
import { getCurrentMode, setCurrentMode } from "./state.js";
|
|
8
|
+
import { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from "./state.js";
|
|
9
9
|
import type { EditorMode, UpstartParentMessage } from "./types.js";
|
|
10
10
|
|
|
11
11
|
let isInitialized = false;
|
|
@@ -19,9 +19,14 @@ export function setMode(mode: EditorMode): void {
|
|
|
19
19
|
setCurrentMode(mode);
|
|
20
20
|
console.log(`[Upstart Editor] Setting mode to: ${mode}`);
|
|
21
21
|
if (mode === "edit") {
|
|
22
|
+
disableSelectMode();
|
|
22
23
|
enableEditMode();
|
|
24
|
+
} else if (mode === "select") {
|
|
25
|
+
disableEditMode();
|
|
26
|
+
enableSelectMode();
|
|
23
27
|
} else {
|
|
24
28
|
disableEditMode();
|
|
29
|
+
disableSelectMode();
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
@@ -80,16 +85,19 @@ export function initUpstartEditor(): void {
|
|
|
80
85
|
// Notify parent on SPA navigation so it can resend the current editMode
|
|
81
86
|
// and track the current page for the next reload.
|
|
82
87
|
window.addEventListener("popstate", () => {
|
|
88
|
+
resetSelection();
|
|
83
89
|
sendToParent({ type: "editor-navigated", path: currentPath() });
|
|
84
90
|
});
|
|
85
91
|
const originalPushState = history.pushState.bind(history);
|
|
86
92
|
history.pushState = (...args) => {
|
|
87
93
|
originalPushState(...args);
|
|
94
|
+
resetSelection();
|
|
88
95
|
sendToParent({ type: "editor-navigated", path: currentPath() });
|
|
89
96
|
};
|
|
90
97
|
const originalReplaceState = history.replaceState.bind(history);
|
|
91
98
|
history.replaceState = (...args) => {
|
|
92
99
|
originalReplaceState(...args);
|
|
100
|
+
resetSelection();
|
|
93
101
|
sendToParent({ type: "editor-navigated", path: currentPath() });
|
|
94
102
|
};
|
|
95
103
|
|
|
@@ -170,6 +178,25 @@ function handleParentMessage(event: MessageEvent): void {
|
|
|
170
178
|
sendToParent({ type: "scroll-position", x: window.scrollX, y: window.scrollY });
|
|
171
179
|
} else if (message.type === "restore-scroll-position") {
|
|
172
180
|
window.scrollTo({ left: message.x, top: message.y, behavior: "auto" });
|
|
181
|
+
} else if (message.type === "clear-selection") {
|
|
182
|
+
resetSelection();
|
|
183
|
+
// Echo the now-empty selection so the parent store (chips / counter) updates.
|
|
184
|
+
sendToParent({ type: "selection-changed", selection: getSelection() });
|
|
185
|
+
} else if (message.type === "deselect") {
|
|
186
|
+
const el = removeSelection(message.key);
|
|
187
|
+
el?.removeAttribute("data-upstart-selected");
|
|
188
|
+
sendToParent({ type: "selection-changed", selection: getSelection() });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Drop the whole "select" mode selection: clear the in-memory set and remove the
|
|
194
|
+
* visual marker from any element still carrying it.
|
|
195
|
+
*/
|
|
196
|
+
function resetSelection(): void {
|
|
197
|
+
clearSelection();
|
|
198
|
+
for (const el of document.querySelectorAll("[data-upstart-selected]")) {
|
|
199
|
+
el.removeAttribute("data-upstart-selected");
|
|
173
200
|
}
|
|
174
201
|
}
|
|
175
202
|
|
|
@@ -188,6 +215,19 @@ function disableEditMode(): void {
|
|
|
188
215
|
hideArrayControls();
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
function enableSelectMode(): void {
|
|
219
|
+
console.log("[Upstart Editor] Select mode enabled");
|
|
220
|
+
// Selectable hover (hover-overlay) and the persistent selected outline are
|
|
221
|
+
// gated on this attribute. The selection Map / data-upstart-selected attrs are
|
|
222
|
+
// kept across mode switches, so toggling back to select restores the outlines.
|
|
223
|
+
document.documentElement.setAttribute("data-upstart-select-mode", "");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function disableSelectMode(): void {
|
|
227
|
+
document.documentElement.removeAttribute("data-upstart-select-mode");
|
|
228
|
+
hideOverlays();
|
|
229
|
+
}
|
|
230
|
+
|
|
191
231
|
export { initTextEditor } from "./text-editor.js";
|
|
192
232
|
export { initClickHandler } from "./click-handler.js";
|
|
193
233
|
export { initErrorHandler } from "./error-handler.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditorMode } from "./types.js";
|
|
1
|
+
import type { EditorMode, SelectionRef } from "./types.js";
|
|
2
2
|
|
|
3
3
|
let currentMode: EditorMode = "preview";
|
|
4
4
|
|
|
@@ -15,3 +15,49 @@ export function getCurrentMode(): EditorMode {
|
|
|
15
15
|
export function setCurrentMode(mode: EditorMode): void {
|
|
16
16
|
currentMode = mode;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Selection set for "select" mode. Keyed by SelectionRef.key (stable across
|
|
21
|
+
* the session for a given element), insertion order preserved for the chips UI.
|
|
22
|
+
* `selectionEls` holds the matching DOM nodes (not serializable, so kept apart
|
|
23
|
+
* from the ref data sent to the parent) to clear their visual marker on removal.
|
|
24
|
+
*/
|
|
25
|
+
const selection = new Map<string, SelectionRef>();
|
|
26
|
+
const selectionEls = new Map<string, HTMLElement>();
|
|
27
|
+
|
|
28
|
+
/** Current selection as an array, in insertion order. */
|
|
29
|
+
export function getSelection(): SelectionRef[] {
|
|
30
|
+
return Array.from(selection.values());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Toggle an element in/out of the selection. Returns true if the element is now
|
|
35
|
+
* selected, false if it was removed.
|
|
36
|
+
*/
|
|
37
|
+
export function toggleSelection(ref: SelectionRef, el: HTMLElement): boolean {
|
|
38
|
+
if (selection.has(ref.key)) {
|
|
39
|
+
selection.delete(ref.key);
|
|
40
|
+
selectionEls.delete(ref.key);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
selection.set(ref.key, ref);
|
|
44
|
+
selectionEls.set(ref.key, el);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Remove a single element from the selection by key. Returns the matching DOM
|
|
50
|
+
* node (if known) so the caller can clear its visual marker.
|
|
51
|
+
*/
|
|
52
|
+
export function removeSelection(key: string): HTMLElement | undefined {
|
|
53
|
+
selection.delete(key);
|
|
54
|
+
const el = selectionEls.get(key);
|
|
55
|
+
selectionEls.delete(key);
|
|
56
|
+
return el;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Clear the whole selection. */
|
|
60
|
+
export function clearSelection(): void {
|
|
61
|
+
selection.clear();
|
|
62
|
+
selectionEls.clear();
|
|
63
|
+
}
|
|
@@ -9,8 +9,31 @@ import type {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Editor mode
|
|
12
|
+
* - preview: navigate the site, no editing
|
|
13
|
+
* - edit: click an element to edit it (inspector / inline text)
|
|
14
|
+
* - select: pick multiple elements to attach to the next chat message
|
|
12
15
|
*/
|
|
13
|
-
export type EditorMode = "preview" | "edit";
|
|
16
|
+
export type EditorMode = "preview" | "edit" | "select";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A reference to an element picked in "select" mode. Built from the data-upstart-*
|
|
20
|
+
* attributes injected by vite-plugin-upstart-attrs. `key` (e.g. `filePath:hash`) is
|
|
21
|
+
* the stable identity used to toggle the element in/out of the selection.
|
|
22
|
+
*/
|
|
23
|
+
export interface SelectionRef {
|
|
24
|
+
key: string;
|
|
25
|
+
hash: string;
|
|
26
|
+
filePath: string;
|
|
27
|
+
componentName?: string;
|
|
28
|
+
i18nKeys?: string[];
|
|
29
|
+
text?: string;
|
|
30
|
+
/**
|
|
31
|
+
* True when the element's visible content is derived from datasource (database) data
|
|
32
|
+
* (element carries / is inside `data-upstart-data-bound`). Its CONTENT is not editable
|
|
33
|
+
* from the editor — only its presentation is.
|
|
34
|
+
*/
|
|
35
|
+
dataBound?: boolean;
|
|
36
|
+
}
|
|
14
37
|
|
|
15
38
|
/**
|
|
16
39
|
* Text editor mode: determines schema constraints and bubble menu options.
|
|
@@ -71,6 +94,7 @@ export type EditorMessage =
|
|
|
71
94
|
| { type: "editor-navigated"; path?: string }
|
|
72
95
|
| { type: "scroll-position"; x: number; y: number }
|
|
73
96
|
| { type: "editor-error"; error: string }
|
|
97
|
+
| { type: "selection-changed"; selection: SelectionRef[] }
|
|
74
98
|
| {
|
|
75
99
|
type: "element-clicked";
|
|
76
100
|
hash: string;
|
|
@@ -95,7 +119,9 @@ export type ParentMessage =
|
|
|
95
119
|
| { type: "preview-classname"; classNameId: string; className: string; previewCSS?: string }
|
|
96
120
|
| { type: "preview-image"; imageId: string; src: string }
|
|
97
121
|
| { type: "request-scroll-position" }
|
|
98
|
-
| { type: "restore-scroll-position"; x: number; y: number }
|
|
122
|
+
| { type: "restore-scroll-position"; x: number; y: number }
|
|
123
|
+
| { type: "clear-selection" }
|
|
124
|
+
| { type: "deselect"; key: string };
|
|
99
125
|
|
|
100
126
|
/**
|
|
101
127
|
* Message wrapper sent via postMessage from iframe.
|