diff --git a/src/App.vue b/src/App.vue index 89efbfc..f6f5290 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,23 +2,40 @@ import PagedJsWrapper from './components/PagedJsWrapper.vue'; import EditorPanel from './components/editor/EditorPanel.vue'; import ElementPopup from './components/ElementPopup.vue'; -import { onMounted, ref, watch } from 'vue'; +import { onMounted, ref, watch, computed } from 'vue'; import { useStylesheetStore } from './stores/stylesheet'; const stylesheetStore = useStylesheetStore(); -const previewFrame = ref(null); +const previewFrame1 = ref(null); +const previewFrame2 = ref(null); const elementPopup = ref(null); let savedScrollPercentage = 0; +const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible +let isTransitioning = false; + +const activeFrame = computed(() => { + return currentFrameIndex.value === 1 ? previewFrame1.value : previewFrame2.value; +}); const renderPreview = async (shouldReloadFromFile = false) => { - const iframe = previewFrame.value; - if (!iframe) return; + if (isTransitioning) return; + isTransitioning = true; - if (iframe.contentWindow && iframe.contentDocument) { - const scrollTop = iframe.contentWindow.scrollY || 0; - const scrollHeight = iframe.contentDocument.documentElement.scrollHeight; - const clientHeight = iframe.contentWindow.innerHeight; + // Determine which iframe is currently visible and which to render to + const visibleFrame = currentFrameIndex.value === 1 ? previewFrame1.value : previewFrame2.value; + const hiddenFrame = currentFrameIndex.value === 1 ? previewFrame2.value : previewFrame1.value; + + if (!hiddenFrame) { + isTransitioning = false; + return; + } + + // Save scroll position from visible frame + if (visibleFrame && visibleFrame.contentWindow && visibleFrame.contentDocument) { + const scrollTop = visibleFrame.contentWindow.scrollY || 0; + const scrollHeight = visibleFrame.contentDocument.documentElement.scrollHeight; + const clientHeight = visibleFrame.contentWindow.innerHeight; const maxScroll = scrollHeight - clientHeight; savedScrollPercentage = maxScroll > 0 ? scrollTop / maxScroll : 0; @@ -28,7 +45,8 @@ const renderPreview = async (shouldReloadFromFile = false) => { await stylesheetStore.loadStylesheet(); } - iframe.srcdoc = ` + // Render to the hidden frame + hiddenFrame.srcdoc = ` @@ -40,20 +58,45 @@ const renderPreview = async (shouldReloadFromFile = false) => { `; - iframe.onload = () => { - iframe.contentDocument.addEventListener( + hiddenFrame.onload = () => { + hiddenFrame.contentDocument.addEventListener( 'click', elementPopup.value.handleIframeClick ); + // Wait for PagedJS to finish rendering setTimeout(() => { - const scrollHeight = iframe.contentDocument.documentElement.scrollHeight; - const clientHeight = iframe.contentWindow.innerHeight; + // Restore scroll position + const scrollHeight = hiddenFrame.contentDocument.documentElement.scrollHeight; + const clientHeight = hiddenFrame.contentWindow.innerHeight; const maxScroll = scrollHeight - clientHeight; const targetScroll = savedScrollPercentage * maxScroll; - iframe.contentWindow.scrollTo(0, targetScroll); - }, 500); + hiddenFrame.contentWindow.scrollTo(0, targetScroll); + + // Start crossfade transition + setTimeout(() => { + // Make hidden frame visible (it's already behind) + hiddenFrame.style.opacity = '1'; + hiddenFrame.style.zIndex = '1'; + + // Fade out visible frame + if (visibleFrame) { + visibleFrame.style.opacity = '0'; + } + + // After fade completes, swap the frames + setTimeout(() => { + if (visibleFrame) { + visibleFrame.style.zIndex = '0'; + } + + // Swap current frame + currentFrameIndex.value = currentFrameIndex.value === 1 ? 2 : 1; + isTransitioning = false; + }, 300); // Match CSS transition duration + }, 100); // Small delay to ensure scroll is set + }, 500); // Wait for PagedJS }; }; @@ -74,18 +117,30 @@ onMounted(() => renderPreview(true)); - + + - + diff --git a/src/components/editor/PageSettings.vue b/src/components/editor/PageSettings.vue index 09630f2..52a9522 100644 --- a/src/components/editor/PageSettings.vue +++ b/src/components/editor/PageSettings.vue @@ -372,7 +372,7 @@ watch(pageNumbers, (enabled) => { if (isUpdatingFromStore) return; immediateUpdate(() => { - // TODO: implement page numbers toggle + updatePageFooters(); }); }); @@ -380,10 +380,85 @@ watch(runningTitle, (enabled) => { if (isUpdatingFromStore) return; immediateUpdate(() => { - // TODO: implement running title toggle + updatePageFooters(); }); }); +const updatePageFooters = () => { + let currentCss = stylesheetStore.content; + + // Remove existing @page:left and @page:right rules + currentCss = currentCss.replace(/@page:left\s*\{[^}]*\}/g, ''); + currentCss = currentCss.replace(/@page:right\s*\{[^}]*\}/g, ''); + + // Remove old @page @bottom-center rule if exists + currentCss = currentCss.replace(/@page\s*\{[^}]*@bottom-center[^}]*\}/g, (match) => { + return match.replace(/@bottom-center\s*\{[^}]*\}/g, ''); + }); + + // Remove string-set rule if running title is disabled + if (!runningTitle.value) { + currentCss = currentCss.replace(/\.chapter\s*>\s*h2\s*\{[^}]*string-set:[^}]*\}\s*/g, ''); + } else if (!currentCss.includes('string-set: title')) { + // Add the string-set rule for h2 titles if running title is enabled + const stringSetRule = '\n.chapter > h2 {\n string-set: title content(text);\n}\n'; + currentCss += stringSetRule; + } + + // Build new rules based on checkboxes + let leftPageRule = ''; + let rightPageRule = ''; + + if (pageNumbers.value || runningTitle.value) { + // Left pages: page number bottom-left, running title right next to it (bottom-left-corner) + let leftBottomLeft = ''; + let leftBottomCenter = ''; + + if (pageNumbers.value && runningTitle.value) { + // Page number on the left, title right next to it + leftBottomLeft = ' @bottom-left {\n content: counter(page) " " string(title);\n }\n'; + } else if (pageNumbers.value) { + leftBottomLeft = ' @bottom-left {\n content: counter(page);\n }\n'; + } else if (runningTitle.value) { + leftBottomLeft = ' @bottom-left {\n content: string(title);\n }\n'; + } + + if (leftBottomLeft || leftBottomCenter) { + leftPageRule = `@page:left {\n${leftBottomLeft}${leftBottomCenter}}\n\n`; + } + + // Right pages: title on the left, page number on the right (next to it) + let rightBottomRight = ''; + + if (pageNumbers.value && runningTitle.value) { + // Title on the left of page number + rightBottomRight = ' @bottom-right {\n content: string(title) " " counter(page);\n }\n'; + } else if (pageNumbers.value) { + rightBottomRight = ' @bottom-right {\n content: counter(page);\n }\n'; + } else if (runningTitle.value) { + rightBottomRight = ' @bottom-right {\n content: string(title);\n }\n'; + } + + if (rightBottomRight) { + rightPageRule = `@page:right {\n${rightBottomRight}}\n\n`; + } + } + + // Insert the new rules after the main @page rule + const pageRuleMatch = currentCss.match(/@page\s*\{[^}]*\}/); + if (pageRuleMatch) { + const insertPosition = pageRuleMatch.index + pageRuleMatch[0].length; + currentCss = + currentCss.slice(0, insertPosition) + + '\n\n' + + leftPageRule + + rightPageRule + + currentCss.slice(insertPosition); + } + + stylesheetStore.content = currentCss; +}; + const syncFromStore = () => { isUpdatingFromStore = true; @@ -421,6 +496,22 @@ const syncFromStore = () => { if (bgMatch) { background.value.value = bgMatch[1].trim(); } + + // Check for page numbers and running title in @page:left and @page:right + const leftPageMatch = stylesheetStore.content.match(/@page:left\s*\{[^}]*\}/); + const rightPageMatch = stylesheetStore.content.match(/@page:right\s*\{[^}]*\}/); + + // Check if page numbers exist (counter(page) in either left or right) + const hasPageNumbers = + (leftPageMatch && leftPageMatch[0].includes('counter(page)')) || + (rightPageMatch && rightPageMatch[0].includes('counter(page)')); + pageNumbers.value = hasPageNumbers; + + // Check if running title exists (string(title) in either left or right) + const hasRunningTitle = + (leftPageMatch && leftPageMatch[0].includes('string(title)')) || + (rightPageMatch && rightPageMatch[0].includes('string(title)')); + runningTitle.value = hasRunningTitle; } finally { isUpdatingFromStore = false; }