initial commit
This commit is contained in:
356
plugins/visual-editor/edit-mode-script.js
Normal file
356
plugins/visual-editor/edit-mode-script.js
Normal file
@@ -0,0 +1,356 @@
|
||||
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;
|
||||
}
|
||||
|
||||
// Don't handle if selection mode is active
|
||||
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
|
||||
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, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/{/g, "{")
|
||||
.replace(/}/g, "}");
|
||||
|
||||
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() {
|
||||
// Don't enable if selection mode is active
|
||||
if (document.getElementById("root")?.getAttribute("data-selection-mode-enabled") === "true") {
|
||||
console.warn("[EDIT MODE] Cannot enable edit mode while selection mode is active");
|
||||
return;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user