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]}`; }