Files
backoffice/src/plugins/visual-editor/edit-mode-script.js
2026-01-01 12:47:02 +01:00

316 lines
9.2 KiB
JavaScript

import { POPUP_STYLES } from './plugins/visual-editor/visual-editor-config.js';
const PLUGIN_APPLY_EDIT_API_URL = '/api/apply-edit';
const ALLOWED_PARENT_ORIGINS = [
'https://horizons.hostinger.com',
'https://horizons.hostinger.dev',
'https://horizons-frontend-local.hostinger.dev',
'http://localhost:4000',
];
let disabledTooltipElement = null;
let currentDisabledHoverElement = null;
let translations = {
disabledTooltipText: "This text can be changed only through chat.",
disabledTooltipTextImage: "This image can only be changed through chat."
};
let areStylesInjected = false;
let globalEventHandlers = null;
let currentEditingInfo = null;
function injectPopupStyles() {
if (areStylesInjected) return;
const styleElement = document.createElement('style');
styleElement.id = 'inline-editor-styles';
styleElement.textContent = POPUP_STYLES;
document.head.appendChild(styleElement);
areStylesInjected = true;
}
function findEditableElementAtPoint(event) {
let editableElement = event.target.closest('[data-edit-id]');
if (editableElement) {
return editableElement;
}
const elementsAtPoint = document.elementsFromPoint(event.clientX, event.clientY);
const found = elementsAtPoint.find(el => el !== event.target && el.hasAttribute('data-edit-id'));
if (found) return found;
return null;
}
function findDisabledElementAtPoint(event) {
const direct = event.target.closest('[data-edit-disabled]');
if (direct) return direct;
const elementsAtPoint = document.elementsFromPoint(event.clientX, event.clientY);
const found = elementsAtPoint.find(el => el !== event.target && el.hasAttribute('data-edit-disabled'));
if (found) return found;
return null;
}
function showPopup(targetElement, editId, currentContent, isImage = false) {
currentEditingInfo = { editId, targetElement };
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const eventType = isImage ? 'imageEditEnter' : 'editEnter';
window.parent.postMessage({
type: eventType,
payload: { currentText: currentContent }
}, parentOrigin);
}
}
function handleGlobalEvent(event) {
if (!document.getElementById('root')?.getAttribute('data-edit-mode-enabled')) {
return;
}
if (event.target.closest('#inline-editor-popup')) {
return;
}
const editableElement = findEditableElementAtPoint(event);
if (editableElement) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (event.type === 'click') {
const editId = editableElement.getAttribute('data-edit-id');
if (!editId) {
console.warn('[INLINE EDITOR] Clicked element missing data-edit-id');
return;
}
const isImage = editableElement.tagName.toLowerCase() === 'img';
let currentContent = '';
if (isImage) {
currentContent = editableElement.getAttribute('src') || '';
} else {
currentContent = editableElement.textContent || '';
}
showPopup(editableElement, editId, currentContent, isImage);
}
} else {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
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 (e) {
console.warn('Invalid referrer URL:', document.referrer);
}
}
return null;
}
async function handleEditSave(updatedText) {
const newText = updatedText
// Replacing characters that cause Babel parser to crash
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/{/g, '&#123;')
.replace(/}/g, '&#125;')
const { editId } = currentEditingInfo;
try {
const response = await fetch(PLUGIN_APPLY_EDIT_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
editId: editId,
newFullText: newText
}),
});
const result = await response.json();
if (result.success) {
const parentOrigin = getParentOrigin();
if (parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
window.parent.postMessage({
type: 'editApplied',
payload: {
editId: editId,
fileContent: result.newFileContent,
beforeCode: result.beforeCode,
afterCode: result.afterCode,
}
}, parentOrigin);
} else {
console.error('Unauthorized parent origin:', parentOrigin);
}
} else {
console.error(`[vite][visual-editor] Error saving changes: ${result.error}`);
}
} catch (error) {
console.error(`[vite][visual-editor] Error during fetch for ${editId}:`, error);
}
}
function createDisabledTooltip() {
if (disabledTooltipElement) return;
disabledTooltipElement = document.createElement('div');
disabledTooltipElement.id = 'inline-editor-disabled-tooltip';
document.body.appendChild(disabledTooltipElement);
}
function showDisabledTooltip(targetElement, isImage = false) {
if (!disabledTooltipElement) createDisabledTooltip();
disabledTooltipElement.textContent = isImage ? translations.disabledTooltipTextImage : translations.disabledTooltipText;
if (!disabledTooltipElement.isConnected) {
document.body.appendChild(disabledTooltipElement);
}
disabledTooltipElement.classList.add('tooltip-active');
const tooltipWidth = disabledTooltipElement.offsetWidth;
const tooltipHeight = disabledTooltipElement.offsetHeight;
const rect = targetElement.getBoundingClientRect();
// Ensures that tooltip is not off the screen with 5px margin
let newLeft = rect.left + window.scrollX + (rect.width / 2) - (tooltipWidth / 2);
let newTop = rect.bottom + window.scrollY + 5;
if (newLeft < window.scrollX) {
newLeft = window.scrollX + 5;
}
if (newLeft + tooltipWidth > window.innerWidth + window.scrollX) {
newLeft = window.innerWidth + window.scrollX - tooltipWidth - 5;
}
if (newTop + tooltipHeight > window.innerHeight + window.scrollY) {
newTop = rect.top + window.scrollY - tooltipHeight - 5;
}
if (newTop < window.scrollY) {
newTop = window.scrollY + 5;
}
disabledTooltipElement.style.left = `${newLeft}px`;
disabledTooltipElement.style.top = `${newTop}px`;
}
function hideDisabledTooltip() {
if (disabledTooltipElement) {
disabledTooltipElement.classList.remove('tooltip-active');
}
}
function handleDisabledElementHover(event) {
const isImage = event.currentTarget.tagName.toLowerCase() === 'img';
showDisabledTooltip(event.currentTarget, isImage);
}
function handleDisabledElementLeave() {
hideDisabledTooltip();
}
function handleDisabledGlobalHover(event) {
const disabledElement = findDisabledElementAtPoint(event);
if (disabledElement) {
if (currentDisabledHoverElement !== disabledElement) {
currentDisabledHoverElement = disabledElement;
const isImage = disabledElement.tagName.toLowerCase() === 'img';
showDisabledTooltip(disabledElement, isImage);
}
} else {
if (currentDisabledHoverElement) {
currentDisabledHoverElement = null;
hideDisabledTooltip();
}
}
}
function enableEditMode() {
document.getElementById('root')?.setAttribute('data-edit-mode-enabled', 'true');
injectPopupStyles();
if (!globalEventHandlers) {
globalEventHandlers = {
mousedown: handleGlobalEvent,
pointerdown: handleGlobalEvent,
click: handleGlobalEvent
};
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.addEventListener(eventType, handler, true);
});
}
document.addEventListener('mousemove', handleDisabledGlobalHover, true);
document.querySelectorAll('[data-edit-disabled]').forEach(el => {
el.removeEventListener('mouseenter', handleDisabledElementHover);
el.addEventListener('mouseenter', handleDisabledElementHover);
el.removeEventListener('mouseleave', handleDisabledElementLeave);
el.addEventListener('mouseleave', handleDisabledElementLeave);
});
}
function disableEditMode() {
document.getElementById('root')?.removeAttribute('data-edit-mode-enabled');
hideDisabledTooltip();
if (globalEventHandlers) {
Object.entries(globalEventHandlers).forEach(([eventType, handler]) => {
document.removeEventListener(eventType, handler, true);
});
globalEventHandlers = null;
}
document.removeEventListener('mousemove', handleDisabledGlobalHover, true);
currentDisabledHoverElement = null;
document.querySelectorAll('[data-edit-disabled]').forEach(el => {
el.removeEventListener('mouseenter', handleDisabledElementHover);
el.removeEventListener('mouseleave', handleDisabledElementLeave);
});
}
window.addEventListener("message", function(event) {
if (event.data?.type === "edit-save") {
handleEditSave(event.data?.payload?.newText);
}
if (event.data?.type === "enable-edit-mode") {
if (event.data?.translations) {
translations = { ...translations, ...event.data.translations };
}
enableEditMode();
}
if (event.data?.type === "disable-edit-mode") {
disableEditMode();
}
});