Initial commit

This commit is contained in:
balvarez
2026-01-01 12:47:02 +01:00
commit 9863b8ecb5
40 changed files with 3027 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
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();
}
});

View File

@@ -0,0 +1,137 @@
export const POPUP_STYLES = `
#inline-editor-popup {
width: 360px;
position: fixed;
z-index: 10000;
background: #161718;
color: white;
border: 1px solid #4a5568;
border-radius: 16px;
padding: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
flex-direction: column;
gap: 10px;
display: none;
}
@media (max-width: 768px) {
#inline-editor-popup {
width: calc(100% - 20px);
}
}
#inline-editor-popup.is-active {
display: flex;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#inline-editor-popup.is-disabled-view {
padding: 10px 15px;
}
#inline-editor-popup textarea {
height: 100px;
padding: 4px 8px;
background: transparent;
color: white;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.42;
resize: none;
outline: none;
}
#inline-editor-popup .button-container {
display: flex;
justify-content: flex-end;
gap: 10px;
}
#inline-editor-popup .popup-button {
border: none;
padding: 6px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.75rem;
font-weight: 700;
height: 34px;
outline: none;
}
#inline-editor-popup .save-button {
background: #673de6;
color: white;
}
#inline-editor-popup .cancel-button {
background: transparent;
border: 1px solid #3b3d4a;
color: white;
&:hover {
background:#474958;
}
}
`;
export function getPopupHTMLTemplate(saveLabel, cancelLabel) {
return `
<textarea></textarea>
<div class="button-container">
<button class="popup-button cancel-button">${cancelLabel}</button>
<button class="popup-button save-button">${saveLabel}</button>
</div>
`;
};
export const EDIT_MODE_STYLES = `
#root[data-edit-mode-enabled="true"] [data-edit-id] {
cursor: pointer;
outline: 2px dashed #357DF9;
outline-offset: 2px;
min-height: 1em;
}
#root[data-edit-mode-enabled="true"] img[data-edit-id] {
outline-offset: -2px;
}
#root[data-edit-mode-enabled="true"] {
cursor: pointer;
}
#root[data-edit-mode-enabled="true"] [data-edit-id]:hover {
background-color: #357DF933;
outline-color: #357DF9;
}
@keyframes fadeInTooltip {
from {
opacity: 0;
transform: translateY(5px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#inline-editor-disabled-tooltip {
display: none;
opacity: 0;
position: absolute;
background-color: #1D1E20;
color: white;
padding: 4px 8px;
border-radius: 8px;
z-index: 10001;
font-size: 14px;
border: 1px solid #3B3D4A;
max-width: 184px;
text-align: center;
}
#inline-editor-disabled-tooltip.tooltip-active {
display: block;
animation: fadeInTooltip 0.2s ease-out forwards;
}
`;

View File

@@ -0,0 +1,32 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { fileURLToPath } from 'url';
import { EDIT_MODE_STYLES } from './visual-editor-config';
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, '..');
export default function inlineEditDevPlugin() {
return {
name: 'vite:inline-edit-dev',
apply: 'serve',
transformIndexHtml() {
const scriptPath = resolve(__dirname, 'edit-mode-script.js');
const scriptContent = readFileSync(scriptPath, 'utf-8');
return [
{
tag: 'script',
attrs: { type: 'module' },
children: scriptContent,
injectTo: 'body'
},
{
tag: 'style',
children: EDIT_MODE_STYLES,
injectTo: 'head'
}
];
}
};
}

View File

@@ -0,0 +1,384 @@
import path from 'path';
import { fileURLToPath } from 'url';
import { parse } from '@babel/parser';
import traverseBabel from '@babel/traverse';
import generate from '@babel/generator';
import * as t from '@babel/types';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const VITE_PROJECT_ROOT = path.resolve(__dirname, '../..');
const EDITABLE_HTML_TAGS = ["a", "Button", "button", "p", "span", "h1", "h2", "h3", "h4", "h5", "h6", "label", "Label", "img"];
function parseEditId(editId) {
const parts = editId.split(':');
if (parts.length < 3) {
return null;
}
const column = parseInt(parts.at(-1), 10);
const line = parseInt(parts.at(-2), 10);
const filePath = parts.slice(0, -2).join(':');
if (!filePath || isNaN(line) || isNaN(column)) {
return null;
}
return { filePath, line, column };
}
function checkTagNameEditable(openingElementNode, editableTagsList) {
if (!openingElementNode || !openingElementNode.name) return false;
const nameNode = openingElementNode.name;
// Check 1: Direct name (for <p>, <Button>)
if (nameNode.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.name)) {
return true;
}
// Check 2: Property name of a member expression (for <motion.h1>, check if "h1" is in editableTagsList)
if (nameNode.type === 'JSXMemberExpression' && nameNode.property && nameNode.property.type === 'JSXIdentifier' && editableTagsList.includes(nameNode.property.name)) {
return true;
}
return false;
}
function validateImageSrc(openingNode) {
if (!openingNode || !openingNode.name || openingNode.name.name !== 'img') {
return { isValid: true, reason: null }; // Not an image, skip validation
}
const hasPropsSpread = openingNode.attributes.some(attr =>
t.isJSXSpreadAttribute(attr) &&
attr.argument &&
t.isIdentifier(attr.argument) &&
attr.argument.name === 'props'
);
if (hasPropsSpread) {
return { isValid: false, reason: 'props-spread' };
}
const srcAttr = openingNode.attributes.find(attr =>
t.isJSXAttribute(attr) &&
attr.name &&
attr.name.name === 'src'
);
if (!srcAttr) {
return { isValid: false, reason: 'missing-src' };
}
if (!t.isStringLiteral(srcAttr.value)) {
return { isValid: false, reason: 'dynamic-src' };
}
if (!srcAttr.value.value || srcAttr.value.value.trim() === '') {
return { isValid: false, reason: 'empty-src' };
}
return { isValid: true, reason: null };
}
export default function inlineEditPlugin() {
return {
name: 'vite-inline-edit-plugin',
enforce: 'pre',
transform(code, id) {
if (!/\.(jsx|tsx)$/.test(id) || !id.startsWith(VITE_PROJECT_ROOT) || id.includes('node_modules')) {
return null;
}
const relativeFilePath = path.relative(VITE_PROJECT_ROOT, id);
const webRelativeFilePath = relativeFilePath.split(path.sep).join('/');
try {
const babelAst = parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true
});
let attributesAdded = 0;
traverseBabel.default(babelAst, {
enter(path) {
if (path.isJSXOpeningElement()) {
const openingNode = path.node;
const elementNode = path.parentPath.node; // The JSXElement itself
if (!openingNode.loc) {
return;
}
const alreadyHasId = openingNode.attributes.some(
(attr) => t.isJSXAttribute(attr) && attr.name.name === 'data-edit-id'
);
if (alreadyHasId) {
return;
}
// Condition 1: Is the current element tag type editable?
const isCurrentElementEditable = checkTagNameEditable(openingNode, EDITABLE_HTML_TAGS);
if (!isCurrentElementEditable) {
return;
}
const imageValidation = validateImageSrc(openingNode);
if (!imageValidation.isValid) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
let shouldBeDisabledDueToChildren = false;
// Condition 2: Does the element have dynamic or editable children
if (t.isJSXElement(elementNode) && elementNode.children) {
// Check if element has {...props} spread attribute - disable editing if it does
const hasPropsSpread = openingNode.attributes.some(attr => t.isJSXSpreadAttribute(attr)
&& attr.argument
&& t.isIdentifier(attr.argument)
&& attr.argument.name === 'props'
);
const hasDynamicChild = elementNode.children.some(child =>
t.isJSXExpressionContainer(child)
);
if (hasDynamicChild || hasPropsSpread) {
shouldBeDisabledDueToChildren = true;
}
}
if (!shouldBeDisabledDueToChildren && t.isJSXElement(elementNode) && elementNode.children) {
const hasEditableJsxChild = elementNode.children.some(child => {
if (t.isJSXElement(child)) {
return checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS);
}
return false;
});
if (hasEditableJsxChild) {
shouldBeDisabledDueToChildren = true;
}
}
if (shouldBeDisabledDueToChildren) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral('true')
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
// Condition 3: Parent is non-editable if AT LEAST ONE child JSXElement is a non-editable type.
if (t.isJSXElement(elementNode) && elementNode.children && elementNode.children.length > 0) {
let hasNonEditableJsxChild = false;
for (const child of elementNode.children) {
if (t.isJSXElement(child)) {
if (!checkTagNameEditable(child.openingElement, EDITABLE_HTML_TAGS)) {
hasNonEditableJsxChild = true;
break;
}
}
}
if (hasNonEditableJsxChild) {
const disabledAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-disabled'),
t.stringLiteral("true")
);
openingNode.attributes.push(disabledAttribute);
attributesAdded++;
return;
}
}
// Condition 4: Is any ancestor JSXElement also editable?
let currentAncestorCandidatePath = path.parentPath.parentPath;
while (currentAncestorCandidatePath) {
const ancestorJsxElementPath = currentAncestorCandidatePath.isJSXElement()
? currentAncestorCandidatePath
: currentAncestorCandidatePath.findParent(p => p.isJSXElement());
if (!ancestorJsxElementPath) {
break;
}
if (checkTagNameEditable(ancestorJsxElementPath.node.openingElement, EDITABLE_HTML_TAGS)) {
return;
}
currentAncestorCandidatePath = ancestorJsxElementPath.parentPath;
}
const line = openingNode.loc.start.line;
const column = openingNode.loc.start.column + 1;
const editId = `${webRelativeFilePath}:${line}:${column}`;
const idAttribute = t.jsxAttribute(
t.jsxIdentifier('data-edit-id'),
t.stringLiteral(editId)
);
openingNode.attributes.push(idAttribute);
attributesAdded++;
}
}
});
if (attributesAdded > 0) {
const generateFunction = generate.default || generate;
const output = generateFunction(babelAst, {
sourceMaps: true,
sourceFileName: webRelativeFilePath
}, code);
return { code: output.code, map: output.map };
}
return null;
} catch (error) {
console.error(`[vite][visual-editor] Error transforming ${id}:`, error);
return null;
}
},
// Updates source code based on the changes received from the client
configureServer(server) {
server.middlewares.use('/api/apply-edit', async (req, res, next) => {
if (req.method !== 'POST') return next();
let body = '';
req.on('data', chunk => { body += chunk.toString(); });
req.on('end', async () => {
let absoluteFilePath = '';
try {
const { editId, newFullText } = JSON.parse(body);
if (!editId || typeof newFullText === 'undefined') {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Missing editId or newFullText' }));
}
const parsedId = parseEditId(editId);
if (!parsedId) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Invalid editId format (filePath:line:column)' }));
}
const { filePath, line, column } = parsedId;
absoluteFilePath = path.resolve(VITE_PROJECT_ROOT, filePath);
if (filePath.includes('..') || !absoluteFilePath.startsWith(VITE_PROJECT_ROOT) || absoluteFilePath.includes('node_modules')) {
res.writeHead(400, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Invalid path' }));
}
const originalContent = fs.readFileSync(absoluteFilePath, 'utf-8');
const babelAst = parse(originalContent, {
sourceType: 'module',
plugins: ['jsx', 'typescript'],
errorRecovery: true
});
let targetNodePath = null;
const visitor = {
JSXOpeningElement(path) {
const node = path.node;
if (node.loc && node.loc.start.line === line && node.loc.start.column + 1 === column) {
targetNodePath = path;
path.stop();
}
}
};
traverseBabel.default(babelAst, visitor);
if (!targetNodePath) {
res.writeHead(404, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Target node not found by line/column', editId }));
}
const generateFunction = generate.default || generate;
const targetOpeningElement = targetNodePath.node;
const parentElementNode = targetNodePath.parentPath?.node;
const isImageElement = targetOpeningElement.name && targetOpeningElement.name.name === 'img';
let beforeCode = '';
let afterCode = '';
let modified = false;
if (isImageElement) {
// Handle image src attribute update
const beforeOutput = generateFunction(targetOpeningElement, {});
beforeCode = beforeOutput.code;
const srcAttr = targetOpeningElement.attributes.find(attr =>
t.isJSXAttribute(attr) && attr.name && attr.name.name === 'src'
);
if (srcAttr && t.isStringLiteral(srcAttr.value)) {
srcAttr.value = t.stringLiteral(newFullText);
modified = true;
const afterOutput = generateFunction(targetOpeningElement, {});
afterCode = afterOutput.code;
}
} else {
if (parentElementNode && t.isJSXElement(parentElementNode)) {
const beforeOutput = generateFunction(parentElementNode, {});
beforeCode = beforeOutput.code;
parentElementNode.children = [];
if (newFullText && newFullText.trim() !== '') {
const newTextNode = t.jsxText(newFullText);
parentElementNode.children.push(newTextNode);
}
modified = true;
const afterOutput = generateFunction(parentElementNode, {});
afterCode = afterOutput.code;
}
}
if (!modified) {
res.writeHead(409, { 'Content-Type': 'application/json' });
return res.end(JSON.stringify({ error: 'Could not apply changes to AST.' }));
}
const output = generateFunction(babelAst, {});
const newContent = output.code;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
newFileContent: newContent,
beforeCode,
afterCode,
}));
} catch (error) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Internal server error during edit application.' }));
}
});
});
}
};
}

