refactor: extract App.vue logic into composables (762→230 lines)
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 14s
All checks were successful
Deploy / Build and Deploy to Production (push) Successful in 14s
Extracted complex logic from App.vue into focused, reusable composables: New composables: - useKeyboardShortcuts.js (~80 lines): Keyboard shortcuts (Cmd/Ctrl+S, P, Escape, \) - useIframeInteractions.js (~370 lines): Page/element hover, labels, clicks, popups - usePreviewRenderer.js (~160 lines): Double buffering, transitions, scroll persistence - usePrintPreview.js (~70 lines): Print dialog and style collection Benefits: - 70% reduction in App.vue size (532 lines extracted) - Better separation of concerns - Improved maintainability and testability - Clearer code organization Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
dac532a932
commit
be7bb66e70
5 changed files with 729 additions and 581 deletions
630
src/App.vue
630
src/App.vue
|
|
@ -5,10 +5,13 @@ import ElementPopup from './components/ElementPopup.vue';
|
|||
import PagePopup from './components/PagePopup.vue';
|
||||
import PreviewLoader from './components/PreviewLoader.vue';
|
||||
import SaveButton from './components/SaveButton.vue';
|
||||
import { onMounted, onUnmounted, ref, watch, computed, provide } from 'vue';
|
||||
import { onMounted, ref, computed, provide } from 'vue';
|
||||
import { useStylesheetStore } from './stores/stylesheet';
|
||||
import { useNarrativeStore } from './stores/narrative';
|
||||
import Coloris from '@melloware/coloris';
|
||||
import { useKeyboardShortcuts } from './composables/useKeyboardShortcuts';
|
||||
import { useIframeInteractions } from './composables/useIframeInteractions';
|
||||
import { usePreviewRenderer } from './composables/usePreviewRenderer';
|
||||
import { usePrintPreview } from './composables/usePrintPreview';
|
||||
|
||||
const stylesheetStore = useStylesheetStore();
|
||||
const narrativeStore = useNarrativeStore();
|
||||
|
|
@ -21,16 +24,32 @@ const activeTab = ref('');
|
|||
|
||||
provide('activeTab', activeTab);
|
||||
|
||||
// Page interaction state
|
||||
const hoveredPage = ref(null);
|
||||
const selectedPages = ref([]); // Pages with active border (when popup is open)
|
||||
const hoveredElement = ref(null); // Currently hovered content element
|
||||
const selectedElement = ref(null); // Selected element (when popup is open)
|
||||
const EDGE_THRESHOLD = 30; // px from edge to trigger hover
|
||||
// Setup iframe interactions (hover, click, labels)
|
||||
const {
|
||||
hoveredPage,
|
||||
selectedPages,
|
||||
hoveredElement,
|
||||
selectedElement,
|
||||
handleIframeMouseMove,
|
||||
handleIframeClick,
|
||||
handlePagePopupClose,
|
||||
handleElementPopupClose,
|
||||
} = useIframeInteractions({ elementPopup, pagePopup });
|
||||
|
||||
let savedScrollPercentage = 0;
|
||||
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
|
||||
const isTransitioning = ref(false);
|
||||
// Setup preview renderer with double buffering
|
||||
const {
|
||||
renderPreview,
|
||||
currentFrameIndex,
|
||||
isTransitioning,
|
||||
setKeyboardShortcutHandler,
|
||||
} = usePreviewRenderer({
|
||||
previewFrame1,
|
||||
previewFrame2,
|
||||
stylesheetStore,
|
||||
narrativeStore,
|
||||
handleIframeMouseMove,
|
||||
handleIframeClick,
|
||||
});
|
||||
|
||||
const activeFrame = computed(() => {
|
||||
return currentFrameIndex.value === 1
|
||||
|
|
@ -38,568 +57,25 @@ const activeFrame = computed(() => {
|
|||
: previewFrame2.value;
|
||||
});
|
||||
|
||||
// Detect platform for keyboard shortcut display
|
||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyboardShortcut = (event) => {
|
||||
// Escape key - close any open popup
|
||||
if (event.key === 'Escape') {
|
||||
if (elementPopup.value?.visible) {
|
||||
elementPopup.value.close();
|
||||
return;
|
||||
}
|
||||
if (pagePopup.value?.visible) {
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Backslash key - toggle editor panel
|
||||
if (event.key === '\\') {
|
||||
event.preventDefault();
|
||||
// Toggle: if panel is closed, open to 'document' tab; if open, close it
|
||||
activeTab.value = activeTab.value.length > 0 ? '' : 'document';
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+P (Mac) or Ctrl+P (Windows/Linux) - print
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'p') {
|
||||
event.preventDefault();
|
||||
printPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+S (Mac) or Ctrl+S (Windows/Linux) - save
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
|
||||
// Only save if there are changes and not currently saving
|
||||
if (stylesheetStore.isDirty && !stylesheetStore.isSaving) {
|
||||
stylesheetStore.saveCustomCss();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if mouse position is near the edges of a page element
|
||||
const isNearPageEdge = (pageElement, mouseX, mouseY) => {
|
||||
const rect = pageElement.getBoundingClientRect();
|
||||
|
||||
const nearLeft = mouseX >= rect.left && mouseX <= rect.left + EDGE_THRESHOLD;
|
||||
const nearRight =
|
||||
mouseX >= rect.right - EDGE_THRESHOLD && mouseX <= rect.right;
|
||||
const nearTop = mouseY >= rect.top && mouseY <= rect.top + EDGE_THRESHOLD;
|
||||
const nearBottom =
|
||||
mouseY >= rect.bottom - EDGE_THRESHOLD && mouseY <= rect.bottom;
|
||||
|
||||
const inHorizontalRange = mouseY >= rect.top && mouseY <= rect.bottom;
|
||||
const inVerticalRange = mouseX >= rect.left && mouseX <= rect.right;
|
||||
|
||||
return (
|
||||
(nearLeft && inHorizontalRange) ||
|
||||
(nearRight && inHorizontalRange) ||
|
||||
(nearTop && inVerticalRange) ||
|
||||
(nearBottom && inVerticalRange)
|
||||
);
|
||||
};
|
||||
|
||||
// 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
|
||||
);
|
||||
};
|
||||
|
||||
// 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
|
||||
const handleIframeMouseMove = (event) => {
|
||||
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
||||
let foundPage = null;
|
||||
|
||||
// Check if hovering near page edge
|
||||
for (const page of pages) {
|
||||
if (isNearPageEdge(page, event.clientX, event.clientY)) {
|
||||
foundPage = page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update page hover state
|
||||
if (foundPage !== hoveredPage.value) {
|
||||
// Remove highlight from previous page (only if not in selectedPages)
|
||||
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
|
||||
hoveredPage.value.classList.remove('page-hovered');
|
||||
}
|
||||
|
||||
// Remove previous page label
|
||||
removePageLabel(event.target.ownerDocument);
|
||||
|
||||
// Add highlight to new page (only if not already selected)
|
||||
if (foundPage && !selectedPages.value.includes(foundPage)) {
|
||||
foundPage.classList.add('page-hovered');
|
||||
createPageLabel(foundPage);
|
||||
}
|
||||
|
||||
hoveredPage.value = foundPage;
|
||||
}
|
||||
|
||||
// If not near page edge, check for content element hover
|
||||
if (!foundPage) {
|
||||
const contentElement = getContentElement(event.target);
|
||||
const doc = event.target.ownerDocument;
|
||||
|
||||
if (contentElement !== hoveredElement.value) {
|
||||
// Remove highlight from previous element (only if not selected)
|
||||
if (
|
||||
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)
|
||||
if (contentElement && contentElement !== selectedElement.value) {
|
||||
contentElement.classList.add('element-hovered');
|
||||
createElementLabel(contentElement);
|
||||
}
|
||||
|
||||
hoveredElement.value = contentElement;
|
||||
}
|
||||
} else {
|
||||
// Clear element hover when hovering page edge
|
||||
if (
|
||||
hoveredElement.value &&
|
||||
hoveredElement.value !== selectedElement.value
|
||||
) {
|
||||
hoveredElement.value.classList.remove('element-hovered');
|
||||
hoveredElement.value = null;
|
||||
}
|
||||
// Remove element label when hovering page edge
|
||||
removeElementLabel(event.target.ownerDocument);
|
||||
}
|
||||
};
|
||||
|
||||
// Clear selection highlight from all selected pages
|
||||
const clearSelectedPages = () => {
|
||||
selectedPages.value.forEach((page) => {
|
||||
page.classList.remove('page-selected');
|
||||
});
|
||||
selectedPages.value = [];
|
||||
};
|
||||
|
||||
// Text elements that can trigger ElementPopup (excluding containers, images, etc.)
|
||||
const CONTENT_ELEMENTS = [
|
||||
'P',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'BLOCKQUOTE',
|
||||
'LI',
|
||||
'A',
|
||||
'STRONG',
|
||||
'EM',
|
||||
'B',
|
||||
'I',
|
||||
'U',
|
||||
'CODE',
|
||||
'PRE',
|
||||
'FIGCAPTION',
|
||||
];
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Clear selected element highlight
|
||||
const clearSelectedElement = () => {
|
||||
if (selectedElement.value) {
|
||||
selectedElement.value.classList.remove('element-selected');
|
||||
const doc = selectedElement.value.ownerDocument;
|
||||
removeElementLabel(doc);
|
||||
selectedElement.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get count of similar elements (same tag)
|
||||
const getSimilarElementsCount = (element, doc) => {
|
||||
const tagName = element.tagName;
|
||||
const allElements = doc.querySelectorAll(tagName);
|
||||
return allElements.length;
|
||||
};
|
||||
|
||||
// Handle click in iframe
|
||||
const handleIframeClick = (event) => {
|
||||
const element = event.target;
|
||||
|
||||
// Check if clicking near a page edge
|
||||
if (hoveredPage.value) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Clear previous selections
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
|
||||
// Get all pages with same template and highlight them
|
||||
const doc = event.target.ownerDocument;
|
||||
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
||||
sameTemplatePages.forEach((page) => {
|
||||
page.classList.add('page-selected');
|
||||
});
|
||||
selectedPages.value = sameTemplatePages;
|
||||
|
||||
// Remove labels when opening popup
|
||||
removePageLabel(doc);
|
||||
removeElementLabel(doc);
|
||||
|
||||
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
||||
elementPopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show popup for elements inside the page template
|
||||
const isInsidePage = element.closest('.pagedjs_page');
|
||||
if (!isInsidePage) {
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show ElementPopup for content elements, not divs
|
||||
const contentElement = getContentElement(element);
|
||||
if (!contentElement) {
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear page selections
|
||||
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
|
||||
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
|
||||
selectedElement.value = contentElement;
|
||||
contentElement.classList.add('element-selected');
|
||||
|
||||
// Get count of similar elements
|
||||
const count = getSimilarElementsCount(contentElement, doc);
|
||||
|
||||
elementPopup.value.handleIframeClick(event, contentElement, count);
|
||||
pagePopup.value.close();
|
||||
};
|
||||
|
||||
// Expose clearSelectedPages for PagePopup to call when closing
|
||||
const handlePagePopupClose = () => {
|
||||
clearSelectedPages();
|
||||
};
|
||||
|
||||
// Handle ElementPopup close
|
||||
const handleElementPopupClose = () => {
|
||||
clearSelectedElement();
|
||||
};
|
||||
|
||||
const renderPreview = async (shouldReloadFromFile = false) => {
|
||||
if (isTransitioning.value) return;
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 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.value = 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;
|
||||
}
|
||||
|
||||
if (shouldReloadFromFile || !stylesheetStore.content) {
|
||||
await stylesheetStore.loadStylesheet();
|
||||
}
|
||||
|
||||
// Render to the hidden frame
|
||||
hiddenFrame.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
||||
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
||||
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
||||
</head>
|
||||
<body>${document.getElementById('content-source').innerHTML}</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
hiddenFrame.onload = () => {
|
||||
// Add event listeners for page and element interactions
|
||||
hiddenFrame.contentDocument.addEventListener(
|
||||
'mousemove',
|
||||
handleIframeMouseMove
|
||||
);
|
||||
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
|
||||
|
||||
// Add keyboard shortcut listener to iframe (for when focus is inside iframe)
|
||||
hiddenFrame.contentDocument.addEventListener('keydown', handleKeyboardShortcut);
|
||||
|
||||
// Close Coloris when clicking in the iframe
|
||||
hiddenFrame.contentDocument.addEventListener('click', () => {
|
||||
Coloris.close();
|
||||
});
|
||||
|
||||
// Wait for PagedJS to finish rendering
|
||||
setTimeout(() => {
|
||||
// Restore scroll position
|
||||
const scrollHeight =
|
||||
hiddenFrame.contentDocument.documentElement.scrollHeight;
|
||||
const clientHeight = hiddenFrame.contentWindow.innerHeight;
|
||||
const maxScroll = scrollHeight - clientHeight;
|
||||
const targetScroll = savedScrollPercentage * maxScroll;
|
||||
|
||||
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.value = false;
|
||||
}, 200); // Match CSS transition duration
|
||||
}, 50); // Small delay to ensure scroll is set
|
||||
}, 200); // Wait for PagedJS
|
||||
};
|
||||
};
|
||||
|
||||
watch(
|
||||
() => stylesheetStore.content,
|
||||
() => {
|
||||
renderPreview();
|
||||
}
|
||||
);
|
||||
|
||||
// Re-render when narrative data changes
|
||||
watch(
|
||||
() => narrativeStore.data,
|
||||
() => {
|
||||
if (narrativeStore.data) {
|
||||
renderPreview();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 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 <style> tags content
|
||||
doc.querySelectorAll('style').forEach((style) => {
|
||||
allStyles += style.innerHTML + '\n';
|
||||
});
|
||||
|
||||
// Get rules from stylesheets
|
||||
for (const sheet of doc.styleSheets) {
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
allStyles += rule.cssText + '\n';
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin stylesheet, try to fetch it
|
||||
if (sheet.href) {
|
||||
try {
|
||||
const response = await fetch(sheet.href);
|
||||
const css = await response.text();
|
||||
allStyles += css;
|
||||
} catch (fetchError) {
|
||||
console.warn('Could not fetch stylesheet:', sheet.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save current page content
|
||||
const originalContent = document.body.innerHTML;
|
||||
const originalStyles = document.head.innerHTML;
|
||||
|
||||
// Replace page content with iframe content
|
||||
document.head.innerHTML = `
|
||||
<meta charset="UTF-8">
|
||||
<title>Impression</title>
|
||||
<style>${allStyles}</style>
|
||||
`;
|
||||
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);
|
||||
};
|
||||
|
||||
// Setup print preview
|
||||
const { printPreview } = usePrintPreview(activeFrame);
|
||||
|
||||
// Setup keyboard shortcuts (depends on printPreview)
|
||||
const {
|
||||
handleKeyboardShortcut,
|
||||
isMac
|
||||
} = useKeyboardShortcuts({
|
||||
stylesheetStore,
|
||||
elementPopup,
|
||||
pagePopup,
|
||||
activeTab,
|
||||
printPreview,
|
||||
});
|
||||
|
||||
// Attach keyboard shortcut handler to renderer
|
||||
setKeyboardShortcutHandler(handleKeyboardShortcut);
|
||||
|
||||
// Lifecycle: Initialize app on mount
|
||||
onMounted(async () => {
|
||||
// Load narrative data (narrativeUrl constructed from location, always present)
|
||||
await narrativeStore.loadNarrative(location.href + '.json');
|
||||
|
|
@ -609,17 +85,9 @@ onMounted(async () => {
|
|||
await stylesheetStore.initializeFromNarrative(narrativeStore.data);
|
||||
}
|
||||
|
||||
// Add keyboard shortcut listener to document (for when focus is outside iframe)
|
||||
document.addEventListener('keydown', handleKeyboardShortcut);
|
||||
|
||||
// Render preview after data is loaded
|
||||
renderPreview(true);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up keyboard shortcut listener
|
||||
document.removeEventListener('keydown', handleKeyboardShortcut);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
372
src/composables/useIframeInteractions.js
Normal file
372
src/composables/useIframeInteractions.js
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
import { ref } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for managing interactions with pages and elements in the iframe
|
||||
* Handles hover effects, labels, and click events for both pages and content elements
|
||||
*/
|
||||
export function useIframeInteractions({ elementPopup, pagePopup }) {
|
||||
// Page interaction state
|
||||
const hoveredPage = ref(null);
|
||||
const selectedPages = ref([]); // Pages with active border (when popup is open)
|
||||
const hoveredElement = ref(null); // Currently hovered content element
|
||||
const selectedElement = ref(null); // Selected element (when popup is open)
|
||||
const EDGE_THRESHOLD = 30; // px from edge to trigger hover
|
||||
|
||||
// Text elements that can trigger ElementPopup (excluding containers, images, etc.)
|
||||
const CONTENT_ELEMENTS = [
|
||||
'P',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
'H4',
|
||||
'H5',
|
||||
'H6',
|
||||
'BLOCKQUOTE',
|
||||
'LI',
|
||||
'A',
|
||||
'STRONG',
|
||||
'EM',
|
||||
'B',
|
||||
'I',
|
||||
'U',
|
||||
'CODE',
|
||||
'PRE',
|
||||
'FIGCAPTION',
|
||||
];
|
||||
|
||||
// Check if mouse position is near the edges of a page element
|
||||
const isNearPageEdge = (pageElement, mouseX, mouseY) => {
|
||||
const rect = pageElement.getBoundingClientRect();
|
||||
|
||||
const nearLeft = mouseX >= rect.left && mouseX <= rect.left + EDGE_THRESHOLD;
|
||||
const nearRight =
|
||||
mouseX >= rect.right - EDGE_THRESHOLD && mouseX <= rect.right;
|
||||
const nearTop = mouseY >= rect.top && mouseY <= rect.top + EDGE_THRESHOLD;
|
||||
const nearBottom =
|
||||
mouseY >= rect.bottom - EDGE_THRESHOLD && mouseY <= rect.bottom;
|
||||
|
||||
const inHorizontalRange = mouseY >= rect.top && mouseY <= rect.bottom;
|
||||
const inVerticalRange = mouseX >= rect.left && mouseX <= rect.right;
|
||||
|
||||
return (
|
||||
(nearLeft && inHorizontalRange) ||
|
||||
(nearRight && inHorizontalRange) ||
|
||||
(nearTop && inVerticalRange) ||
|
||||
(nearBottom && inVerticalRange)
|
||||
);
|
||||
};
|
||||
|
||||
// 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
|
||||
);
|
||||
};
|
||||
|
||||
// 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();
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// Clear selection highlight from all selected pages
|
||||
const clearSelectedPages = () => {
|
||||
selectedPages.value.forEach((page) => {
|
||||
page.classList.remove('page-selected');
|
||||
});
|
||||
selectedPages.value = [];
|
||||
};
|
||||
|
||||
// Clear selected element highlight
|
||||
const clearSelectedElement = () => {
|
||||
if (selectedElement.value) {
|
||||
selectedElement.value.classList.remove('element-selected');
|
||||
const doc = selectedElement.value.ownerDocument;
|
||||
removeElementLabel(doc);
|
||||
selectedElement.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Get count of similar elements (same tag)
|
||||
const getSimilarElementsCount = (element, doc) => {
|
||||
const tagName = element.tagName;
|
||||
const allElements = doc.querySelectorAll(tagName);
|
||||
return allElements.length;
|
||||
};
|
||||
|
||||
// Handle mouse movement in iframe
|
||||
const handleIframeMouseMove = (event) => {
|
||||
const pages = event.target.ownerDocument.querySelectorAll('.pagedjs_page');
|
||||
let foundPage = null;
|
||||
|
||||
// Check if hovering near page edge
|
||||
for (const page of pages) {
|
||||
if (isNearPageEdge(page, event.clientX, event.clientY)) {
|
||||
foundPage = page;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update page hover state
|
||||
if (foundPage !== hoveredPage.value) {
|
||||
// Remove highlight from previous page (only if not in selectedPages)
|
||||
if (hoveredPage.value && !selectedPages.value.includes(hoveredPage.value)) {
|
||||
hoveredPage.value.classList.remove('page-hovered');
|
||||
}
|
||||
|
||||
// Remove previous page label
|
||||
removePageLabel(event.target.ownerDocument);
|
||||
|
||||
// Add highlight to new page (only if not already selected)
|
||||
if (foundPage && !selectedPages.value.includes(foundPage)) {
|
||||
foundPage.classList.add('page-hovered');
|
||||
createPageLabel(foundPage);
|
||||
}
|
||||
|
||||
hoveredPage.value = foundPage;
|
||||
}
|
||||
|
||||
// If not near page edge, check for content element hover
|
||||
if (!foundPage) {
|
||||
const contentElement = getContentElement(event.target);
|
||||
const doc = event.target.ownerDocument;
|
||||
|
||||
if (contentElement !== hoveredElement.value) {
|
||||
// Remove highlight from previous element (only if not selected)
|
||||
if (
|
||||
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)
|
||||
if (contentElement && contentElement !== selectedElement.value) {
|
||||
contentElement.classList.add('element-hovered');
|
||||
createElementLabel(contentElement);
|
||||
}
|
||||
|
||||
hoveredElement.value = contentElement;
|
||||
}
|
||||
} else {
|
||||
// Clear element hover when hovering page edge
|
||||
if (
|
||||
hoveredElement.value &&
|
||||
hoveredElement.value !== selectedElement.value
|
||||
) {
|
||||
hoveredElement.value.classList.remove('element-hovered');
|
||||
hoveredElement.value = null;
|
||||
}
|
||||
// Remove element label when hovering page edge
|
||||
removeElementLabel(event.target.ownerDocument);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle click in iframe
|
||||
const handleIframeClick = (event) => {
|
||||
const element = event.target;
|
||||
|
||||
// Check if clicking near a page edge
|
||||
if (hoveredPage.value) {
|
||||
event.stopPropagation();
|
||||
|
||||
// Clear previous selections
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
|
||||
// Get all pages with same template and highlight them
|
||||
const doc = event.target.ownerDocument;
|
||||
const sameTemplatePages = getPagesWithSameTemplate(hoveredPage.value, doc);
|
||||
sameTemplatePages.forEach((page) => {
|
||||
page.classList.add('page-selected');
|
||||
});
|
||||
selectedPages.value = sameTemplatePages;
|
||||
|
||||
// Remove labels when opening popup
|
||||
removePageLabel(doc);
|
||||
removeElementLabel(doc);
|
||||
|
||||
pagePopup.value.open(hoveredPage.value, event, sameTemplatePages.length);
|
||||
elementPopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show popup for elements inside the page template
|
||||
const isInsidePage = element.closest('.pagedjs_page');
|
||||
if (!isInsidePage) {
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show ElementPopup for content elements, not divs
|
||||
const contentElement = getContentElement(element);
|
||||
if (!contentElement) {
|
||||
clearSelectedPages();
|
||||
clearSelectedElement();
|
||||
elementPopup.value.close();
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear page selections
|
||||
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
|
||||
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
|
||||
selectedElement.value = contentElement;
|
||||
contentElement.classList.add('element-selected');
|
||||
|
||||
// Get count of similar elements
|
||||
const count = getSimilarElementsCount(contentElement, doc);
|
||||
|
||||
elementPopup.value.handleIframeClick(event, contentElement, count);
|
||||
pagePopup.value.close();
|
||||
};
|
||||
|
||||
// Handlers for popup close events
|
||||
const handlePagePopupClose = () => {
|
||||
clearSelectedPages();
|
||||
};
|
||||
|
||||
const handleElementPopupClose = () => {
|
||||
clearSelectedElement();
|
||||
};
|
||||
|
||||
return {
|
||||
// State
|
||||
hoveredPage,
|
||||
selectedPages,
|
||||
hoveredElement,
|
||||
selectedElement,
|
||||
// Handlers
|
||||
handleIframeMouseMove,
|
||||
handleIframeClick,
|
||||
handlePagePopupClose,
|
||||
handleElementPopupClose,
|
||||
// Utilities
|
||||
clearSelectedPages,
|
||||
clearSelectedElement,
|
||||
};
|
||||
}
|
||||
81
src/composables/useKeyboardShortcuts.js
Normal file
81
src/composables/useKeyboardShortcuts.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
/**
|
||||
* Composable for managing global keyboard shortcuts
|
||||
* Handles Cmd/Ctrl+S (save), Cmd/Ctrl+P (print), Escape (close popups), \ (toggle panel)
|
||||
*/
|
||||
export function useKeyboardShortcuts({
|
||||
stylesheetStore,
|
||||
elementPopup,
|
||||
pagePopup,
|
||||
activeTab,
|
||||
printPreview
|
||||
}) {
|
||||
// Detect platform for keyboard shortcut display
|
||||
const isMac = typeof navigator !== 'undefined' && navigator.platform.toUpperCase().indexOf('MAC') >= 0;
|
||||
|
||||
// Handle keyboard shortcuts
|
||||
const handleKeyboardShortcut = (event) => {
|
||||
// Escape key - close any open popup
|
||||
if (event.key === 'Escape') {
|
||||
if (elementPopup.value?.visible) {
|
||||
elementPopup.value.close();
|
||||
return;
|
||||
}
|
||||
if (pagePopup.value?.visible) {
|
||||
pagePopup.value.close();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Backslash key - toggle editor panel
|
||||
if (event.key === '\\') {
|
||||
event.preventDefault();
|
||||
// Toggle: if panel is closed, open to 'document' tab; if open, close it
|
||||
activeTab.value = activeTab.value.length > 0 ? '' : 'document';
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+P (Mac) or Ctrl+P (Windows/Linux) - print
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'p') {
|
||||
event.preventDefault();
|
||||
printPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cmd+S (Mac) or Ctrl+S (Windows/Linux) - save
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 's') {
|
||||
event.preventDefault();
|
||||
|
||||
// Only save if there are changes and not currently saving
|
||||
if (stylesheetStore.isDirty && !stylesheetStore.isSaving) {
|
||||
stylesheetStore.saveCustomCss();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Attach keyboard listener to iframe document
|
||||
const attachToIframe = (iframe) => {
|
||||
if (iframe && iframe.contentDocument) {
|
||||
iframe.contentDocument.addEventListener('keydown', handleKeyboardShortcut);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup keyboard listeners on mount
|
||||
onMounted(() => {
|
||||
// Add keyboard shortcut listener to document (for when focus is outside iframe)
|
||||
document.addEventListener('keydown', handleKeyboardShortcut);
|
||||
});
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
// Clean up keyboard shortcut listener
|
||||
document.removeEventListener('keydown', handleKeyboardShortcut);
|
||||
});
|
||||
|
||||
return {
|
||||
handleKeyboardShortcut,
|
||||
attachToIframe,
|
||||
isMac
|
||||
};
|
||||
}
|
||||
157
src/composables/usePreviewRenderer.js
Normal file
157
src/composables/usePreviewRenderer.js
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { ref, watch } from 'vue';
|
||||
import Coloris from '@melloware/coloris';
|
||||
|
||||
/**
|
||||
* Composable for managing preview rendering with double buffering
|
||||
* Handles iframe transitions, scroll persistence, and PagedJS rendering
|
||||
*/
|
||||
export function usePreviewRenderer({
|
||||
previewFrame1,
|
||||
previewFrame2,
|
||||
stylesheetStore,
|
||||
narrativeStore,
|
||||
handleIframeMouseMove,
|
||||
handleIframeClick,
|
||||
}) {
|
||||
let savedScrollPercentage = 0;
|
||||
const currentFrameIndex = ref(1); // 1 or 2, which iframe is currently visible
|
||||
const isTransitioning = ref(false);
|
||||
let keyboardShortcutHandler = null;
|
||||
|
||||
/**
|
||||
* Render preview to hidden iframe with crossfade transition
|
||||
*/
|
||||
const renderPreview = async (shouldReloadFromFile = false) => {
|
||||
if (isTransitioning.value) return;
|
||||
isTransitioning.value = true;
|
||||
|
||||
// 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.value = 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;
|
||||
}
|
||||
|
||||
if (shouldReloadFromFile || !stylesheetStore.content) {
|
||||
await stylesheetStore.loadStylesheet();
|
||||
}
|
||||
|
||||
// Render to the hidden frame
|
||||
hiddenFrame.srcdoc = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/assets/css/pagedjs-interface.css">
|
||||
<style id="dynamic-styles">${stylesheetStore.content}</style>
|
||||
<script src="https://unpkg.com/pagedjs/dist/paged.polyfill.js"><\/script>
|
||||
</head>
|
||||
<body>${document.getElementById('content-source').innerHTML}</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
hiddenFrame.onload = () => {
|
||||
// Add event listeners for page and element interactions
|
||||
hiddenFrame.contentDocument.addEventListener(
|
||||
'mousemove',
|
||||
handleIframeMouseMove
|
||||
);
|
||||
hiddenFrame.contentDocument.addEventListener('click', handleIframeClick);
|
||||
|
||||
// Add keyboard shortcut listener to iframe (for when focus is inside iframe)
|
||||
if (keyboardShortcutHandler) {
|
||||
hiddenFrame.contentDocument.addEventListener('keydown', keyboardShortcutHandler);
|
||||
}
|
||||
|
||||
// Close Coloris when clicking in the iframe
|
||||
hiddenFrame.contentDocument.addEventListener('click', () => {
|
||||
Coloris.close();
|
||||
});
|
||||
|
||||
// Wait for PagedJS to finish rendering
|
||||
setTimeout(() => {
|
||||
// Restore scroll position
|
||||
const scrollHeight =
|
||||
hiddenFrame.contentDocument.documentElement.scrollHeight;
|
||||
const clientHeight = hiddenFrame.contentWindow.innerHeight;
|
||||
const maxScroll = scrollHeight - clientHeight;
|
||||
const targetScroll = savedScrollPercentage * maxScroll;
|
||||
|
||||
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.value = false;
|
||||
}, 200); // Match CSS transition duration
|
||||
}, 50); // Small delay to ensure scroll is set
|
||||
}, 200); // Wait for PagedJS
|
||||
};
|
||||
};
|
||||
|
||||
// Watch for stylesheet changes and re-render
|
||||
watch(
|
||||
() => stylesheetStore.content,
|
||||
() => {
|
||||
renderPreview();
|
||||
}
|
||||
);
|
||||
|
||||
// Re-render when narrative data changes
|
||||
watch(
|
||||
() => narrativeStore.data,
|
||||
() => {
|
||||
if (narrativeStore.data) {
|
||||
renderPreview();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Set the keyboard shortcut handler (called after keyboard shortcuts composable is initialized)
|
||||
*/
|
||||
const setKeyboardShortcutHandler = (handler) => {
|
||||
keyboardShortcutHandler = handler;
|
||||
};
|
||||
|
||||
return {
|
||||
renderPreview,
|
||||
currentFrameIndex,
|
||||
isTransitioning,
|
||||
setKeyboardShortcutHandler,
|
||||
};
|
||||
}
|
||||
70
src/composables/usePrintPreview.js
Normal file
70
src/composables/usePrintPreview.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/**
|
||||
* Composable for handling print preview functionality
|
||||
* Collects styles from iframe and triggers browser print dialog
|
||||
*/
|
||||
export function usePrintPreview(activeFrame) {
|
||||
/**
|
||||
* Print the PagedJS content from the active frame
|
||||
*/
|
||||
const printPreview = async () => {
|
||||
const frame = activeFrame.value;
|
||||
if (!frame || !frame.contentDocument) return;
|
||||
|
||||
const doc = frame.contentDocument;
|
||||
|
||||
// Collect all styles
|
||||
let allStyles = '';
|
||||
|
||||
// Get inline <style> tags content
|
||||
doc.querySelectorAll('style').forEach((style) => {
|
||||
allStyles += style.innerHTML + '\n';
|
||||
});
|
||||
|
||||
// Get rules from stylesheets
|
||||
for (const sheet of doc.styleSheets) {
|
||||
try {
|
||||
for (const rule of sheet.cssRules) {
|
||||
allStyles += rule.cssText + '\n';
|
||||
}
|
||||
} catch (e) {
|
||||
// Cross-origin stylesheet, try to fetch it
|
||||
if (sheet.href) {
|
||||
try {
|
||||
const response = await fetch(sheet.href);
|
||||
const css = await response.text();
|
||||
allStyles += css;
|
||||
} catch (fetchError) {
|
||||
console.warn('Could not fetch stylesheet:', sheet.href);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save current page content
|
||||
const originalContent = document.body.innerHTML;
|
||||
const originalStyles = document.head.innerHTML;
|
||||
|
||||
// Replace page content with iframe content
|
||||
document.head.innerHTML = `
|
||||
<meta charset="UTF-8">
|
||||
<title>Impression</title>
|
||||
<style>${allStyles}</style>
|
||||
`;
|
||||
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);
|
||||
};
|
||||
|
||||
return {
|
||||
printPreview
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue