diff --git a/public/assets/css/pagedjs-interface.css b/public/assets/css/pagedjs-interface.css
index bcef24a..c03eeb7 100644
--- a/public/assets/css/pagedjs-interface.css
+++ b/public/assets/css/pagedjs-interface.css
@@ -144,6 +144,54 @@
/*--------------------------------------------------------------------------------------*/
}
+/* Hover and selection states for pages and elements */
+.page-hovered {
+ outline: 2px solid #ff8a5050 !important;
+ cursor: pointer !important;
+}
+
+.page-selected {
+ outline: 2px solid #ff8a50 !important;
+}
+
+.element-hovered {
+ outline: 2px solid #7136ff50 !important;
+ cursor: pointer !important;
+}
+
+.element-selected {
+ outline: 2px dashed #7136ff !important;
+ background-color: #7136ff1a !important;
+}
+
+.element-hover-label {
+ position: absolute;
+ background: #7136ff;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ opacity: 0.3;
+ pointer-events: none;
+ z-index: 9999;
+ font-family: sans-serif;
+}
+
+.page-hover-label {
+ position: absolute;
+ background: #ff8a50;
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ opacity: 0.3;
+ pointer-events: none;
+ z-index: 9999;
+ font-family: sans-serif;
+}
+
/* Marks (to delete when merge in paged.js) */
.pagedjs_marks-crop {
diff --git a/src/App.vue b/src/App.vue
index 44ce2a6..5d129f6 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -41,9 +41,11 @@ 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 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 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;
@@ -65,6 +67,90 @@ const getPagesWithSameTemplate = (page, doc) => {
);
};
+// 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();
+ }
+};
+
+// 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();
+ }
+};
+
// Handle mouse movement in iframe
const handleIframeMouseMove = (event) => {
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
@@ -82,12 +168,16 @@ const handleIframeMouseMove = (event) => {
if (foundPage !== hoveredPage.value) {
// Remove highlight from previous page (only if not in selectedPages)
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
- hoveredPage.value.style.outline = '';
+ 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.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}50`;
+ foundPage.classList.add('page-hovered');
+ createPageLabel(foundPage);
}
hoveredPage.value = foundPage;
@@ -96,46 +186,88 @@ const handleIframeMouseMove = (event) => {
// If not near page edge, check for content element hover
if (!foundPage) {
const contentElement = getContentElement(event.target);
+ const doc = event.target.ownerDocument;
if (contentElement !== hoveredElement.value) {
// Remove highlight from previous element (only if not selected)
- if (hoveredElement.value && hoveredElement.value !== selectedElement.value) {
- hoveredElement.value.style.outline = '';
+ if (
+ hoveredElement.value &&
+ hoveredElement.value !== selectedElement.value
+ ) {
+ hoveredElement.value.classList.remove('element-hovered');
}
+ // Remove previous labels
+ removeElementLabel(doc);
+ removePageLabel(doc);
+
// Add highlight to new element (only if not already selected)
if (contentElement && contentElement !== selectedElement.value) {
- contentElement.style.outline = `2px dashed ${ELEMENT_HIGHLIGHT_COLOR}`;
+ contentElement.classList.add('element-hovered');
+ createElementLabel(contentElement);
}
hoveredElement.value = contentElement;
}
} else {
// Clear element hover when hovering page edge
- if (hoveredElement.value && hoveredElement.value !== selectedElement.value) {
- hoveredElement.value.style.outline = '';
+ if (
+ hoveredElement.value &&
+ hoveredElement.value !== selectedElement.value
+ ) {
+ hoveredElement.value.classList.remove('element-hovered');
hoveredElement.value = null;
}
+ // Remove element label when hovering page edge
+ removeElementLabel(event.target.ownerDocument);
}
};
// Clear selection highlight from all selected pages
const clearSelectedPages = () => {
selectedPages.value.forEach((page) => {
- page.style.outline = '';
+ page.classList.remove('page-selected');
});
selectedPages.value = [];
};
// Content elements that can trigger ElementPopup
const CONTENT_ELEMENTS = [
- 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
- 'IMG', 'FIGURE', 'FIGCAPTION',
- 'UL', 'OL', 'LI',
- 'BLOCKQUOTE', 'PRE', 'CODE',
- 'TABLE', 'THEAD', 'TBODY', 'TR', 'TH', 'TD',
- 'A', 'SPAN', 'STRONG', 'EM', 'B', 'I', 'U',
- 'ARTICLE', 'SECTION', 'ASIDE', 'HEADER', 'FOOTER', 'NAV'
+ 'P',
+ 'H1',
+ 'H2',
+ 'H3',
+ 'H4',
+ 'H5',
+ 'H6',
+ 'IMG',
+ 'FIGURE',
+ 'FIGCAPTION',
+ 'UL',
+ 'OL',
+ 'LI',
+ 'BLOCKQUOTE',
+ 'PRE',
+ 'CODE',
+ 'TABLE',
+ 'THEAD',
+ 'TBODY',
+ 'TR',
+ 'TH',
+ 'TD',
+ 'A',
+ 'SPAN',
+ 'STRONG',
+ 'EM',
+ 'B',
+ 'I',
+ 'U',
+ 'ARTICLE',
+ 'SECTION',
+ 'ASIDE',
+ 'HEADER',
+ 'FOOTER',
+ 'NAV',
];
// Check if element is a content element (or find closest content parent)
@@ -153,8 +285,9 @@ const getContentElement = (element) => {
// Clear selected element highlight
const clearSelectedElement = () => {
if (selectedElement.value) {
- selectedElement.value.style.outline = '';
- selectedElement.value.style.backgroundColor = '';
+ selectedElement.value.classList.remove('element-selected');
+ const doc = selectedElement.value.ownerDocument;
+ removeElementLabel(doc);
selectedElement.value = null;
}
};
@@ -182,10 +315,14 @@ const handleIframeClick = (event) => {
const doc = event.target.ownerDocument;
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
sameTemplatePages.forEach((page) => {
- page.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}`;
+ 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;
@@ -214,16 +351,35 @@ const handleIframeClick = (event) => {
// Clear page selections
clearSelectedPages();
+ // If popup is already open and we're clicking another element, close it
+ if (elementPopup.value.visible) {
+ clearSelectedElement();
+ elementPopup.value.close();
+ pagePopup.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);
+ removePageLabel(doc);
+
// Select the new element
selectedElement.value = contentElement;
- contentElement.style.outline = '';
- contentElement.style.backgroundColor = `${ELEMENT_HIGHLIGHT_COLOR}4D`; // 30% opacity
+ contentElement.classList.add('element-selected');
// Get count of similar elements
- const doc = event.target.ownerDocument;
const count = getSimilarElementsCount(contentElement, doc);
elementPopup.value.handleIframeClick(event, contentElement, count);
@@ -289,7 +445,10 @@ const renderPreview = async (shouldReloadFromFile = false) => {
hiddenFrame.onload = () => {
// Add event listeners for page and element interactions
- hiddenFrame.contentDocument.addEventListener('mousemove', handleIframeMouseMove);
+ hiddenFrame.contentDocument.addEventListener(
+ 'mousemove',
+ handleIframeMouseMove
+ );
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
// Close Coloris when clicking in the iframe
@@ -423,12 +582,26 @@ onMounted(() => renderPreview(true));
-
-
+
+
@@ -478,7 +651,9 @@ onMounted(() => renderPreview(true));
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
- transition: transform 0.2s ease, box-shadow 0.2s ease;
+ transition:
+ transform 0.2s ease,
+ box-shadow 0.2s ease;
z-index: 1000;
}
diff --git a/src/components/ElementPopup.vue b/src/components/ElementPopup.vue
index a6ebd79..a8d9a73 100644
--- a/src/components/ElementPopup.vue
+++ b/src/components/ElementPopup.vue
@@ -337,8 +337,10 @@ const getSelectorFromElement = (element) => {
// Get tag name
const tagName = element.tagName.toLowerCase();
- // Get first class if available
- const classes = Array.from(element.classList);
+ // Get first class if available (filter out state classes)
+ 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]}`;
}