import { findElementsById, updateElementClasses } from "./utils.js"; export function setupVisualEditAgent() { // State variables (replacing React useState/useRef) let isVisualEditMode = false; let isPopoverDragging = false; let isDropdownOpen = false; let hoverOverlays = []; let selectedOverlays = []; let currentHighlightedElements = []; let selectedElementId = 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.top = "-27px"; 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 ? "#526cff" : "#DBEAFE"; label.style.borderRadius = "3px"; label.style.minWidth = "24px"; label.style.textAlign = "center"; overlay.appendChild(label); } }; // Clear hover overlays const clearHoverOverlays = () => { hoverOverlays.forEach((overlay) => { if (overlay && overlay.parentNode) { overlay.remove(); } }); hoverOverlays = []; currentHighlightedElements = []; }; // Handle mouse over event const handleMouseOver = (e) => { if (!isVisualEditMode || isPopoverDragging) return; const target = e.target; // Prevent hover effects when a dropdown is open if (isDropdownOpen) { clearHoverOverlays(); return; } // Prevent hover effects on SVG path elements if (target.tagName.toLowerCase() === "path") { clearHoverOverlays(); return; } // Support both data-source-location and data-visual-selector-id const element = target.closest("[data-source-location], [data-visual-selector-id]"); if (!element) { clearHoverOverlays(); return; } // Prefer data-source-location, fallback to data-visual-selector-id const htmlElement = element; const selectorId = htmlElement.dataset.sourceLocation || htmlElement.dataset.visualSelectorId; // Skip if this element is already selected if (selectedElementId === selectorId) { clearHoverOverlays(); return; } // Find all elements with the same ID const elements = findElementsById(selectorId || null); // Clear previous hover overlays clearHoverOverlays(); // Create overlays for all matching elements elements.forEach((el) => { const overlay = createOverlay(false); document.body.appendChild(overlay); hoverOverlays.push(overlay); positionOverlay(overlay, el); }); currentHighlightedElements = elements; }; // Handle mouse out event const handleMouseOut = () => { if (isPopoverDragging) return; clearHoverOverlays(); }; // Handle element click const handleElementClick = (e) => { if (!isVisualEditMode) return; const target = e.target; // 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 clicking on SVG path elements if (target.tagName.toLowerCase() === "path") { return; } // Prevent default behavior immediately when in visual edit mode e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); // Support both data-source-location and data-visual-selector-id const element = target.closest("[data-source-location], [data-visual-selector-id]"); if (!element) { return; } const htmlElement = element; const visualSelectorId = htmlElement.dataset.sourceLocation || htmlElement.dataset.visualSelectorId; // Clear any existing selected overlays selectedOverlays.forEach((overlay) => { if (overlay && overlay.parentNode) { overlay.remove(); } }); selectedOverlays = []; // Find all elements with the same ID const elements = findElementsById(visualSelectorId || null); // Create selected overlays for all matching elements elements.forEach((el) => { const overlay = createOverlay(true); document.body.appendChild(overlay); selectedOverlays.push(overlay); positionOverlay(overlay, el, true); }); selectedElementId = visualSelectorId || null; // Clear hover overlays clearHoverOverlays(); // Calculate element position for popover positioning const rect = element.getBoundingClientRect(); 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, }; // Send message to parent window with element info including position const svgElement = element; const elementData = { type: "element-selected", tagName: element.tagName, classes: svgElement.className?.baseVal || element.className || "", visualSelectorId: visualSelectorId, content: element.innerText, dataSourceLocation: htmlElement.dataset.sourceLocation, isDynamicContent: htmlElement.dataset.dynamicContent === "true", linenumber: htmlElement.dataset.linenumber, filename: htmlElement.dataset.filename, position: elementPosition, }; window.parent.postMessage(elementData, "*"); }; // Unselect the current element const unselectElement = () => { selectedOverlays.forEach((overlay) => { if (overlay && overlay.parentNode) { overlay.remove(); } }); selectedOverlays = []; selectedElementId = null; }; 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]); } }); } } }, 50); }; // Update element content by visual selector ID const updateElementContent = (visualSelectorId, content) => { const elements = findElementsById(visualSelectorId); if (elements.length === 0) { return; } elements.forEach((element) => { element.innerText = content; }); setTimeout(() => { if (selectedElementId === visualSelectorId) { selectedOverlays.forEach((overlay, index) => { if (index < elements.length) { positionOverlay(overlay, elements[index]); } }); } }, 50); }; // Toggle visual edit mode const toggleVisualEditMode = (isEnabled) => { isVisualEditMode = isEnabled; if (!isEnabled) { clearHoverOverlays(); selectedOverlays.forEach((overlay) => { if (overlay && overlay.parentNode) { overlay.remove(); } }); selectedOverlays = []; currentHighlightedElements = []; selectedElementId = null; document.body.style.cursor = "default"; document.removeEventListener("mouseover", handleMouseOver); document.removeEventListener("mouseout", handleMouseOut); document.removeEventListener("click", handleElementClick, true); } else { document.body.style.cursor = "crosshair"; document.addEventListener("mouseover", handleMouseOver); document.addEventListener("mouseout", handleMouseOut); document.addEventListener("click", handleElementClick, true); } }; // Handle scroll events to update popover position const handleScroll = () => { if (selectedElementId) { const elements = findElementsById(selectedElementId); if (elements.length > 0) { const element = elements[0]; 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); 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 "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); } else { console.warn("[VisualEditAgent] Invalid update-content message:", message); } break; case "request-element-position": if (selectedElementId) { const elements = findElementsById(selectedElementId); if (elements.length > 0) { const element = elements[0]; 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, }, "*"); } } 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; 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, 50); } }); // 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