diff --git a/public/assets/css/src/_variables.scss b/public/assets/css/src/_variables.scss
index 43bb561..c632964 100644
--- a/public/assets/css/src/_variables.scss
+++ b/public/assets/css/src/_variables.scss
@@ -4,6 +4,8 @@
--color-browngray-200: #d0c4ba;
--color-browngray-300: #b5a9a1;
+ --color-page-highlight: #ff8a50;
+
--border-radius: 0.2rem;
--space-xs: 0.5rem;
diff --git a/public/assets/css/style.css b/public/assets/css/style.css
index 4be3450..ce20e3f 100644
--- a/public/assets/css/style.css
+++ b/public/assets/css/style.css
@@ -30,6 +30,7 @@ button {
--color-browngray-050: #f5f3f0;
--color-browngray-200: #d0c4ba;
--color-browngray-300: #b5a9a1;
+ --color-page-highlight: #ff8a50;
--border-radius: 0.2rem;
--space-xs: 0.5rem;
--curve: cubic-bezier(0.86, 0, 0.07, 1);
diff --git a/public/assets/css/style.css.map b/public/assets/css/style.css.map
index 6140ba4..67cbf6d 100644
--- a/public/assets/css/style.css.map
+++ b/public/assets/css/style.css.map
@@ -1 +1 @@
-{"version":3,"sources":["src/_reset.scss","style.css","src/_variables.scss","src/_text.scss","src/_print-styles.scss","src/_forms.scss","src/_buttons.scss"],"names":[],"mappings":"AAAA;;EAEE,UAAA;EACA,SAAA;ACCF;;ADEA;;;;;;EAME,SAAA;ACCF;;ADEA;;EAEE,YAAA;EACA,aAAA;EAEA,mCAAA;ACAF;;ADGA;EACE,6BAAA;EACA,YAAA;ACAF;;ACzBA;EACE,yBAAA;EACA,8BAAA;EACA,8BAAA;EACA,8BAAA;EAEA,uBAAA;EAEA,kBAAA;EAEA,uCAAA;ADyBF;;AEnCA;;;;;;;;;;;;;EAaE,uBAAA;AFsCF;;AGnDA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AHsDF;AGpDA;EACE,8BAAA;OAAA,kBAAA;AHsDF;;AGnDA;EACE;IACE,sBAAA;EHsDF;AACF;AGpDA;EACE,+BAAA;AHsDF;;AIrEA;;;EAGE,4CAAA;AJwEF;;AIrEA;EACE,YAAA;AJwEF;;AIrEA,2BAAA;AACA;EACE,yCAAA;UAAA,iCAAA;EACA,iDAAA;EACA,0BAAA;EACA,YAAA;EACA,kBAAA;AJwEF;AItEE;EACE,uBAAA;EACA,kBAAA;EACA,YAAA;EACA,OAAA;EACA,kBAAA;EACA,uBAAA;EACA,sCAAA;EACA,iCAAA;EACA,8CAAA;EACA,kBAAA;EACA,kBAAA;EACA,mBAAA;EACA,UAAA;EACA,kBAAA;EACA,qDACE;EAEF,WAAA;AJsEJ;AInEE;EACE,UAAA;EACA,mBAAA;AJqEJ;;AIhEE;EACE,6BAAA;EACA,8BAAA;AJmEJ;AIjEE;EACE,mDAAA;AJmEJ;AIhEE;EACE,0BAAA;AJkEJ;AIhEI;EACE,8BAAA;AJkEN;AI/DI;EACE,aAAA;AJiEN;AI/DM;;EAEE,UAAA;AJiER;AI9DM;EACE,oCAAA;AJgER;AI7DM;EACE,aAAA;EACA,WAAA;AJ+DR;AI7DQ;EACE,aAAA;EACA,WAAA;AJ+DV;AI3DM;EACE,UAAA;AJ6DR;AI5DQ;EACE,aAAA;AJ8DV;AI7DU;EACE,kBAAA;EACA,eAAA;EACA,cAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;AJ+DZ;AI5DU;EACE,oBAAA;EACA,WAAA;AJ8DZ;AIxDI;EACE,aAAA;EACA,eAAA;EACA,wBAAA;AJ0DN;AIxDM;EACE,WAAA;AJ0DR;AIxDM;EACE,UAAA;AJ0DR;AIxDQ;EACE,UAAA;AJ0DV;AItDU;EACE,UAAA;AJwDZ;;AKjLA;EACE,eAAA;EAEA,4CAAA;EACA,iCAAA;EACA,uCAAA;EACA,mCAAA;EACA,sBAAA;ALmLF;AKjLE;EACE,sBAAA;EACA,WAAA;ALmLJ;AK/KI;EACE,sBAAA;EACA,WAAA;EACA,YAAA;ALiLN","file":"style.css"}
\ No newline at end of file
+{"version":3,"sources":["src/_reset.scss","style.css","src/_variables.scss","src/_text.scss","src/_print-styles.scss","src/_forms.scss","src/_buttons.scss"],"names":[],"mappings":"AAAA;;EAEE,UAAA;EACA,SAAA;ACCF;;ADEA;;;;;;EAME,SAAA;ACCF;;ADEA;;EAEE,YAAA;EACA,aAAA;EAEA,mCAAA;ACAF;;ADGA;EACE,6BAAA;EACA,YAAA;ACAF;;ACzBA;EACE,yBAAA;EACA,8BAAA;EACA,8BAAA;EACA,8BAAA;EAEA,+BAAA;EAEA,uBAAA;EAEA,kBAAA;EAEA,uCAAA;ADwBF;;AEpCA;;;;;;;;;;;;;EAaE,uBAAA;AFuCF;;AGpDA,yBAAA;AACA;EACE,QAAA;EACA,2BAAA;AHuDF;AGrDA;EACE,8BAAA;OAAA,kBAAA;AHuDF;;AGpDA;EACE;IACE,sBAAA;EHuDF;AACF;AGrDA;EACE,+BAAA;AHuDF;;AItEA;;;EAGE,4CAAA;AJyEF;;AItEA;EACE,YAAA;AJyEF;;AItEA,2BAAA;AACA;EACE,yCAAA;UAAA,iCAAA;EACA,iDAAA;EACA,0BAAA;EACA,YAAA;EACA,kBAAA;AJyEF;AIvEE;EACE,uBAAA;EACA,kBAAA;EACA,YAAA;EACA,OAAA;EACA,kBAAA;EACA,uBAAA;EACA,sCAAA;EACA,iCAAA;EACA,8CAAA;EACA,kBAAA;EACA,kBAAA;EACA,mBAAA;EACA,UAAA;EACA,kBAAA;EACA,qDACE;EAEF,WAAA;AJuEJ;AIpEE;EACE,UAAA;EACA,mBAAA;AJsEJ;;AIjEE;EACE,6BAAA;EACA,8BAAA;AJoEJ;AIlEE;EACE,mDAAA;AJoEJ;AIjEE;EACE,0BAAA;AJmEJ;AIjEI;EACE,8BAAA;AJmEN;AIhEI;EACE,aAAA;AJkEN;AIhEM;;EAEE,UAAA;AJkER;AI/DM;EACE,oCAAA;AJiER;AI9DM;EACE,aAAA;EACA,WAAA;AJgER;AI9DQ;EACE,aAAA;EACA,WAAA;AJgEV;AI5DM;EACE,UAAA;AJ8DR;AI7DQ;EACE,aAAA;AJ+DV;AI9DU;EACE,kBAAA;EACA,eAAA;EACA,cAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;AJgEZ;AI7DU;EACE,oBAAA;EACA,WAAA;AJ+DZ;AIzDI;EACE,aAAA;EACA,eAAA;EACA,wBAAA;AJ2DN;AIzDM;EACE,WAAA;AJ2DR;AIzDM;EACE,UAAA;AJ2DR;AIzDQ;EACE,UAAA;AJ2DV;AIvDU;EACE,UAAA;AJyDZ;;AKlLA;EACE,eAAA;EAEA,4CAAA;EACA,iCAAA;EACA,uCAAA;EACA,mCAAA;EACA,sBAAA;ALoLF;AKlLE;EACE,sBAAA;EACA,WAAA;ALoLJ;AKhLI;EACE,sBAAA;EACA,WAAA;EACA,YAAA;ALkLN","file":"style.css"}
\ No newline at end of file
diff --git a/public/assets/svg/arrow-left-double-line.svg b/public/assets/svg/arrow-left-double-line.svg
new file mode 100644
index 0000000..9091346
--- /dev/null
+++ b/public/assets/svg/arrow-left-double-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/assets/svg/lock-line.svg b/public/assets/svg/lock-line.svg
new file mode 100644
index 0000000..c8f7d93
--- /dev/null
+++ b/public/assets/svg/lock-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/assets/svg/lock-unlock-line.svg b/public/assets/svg/lock-unlock-line.svg
new file mode 100644
index 0000000..205f472
--- /dev/null
+++ b/public/assets/svg/lock-unlock-line.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
index 59bee17..cee7e1b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -19,7 +19,9 @@ provide('activeTab', activeTab);
// Page interaction state
const hoveredPage = ref(null);
+const selectedPages = ref([]); // Pages with active border (when popup is open)
const EDGE_THRESHOLD = 30; // px from edge to trigger hover
+const PAGE_HIGHLIGHT_COLOR = '#ff8a50';
let savedScrollPercentage = 0;
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
@@ -51,6 +53,15 @@ const isNearPageEdge = (pageElement, mouseX, mouseY) => {
);
};
+// 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
+ );
+};
+
// Handle mouse movement in iframe
const handleIframeMouseMove = (event) => {
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
@@ -65,20 +76,51 @@ const handleIframeMouseMove = (event) => {
// Update hover state
if (foundPage !== hoveredPage.value) {
- // Remove highlight from previous page
- if (hoveredPage.value) {
+ // Remove highlight from previous page (only if not in selectedPages)
+ if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
hoveredPage.value.style.outline = '';
}
- // Add highlight to new page
- if (foundPage) {
- foundPage.style.outline = '2px solid rgba(97, 175, 239, 0.3)';
+ // 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`;
}
hoveredPage.value = foundPage;
}
};
+// Clear selection highlight from all selected pages
+const clearSelectedPages = () => {
+ selectedPages.value.forEach((page) => {
+ page.style.outline = '';
+ });
+ 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'
+];
+
+// 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;
+};
+
// Handle click in iframe
const handleIframeClick = (event) => {
const element = event.target;
@@ -86,22 +128,51 @@ const handleIframeClick = (event) => {
// Check if clicking near a page edge
if (hoveredPage.value) {
event.stopPropagation();
- pagePopup.value.open(hoveredPage.value, event);
+
+ // Clear previous selection
+ clearSelectedPages();
+
+ // Get all pages with same template and highlight them
+ const doc = event.target.ownerDocument;
+ const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
+ sameTemplatePages.forEach((page) => {
+ page.style.outline = `2px solid ${PAGE_HIGHLIGHT_COLOR}`;
+ });
+ selectedPages.value = sameTemplatePages;
+
+ pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
elementPopup.value.close();
return;
}
- // Otherwise handle as element click
- if (element.tagName === 'BODY' || element.tagName === 'HTML') {
+ // Only show popup for elements inside the page template
+ const isInsidePage = element.closest('.pagedjs_page');
+ if (!isInsidePage) {
+ clearSelectedPages();
elementPopup.value.close();
pagePopup.value.close();
return;
}
- elementPopup.value.handleIframeClick(event);
+ // Only show ElementPopup for content elements, not divs
+ const contentElement = getContentElement(element);
+ if (!contentElement) {
+ clearSelectedPages();
+ elementPopup.value.close();
+ pagePopup.value.close();
+ return;
+ }
+
+ clearSelectedPages();
+ elementPopup.value.handleIframeClick(event, contentElement);
pagePopup.value.close();
};
+// Expose clearSelectedPages for PagePopup to call when closing
+const handlePagePopupClose = () => {
+ clearSelectedPages();
+};
+
const renderPreview = async (shouldReloadFromFile = false) => {
if (isTransitioning.value) return;
isTransitioning.value = true;
@@ -203,6 +274,65 @@ watch(
}
);
+// Print the PagedJS content
+const printPreview = async () => {
+ const frame = activeFrame.value;
+ if (!frame || !frame.contentDocument) return;
+
+ const doc = frame.contentDocument;
+
+ // Collect all styles
+ let allStyles = '';
+
+ // Get inline
+ `;
+ document.body.innerHTML = doc.body.innerHTML;
+
+ // Print
+ window.print();
+
+ // Restore original content after print dialog closes
+ setTimeout(() => {
+ document.head.innerHTML = originalStyles;
+ document.body.innerHTML = originalContent;
+ // Re-mount Vue app would be needed, so we reload instead
+ window.location.reload();
+ }, 100);
+};
+
onMounted(() => renderPreview(true));
@@ -227,7 +357,13 @@ onMounted(() => renderPreview(true));