View File

@@ -0,0 +1,125 @@
export default function iframeRouteRestorationPlugin() {
return {
name: 'vite:iframe-route-restoration',
apply: 'serve',
transformIndexHtml() {
const script = `
const ALLOWED_PARENT_ORIGINS = [
"https://horizons.hostinger.com",
"https://horizons.hostinger.dev",
"https://horizons-frontend-local.hostinger.dev",
];
// Check to see if the page is in an iframe
if (window.self !== window.top) {
const STORAGE_KEY = 'horizons-iframe-saved-route';
const getCurrentRoute = () => location.pathname + location.search + location.hash;
const save = () => {
try {
const currentRoute = getCurrentRoute();
sessionStorage.setItem(STORAGE_KEY, currentRoute);
window.parent.postMessage({message: 'route-changed', route: currentRoute}, '*');
} catch {}
};
const replaceHistoryState = (url) => {
try {
history.replaceState(null, '', url);
window.dispatchEvent(new PopStateEvent('popstate', { state: history.state }));
return true;
} catch {}
return false;
};
const restore = () => {
try {
const saved = sessionStorage.getItem(STORAGE_KEY);
if (!saved) return;
if (!saved.startsWith('/')) {
sessionStorage.removeItem(STORAGE_KEY);
return;
}
const current = getCurrentRoute();
if (current !== saved) {
if (!replaceHistoryState(saved)) {
replaceHistoryState('/');
}
requestAnimationFrame(() => setTimeout(() => {
try {
const text = (document.body?.innerText || '').trim();
// If the restored route results in too little content, assume it is invalid and navigate home
if (text.length < 50) {
replaceHistoryState('/');
}
} catch {}
}, 1000));
}
} catch {}
};
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(this, args);
save();
};
const originalReplaceState = history.replaceState;
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
save();
};
const 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;
};
window.addEventListener('popstate', save);
window.addEventListener('hashchange', save);
window.addEventListener("message", function (event) {
const parentOrigin = getParentOrigin();
if (event.data?.type === "redirect-home" && parentOrigin && ALLOWED_PARENT_ORIGINS.includes(parentOrigin)) {
const saved = sessionStorage.getItem(STORAGE_KEY);
if(saved && saved !== '/') {
replaceHistoryState('/')
}
}
});
restore();
}
`;
return [
{
tag: 'script',
attrs: { type: 'module' },
children: script,
injectTo: 'head'
}
];
}
};
}