2026-01-09 17:20:10 +01:00
|
|
|
import { ref } from 'vue';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Composable for managing interactions with pages and elements in the iframe
|
|
|
|
|
* Handles hover effects, labels, and click events for both pages and content elements
|
|
|
|
|
*/
|
2026-02-24 13:37:07 +01:00
|
|
|
export function useIframeInteractions({ elementPopup /*, pagePopup // DISABLED: page template styling feature */ }) {
|
|
|
|
|
// DISABLED: page template styling feature
|
|
|
|
|
// const hoveredPage = ref(null);
|
|
|
|
|
// const selectedPages = ref([]); // Pages with active border (when popup is open)
|
2026-01-09 17:20:10 +01:00
|
|
|
const hoveredElement = ref(null); // Currently hovered content element
|
|
|
|
|
const selectedElement = ref(null); // Selected element (when popup is open)
|
2026-02-24 13:37:07 +01:00
|
|
|
// const EDGE_THRESHOLD = 30; // px from edge to trigger hover // DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
// Text elements that can trigger ElementPopup (excluding containers, images, etc.)
|
|
|
|
|
const CONTENT_ELEMENTS = [
|
|
|
|
|
'P',
|
|
|
|
|
'H1',
|
|
|
|
|
'H2',
|
|
|
|
|
'H3',
|
|
|
|
|
'H4',
|
|
|
|
|
'H5',
|
|
|
|
|
'H6',
|
|
|
|
|
'BLOCKQUOTE',
|
|
|
|
|
'LI',
|
|
|
|
|
'A',
|
|
|
|
|
'STRONG',
|
|
|
|
|
'EM',
|
|
|
|
|
'B',
|
|
|
|
|
'I',
|
|
|
|
|
'U',
|
|
|
|
|
'CODE',
|
|
|
|
|
'PRE',
|
|
|
|
|
'FIGCAPTION',
|
|
|
|
|
];
|
|
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
// Check if mouse position is near the edges of a page element
|
|
|
|
|
const isNearPageEdge = (pageElement, mouseX, mouseY) => {
|
|
|
|
|
const rect = pageElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
const nearLeft = mouseX >= rect.left && mouseX <= rect.left + EDGE_THRESHOLD;
|
|
|
|
|
const nearRight =
|
|
|
|
|
mouseX >= rect.right - EDGE_THRESHOLD && mouseX <= rect.right;
|
|
|
|
|
const nearTop = mouseY >= rect.top && mouseY <= rect.top + EDGE_THRESHOLD;
|
|
|
|
|
const nearBottom =
|
|
|
|
|
mouseY >= rect.bottom - EDGE_THRESHOLD && mouseY <= rect.bottom;
|
|
|
|
|
|
|
|
|
|
const inHorizontalRange = mouseY >= rect.top && mouseY <= rect.bottom;
|
|
|
|
|
const inVerticalRange = mouseX >= rect.left && mouseX <= rect.right;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
(nearLeft && inHorizontalRange) ||
|
|
|
|
|
(nearRight && inHorizontalRange) ||
|
|
|
|
|
(nearTop && inVerticalRange) ||
|
|
|
|
|
(nearBottom && inVerticalRange)
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get all pages using the same template as the given page
|
|
|
|
|
const getPagesWithSameTemplate = (page, doc) => {
|
|
|
|
|
const pageType = page.getAttribute('data-page-type') || 'default';
|
|
|
|
|
const allPages = doc.querySelectorAll('.pagedjs_page');
|
|
|
|
|
return Array.from(allPages).filter(
|
|
|
|
|
(p) => (p.getAttribute('data-page-type') || 'default') === pageType
|
|
|
|
|
);
|
|
|
|
|
};
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
// Get selector for element (same logic as ElementPopup)
|
|
|
|
|
const getSelectorFromElement = (element) => {
|
|
|
|
|
if (element.id) {
|
|
|
|
|
return `#${element.id}`;
|
|
|
|
|
}
|
|
|
|
|
const tagName = element.tagName.toLowerCase();
|
|
|
|
|
// Filter out state classes (element-hovered, element-selected, page-hovered, page-selected)
|
|
|
|
|
const classes = Array.from(element.classList).filter(
|
|
|
|
|
(cls) =>
|
|
|
|
|
![
|
|
|
|
|
'element-hovered',
|
|
|
|
|
'element-selected',
|
|
|
|
|
'page-hovered',
|
|
|
|
|
'page-selected',
|
|
|
|
|
].includes(cls)
|
|
|
|
|
);
|
|
|
|
|
if (classes.length > 0) {
|
|
|
|
|
return `${tagName}.${classes[0]}`;
|
|
|
|
|
}
|
|
|
|
|
return tagName;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Create and position element label on hover
|
|
|
|
|
const createElementLabel = (element) => {
|
|
|
|
|
const doc = element.ownerDocument;
|
|
|
|
|
const existingLabel = doc.querySelector('.element-hover-label');
|
|
|
|
|
if (existingLabel) {
|
|
|
|
|
existingLabel.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
|
|
|
|
|
const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
|
|
|
|
|
|
|
|
|
const label = doc.createElement('div');
|
|
|
|
|
label.className = 'element-hover-label';
|
|
|
|
|
label.textContent = getSelectorFromElement(element);
|
|
|
|
|
label.style.top = `${rect.top + scrollTop - 32}px`;
|
|
|
|
|
label.style.left = `${rect.left + scrollLeft}px`;
|
|
|
|
|
|
|
|
|
|
doc.body.appendChild(label);
|
|
|
|
|
return label;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Remove element label
|
|
|
|
|
const removeElementLabel = (doc) => {
|
|
|
|
|
const label = doc.querySelector('.element-hover-label');
|
|
|
|
|
if (label) {
|
|
|
|
|
label.remove();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
// Create and position page label on hover
|
|
|
|
|
const createPageLabel = (page) => {
|
|
|
|
|
const doc = page.ownerDocument;
|
|
|
|
|
const existingLabel = doc.querySelector('.page-hover-label');
|
|
|
|
|
if (existingLabel) {
|
|
|
|
|
existingLabel.remove();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rect = page.getBoundingClientRect();
|
|
|
|
|
const scrollTop = doc.documentElement.scrollTop || doc.body.scrollTop;
|
|
|
|
|
const scrollLeft = doc.documentElement.scrollLeft || doc.body.scrollLeft;
|
|
|
|
|
|
|
|
|
|
const templateName = page.getAttribute('data-page-type') || 'default';
|
|
|
|
|
|
|
|
|
|
const label = doc.createElement('div');
|
|
|
|
|
label.className = 'page-hover-label';
|
|
|
|
|
label.textContent = `@page ${templateName}`;
|
|
|
|
|
label.style.top = `${rect.top + scrollTop - 32}px`;
|
|
|
|
|
label.style.left = `${rect.left + scrollLeft}px`;
|
|
|
|
|
|
|
|
|
|
doc.body.appendChild(label);
|
|
|
|
|
return label;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Remove page label
|
|
|
|
|
const removePageLabel = (doc) => {
|
|
|
|
|
const label = doc.querySelector('.page-hover-label');
|
|
|
|
|
if (label) {
|
|
|
|
|
label.remove();
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
// Check if element is a content element (or find closest content parent)
|
|
|
|
|
const getContentElement = (element) => {
|
|
|
|
|
let current = element;
|
|
|
|
|
while (current && current.tagName !== 'BODY') {
|
|
|
|
|
if (CONTENT_ELEMENTS.includes(current.tagName)) {
|
|
|
|
|
return current;
|
|
|
|
|
}
|
|
|
|
|
current = current.parentElement;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
// Clear selection highlight from all selected pages
|
|
|
|
|
const clearSelectedPages = () => {
|
|
|
|
|
selectedPages.value.forEach((page) => {
|
|
|
|
|
page.classList.remove('page-selected');
|
|
|
|
|
});
|
|
|
|
|
selectedPages.value = [];
|
|
|
|
|
};
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
// Clear selected element highlight
|
|
|
|
|
const clearSelectedElement = () => {
|
|
|
|
|
if (selectedElement.value) {
|
|
|
|
|
selectedElement.value.classList.remove('element-selected');
|
|
|
|
|
const doc = selectedElement.value.ownerDocument;
|
|
|
|
|
removeElementLabel(doc);
|
|
|
|
|
selectedElement.value = null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Get count of similar elements (same tag)
|
|
|
|
|
const getSimilarElementsCount = (element, doc) => {
|
|
|
|
|
const tagName = element.tagName;
|
|
|
|
|
const allElements = doc.querySelectorAll(tagName);
|
|
|
|
|
return allElements.length;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle mouse movement in iframe
|
|
|
|
|
const handleIframeMouseMove = (event) => {
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
|
|
|
|
let foundPage = null;
|
|
|
|
|
|
|
|
|
|
// Check if hovering near page edge
|
|
|
|
|
for (const page of pages) {
|
|
|
|
|
if (isNearPageEdge(page, event.clientX, event.clientY)) {
|
|
|
|
|
foundPage = page;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update page hover state
|
|
|
|
|
if (foundPage !== hoveredPage.value) {
|
|
|
|
|
// Remove highlight from previous page (only if not in selectedPages)
|
|
|
|
|
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
|
|
|
|
|
hoveredPage.value.classList.remove('page-hovered');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove previous page label
|
|
|
|
|
removePageLabel(event.target.ownerDocument);
|
|
|
|
|
|
|
|
|
|
// Add highlight to new page (only if not already selected)
|
|
|
|
|
if (foundPage && !selectedPages.value.includes(foundPage)) {
|
|
|
|
|
foundPage.classList.add('page-hovered');
|
|
|
|
|
createPageLabel(foundPage);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hoveredPage.value = foundPage;
|
|
|
|
|
}
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
// Check for content element hover
|
|
|
|
|
const contentElement = getContentElement(event.target);
|
|
|
|
|
const doc = event.target.ownerDocument;
|
2026-01-09 17:20:10 +01:00
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
if (contentElement !== hoveredElement.value) {
|
|
|
|
|
// Remove highlight from previous element (only if not selected)
|
2026-01-09 17:20:10 +01:00
|
|
|
if (
|
|
|
|
|
hoveredElement.value &&
|
|
|
|
|
hoveredElement.value !== selectedElement.value
|
|
|
|
|
) {
|
|
|
|
|
hoveredElement.value.classList.remove('element-hovered');
|
|
|
|
|
}
|
2026-02-24 13:37:07 +01:00
|
|
|
|
|
|
|
|
// Remove previous label
|
|
|
|
|
removeElementLabel(doc);
|
|
|
|
|
|
|
|
|
|
// Add highlight to new element (only if not already selected)
|
|
|
|
|
if (contentElement && contentElement !== selectedElement.value) {
|
|
|
|
|
contentElement.classList.add('element-hovered');
|
|
|
|
|
createElementLabel(contentElement);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hoveredElement.value = contentElement;
|
2026-01-09 17:20:10 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handle click in iframe
|
|
|
|
|
const handleIframeClick = (event) => {
|
|
|
|
|
const element = event.target;
|
|
|
|
|
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
// Check if clicking near a page edge
|
|
|
|
|
if (hoveredPage.value) {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
|
|
// Clear previous selections
|
|
|
|
|
clearSelectedPages();
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
|
|
|
|
|
// Get all pages with same template and highlight them
|
|
|
|
|
const doc = event.target.ownerDocument;
|
|
|
|
|
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
|
|
|
|
sameTemplatePages.forEach((page) => {
|
|
|
|
|
page.classList.add('page-selected');
|
|
|
|
|
});
|
|
|
|
|
selectedPages.value = sameTemplatePages;
|
|
|
|
|
|
|
|
|
|
// Remove labels when opening popup
|
|
|
|
|
removePageLabel(doc);
|
|
|
|
|
removeElementLabel(doc);
|
|
|
|
|
|
|
|
|
|
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
|
|
|
|
elementPopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
// Only show popup for elements inside the page template
|
|
|
|
|
const isInsidePage = element.closest('.pagedjs_page');
|
|
|
|
|
if (!isInsidePage) {
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
elementPopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only show ElementPopup for content elements, not divs
|
|
|
|
|
const contentElement = getContentElement(element);
|
|
|
|
|
if (!contentElement) {
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
elementPopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If popup is already open and we're clicking another element, close it
|
|
|
|
|
if (elementPopup.value.visible) {
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
elementPopup.value.close();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clear previous element selection
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
|
|
|
|
|
// Remove hovered class from the element we're about to select
|
|
|
|
|
contentElement.classList.remove('element-hovered');
|
|
|
|
|
|
|
|
|
|
// Clear the hoveredElement ref if it's the same as what we're selecting
|
|
|
|
|
if (hoveredElement.value === contentElement) {
|
|
|
|
|
hoveredElement.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get document and remove labels when opening popup
|
|
|
|
|
const doc = event.target.ownerDocument;
|
|
|
|
|
removeElementLabel(doc);
|
|
|
|
|
|
|
|
|
|
// Select the new element
|
|
|
|
|
selectedElement.value = contentElement;
|
|
|
|
|
contentElement.classList.add('element-selected');
|
|
|
|
|
|
|
|
|
|
// Get count of similar elements
|
|
|
|
|
const count = getSimilarElementsCount(contentElement, doc);
|
|
|
|
|
|
|
|
|
|
elementPopup.value.handleIframeClick(event, contentElement, count);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Handlers for popup close events
|
2026-02-24 13:37:07 +01:00
|
|
|
/* DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
const handlePagePopupClose = () => {
|
|
|
|
|
clearSelectedPages();
|
|
|
|
|
};
|
2026-02-24 13:37:07 +01:00
|
|
|
*/
|
2026-01-09 17:20:10 +01:00
|
|
|
|
|
|
|
|
const handleElementPopupClose = () => {
|
|
|
|
|
clearSelectedElement();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// State
|
2026-02-24 13:37:07 +01:00
|
|
|
// hoveredPage, // DISABLED: page template styling feature
|
|
|
|
|
// selectedPages, // DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
hoveredElement,
|
|
|
|
|
selectedElement,
|
|
|
|
|
// Handlers
|
|
|
|
|
handleIframeMouseMove,
|
|
|
|
|
handleIframeClick,
|
2026-02-24 13:37:07 +01:00
|
|
|
// handlePagePopupClose, // DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
handleElementPopupClose,
|
|
|
|
|
// Utilities
|
2026-02-24 13:37:07 +01:00
|
|
|
// clearSelectedPages, // DISABLED: page template styling feature
|
2026-01-09 17:20:10 +01:00
|
|
|
clearSelectedElement,
|
|
|
|
|
};
|
|
|
|
|
}
|