459 lines
18 KiB
JavaScript
459 lines
18 KiB
JavaScript
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
|