Compare commits
4 commits
b903c75f98
...
446b6cd9e7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
446b6cd9e7 | ||
|
|
5b5c65722b | ||
|
|
f9e9e65712 | ||
|
|
c523b4e335 |
3 changed files with 256 additions and 31 deletions
|
|
@ -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) */
|
/* Marks (to delete when merge in paged.js) */
|
||||||
|
|
||||||
.pagedjs_marks-crop {
|
.pagedjs_marks-crop {
|
||||||
|
|
|
||||||
233
src/App.vue
233
src/App.vue
|
|
@ -41,9 +41,11 @@ const isNearPageEdge = (pageElement, mouseX, mouseY) => {
|
||||||
const rect = pageElement.getBoundingClientRect();
|
const rect = pageElement.getBoundingClientRect();
|
||||||
|
|
||||||
const nearLeft = mouseX >= rect.left && mouseX <= rect.left + EDGE_THRESHOLD;
|
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 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 inHorizontalRange = mouseY >= rect.top && mouseY <= rect.bottom;
|
||||||
const inVerticalRange = mouseX >= rect.left && mouseX <= rect.right;
|
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
|
// Handle mouse movement in iframe
|
||||||
const handleIframeMouseMove = (event) => {
|
const handleIframeMouseMove = (event) => {
|
||||||
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
||||||
|
|
@ -82,12 +168,16 @@ const handleIframeMouseMove = (event) => {
|
||||||
if (foundPage !== hoveredPage.value) {
|
if (foundPage !== hoveredPage.value) {
|
||||||
// Remove highlight from previous page (only if not in selectedPages)
|
// Remove highlight from previous page (only if not in selectedPages)
|
||||||
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
|
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)
|
// Add highlight to new page (only if not already selected)
|
||||||
if (foundPage && !selectedPages.value.includes(foundPage)) {
|
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;
|
hoveredPage.value = foundPage;
|
||||||
|
|
@ -96,46 +186,88 @@ const handleIframeMouseMove = (event) => {
|
||||||
// If not near page edge, check for content element hover
|
// If not near page edge, check for content element hover
|
||||||
if (!foundPage) {
|
if (!foundPage) {
|
||||||
const contentElement = getContentElement(event.target);
|
const contentElement = getContentElement(event.target);
|
||||||
|
const doc = event.target.ownerDocument;
|
||||||
|
|
||||||
if (contentElement !== hoveredElement.value) {
|
if (contentElement !== hoveredElement.value) {
|
||||||
// Remove highlight from previous element (only if not selected)
|
// Remove highlight from previous element (only if not selected)
|
||||||
if (hoveredElement.value && hoveredElement.value !== selectedElement.value) {
|
if (
|
||||||
hoveredElement.value.style.outline = '';
|
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)
|
// Add highlight to new element (only if not already selected)
|
||||||
if (contentElement && contentElement !== selectedElement.value) {
|
if (contentElement && contentElement !== selectedElement.value) {
|
||||||
contentElement.style.outline = `2px dashed ${ELEMENT_HIGHLIGHT_COLOR}`;
|
contentElement.classList.add('element-hovered');
|
||||||
|
createElementLabel(contentElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
hoveredElement.value = contentElement;
|
hoveredElement.value = contentElement;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Clear element hover when hovering page edge
|
// Clear element hover when hovering page edge
|
||||||
if (hoveredElement.value && hoveredElement.value !== selectedElement.value) {
|
if (
|
||||||
hoveredElement.value.style.outline = '';
|
hoveredElement.value &&
|
||||||
|
hoveredElement.value !== selectedElement.value
|
||||||
|
) {
|
||||||
|
hoveredElement.value.classList.remove('element-hovered');
|
||||||
hoveredElement.value = null;
|
hoveredElement.value = null;
|
||||||
}
|
}
|
||||||
|
// Remove element label when hovering page edge
|
||||||
|
removeElementLabel(event.target.ownerDocument);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Clear selection highlight from all selected pages
|
// Clear selection highlight from all selected pages
|
||||||
const clearSelectedPages = () => {
|
const clearSelectedPages = () => {
|
||||||
selectedPages.value.forEach((page) => {
|
selectedPages.value.forEach((page) => {
|
||||||
page.style.outline = '';
|
page.classList.remove('page-selected');
|
||||||
});
|
});
|
||||||
selectedPages.value = [];
|
selectedPages.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Content elements that can trigger ElementPopup
|
// Content elements that can trigger ElementPopup
|
||||||
const CONTENT_ELEMENTS = [
|
const CONTENT_ELEMENTS = [
|
||||||
'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
|
'P',
|
||||||
'IMG', 'FIGURE', 'FIGCAPTION',
|
'H1',
|
||||||
'UL', 'OL', 'LI',
|
'H2',
|
||||||
'BLOCKQUOTE', 'PRE', 'CODE',
|
'H3',
|
||||||
'TABLE', 'THEAD', 'TBODY', 'TR', 'TH', 'TD',
|
'H4',
|
||||||
'A', 'SPAN', 'STRONG', 'EM', 'B', 'I', 'U',
|
'H5',
|
||||||
'ARTICLE', 'SECTION', 'ASIDE', 'HEADER', 'FOOTER', 'NAV'
|
'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)
|
// Check if element is a content element (or find closest content parent)
|
||||||
|
|
@ -153,8 +285,9 @@ const getContentElement = (element) => {
|
||||||
// Clear selected element highlight
|
// Clear selected element highlight
|
||||||
const clearSelectedElement = () => {
|
const clearSelectedElement = () => {
|
||||||
if (selectedElement.value) {
|
if (selectedElement.value) {
|
||||||
selectedElement.value.style.outline = '';
|
selectedElement.value.classList.remove('element-selected');
|
||||||
selectedElement.value.style.backgroundColor = '';
|
const doc = selectedElement.value.ownerDocument;
|
||||||
|
removeElementLabel(doc);
|
||||||
selectedElement.value = null;
|
selectedElement.value = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -182,10 +315,14 @@ const handleIframeClick = (event) => {
|
||||||
const doc = event.target.ownerDocument;
|
const doc = event.target.ownerDocument;
|
||||||
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
||||||
sameTemplatePages.forEach((page) => {
|
sameTemplatePages.forEach((page) => {
|
||||||
page.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}`;
|
page.classList.add('page-selected');
|
||||||
});
|
});
|
||||||
selectedPages.value = sameTemplatePages;
|
selectedPages.value = sameTemplatePages;
|
||||||
|
|
||||||
|
// Remove labels when opening popup
|
||||||
|
removePageLabel(doc);
|
||||||
|
removeElementLabel(doc);
|
||||||
|
|
||||||
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
||||||
elementPopup.value.close();
|
elementPopup.value.close();
|
||||||
return;
|
return;
|
||||||
|
|
@ -214,16 +351,35 @@ const handleIframeClick = (event) => {
|
||||||
// Clear page selections
|
// Clear page selections
|
||||||
clearSelectedPages();
|
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
|
// Clear previous element selection
|
||||||
clearSelectedElement();
|
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
|
// Select the new element
|
||||||
selectedElement.value = contentElement;
|
selectedElement.value = contentElement;
|
||||||
contentElement.style.outline = '';
|
contentElement.classList.add('element-selected');
|
||||||
contentElement.style.backgroundColor = `${ELEMENT_HIGHLIGHT_COLOR}4D`; // 30% opacity
|
|
||||||
|
|
||||||
// Get count of similar elements
|
// Get count of similar elements
|
||||||
const doc = event.target.ownerDocument;
|
|
||||||
const count = getSimilarElementsCount(contentElement, doc);
|
const count = getSimilarElementsCount(contentElement, doc);
|
||||||
|
|
||||||
elementPopup.value.handleIframeClick(event, contentElement, count);
|
elementPopup.value.handleIframeClick(event, contentElement, count);
|
||||||
|
|
@ -289,7 +445,10 @@ const renderPreview = async (shouldReloadFromFile = false) => {
|
||||||
|
|
||||||
hiddenFrame.onload = () => {
|
hiddenFrame.onload = () => {
|
||||||
// Add event listeners for page and element interactions
|
// Add event listeners for page and element interactions
|
||||||
hiddenFrame.contentDocument.addEventListener('mousemove', handleIframeMouseMove);
|
hiddenFrame.contentDocument.addEventListener(
|
||||||
|
'mousemove',
|
||||||
|
handleIframeMouseMove
|
||||||
|
);
|
||||||
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
|
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
|
||||||
|
|
||||||
// Close Coloris when clicking in the iframe
|
// Close Coloris when clicking in the iframe
|
||||||
|
|
@ -423,12 +582,26 @@ onMounted(() => renderPreview(true));
|
||||||
|
|
||||||
<PreviewLoader :isLoading="isTransitioning" :shifted="activeTab.length > 0" />
|
<PreviewLoader :isLoading="isTransitioning" :shifted="activeTab.length > 0" />
|
||||||
|
|
||||||
<ElementPopup ref="elementPopup" :iframeRef="activeFrame" @close="handleElementPopupClose" />
|
<ElementPopup
|
||||||
<PagePopup ref="pagePopup" :iframeRef="activeFrame" @close="handlePagePopupClose" />
|
ref="elementPopup"
|
||||||
|
:iframeRef="activeFrame"
|
||||||
|
@close="handleElementPopupClose"
|
||||||
|
/>
|
||||||
|
<PagePopup
|
||||||
|
ref="pagePopup"
|
||||||
|
:iframeRef="activeFrame"
|
||||||
|
@close="handlePagePopupClose"
|
||||||
|
/>
|
||||||
|
|
||||||
<button class="print-btn" @click="printPreview" title="Imprimer">
|
<button class="print-btn" @click="printPreview" title="Imprimer">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg
|
||||||
<path d="M17 2H7V6H17V2ZM19 8H5C3.34 8 2 9.34 2 11V17H6V21H18V17H22V11C22 9.34 20.66 8 19 8ZM16 19H8V14H16V19ZM19 12C18.45 12 18 11.55 18 11C18 10.45 18.45 10 19 10C19.55 10 20 10.45 20 11C20 11.55 19.55 12 19 12Z"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17 2H7V6H17V2ZM19 8H5C3.34 8 2 9.34 2 11V17H6V21H18V17H22V11C22 9.34 20.66 8 19 8ZM16 19H8V14H16V19ZM19 12C18.45 12 18 11.55 18 11C18 10.45 18.45 10 19 10C19.55 10 20 10.45 20 11C20 11.55 19.55 12 19 12Z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -478,7 +651,9 @@ onMounted(() => renderPreview(true));
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
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;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -337,8 +337,10 @@ const getSelectorFromElement = (element) => {
|
||||||
// Get tag name
|
// Get tag name
|
||||||
const tagName = element.tagName.toLowerCase();
|
const tagName = element.tagName.toLowerCase();
|
||||||
|
|
||||||
// Get first class if available
|
// Get first class if available (filter out state classes)
|
||||||
const classes = Array.from(element.classList);
|
const classes = Array.from(element.classList).filter(
|
||||||
|
(cls) => !['element-hovered', 'element-selected', 'page-hovered', 'page-selected'].includes(cls)
|
||||||
|
);
|
||||||
if (classes.length > 0) {
|
if (classes.length > 0) {
|
||||||
return `${tagName}.${classes[0]}`;
|
return `${tagName}.${classes[0]}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue