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 }