430 lines
9.9 KiB
JavaScript
430 lines
9.9 KiB
JavaScript
const ALLOWED_PARENT_ORIGINS = [
|
|
'https://horizons.hostinger.com',
|
|
'https://horizons.hostinger.dev',
|
|
'https://horizons-frontend-local.hostinger.dev',
|
|
'http://localhost:4000',
|
|
];
|
|
|
|
const IMPORTANT_STYLES = [
|
|
'display',
|
|
'position',
|
|
'flex-direction',
|
|
'justify-content',
|
|
'align-items',
|
|
'width',
|
|
'height',
|
|
'padding',
|
|
'margin',
|
|
'border',
|
|
'background-color',
|
|
'color',
|
|
'font-size',
|
|
'font-weight',
|
|
'font-family',
|
|
'border-radius',
|
|
'box-shadow',
|
|
'gap',
|
|
'grid-template-columns',
|
|
];
|
|
|
|
const PRIMARY_400_COLOR = '#7B68EE';
|
|
const TEXT_CONTEXT_MAX_LENGTH = 500;
|
|
const DATA_SELECTION_MODE_ENABLED_ATTRIBUTE = 'data-selection-mode-enabled';
|
|
const MESSAGE_TYPE_ENABLE_SELECTION_MODE = 'enableSelectionMode';
|
|
const MESSAGE_TYPE_DISABLE_SELECTION_MODE = 'disableSelectionMode';
|
|
|
|
let selectionModeEnabled = false;
|
|
let currentHoverElement = null;
|
|
let overlayDiv = null;
|
|
let selectedOverlayDiv = null;
|
|
let selectedElement = null;
|
|
|
|
|
|
function injectStyles() {
|
|
if (document.getElementById('selection-mode-styles')) {
|
|
return;
|
|
}
|
|
|
|
const style = document.createElement('style');
|
|
style.id = 'selection-mode-styles';
|
|
style.textContent = `
|
|
#selection-mode-overlay {
|
|
position: absolute;
|
|
border: 2px dashed ${PRIMARY_400_COLOR};
|
|
pointer-events: none;
|
|
z-index: 999999;
|
|
}
|
|
#selection-mode-selected-overlay {
|
|
position: absolute;
|
|
border: 3px solid ${PRIMARY_400_COLOR};
|
|
pointer-events: none;
|
|
z-index: 999998;
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
function getParentOrigin() {
|
|
if (
|
|
window.location.ancestorOrigins
|
|
&& window.location.ancestorOrigins.length > 0
|
|
) {
|
|
return window.location.ancestorOrigins[0];
|
|
}
|
|
|
|
if (document.referrer) {
|
|
try {
|
|
return new URL(document.referrer).origin;
|
|
} catch {
|
|
console.warn('[SELECTION MODE] Invalid referrer URL:', document.referrer);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Extract file path from React Fiber metadata (simplified - only for filePath)
|
|
* @param {*} node - DOM node
|
|
* @returns {string|null} - File path if found, null otherwise
|
|
*/
|
|
function getFilePathFromNode(node) {
|
|
const fiberKey = Object.keys(node).find(k => k.startsWith('__reactFiber'));
|
|
if (!fiberKey) {
|
|
return null;
|
|
}
|
|
|
|
const fiber = node[fiberKey];
|
|
if (!fiber) {
|
|
return null;
|
|
}
|
|
|
|
// Traverse up the fiber tree to find source metadata
|
|
let currentFiber = fiber;
|
|
while (currentFiber) {
|
|
const source = currentFiber._debugSource
|
|
|| currentFiber.memoizedProps?.__source
|
|
|| currentFiber.pendingProps?.__source;
|
|
|
|
if (source?.fileName) {
|
|
return source.fileName;
|
|
}
|
|
|
|
currentFiber = currentFiber.return;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Generate a CSS selector path to uniquely identify the element
|
|
* @param {*} element
|
|
* @returns {string} CSS selector path
|
|
*/
|
|
function getPathToElement(element) {
|
|
const path = [];
|
|
let current = element;
|
|
let depth = 0;
|
|
const maxDepth = 20; // Prevent infinite loops
|
|
|
|
while (current && current.nodeType === Node.ELEMENT_NODE && depth < maxDepth) {
|
|
let selector = current.nodeName.toLowerCase();
|
|
|
|
if (current.id) {
|
|
selector += `#${current.id}`;
|
|
path.unshift(selector);
|
|
break; // ID is unique, stop here
|
|
}
|
|
|
|
if (current.className && typeof current.className === 'string') {
|
|
const classes = current.className.trim().split(/\s+/).filter(c => c.length > 0);
|
|
if (classes.length > 0) {
|
|
selector += `.${classes.join('.')}`;
|
|
}
|
|
}
|
|
|
|
if (current.parentElement) {
|
|
const siblings = Array.from(current.parentElement.children);
|
|
const sameTypeSiblings = siblings.filter(s => s.nodeName === current.nodeName);
|
|
if (sameTypeSiblings.length > 1) {
|
|
const index = sameTypeSiblings.indexOf(current) + 1;
|
|
selector += `:nth-of-type(${index})`;
|
|
}
|
|
}
|
|
|
|
path.unshift(selector);
|
|
current = current.parentElement;
|
|
depth++;
|
|
}
|
|
|
|
return path.join(' > ');
|
|
}
|
|
|
|
function getComputedStyles(element) {
|
|
const computedStyles = window.getComputedStyle(element);
|
|
|
|
return Object.fromEntries(IMPORTANT_STYLES.map((style) => {
|
|
const styleValue = computedStyles.getPropertyValue(style)?.trim();
|
|
|
|
return styleValue && styleValue !== 'none' && styleValue !== 'normal'
|
|
? [style, styleValue]
|
|
: null;
|
|
})
|
|
.filter(Boolean));
|
|
}
|
|
|
|
function extractDOMContext(element) {
|
|
if (!element) {
|
|
return null;
|
|
}
|
|
|
|
const textContent = element.textContent?.trim();
|
|
|
|
return {
|
|
outerHTML: element.outerHTML,
|
|
selector: getPathToElement(element),
|
|
attributes: (element.attributes && element.attributes.length > 0)
|
|
? Object.fromEntries(Array.from(element.attributes).map((attr) => [attr.name, attr.value]))
|
|
: {},
|
|
computedStyles: getComputedStyles(element),
|
|
textContent: (textContent && textContent.length > 0 && textContent.length < TEXT_CONTEXT_MAX_LENGTH)
|
|
? element.textContent?.trim()
|
|
: null
|
|
};
|
|
}
|
|
|
|
function createOverlay() {
|
|
if (overlayDiv) {
|
|
return;
|
|
}
|
|
|
|
injectStyles();
|
|
|
|
overlayDiv = document.createElement('div');
|
|
overlayDiv.id = 'selection-mode-overlay';
|
|
document.body.appendChild(overlayDiv);
|
|
}
|
|
|
|
function createSelectedOverlay() {
|
|
if (selectedOverlayDiv) {
|
|
return;
|
|
}
|
|
|
|
injectStyles();
|
|
|
|
selectedOverlayDiv = document.createElement('div');
|
|
selectedOverlayDiv.id = 'selection-mode-selected-overlay';
|
|
document.body.appendChild(selectedOverlayDiv);
|
|
}
|
|
|
|
function removeOverlay() {
|
|
if (overlayDiv && overlayDiv.parentNode) {
|
|
overlayDiv.parentNode.removeChild(overlayDiv);
|
|
overlayDiv = null;
|
|
}
|
|
if (selectedOverlayDiv && selectedOverlayDiv.parentNode) {
|
|
selectedOverlayDiv.parentNode.removeChild(selectedOverlayDiv);
|
|
selectedOverlayDiv = null;
|
|
}
|
|
}
|
|
|
|
function showOverlay(element) {
|
|
if (!overlayDiv) {
|
|
createOverlay();
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
overlayDiv.style.left = `${rect.left + window.scrollX}px`;
|
|
overlayDiv.style.top = `${rect.top + window.scrollY}px`;
|
|
overlayDiv.style.width = `${rect.width}px`;
|
|
overlayDiv.style.height = `${rect.height}px`;
|
|
overlayDiv.style.display = 'block';
|
|
}
|
|
|
|
function showSelectedOverlay(element) {
|
|
if (!selectedOverlayDiv) {
|
|
createSelectedOverlay();
|
|
}
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
selectedOverlayDiv.style.left = `${rect.left + window.scrollX}px`;
|
|
selectedOverlayDiv.style.top = `${rect.top + window.scrollY}px`;
|
|
selectedOverlayDiv.style.width = `${rect.width}px`;
|
|
selectedOverlayDiv.style.height = `${rect.height}px`;
|
|
selectedOverlayDiv.style.display = 'block';
|
|
}
|
|
|
|
function hideOverlay() {
|
|
if (overlayDiv) {
|
|
overlayDiv.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function handleMouseMove(event) {
|
|
if (!selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
const element = document.elementFromPoint(event.clientX, event.clientY);
|
|
if (!element) {
|
|
hideOverlay();
|
|
currentHoverElement = null;
|
|
return;
|
|
}
|
|
|
|
if (element === overlayDiv || element === selectedOverlayDiv) {
|
|
return;
|
|
}
|
|
|
|
// Only update if we're hovering a different element
|
|
if (currentHoverElement !== element) {
|
|
currentHoverElement = element;
|
|
|
|
// Show outline on the element
|
|
showOverlay(element);
|
|
}
|
|
}
|
|
|
|
function handleTouchStart(event) {
|
|
if (!selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
const touch = event.touches[0];
|
|
if (!touch) {
|
|
return;
|
|
}
|
|
|
|
const element = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
if (!element) {
|
|
currentHoverElement = null;
|
|
return;
|
|
}
|
|
|
|
if (element === overlayDiv || element === selectedOverlayDiv) {
|
|
return;
|
|
}
|
|
|
|
currentHoverElement = element;
|
|
|
|
showOverlay(element);
|
|
}
|
|
|
|
function stripFilePath(filePath) {
|
|
if (!filePath) {
|
|
return filePath;
|
|
}
|
|
|
|
const publicHtmlIndex = filePath.indexOf('public_html/');
|
|
if (publicHtmlIndex !== -1) {
|
|
return filePath.substring(publicHtmlIndex + 'public_html/'.length);
|
|
}
|
|
|
|
return filePath;
|
|
}
|
|
|
|
function handleClick(event) {
|
|
if (!selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
if (!currentHoverElement) {
|
|
const element = document.elementFromPoint(event.clientX, event.clientY);
|
|
|
|
if (!element || element === overlayDiv || element === selectedOverlayDiv) {
|
|
return;
|
|
}
|
|
|
|
currentHoverElement = element;
|
|
}
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
event.stopImmediatePropagation();
|
|
|
|
const domContext = extractDOMContext(currentHoverElement);
|
|
|
|
if (!domContext) {
|
|
return;
|
|
}
|
|
|
|
selectedElement = currentHoverElement;
|
|
if (selectedElement) {
|
|
showSelectedOverlay(selectedElement);
|
|
}
|
|
|
|
// Extract file path from React Fiber (if available)
|
|
const filePath = getFilePathFromNode(currentHoverElement);
|
|
const strippedFilePath = filePath ? stripFilePath(filePath) : undefined;
|
|
|
|
// Send domContext and filePath to parent window
|
|
const parentOrigin = getParentOrigin();
|
|
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
|
|
window.parent.postMessage(
|
|
{
|
|
type: 'elementSelected',
|
|
payload: {
|
|
filePath: strippedFilePath,
|
|
domContext,
|
|
},
|
|
},
|
|
parentOrigin,
|
|
);
|
|
}
|
|
}
|
|
|
|
function handleMouseLeave() {
|
|
if (!selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
hideOverlay();
|
|
currentHoverElement = null;
|
|
}
|
|
|
|
function enableSelectionMode() {
|
|
if (selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
selectionModeEnabled = true;
|
|
document.getElementById('root')?.setAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE, 'true');
|
|
|
|
document.body.style.userSelect = 'none';
|
|
|
|
createOverlay();
|
|
document.addEventListener('mousemove', handleMouseMove, true);
|
|
document.addEventListener('touchstart', handleTouchStart, true);
|
|
document.addEventListener('click', handleClick, true);
|
|
document.addEventListener('mouseleave', handleMouseLeave, true);
|
|
}
|
|
|
|
function disableSelectionMode() {
|
|
if (!selectionModeEnabled) {
|
|
return;
|
|
}
|
|
|
|
selectionModeEnabled = false;
|
|
document.getElementById('root')?.removeAttribute(DATA_SELECTION_MODE_ENABLED_ATTRIBUTE);
|
|
|
|
document.body.style.userSelect = '';
|
|
|
|
hideOverlay();
|
|
removeOverlay();
|
|
currentHoverElement = null;
|
|
selectedElement = null;
|
|
|
|
document.removeEventListener('mousemove', handleMouseMove, true);
|
|
document.removeEventListener('touchstart', handleTouchStart, true);
|
|
document.removeEventListener('click', handleClick, true);
|
|
document.removeEventListener('mouseleave', handleMouseLeave, true);
|
|
}
|
|
|
|
window.addEventListener('message', (event) => {
|
|
if (event.data?.type === MESSAGE_TYPE_ENABLE_SELECTION_MODE) {
|
|
enableSelectionMode();
|
|
}
|
|
if (event.data?.type === MESSAGE_TYPE_DISABLE_SELECTION_MODE) {
|
|
disableSelectionMode();
|
|
}
|
|
});
|