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)); - + + + diff --git a/src/components/ElementPopup.vue b/src/components/ElementPopup.vue index b383d12..9744fcf 100644 --- a/src/components/ElementPopup.vue +++ b/src/components/ElementPopup.vue @@ -639,8 +639,8 @@ const close = () => { selectedElement.value = null; }; -const handleIframeClick = (event) => { - const element = event.target; +const handleIframeClick = (event, targetElement = null) => { + const element = targetElement || event.target; if (element.tagName === 'BODY' || element.tagName === 'HTML') { close(); diff --git a/src/components/PagePopup.vue b/src/components/PagePopup.vue index 804ab50..8ec9449 100644 --- a/src/components/PagePopup.vue +++ b/src/components/PagePopup.vue @@ -7,8 +7,8 @@ @@ -298,9 +298,13 @@ const props = defineProps({ iframeRef: Object, }); +const emit = defineEmits(['close']); + const visible = ref(false); const position = ref({ x: 0, y: 0 }); const selectedPageElement = ref(null); +const pageCount = ref(0); +const templateName = ref(''); const isEditable = ref(false); const inheritanceLocked = ref(true); const backgroundColorInput = ref(null); @@ -309,10 +313,10 @@ let isUpdatingFromStore = false; let updateTimer = null; const margins = ref({ - top: { value: 6, unit: 'mm' }, - bottom: { value: 7, unit: 'mm' }, - left: { value: 6, unit: 'mm' }, - right: { value: 7, unit: 'mm' }, + top: { value: 0, unit: 'mm' }, + bottom: { value: 0, unit: 'mm' }, + left: { value: 0, unit: 'mm' }, + right: { value: 0, unit: 'mm' }, }); const background = ref({ @@ -336,27 +340,89 @@ const POPUP_HEIGHT = 600; const { calculatePosition } = usePopupPosition(POPUP_WIDTH, POPUP_HEIGHT); +// Get the selector for the current template's @page rule +const getTemplateSelector = () => { + return templateName.value ? `@page ${templateName.value}` : '@page'; +}; + +// Get or create the template-specific @page block +const getOrCreateTemplateBlock = () => { + const selector = getTemplateSelector(); + let block = stylesheetStore.extractBlock(selector); + + if (!block && templateName.value) { + // Create new block with current values from @page + const baseBlock = stylesheetStore.extractBlock('@page'); + if (baseBlock) { + // Insert the new template block after @page + const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`; + const newBlock = `\n@page ${templateName.value} {\n margin: ${marginValue};${background.value.value ? `\n background: ${background.value.value};` : ''}\n}\n`; + + stylesheetStore.content = stylesheetStore.content.replace( + baseBlock, + baseBlock + newBlock + ); + block = stylesheetStore.extractBlock(selector); + } + } + + return block; +}; + +// Remove the template-specific @page block +const removeTemplateBlock = () => { + if (!templateName.value) return; + + const selector = `@page ${templateName.value}`; + const block = stylesheetStore.extractBlock(selector); + + if (block) { + // Remove the block and any surrounding whitespace + stylesheetStore.content = stylesheetStore.content.replace( + new RegExp(`\\n?${selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{[^}]*\\}\\n?`), + '\n' + ); + } +}; + const updateMargins = () => { + // Only update if inheritance is unlocked + if (inheritanceLocked.value) return; + const marginValue = `${margins.value.top.value}${margins.value.top.unit} ${margins.value.right.value}${margins.value.right.unit} ${margins.value.bottom.value}${margins.value.bottom.unit} ${margins.value.left.value}${margins.value.left.unit}`; - const currentBlock = stylesheetStore.extractBlock('@page'); + const currentBlock = getOrCreateTemplateBlock(); if (!currentBlock) return; - const updatedBlock = currentBlock.replace( - /(margin:\s*)[^;]+/, - `$1${marginValue}` - ); + const selector = getTemplateSelector(); - stylesheetStore.content = stylesheetStore.content.replace( - currentBlock, - updatedBlock - ); + if (currentBlock.includes('margin:')) { + const updatedBlock = currentBlock.replace( + /(margin:\s*)[^;]+/, + `$1${marginValue}` + ); + stylesheetStore.content = stylesheetStore.content.replace( + currentBlock, + updatedBlock + ); + } else { + const updatedBlock = currentBlock.replace( + /(\s*})$/, + ` margin: ${marginValue};\n$1` + ); + stylesheetStore.content = stylesheetStore.content.replace( + currentBlock, + updatedBlock + ); + } }; const updateBackground = () => { + // Only update if inheritance is unlocked + if (inheritanceLocked.value) return; if (!background.value.value) return; - const currentBlock = stylesheetStore.extractBlock('@page'); + const currentBlock = getOrCreateTemplateBlock(); if (!currentBlock) return; if (currentBlock.includes('background:')) { @@ -421,39 +487,37 @@ const loadValuesFromStylesheet = () => { try { isUpdatingFromStore = true; - // Extract margin from @page - const marginData = stylesheetStore.extractValue('@page', 'margin'); - if (marginData) { - // Parse margin shorthand (top right bottom left) - const marginStr = typeof marginData === 'string' ? marginData : marginData.value; - const marginValues = marginStr.split(' '); + // Extract values from @page block (same logic as PageSettings) + const pageBlock = stylesheetStore.extractBlock('@page'); + if (!pageBlock) return; - if (marginValues.length === 4) { - // Parse each margin value - const parseMargin = (value) => { - const match = value.match(/^([\d.]+)(\w+)$/); - if (match) { - return { value: parseFloat(match[1]), unit: match[2] }; - } - return { value: 0, unit: 'mm' }; - }; - - const top = parseMargin(marginValues[0]); - const right = parseMargin(marginValues[1]); - const bottom = parseMargin(marginValues[2]); - const left = parseMargin(marginValues[3]); - - margins.value.top = top; - margins.value.right = right; - margins.value.bottom = bottom; - margins.value.left = left; - } + // Parse margins with regex (top right bottom left) + const marginMatch = pageBlock.match( + /margin:\s*([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)\s+([0-9.]+)([a-z]+)/i + ); + if (marginMatch) { + margins.value.top = { + value: parseFloat(marginMatch[1]), + unit: marginMatch[2], + }; + margins.value.right = { + value: parseFloat(marginMatch[3]), + unit: marginMatch[4], + }; + margins.value.bottom = { + value: parseFloat(marginMatch[5]), + unit: marginMatch[6], + }; + margins.value.left = { + value: parseFloat(marginMatch[7]), + unit: marginMatch[8], + }; } - // Extract background from @page - const backgroundData = stylesheetStore.extractValue('@page', 'background'); - if (backgroundData) { - background.value.value = typeof backgroundData === 'string' ? backgroundData : backgroundData.value; + // Extract background + const bgMatch = pageBlock.match(/background:\s*([^;]+)/); + if (bgMatch) { + background.value.value = bgMatch[1].trim(); } } catch (error) { console.error('Error loading values from stylesheet:', error); @@ -462,14 +526,18 @@ const loadValuesFromStylesheet = () => { } }; -const open = (pageElement, event) => { +const open = (pageElement, event, count = 1) => { selectedPageElement.value = pageElement; + pageCount.value = count; position.value = calculatePosition(event); - // Add border to the selected page - pageElement.style.outline = '2px solid #61afef'; + // Extract template name from data-page-type attribute + templateName.value = pageElement.getAttribute('data-page-type') || ''; - // Load values from stylesheet + // Reset inheritance state when opening + inheritanceLocked.value = true; + + // Load values from stylesheet (@page block) loadValuesFromStylesheet(); visible.value = true; @@ -507,21 +575,30 @@ const open = (pageElement, event) => { }; const close = () => { - // Remove border from the selected page - if (selectedPageElement.value) { - selectedPageElement.value.style.outline = ''; - selectedPageElement.value = null; - } + selectedPageElement.value = null; visible.value = false; isEditable.value = false; + emit('close'); }; const toggleInheritance = () => { + const wasLocked = inheritanceLocked.value; inheritanceLocked.value = !inheritanceLocked.value; - // TODO: Implement CSS priority logic when unlocked + + if (inheritanceLocked.value && !wasLocked) { + // Re-locking: remove the template-specific block + // Fields keep their values, but preview returns to @page defaults + removeTemplateBlock(); + } + // When unlocking: fields already have values, block will be created on first edit }; const pageCss = computed(() => { + // Show template-specific block if unlocked and exists, otherwise show @page + if (!inheritanceLocked.value && templateName.value) { + const templateBlock = stylesheetStore.extractBlock(`@page ${templateName.value}`); + if (templateBlock) return templateBlock; + } return stylesheetStore.extractBlock('@page') || ''; }); @@ -602,7 +679,7 @@ defineExpose({ open, close, visible }); } .page-label { - background: #ff8a50; + background: var(--color-page-highlight); color: white; padding: 0.25rem 0.5rem; border-radius: 4px; @@ -616,7 +693,7 @@ defineExpose({ open, close, visible }); } .page-count { - color: #ff8a50; + color: var(--color-page-highlight); font-size: 0.875rem; }