preview-manager.js (7.4 KB)
1 import { insertAfterHead } from './html-utils.js'; 2 import { extractTitleAndFavicon, updateMainPageTitleAndFavicon } from './html-utils.js'; 3 import { generateSpreadCSS, ZINE_PAGE_CSS, ZINE_FLIP_CSS, ZINE_PRINT_CSS } from './zine-styles.js'; 4 import { SPREADS } from './constants.js'; 5 import { getCurrentSpread } from './spread-navigation.js'; 6 import { isMobileDevice } from './mobile-keyboard.js'; 7 import { setupSpreadLayout, scaleSpreadToFit, navigateToSpread } from './spread-layout.js'; 8 import { setSpreadImmediate } from './page-flip-animation.js'; 9 import { generateFontFaceCSS } from './font-registry.js'; 10 11 // Track if flip mode is enabled 12 let flipModeEnabled = true; // Default to enabled 13 let previousSpread = 0; 14 15 export function setFlipMode(enabled) { 16 flipModeEnabled = enabled; 17 } 18 19 export function isFlipModeEnabled() { 20 return flipModeEnabled; 21 } 22 23 // Generate CSS for flip mode 24 function generateFlipModeCSS() { 25 return ` 26 * { margin: 0; padding: 0; box-sizing: border-box; } 27 28 @media screen { 29 html, body { 30 height: 100% !important; 31 overflow: hidden !important; 32 } 33 34 body { 35 display: flex !important; 36 justify-content: center !important; 37 align-items: center !important; 38 } 39 40 body > *:not(.zine-spread-container):not(.iframe-fullscreen-toggle) { 41 display: none !important; 42 } 43 44 ${ZINE_PAGE_CSS} 45 ${ZINE_FLIP_CSS} 46 47 .zine-spread-container { 48 display: flex !important; 49 position: relative; 50 } 51 52 .page { 53 display: block !important; 54 } 55 } 56 57 @media print { 58 ${ZINE_PRINT_CSS} 59 } 60 `; 61 } 62 63 export function updatePreview(editorView, isEditorFocused) { 64 const preview = document.getElementById('preview'); 65 66 if (isMobileDevice() && isEditorFocused && document.body.classList.contains('mobile-keyboard-open')) { 67 return; 68 } 69 70 const code = editorView.state.doc.toString(); 71 72 // Extract and update title and favicon from user's HTML 73 const { title, favicon } = extractTitleAndFavicon(code); 74 updateMainPageTitleAndFavicon(title, favicon); 75 76 let scrollX = 0, scrollY = 0; 77 try { 78 if (preview.contentWindow?.scrollX !== undefined) { 79 scrollX = preview.contentWindow.scrollX; 80 scrollY = preview.contentWindow.scrollY; 81 } 82 } catch (e) {} 83 84 // Inject font-face declarations and spread CSS right after <head> so user styles come last and take precedence 85 const currentSpread = getCurrentSpread(); 86 const spreadCSS = flipModeEnabled ? generateFlipModeCSS() : generateSpreadCSS(currentSpread); 87 const fontCSS = generateFontFaceCSS(); 88 const processedCode = insertAfterHead(code, `<style id="zine-editor-fonts">${fontCSS}</style><style id="zine-editor-spread-css">${spreadCSS}</style>`); 89 90 preview.srcdoc = processedCode || '<!DOCTYPE html><html><head></head><body></body></html>'; 91 92 const onLoad = () => { 93 try { 94 const doc = preview.contentDocument; 95 if (!doc) return; 96 97 // Add CSS for overscroll and fullscreen button 98 const style = doc.createElement('style'); 99 style.textContent = '* { overscroll-behavior: none !important; } .iframe-fullscreen-toggle { position: fixed; top: 5px; right: 5px; z-index: 10000; background: rgba(0, 0, 0, 0.2); color: white; border: none; border-radius: 4px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.2s; box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); -webkit-tap-highlight-color: transparent; outline: none; user-select: none; } .iframe-fullscreen-toggle svg { filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.3)); opacity: 0.8; transition: opacity 0.2s; } @media (hover: hover) and (pointer: fine) { .iframe-fullscreen-toggle:hover { background: rgba(0, 0, 0, 0.35); } .iframe-fullscreen-toggle:hover svg { opacity: 1; } }'; 100 doc.head.appendChild(style); 101 102 // Create fullscreen button 103 const existingButton = doc.getElementById('fullscreenToggle'); 104 if (existingButton) existingButton.remove(); 105 106 const button = doc.createElement('button'); 107 button.id = 'fullscreenToggle'; 108 button.className = 'iframe-fullscreen-toggle'; 109 button.title = 'Toggle fullscreen'; 110 button.innerHTML = '<svg width="20" height="20" viewBox="0 0 14 14" fill="white"><path d="M 7,14 H 5 v 5 h 5 V 17 H 7 Z M 5,10 H 7 V 7 h 3 V 5 H 5 Z m 12,7 h -3 v 2 h 5 V 14 H 17 Z M 14,5 v 2 h 3 v 3 h 2 V 5 Z" transform="translate(-5,-5)"/></svg>'; 111 button.addEventListener('click', function() { 112 parent.postMessage('toggleFullscreen', '*'); 113 }); 114 115 if (doc.body) { 116 doc.body.appendChild(button); 117 118 // Create container for the spread and wrap visible pages 119 const spread = SPREADS[currentSpread]; 120 const container = doc.createElement('div'); 121 container.className = 'zine-spread-container'; 122 123 if (flipModeEnabled) { 124 // Setup flip animation mode 125 setupSpreadLayout(container, spread, doc, true); 126 doc.body.appendChild(container); 127 128 // Set initial spread state without animation 129 setTimeout(() => { 130 scaleSpreadToFit(container, doc); // Must call this BEFORE setSpreadImmediate so zoom is set 131 setSpreadImmediate(currentSpread, doc); 132 133 // On resize, update zoom and reposition book 134 const handleResize = () => { 135 scaleSpreadToFit(container, doc); 136 // Get the actual current spread from the leaf states 137 const leaves = doc.querySelectorAll('.zine-leaf'); 138 let activeSpread = 0; 139 leaves.forEach((leaf, index) => { 140 if (leaf.dataset.state === 'open') { 141 activeSpread = index + 1; 142 } 143 }); 144 setSpreadImmediate(activeSpread, doc); 145 }; 146 doc.defaultView.addEventListener('resize', handleResize); 147 }, 0); 148 } else { 149 // Original non-animated mode 150 setupSpreadLayout(container, spread, doc, false); 151 doc.body.appendChild(container); 152 153 // Scale container to fit viewport 154 const scaleToFit = () => scaleSpreadToFit(container, doc); 155 scaleToFit(); 156 doc.defaultView.addEventListener('resize', scaleToFit); 157 } 158 159 // Store container and flip mode state for navigation updates 160 doc._zineContainer = container; 161 doc._flipModeEnabled = flipModeEnabled; 162 } 163 164 // Add keyboard listener for navigation and shortcuts 165 doc.addEventListener('keydown', function(e) { 166 if (e.key === 'ArrowLeft') { 167 e.preventDefault(); 168 parent.postMessage('prevSpread', '*'); 169 } else if (e.key === 'ArrowRight') { 170 e.preventDefault(); 171 parent.postMessage('nextSpread', '*'); 172 } else if ((e.metaKey || e.ctrlKey) && e.key === 'p') { 173 e.preventDefault(); 174 window.print(); 175 } else if ((e.metaKey || e.ctrlKey) && e.key === 's') { 176 e.preventDefault(); 177 parent.postMessage('saveFile', '*'); 178 } 179 }); 180 181 setTimeout(() => { 182 try { 183 preview.contentWindow?.scrollTo(scrollX, scrollY); 184 } catch (e) {} 185 }, 10); 186 } catch (e) {} 187 188 preview.removeEventListener('load', onLoad); 189 }; 190 preview.addEventListener('load', onLoad); 191 } 192 193 // Setup preview pane interactions 194 export function setupPreviewPane(updatePreviewCallback) { 195 const previewPane = document.querySelector('.preview-pane'); 196 const preview = document.getElementById('preview'); 197 198 // Focus preview when clicking anywhere in the preview pane (for keyboard nav) 199 previewPane.addEventListener('click', (e) => { 200 // Don't steal focus if clicking nav buttons 201 if (!e.target.closest('.spread-nav button')) { 202 preview.focus(); 203 } 204 }); 205 }