diff --git a/assets/runtime.js b/assets/runtime.js index b691d80..2c0acdc 100644 --- a/assets/runtime.js +++ b/assets/runtime.js @@ -25,22 +25,59 @@ function ready(fn){ if(document.readyState!='loading')fn(); else document.addEventListener('DOMContentLoaded',fn);} + /* ========== Parse URL for preview-only mode ========== + * When loaded as iframe.src = "index.html?preview=3", runtime enters a + * locked single-slide mode: only slide N is visible, no chrome, no keys, + * no hash updates. This is how the presenter window shows pixel-perfect + * previews — by loading the actual deck file in an iframe and telling it + * to display only a specific slide. + */ + function getPreviewIdx() { + const m = /[?&]preview=(\d+)/.exec(location.search || ''); + return m ? parseInt(m[1], 10) - 1 : -1; + } + ready(function () { const deck = document.querySelector('.deck'); if (!deck) return; const slides = Array.from(deck.querySelectorAll('.slide')); if (!slides.length) return; + const previewOnlyIdx = getPreviewIdx(); + const isPreviewMode = previewOnlyIdx >= 0 && previewOnlyIdx < slides.length; + + /* ===== Preview-only mode: show one slide, hide everything else ===== */ + if (isPreviewMode) { + slides.forEach((s, i) => { + s.classList.toggle('is-active', i === previewOnlyIdx); + if (i !== previewOnlyIdx) { + s.style.display = 'none'; + } else { + s.style.opacity = '1'; + s.style.transform = 'none'; + s.style.pointerEvents = 'auto'; + } + }); + /* Hide chrome that the presenter shouldn't see in preview */ + const hideSel = '.progress-bar, .notes-overlay, .overview, .notes, aside.notes, .speaker-notes'; + document.querySelectorAll(hideSel).forEach(el => { el.style.display = 'none'; }); + /* Also add a data attr so templates can style preview mode if needed */ + document.documentElement.setAttribute('data-preview', '1'); + document.body.setAttribute('data-preview', '1'); + /* Don't register key handlers, don't start broadcast channel, don't auto-hash */ + return; + } + let idx = 0; const total = slides.length; /* ===== BroadcastChannel for presenter sync ===== */ - const CHANNEL_NAME = 'html-ppt-presenter-' + (location.pathname + location.search); + const CHANNEL_NAME = 'html-ppt-presenter-' + location.pathname; let bc; try { bc = new BroadcastChannel(CHANNEL_NAME); } catch(e) { bc = null; } - // Are we running inside the presenter popup? - const isPresenterWindow = location.hash.indexOf('__presenter__') !== -1; + // Are we running inside the presenter popup? (legacy flag, now unused) + const isPresenterWindow = false; /* ===== progress bar ===== */ let bar = document.querySelector('.progress-bar'); @@ -140,7 +177,15 @@ function toggleNotes(force){ notes.classList.toggle('open', force!==undefined?force:!notes.classList.contains('open')); } function toggleOverview(force){ overview.classList.toggle('open', force!==undefined?force:!overview.classList.contains('open')); } - /* ========== PRESENTER MODE (new window) ========== */ + /* ========== PRESENTER MODE — Magnetic-card popup window ========== */ + /* Opens a new window with 4 draggable, resizable cards: + * CURRENT — iframe(?preview=N) pixel-perfect preview of current slide + * NEXT — iframe(?preview=N+1) pixel-perfect preview of next slide + * SCRIPT — large speaker notes (逐字稿) + * TIMER — elapsed timer + page counter + controls + * Cards remember position/size in localStorage. + * Two windows sync via BroadcastChannel. + */ let presenterWin = null; function openPresenterWindow() { @@ -149,31 +194,22 @@ return; } - // Collect all slides' HTML and notes - const slideData = slides.map((s, i) => { + // Build absolute URL of THIS deck file (without hash/query) + const deckUrl = location.protocol + '//' + location.host + location.pathname; + + // Collect slide titles + notes (HTML strings) + const slideMeta = slides.map((s, i) => { const note = s.querySelector('.notes, aside.notes, .speaker-notes'); return { - html: s.outerHTML, - notes: note ? note.innerHTML : '', title: s.getAttribute('data-title') || - (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)) + (s.querySelector('h1,h2,h3')||{}).textContent || ('Slide '+(i+1)), + notes: note ? note.innerHTML : '' }; }); - // Collect all stylesheets — use absolute URLs so popup can resolve them - const styleSheets = Array.from(document.querySelectorAll('link[rel="stylesheet"], style')).map(el => { - if (el.tagName === 'LINK') return ''; - return ''; - }).join('\n'); + const presenterHTML = buildPresenterHTML(deckUrl, slideMeta, total, idx, CHANNEL_NAME); - // Collect body classes (e.g. tpl-presenter-mode-reveal) so scoped CSS works - const bodyClasses = document.body.className || ''; - // Collect attributes for theme variables - const htmlAttrs = Array.from(root.attributes).map(a => a.name+'="'+a.value+'"').join(' '); - - const presenterHTML = buildPresenterHTML(slideData, styleSheets, total, idx, bodyClasses, htmlAttrs); - - presenterWin = window.open('', 'html-ppt-presenter', 'width=1200,height=800,menubar=no,toolbar=no'); + presenterWin = window.open('', 'html-ppt-presenter', 'width=1280,height=820,menubar=no,toolbar=no'); if (!presenterWin) { alert('请允许弹出窗口以使用演讲者视图'); return; @@ -183,258 +219,479 @@ presenterWin.document.close(); } - function buildPresenterHTML(slideData, styleSheets, total, startIdx, bodyClasses, htmlAttrs) { - const slidesJSON = JSON.stringify(slideData); + function buildPresenterHTML(deckUrl, slideMeta, total, startIdx, channelName) { + const metaJSON = JSON.stringify(slideMeta); + const deckUrlJSON = JSON.stringify(deckUrl); + const channelJSON = JSON.stringify(channelName); + const storageKey = 'html-ppt-presenter:' + location.pathname; - // Build iframe document template. Each iframe gets its own viewport - // at 1920x1080 so vw/vh/clamp all resolve exactly like the audience view. - // We inject via contentDocument.write() so there's ZERO HTML escaping issues. - // Template is a JS string (not embedded HTML attribute), so quotes stay raw. - // IMPORTANT: Do NOT override .slide or .deck styling. The original - // base.css + theme + scoped CSS handles flex centering, padding, etc. - // We only: (1) reset margins, (2) force the single slide to be visible - // (.is-active), (3) hide the speaker notes and runtime chrome. - const iframeDocTemplate = '' - + '' - + '
' - + styleSheets - + '' - + '' - + '