database-petani-mobile/node_modules/@base44/vite-plugin/dist/injections/visual-edit-agent.js
2026-02-23 16:39:35 +07:00

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