/** * Green-blueprint storybook — upload to WP Media Library as .txt (see gbp-avada-embed-LIVE-with-loader.html). */ (function () { const root = document.querySelector('[data-gbp-storybook]'); if (!root) return; const renderMode = (root.getAttribute('data-render-mode') || 'images').toLowerCase(); const pdfUrl = root.getAttribute('data-pdf-url'); const textUrl = root.getAttribute('data-text-url'); const imagesBase = (root.getAttribute('data-images-base') || '').replace(/\/?$/, '/'); const coverUrl = root.getAttribute('data-cover-url') || (imagesBase ? imagesBase + 'cover.webp' : ''); const spreadCount = parseInt(root.getAttribute('data-spread-count') || '18', 10); const manifestUrl = root.getAttribute('data-manifest-url'); const shell = root.querySelector('.gbp-book-shell'); const coverSlot = root.querySelector('[data-cover-canvas]'); const coverBtn = root.querySelector('.gbp-cover-hit') || coverSlot; const spreadEl = root.querySelector('.gbp-spread'); const spreadLeft = root.querySelector('[data-spread-left]'); const spreadRight = root.querySelector('[data-spread-right]'); const prevBtn = root.querySelector('[data-prev]'); const nextBtn = root.querySelector('[data-next]'); const closeBtn = root.querySelector('[data-close-book]'); const metaEl = root.querySelector('[data-spread-meta]'); const zhBody = root.querySelector('[data-text-zh]'); const enBody = root.querySelector('[data-text-en]'); const scFallback = root.querySelector('[data-sc-fallback]'); const textPanel = root.querySelector('.gbp-text-panel'); let pdfDoc = null; let spreads = []; let spreadById = {}; let textReady = false; function buildSpreadById(list) { spreadById = {}; (list || []).forEach(function (s) { if (s && typeof s.id === 'number') spreadById[s.id] = s; }); } function getSpread(index) { return spreadById[index] || spreads[index] || {}; } let spreadIndex = 0; let renderToken = 0; let imageUrls = []; let usePdf = renderMode === 'pdf' || (!imagesBase && !manifestUrl && !!pdfUrl); const pdfjsLocalBase = (root.getAttribute('data-pdfjs-base') || '').replace(/\/?$/, '/') || 'green-blueprint/vendor/pdfjs-3.11.174/'; const pdfjsCdnBase = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/'; const pdfCMapBase = root.getAttribute('data-cmap-base') || 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/cmaps/'; const pdfFontBase = root.getAttribute('data-font-base') || 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/standard_fonts/'; function loadScript(src) { return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.onload = () => resolve(); s.onerror = () => reject(new Error('Failed to load ' + src)); document.head.appendChild(s); }); } async function ensurePdfJs() { if (window.pdfjsLib) return window.pdfjsLib; try { await loadScript(pdfjsLocalBase + 'pdf.min.js'); window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsLocalBase + 'pdf.worker.min.js'; } catch (localErr) { await loadScript(pdfjsCdnBase + 'pdf.min.js'); window.pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsCdnBase + 'pdf.worker.min.js'; } return window.pdfjsLib; } function setHostImage(host, src, alt) { if (!host) return; if (!src) { host.classList.add('is-empty'); host.replaceChildren(); return; } host.classList.remove('is-empty'); const img = document.createElement('img'); img.src = src; img.alt = alt || ''; img.loading = spreadIndex < 2 ? 'eager' : 'lazy'; img.decoding = 'async'; host.replaceChildren(img); } function spreadFileName(index) { const n = index + 1; return 'spread-' + String(n).padStart(2, '0') + '.webp'; } function defaultImageUrls() { const urls = []; for (let i = 0; i < spreadCount; i++) { urls.push(imagesBase + spreadFileName(i)); } return urls; } async function resolveImageUrls() { if (manifestUrl) { const res = await fetch(manifestUrl); const data = await res.json(); if (data.cover) root.setAttribute('data-cover-resolved', data.cover); if (Array.isArray(data.spreads) && data.spreads.length) { return data.spreads; } } return defaultImageUrls(); } function syncTextPanel() { const spread = getSpread(spreadIndex); const spreadTotal = imageUrls.length || spreads.length || spreadCount; if (metaEl) { metaEl.textContent = 'Spread ' + (spreadIndex + 1) + ' / ' + spreadTotal + (usePdf ? '' : ' · ' + spreadFileName(spreadIndex)); } if (!textReady && textUrl) { if (textPanel) textPanel.classList.add('is-loading'); if (zhBody) zhBody.textContent = '載入繁中/英文對照稿中…'; if (enBody) enBody.textContent = 'Loading Traditional Chinese and English text…'; return; } if (textPanel) textPanel.classList.remove('is-loading'); if (zhBody) { zhBody.textContent = spread.zh || '(此跨頁之繁體中文對照稿整理中。圖書內頁為簡體印刷,請先參閱英文及下方簡體書内文本。)'; } if (enBody) { enBody.textContent = spread.en || 'English text for this spread is being prepared. The illustrated pages use Simplified Chinese; use this panel for Traditional Chinese and English alongside the artwork.'; } if (scFallback) { if (spread.sc && !spread.zh) { scFallback.hidden = false; scFallback.textContent = '簡體書内文本(自動抽取,供校對):\n' + spread.sc; } else { scFallback.hidden = true; scFallback.textContent = ''; } } if (prevBtn) prevBtn.disabled = spreadIndex <= 0; if (nextBtn) nextBtn.disabled = spreadIndex >= imageUrls.length - 1; } async function renderSpreadImages() { const token = ++renderToken; const src = imageUrls[spreadIndex]; if (!src) return; if (spreadEl) spreadEl.classList.add('gbp-spread--combined'); if (spreadRight) { spreadRight.classList.add('is-empty'); spreadRight.replaceChildren(); } setHostImage(spreadLeft, src, 'Storybook spread ' + (spreadIndex + 1)); if (token !== renderToken) return; syncTextPanel(); } async function renderPage(pageNum, host) { if (!pdfDoc || !pageNum) { host.classList.add('is-empty'); host.replaceChildren(); return; } host.classList.remove('is-empty'); const page = await pdfDoc.getPage(pageNum); const viewport = page.getViewport({ scale: 1 }); const maxW = host.clientWidth || 420; const scale = maxW / viewport.width; const scaled = page.getViewport({ scale }); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d', { alpha: false }); canvas.width = Math.floor(scaled.width); canvas.height = Math.floor(scaled.height); await page.render({ canvasContext: ctx, viewport: scaled }).promise; host.replaceChildren(canvas); } async function renderSpreadPdf() { const token = ++renderToken; const spread = getSpread(spreadIndex); if (!spread || !spread.pages) return; if (spreadEl) spreadEl.classList.remove('gbp-spread--combined'); const [leftPage, rightPage] = spread.pages; await renderPage(leftPage, spreadLeft); if (token !== renderToken) return; await renderPage(rightPage, spreadRight); if (token !== renderToken) return; if (metaEl) { metaEl.textContent = 'Spread ' + (spreadIndex + 1) + ' / ' + spreads.length + (leftPage && rightPage ? ' · pp. ' + leftPage + '–' + rightPage : leftPage ? ' · p. ' + leftPage : ''); } syncTextPanel(); } function renderSpread() { if (usePdf) return renderSpreadPdf(); return renderSpreadImages(); } function setTextPanelOpen(isOpen) { if (!textPanel) return; textPanel.classList.toggle('gbp-text-panel--collapsed', !isOpen); textPanel.classList.toggle('gbp-text-panel--book-open', isOpen); textPanel.removeAttribute('hidden'); if (isOpen) { textPanel.removeAttribute('aria-hidden'); } else { textPanel.setAttribute('aria-hidden', 'true'); } } function openBook() { if (!shell) return; shell.classList.remove('is-closed'); shell.classList.add('is-open'); spreadIndex = 0; setTextPanelOpen(true); renderSpread(); } function closeBook() { if (!shell) return; shell.classList.add('is-closed'); shell.classList.remove('is-open'); setTextPanelOpen(false); } function bindClick(el, handler) { if (!el) return; el.addEventListener('click', handler); } bindClick(coverBtn, openBook); if (coverBtn && coverBtn.getAttribute('tabindex') === '0') { coverBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openBook(); } }); } bindClick(closeBtn, closeBook); bindClick(prevBtn, () => { if (spreadIndex > 0) { spreadIndex -= 1; renderSpread(); } }); bindClick(nextBtn, () => { if (spreadIndex < (usePdf ? spreads.length : imageUrls.length) - 1) { spreadIndex += 1; renderSpread(); } }); document.addEventListener('keydown', (e) => { if (!shell || !shell.classList.contains('is-open')) return; if (e.key === 'ArrowLeft' && prevBtn) prevBtn.click(); if (e.key === 'ArrowRight' && nextBtn) nextBtn.click(); if (e.key === 'Escape') closeBook(); }); function showError(err) { console.error(err); const hint = location.protocol === 'file:' ? '請用 serve8766 開啟預覽頁,不要用檔案總管雙擊 HTML。' : '請確認圖片 URL/媒體庫路徑正確,或聯絡 Agency 上傳至 wp-content/uploads/green-blueprint/。'; root.insertAdjacentHTML( 'beforeend', '' ); } syncTextPanel(); (async function init() { try { if (textUrl) { const textRes = await fetch(textUrl); if (!textRes.ok) throw new Error('Text JSON ' + textRes.status); const textJson = await textRes.json(); const raw = textJson.spreads || []; spreads = raw .filter(function (s) { return s && typeof s.id === 'number' && s.id >= 0 && s.id < spreadCount; }) .sort(function (a, b) { return a.id - b.id; }); buildSpreadById(spreads); if (spreads.length < spreadCount) { console.warn( 'GBP storybook: JSON 應有 ' + spreadCount + ' 個 spread(id 0–' + (spreadCount - 1) + '),目前只有 ' + spreads.length + '。請重新上傳 repo 的 storybook-spreads-text.json。' ); } textReady = true; syncTextPanel(); } else { textReady = true; } if (!usePdf && (imagesBase || manifestUrl)) { imageUrls = await resolveImageUrls(); const resolvedCover = root.getAttribute('data-cover-resolved') || coverUrl; setHostImage(coverSlot, resolvedCover, 'Storybook cover'); if (shell) shell.classList.add('is-closed'); setTextPanelOpen(false); return; } if (!pdfUrl) throw new Error('Missing images base/manifest or PDF URL'); usePdf = true; const pdfjs = await ensurePdfJs(); const loadingTask = pdfjs.getDocument({ url: pdfUrl, withCredentials: false, cMapUrl: pdfCMapBase, cMapPacked: true, standardFontDataUrl: pdfFontBase, }); pdfDoc = await loadingTask.promise; await renderPage(1, coverSlot); if (shell) shell.classList.add('is-closed'); setTextPanelOpen(false); } catch (err) { showError(err); } })(); })();