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