631 lines
27 KiB
JavaScript
631 lines
27 KiB
JavaScript
import { findElementsById, updateElementClasses, updateElementAttribute, collectAllowedAttributes, ALLOWED_ATTRIBUTES, getElementSelectorId, stopAnimations, resumeAnimations, findInstrumentedElement, resolveHoverTarget, positionLabel, injectFontFaceCss } from "./utils.js";
|
|
import { createLayerController } from "./layer-dropdown/controller.js";
|
|
import { LAYER_DROPDOWN_ATTR } from "./layer-dropdown/consts.js";
|
|
import { createInlineEditController } from "../capabilities/inline-edit/index.js";
|
|
import { THEME_FONT_PREVIEW_ID } from "../consts.js";
|
|
import { createCanvasWheelZoomBridgeController } from "./canvas-wheel-zoom-bridge.js";
|
|
import { createPageHeightBridgeController } from "./page-height-bridge.js";
|
|
import { createAutoPopupSuppressorController } from "./auto-popup-suppressor.js";
|
|
const REPOSITION_DELAY_MS = 50;
|
|
export function setupVisualEditAgent() {
|
|
const canvasWheelZoomBridge = createCanvasWheelZoomBridgeController();
|
|
const pageHeightBridge = createPageHeightBridgeController();
|
|
const autoPopupSuppressor = createAutoPopupSuppressorController();
|
|
// State variables (replacing React useState/useRef)
|
|
let isVisualEditMode = false;
|
|
let isPopoverDragging = false;
|
|
let isDropdownOpen = false;
|
|
let hoverOverlays = [];
|
|
let selectedOverlays = [];
|
|
let currentHighlightedElements = [];
|
|
let selectedElementId = null;
|
|
let selectedElement = null;
|
|
// Create overlay element
|
|
const createOverlay = (isSelected = false) => {
|
|
const overlay = document.createElement("div");
|
|
overlay.style.position = "absolute";
|
|
overlay.style.pointerEvents = "none";
|
|
overlay.style.transition = "all 0.1s ease-in-out";
|
|
overlay.style.zIndex = "9999";
|
|
if (isSelected) {
|
|
overlay.style.border = "2px solid #2563EB";
|
|
}
|
|
else {
|
|
overlay.style.border = "2px solid #95a5fc";
|
|
overlay.style.backgroundColor = "rgba(99, 102, 241, 0.05)";
|
|
}
|
|
return overlay;
|
|
};
|
|
// Position overlay relative to element
|
|
const positionOverlay = (overlay, element, isSelected = false) => {
|
|
if (!element || !isVisualEditMode)
|
|
return;
|
|
const htmlElement = element;
|
|
// Force layout recalculation
|
|
void htmlElement.offsetWidth;
|
|
const rect = element.getBoundingClientRect();
|
|
overlay.style.top = `${rect.top + window.scrollY}px`;
|
|
overlay.style.left = `${rect.left + window.scrollX}px`;
|
|
overlay.style.width = `${rect.width}px`;
|
|
overlay.style.height = `${rect.height}px`;
|
|
// Check if label already exists in overlay
|
|
let label = overlay.querySelector("div");
|
|
if (!label) {
|
|
label = document.createElement("div");
|
|
label.textContent = element.tagName.toLowerCase();
|
|
label.style.position = "absolute";
|
|
label.style.left = "-2px";
|
|
label.style.padding = "2px 8px";
|
|
label.style.fontSize = "11px";
|
|
label.style.fontWeight = isSelected ? "500" : "400";
|
|
label.style.color = isSelected ? "#ffffff" : "#526cff";
|
|
label.style.backgroundColor = isSelected ? "#2563EB" : "#DBEAFE";
|
|
label.style.borderRadius = "3px";
|
|
label.style.minWidth = "24px";
|
|
label.style.textAlign = "center";
|
|
overlay.appendChild(label);
|
|
}
|
|
positionLabel(label, rect);
|
|
};
|
|
// --- Inline edit controller ---
|
|
const inlineEdit = createInlineEditController({
|
|
findElementsById,
|
|
getSelectedElementId: () => selectedElementId,
|
|
getSelectedOverlays: () => selectedOverlays,
|
|
positionOverlay,
|
|
clearSelection: () => {
|
|
inlineEdit.clearSelectedMarks(selectedElementId);
|
|
clearSelectedOverlays();
|
|
selectedElementId = null;
|
|
selectedElement = null;
|
|
},
|
|
createSelectionOverlays: (elements, elementId) => {
|
|
elements.forEach((el) => {
|
|
const overlay = createOverlay(true);
|
|
document.body.appendChild(overlay);
|
|
selectedOverlays.push(overlay);
|
|
positionOverlay(overlay, el, true);
|
|
});
|
|
selectedElementId = elementId;
|
|
},
|
|
});
|
|
const clearSelection = () => {
|
|
inlineEdit.clearSelectedMarks(selectedElementId);
|
|
clearSelectedOverlays();
|
|
selectedElementId = null;
|
|
selectedElement = null;
|
|
};
|
|
// Clear hover overlays
|
|
const clearHoverOverlays = () => {
|
|
hoverOverlays.forEach((overlay) => {
|
|
if (overlay && overlay.parentNode) {
|
|
overlay.remove();
|
|
}
|
|
});
|
|
hoverOverlays = [];
|
|
currentHighlightedElements = [];
|
|
};
|
|
const clearSelectedOverlays = () => {
|
|
selectedOverlays.forEach((overlay) => {
|
|
if (overlay && overlay.parentNode) {
|
|
overlay.remove();
|
|
}
|
|
});
|
|
selectedOverlays = [];
|
|
};
|
|
const TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'a', 'label'];
|
|
const notifyElementSelected = (element) => {
|
|
const htmlElement = element;
|
|
const rect = element.getBoundingClientRect();
|
|
const svgElement = element;
|
|
const isTextElement = TEXT_TAGS.includes(element.tagName?.toLowerCase());
|
|
const arrEl = htmlElement.closest("[data-arr-variable-name]");
|
|
const staticArrayName = arrEl?.dataset?.arrVariableName || null;
|
|
const rawIdx = arrEl?.dataset?.arrIndex;
|
|
const staticArrayIndex = rawIdx != null ? parseInt(rawIdx, 10) : null;
|
|
const staticArrayField = htmlElement.dataset?.arrField || null;
|
|
const collectionEl = htmlElement.closest("[data-collection-id]");
|
|
const itemFieldEl = htmlElement.closest("[data-collection-item-field]");
|
|
const itemIdEl = htmlElement.closest("[data-collection-item-id]");
|
|
window.parent.postMessage({
|
|
type: "element-selected",
|
|
tagName: element.tagName,
|
|
classes: svgElement.className?.baseVal ||
|
|
element.className ||
|
|
"",
|
|
visualSelectorId: getElementSelectorId(element),
|
|
content: isTextElement ? htmlElement.innerText : undefined,
|
|
dataSourceLocation: htmlElement.dataset.sourceLocation,
|
|
isDynamicContent: htmlElement.dataset.dynamicContent === "true",
|
|
linenumber: htmlElement.dataset.linenumber,
|
|
filename: htmlElement.dataset.filename,
|
|
position: {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
centerX: rect.left + rect.width / 2,
|
|
centerY: rect.top + rect.height / 2,
|
|
},
|
|
attributes: collectAllowedAttributes(element, ALLOWED_ATTRIBUTES),
|
|
isTextElement,
|
|
staticArrayName,
|
|
staticArrayIndex,
|
|
staticArrayField,
|
|
collectionId: collectionEl?.dataset?.collectionId || null,
|
|
collectionItemField: itemFieldEl?.dataset?.collectionItemField || null,
|
|
collectionItemId: itemIdEl?.dataset?.collectionItemId || null,
|
|
}, "*");
|
|
};
|
|
// Select an element: create overlays, update state, notify parent
|
|
const selectElement = (element) => {
|
|
const visualSelectorId = getElementSelectorId(element);
|
|
clearSelectedOverlays();
|
|
const elements = findElementsById(visualSelectorId || null);
|
|
elements.forEach((el) => {
|
|
const overlay = createOverlay(true);
|
|
document.body.appendChild(overlay);
|
|
selectedOverlays.push(overlay);
|
|
positionOverlay(overlay, el, true);
|
|
});
|
|
selectedElementId = visualSelectorId || null;
|
|
selectedElement = element;
|
|
clearHoverOverlays();
|
|
notifyElementSelected(element);
|
|
return selectedOverlays[0];
|
|
};
|
|
const notifyDeselection = () => {
|
|
selectedElementId = null;
|
|
window.parent.postMessage({ type: "unselect-element" }, "*");
|
|
};
|
|
// Hover detection via mousemove + elementFromPoint (since app elements have pointer-events: none)
|
|
let lastHoveredSelectorId = null;
|
|
let pendingMouseMoveRaf = null;
|
|
const clearHoverState = () => {
|
|
clearHoverOverlays();
|
|
lastHoveredSelectorId = null;
|
|
};
|
|
const applyHoverOverlays = (selectorId) => {
|
|
const elements = findElementsById(selectorId);
|
|
clearHoverOverlays();
|
|
elements.forEach((el) => {
|
|
const overlay = createOverlay(false);
|
|
document.body.appendChild(overlay);
|
|
hoverOverlays.push(overlay);
|
|
positionOverlay(overlay, el);
|
|
});
|
|
currentHighlightedElements = elements;
|
|
lastHoveredSelectorId = selectorId;
|
|
};
|
|
const handleMouseMove = (e) => {
|
|
if (!isVisualEditMode || isPopoverDragging || inlineEdit.isEditing())
|
|
return;
|
|
if (pendingMouseMoveRaf !== null)
|
|
return;
|
|
pendingMouseMoveRaf = requestAnimationFrame(() => {
|
|
pendingMouseMoveRaf = null;
|
|
if (isDropdownOpen) {
|
|
clearHoverState();
|
|
return;
|
|
}
|
|
const selectorId = resolveHoverTarget(e.clientX, e.clientY, selectedElementId);
|
|
if (!selectorId) {
|
|
clearHoverState();
|
|
return;
|
|
}
|
|
if (lastHoveredSelectorId === selectorId)
|
|
return;
|
|
applyHoverOverlays(selectorId);
|
|
});
|
|
};
|
|
// Clear hover overlays when mouse leaves the viewport
|
|
const handleMouseLeave = () => {
|
|
if (pendingMouseMoveRaf !== null) {
|
|
cancelAnimationFrame(pendingMouseMoveRaf);
|
|
pendingMouseMoveRaf = null;
|
|
}
|
|
clearHoverState();
|
|
};
|
|
// Handle element click
|
|
const handleElementClick = (e) => {
|
|
if (!isVisualEditMode)
|
|
return;
|
|
const target = e.target;
|
|
// Let layer dropdown clicks pass through without interference
|
|
if (target.closest(`[${LAYER_DROPDOWN_ATTR}]`))
|
|
return;
|
|
// Let clicks inside the editable element pass through to the browser
|
|
// so the user can reposition the cursor and select text naturally.
|
|
if (inlineEdit.enabled && target instanceof HTMLElement && target.contentEditable === "true") {
|
|
return;
|
|
}
|
|
// Clicking outside the editable element exits inline editing mode.
|
|
if (inlineEdit.isEditing()) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
inlineEdit.stopEditing();
|
|
return;
|
|
}
|
|
// Close dropdowns when clicking anywhere in iframe if a dropdown is open
|
|
if (isDropdownOpen) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
window.parent.postMessage({ type: "close-dropdowns" }, "*");
|
|
return;
|
|
}
|
|
// Prevent default behavior immediately when in visual edit mode
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.stopImmediatePropagation();
|
|
const element = findInstrumentedElement(e.clientX, e.clientY);
|
|
if (!element) {
|
|
return;
|
|
}
|
|
const htmlElement = element;
|
|
const visualSelectorId = getElementSelectorId(element);
|
|
const isAlreadySelected = selectedElementId === visualSelectorId &&
|
|
htmlElement.dataset.selected === "true";
|
|
if (isAlreadySelected && inlineEdit.enabled && inlineEdit.canEdit(htmlElement)) {
|
|
inlineEdit.startEditing(htmlElement);
|
|
return;
|
|
}
|
|
inlineEdit.stopEditing();
|
|
if (inlineEdit.enabled) {
|
|
inlineEdit.markElementsSelected(findElementsById(visualSelectorId));
|
|
}
|
|
const selectedOverlay = selectElement(element);
|
|
layerController.attachToOverlay(selectedOverlay, element);
|
|
};
|
|
const unselectElement = () => {
|
|
inlineEdit.stopEditing();
|
|
clearSelection();
|
|
};
|
|
const updateElementClassesAndReposition = (visualSelectorId, classes) => {
|
|
const elements = findElementsById(visualSelectorId);
|
|
if (elements.length === 0)
|
|
return;
|
|
updateElementClasses(elements, classes);
|
|
// Use a small delay to allow the browser to recalculate layout before repositioning
|
|
setTimeout(() => {
|
|
// Reposition selected overlays
|
|
if (selectedElementId === visualSelectorId) {
|
|
selectedOverlays.forEach((overlay, index) => {
|
|
if (index < elements.length) {
|
|
positionOverlay(overlay, elements[index]);
|
|
}
|
|
});
|
|
}
|
|
// Reposition hover overlays if needed
|
|
if (currentHighlightedElements.length > 0) {
|
|
const hoveredElement = currentHighlightedElements[0];
|
|
const hoveredId = hoveredElement?.dataset?.visualSelectorId;
|
|
if (hoveredId === visualSelectorId) {
|
|
hoverOverlays.forEach((overlay, index) => {
|
|
if (index < currentHighlightedElements.length) {
|
|
positionOverlay(overlay, currentHighlightedElements[index]);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}, REPOSITION_DELAY_MS);
|
|
};
|
|
// Update element attribute by visual selector ID
|
|
const updateElementAttributeAndReposition = (visualSelectorId, attribute, value, arrIndex) => {
|
|
const elements = findElementsById(visualSelectorId);
|
|
if (elements.length === 0)
|
|
return;
|
|
const targetArrIndex = arrIndex ??
|
|
(selectedElementId === visualSelectorId
|
|
? selectedElement?.dataset.arrIndex
|
|
: undefined);
|
|
updateElementAttribute(elements, attribute, value, targetArrIndex);
|
|
// Reposition overlays after attribute change (e.g. image src swap can affect layout)
|
|
setTimeout(() => {
|
|
if (selectedElementId === visualSelectorId) {
|
|
selectedOverlays.forEach((overlay, index) => {
|
|
if (index < elements.length) {
|
|
positionOverlay(overlay, elements[index]);
|
|
}
|
|
});
|
|
}
|
|
}, REPOSITION_DELAY_MS);
|
|
};
|
|
const updateElementContent = (visualSelectorId, content, arrIndex) => {
|
|
let elements = findElementsById(visualSelectorId);
|
|
if (elements.length === 0) {
|
|
return;
|
|
}
|
|
if (arrIndex != null) {
|
|
elements = elements.filter((el) => el.dataset.arrIndex === String(arrIndex));
|
|
}
|
|
elements.forEach((element) => {
|
|
element.innerText = content;
|
|
});
|
|
setTimeout(() => {
|
|
if (selectedElementId === visualSelectorId) {
|
|
selectedOverlays.forEach((overlay, index) => {
|
|
if (index < elements.length) {
|
|
positionOverlay(overlay, elements[index]);
|
|
}
|
|
});
|
|
}
|
|
}, REPOSITION_DELAY_MS);
|
|
};
|
|
// --- Layer dropdown controller ---
|
|
const layerController = createLayerController({
|
|
createPreviewOverlay: (element) => {
|
|
const overlay = createOverlay(false);
|
|
overlay.style.zIndex = "9998";
|
|
document.body.appendChild(overlay);
|
|
positionOverlay(overlay, element);
|
|
return overlay;
|
|
},
|
|
getSelectedElementId: () => selectedElementId,
|
|
selectElement,
|
|
onDeselect: notifyDeselection,
|
|
});
|
|
// Toggle visual edit mode
|
|
const toggleVisualEditMode = (isEnabled) => {
|
|
isVisualEditMode = isEnabled;
|
|
if (!isEnabled) {
|
|
canvasWheelZoomBridge.disable();
|
|
resumeAnimations();
|
|
inlineEdit.stopEditing();
|
|
clearSelection();
|
|
layerController.cleanup();
|
|
handleMouseLeave();
|
|
document.body.style.cursor = "default";
|
|
document.removeEventListener("mousemove", handleMouseMove);
|
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
document.removeEventListener("click", handleElementClick, true);
|
|
}
|
|
else {
|
|
document.body.style.cursor = "crosshair";
|
|
stopAnimations();
|
|
document.addEventListener("mousemove", handleMouseMove);
|
|
document.addEventListener("mouseleave", handleMouseLeave);
|
|
document.addEventListener("click", handleElementClick, true);
|
|
}
|
|
};
|
|
// Handle scroll events to update popover position
|
|
const handleScroll = () => {
|
|
if (selectedElementId) {
|
|
const element = selectedElement;
|
|
if (element && element.isConnected) {
|
|
const rect = element.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const viewportWidth = window.innerWidth;
|
|
const isInViewport = rect.top < viewportHeight &&
|
|
rect.bottom > 0 &&
|
|
rect.left < viewportWidth &&
|
|
rect.right > 0;
|
|
const elementPosition = {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
centerX: rect.left + rect.width / 2,
|
|
centerY: rect.top + rect.height / 2,
|
|
};
|
|
window.parent.postMessage({
|
|
type: "element-position-update",
|
|
position: elementPosition,
|
|
isInViewport: isInViewport,
|
|
visualSelectorId: selectedElementId,
|
|
}, "*");
|
|
}
|
|
}
|
|
};
|
|
// Handle messages from parent window
|
|
const handleMessage = (event) => {
|
|
const message = event.data;
|
|
switch (message.type) {
|
|
case "toggle-visual-edit-mode":
|
|
toggleVisualEditMode(message.data.enabled);
|
|
if (message.data.specs?.newInlineEditEnabled !== undefined) {
|
|
inlineEdit.enabled = message.data.specs.newInlineEditEnabled;
|
|
}
|
|
break;
|
|
case "update-classes":
|
|
if (message.data && message.data.classes !== undefined) {
|
|
updateElementClassesAndReposition(message.data.visualSelectorId, message.data.classes);
|
|
}
|
|
else {
|
|
console.warn("[VisualEditAgent] Invalid update-classes message:", message);
|
|
}
|
|
break;
|
|
case "update-attribute":
|
|
if (message.data &&
|
|
message.data.visualSelectorId &&
|
|
message.data.attribute !== undefined &&
|
|
message.data.value !== undefined) {
|
|
updateElementAttributeAndReposition(message.data.visualSelectorId, message.data.attribute, message.data.value, message.data.arrIndex);
|
|
}
|
|
else {
|
|
console.warn("[VisualEditAgent] Invalid update-attribute message:", message);
|
|
}
|
|
break;
|
|
case "unselect-element":
|
|
unselectElement();
|
|
break;
|
|
case "refresh-page":
|
|
window.location.reload();
|
|
break;
|
|
case "update-content":
|
|
if (message.data && message.data.content !== undefined) {
|
|
updateElementContent(message.data.visualSelectorId, message.data.content, message.data.arrIndex);
|
|
}
|
|
else {
|
|
console.warn("[VisualEditAgent] Invalid update-content message:", message);
|
|
}
|
|
break;
|
|
case "request-element-position":
|
|
if (selectedElementId && selectedElement && selectedElement.isConnected) {
|
|
const rect = selectedElement.getBoundingClientRect();
|
|
const viewportHeight = window.innerHeight;
|
|
const viewportWidth = window.innerWidth;
|
|
const isInViewport = rect.top < viewportHeight &&
|
|
rect.bottom > 0 &&
|
|
rect.left < viewportWidth &&
|
|
rect.right > 0;
|
|
const elementPosition = {
|
|
top: rect.top,
|
|
left: rect.left,
|
|
right: rect.right,
|
|
bottom: rect.bottom,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
centerX: rect.left + rect.width / 2,
|
|
centerY: rect.top + rect.height / 2,
|
|
};
|
|
window.parent.postMessage({
|
|
type: "element-position-update",
|
|
position: elementPosition,
|
|
isInViewport: isInViewport,
|
|
visualSelectorId: selectedElementId,
|
|
}, "*");
|
|
}
|
|
break;
|
|
case "popover-drag-state":
|
|
if (message.data && message.data.isDragging !== undefined) {
|
|
isPopoverDragging = message.data.isDragging;
|
|
if (message.data.isDragging) {
|
|
clearHoverOverlays();
|
|
}
|
|
}
|
|
break;
|
|
case "dropdown-state":
|
|
if (message.data && message.data.isOpen !== undefined) {
|
|
isDropdownOpen = message.data.isOpen;
|
|
if (message.data.isOpen) {
|
|
clearHoverOverlays();
|
|
}
|
|
}
|
|
break;
|
|
case "update-theme-variables":
|
|
if (message.data?.variables) {
|
|
const target = message.data.mode === 'dark'
|
|
? document.querySelector('.dark')
|
|
: document.documentElement;
|
|
if (target) {
|
|
for (const [name, value] of Object.entries(message.data.variables)) {
|
|
target.style.setProperty(name, value);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case "inject-font-import":
|
|
if (message.data?.fontUrl) {
|
|
let fontStyle = document.getElementById(THEME_FONT_PREVIEW_ID);
|
|
if (!fontStyle) {
|
|
fontStyle = document.createElement('style');
|
|
fontStyle.id = THEME_FONT_PREVIEW_ID;
|
|
document.head.appendChild(fontStyle);
|
|
}
|
|
fontStyle.textContent = `@import url('${message.data.fontUrl}');`;
|
|
}
|
|
break;
|
|
case "inject-font-face":
|
|
if (message.data) {
|
|
injectFontFaceCss(message.data.id, message.data.css);
|
|
}
|
|
break;
|
|
case "toggle-inline-edit-mode":
|
|
if (message.data) {
|
|
inlineEdit.handleToggleMessage(message.data);
|
|
}
|
|
break;
|
|
case "freeze-vh-units":
|
|
pageHeightBridge.freezeVhUnits(typeof message.referenceVhBase === "number"
|
|
? message.referenceVhBase
|
|
: undefined);
|
|
break;
|
|
case "measure-page-height":
|
|
pageHeightBridge.measurePageHeight(event.origin && event.origin !== "null" ? event.origin : "*", typeof message.settleMs === "number" ? message.settleMs : undefined);
|
|
break;
|
|
case "suppress-auto-popups":
|
|
autoPopupSuppressor.suppress();
|
|
break;
|
|
case "toggle-canvas-wheel-zoom-bridge":
|
|
canvasWheelZoomBridge.enable();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
// Handle window resize to reposition overlays
|
|
const handleResize = () => {
|
|
if (selectedElementId) {
|
|
const elements = findElementsById(selectedElementId);
|
|
selectedOverlays.forEach((overlay, index) => {
|
|
if (index < elements.length) {
|
|
positionOverlay(overlay, elements[index]);
|
|
}
|
|
});
|
|
}
|
|
if (currentHighlightedElements.length > 0) {
|
|
hoverOverlays.forEach((overlay, index) => {
|
|
if (index < currentHighlightedElements.length) {
|
|
positionOverlay(overlay, currentHighlightedElements[index]);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
// Initialize: Add IDs to elements that don't have them but have linenumbers
|
|
const elementsWithLineNumber = document.querySelectorAll("[data-linenumber]:not([data-visual-selector-id])");
|
|
elementsWithLineNumber.forEach((el, index) => {
|
|
const htmlEl = el;
|
|
const id = `visual-id-${htmlEl.dataset.filename}-${htmlEl.dataset.linenumber}-${index}`;
|
|
htmlEl.dataset.visualSelectorId = id;
|
|
});
|
|
// Create mutation observer to detect layout changes
|
|
const mutationObserver = new MutationObserver((mutations) => {
|
|
const needsUpdate = mutations.some((mutation) => {
|
|
const hasVisualId = (node) => {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const el = node;
|
|
if (el.dataset && el.dataset.visualSelectorId) {
|
|
return true;
|
|
}
|
|
for (let i = 0; i < el.children.length; i++) {
|
|
if (hasVisualId(el.children[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
const isLayoutChange = mutation.type === "attributes" &&
|
|
(mutation.attributeName === "style" ||
|
|
mutation.attributeName === "class" ||
|
|
mutation.attributeName === "width" ||
|
|
mutation.attributeName === "height");
|
|
return isLayoutChange && hasVisualId(mutation.target);
|
|
});
|
|
if (needsUpdate) {
|
|
setTimeout(handleResize, REPOSITION_DELAY_MS);
|
|
}
|
|
});
|
|
// Set up event listeners
|
|
window.addEventListener("message", handleMessage);
|
|
window.addEventListener("scroll", handleScroll, true);
|
|
document.addEventListener("scroll", handleScroll, true);
|
|
window.addEventListener("resize", handleResize);
|
|
window.addEventListener("scroll", handleResize);
|
|
// Start observing DOM mutations
|
|
mutationObserver.observe(document.body, {
|
|
attributes: true,
|
|
childList: true,
|
|
subtree: true,
|
|
attributeFilter: ["style", "class", "width", "height"],
|
|
});
|
|
// Send ready message to parent
|
|
window.parent.postMessage({ type: "visual-edit-agent-ready" }, "*");
|
|
}
|
|
//# sourceMappingURL=visual-edit-agent.js.map
|