1(function() { 2 'use strict'; 3 4 // We check for pdfjsLib inside the initialization functions instead of at the root level 5 6 function initSmartEmbedViewer(container) { 7 let options; 8 try { 9 options = JSON.parse(container.getAttribute('data-options')); 10 } catch (e) { 11 console.error('Invalid options', e); 12 return; 13 } 14 15 // Thumbnail Mode Check 16 if (options.thumbnailMode && options.thumbnailUrl) { 17 container.innerHTML = ` 18 <div class="smartembed-thumbnail-wrapper" style="position:relative; cursor:pointer; text-align:center;"> 19 <img src="${options.thumbnailUrl}" style="max-width:100%; height:auto; display:block; margin:0 auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15); border-radius:4px;" alt="PDF Cover"> 20 <div class="smartembed-play-btn" style="position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); background:rgba(0,0,0,0.6); color:#fff; border-radius:50%; width:64px; height:64px; display:flex; align-items:center; justify-content:center; box-shadow: 0 4px 10px rgba(0,0,0,0.3); transition: background 0.3s;"> 21 <svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="2" fill="white" style="margin-left: 4px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> 22 </div> 23 </div> 24 `; 25 26 const wrapper = container.querySelector('.smartembed-thumbnail-wrapper'); 27 const playBtn = container.querySelector('.smartembed-play-btn'); 28 wrapper.addEventListener('mouseenter', () => playBtn.style.background = 'rgba(0,0,0,0.8)'); 29 wrapper.addEventListener('mouseleave', () => playBtn.style.background = 'rgba(0,0,0,0.6)'); 30 31 wrapper.addEventListener('click', () => { 32 container.innerHTML = '<div style="padding: 40px; text-align: center; color: #666;">Loading PDF...</div>'; 33 options.thumbnailMode = false; 34 container.setAttribute('data-options', JSON.stringify(options)); 35 initSmartEmbedViewer(container); 36 }); 37 return; 38 } 39 40 // Force Single Page Mode (Continuous) on mobile/tablet devices 41 if (window.innerWidth <= 850) { 42 options.mode = 'continuous'; 43 } 44 45 // Don't override container dimensions - keep the grey background from shortcode HTML 46 // Only apply protection styles 47 if (options.fullscreen) container.style.position = 'relative'; 48 49 // Apply Protection 50 if (options.noSelect) { 51 container.style.userSelect = 'none'; 52 container.style.webkitUserSelect = 'none'; 53 } 54 if (options.noRightClick) { 55 container.addEventListener('contextmenu', e => e.preventDefault()); 56 } 57 58 // Build Template Selector UI if enabled via Elementor 59 let templatesHtml = ''; 60 if (options.allowedTemplates) { 61 const allowedArr = options.allowedTemplates.split(',').filter(Boolean); 62 if (allowedArr.length > 1) { // Only show if more than 1 enabled 63 if (!options.currentTemplate) { 64 options.currentTemplate = allowedArr[0]; // Set default 65 } 66 const activeTpl = options.currentTemplate; 67 68 // Map templates to viewer modes 69 if (activeTpl === 'vertical_reader') options.mode = 'continuous'; 70 else if (activeTpl === 'slider') options.mode = 'presentation'; 71 else options.mode = 'flipbook'; 72 73 templatesHtml = ` 74 <div class="smartembed-template-selector"> 75 <div class="smartembed-template-title">Select Flipbook Template</div> 76 <div class="smartembed-template-grid"> 77 ${allowedArr.includes('3d_flipbook') ? `<div class="smartembed-tpl-card ${activeTpl==='3d_flipbook'?'active':''}" data-tpl="3d_flipbook"> 78 <div class="smartembed-tpl-icon"> 79 <img src="${window.smartEmbedFront ? window.smartEmbedFront.pluginUrl : ''}assets/Images/3d%20book%20icon%20image.png" alt="3D Flipbook" class="smartembed-tpl-img" /> 80 </div> 81 <div class="smartembed-tpl-name">3D Flipbook</div> 82 <div class="smartembed-tpl-desc">3D Flip Depth</div> 83 </div>` : ''} 84 ${allowedArr.includes('2d_flipbook') ? `<div class="smartembed-tpl-card ${activeTpl==='2d_flipbook'?'active':''}" data-tpl="2d_flipbook"> 85 <div class="smartembed-tpl-icon"> 86 <img src="${window.smartEmbedFront ? window.smartEmbedFront.pluginUrl : ''}assets/Images/2d%20book%20icon%20image.png" alt="2D Flipbook" class="smartembed-tpl-img" /> 87 </div> 88 <div class="smartembed-tpl-name">
882D Flipbook</div> 89 <div class="smartembed-tpl-desc">Flat 2D Page Flip</div> 90 </div>` : ''} 91 ${allowedArr.includes('hard_cover') ? `<div class="smartembed-tpl-card ${activeTpl==='hard_cover'?'active':''}" data-tpl="hard_cover"> 92 <div class="smartembed-tpl-icon"> 93 <img src="${window.smartEmbedFront ? window.smartEmbedFront.pluginUrl : ''}assets/Images/Hard%20Cover%20book%20icon%20image.png" alt="Hard Cover" class="smartembed-tpl-img" /> 94 </div> 95 <div class="smartembed-tpl-name">Hard Cover</div> 96 <div class="smartembed-tpl-desc">Realistic Book</div> 97 </div>` : ''} 98 ${allowedArr.includes('binder') ? `<div class="smartembed-tpl-card ${activeTpl==='binder'?'active':''}" data-tpl="binder"> 99 <div class="smartembed-tpl-icon"> 100 <img src="${window.smartEmbedFront ? window.smartEmbedFront.pluginUrl : ''}assets/Images/Binding%20Cover%20book%20icon%20image.png" alt="Binder" class="smartembed-tpl-img" /> 101 </div> 102 <div class="smartembed-tpl-name">Binder Flipbook</div> 103 <div class="smartembed-tpl-desc">3D Spiral Binding</div> 104 </div>` : ''} 105 106 ${allowedArr.includes('vertical_reader') ? `<div class="smartembed-tpl-card ${activeTpl==='vertical_reader'?'active':''}" data-tpl="vertical_reader"> 107 <div class="smartembed-tpl-icon"> 108 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><polyline points="19 12 12 19 5 12"></polyline></svg> 109 </div> 110 <div class="smartembed-tpl-name">Vertical Reader</div> 111 <div class="smartembed-tpl-desc">Scroll Pages</div> 112 </div>` : ''} 113 </div> 114 </div> 115 `; 116 } else if (allowedArr.length === 1) { 117 // If only 1 template enabled, just set the mode automatically without showing UI 118 options.currentTemplate = allowedArr[0]; 119 if (options.currentTemplate === 'vertical_reader') options.mode = 'continuous'; 120 else if (options.currentTemplate === 'slider') options.mode = 'presentation'; 121 else options.mode = 'flipbook'; 122 } 123 } 124 125 // Build UI 126 container.innerHTML = ` 127 ${templatesHtml} 128 <div class="smartembed-pdf-viewer-ui custom-premium-layout ${options.darkMode ? 'smartembed-dark-mode' : ''}"> 129 <div class="smartembed-pdf-scroll-area ${options.mode === 'flipbook' ? 'flipbook-mode' : ''}"> 130 <div class="smartembed-pdf-pages-container"></div> 131 </div> 132 133 <!-- Thumbnails Sidebar --> 134 <div class="smartembed-thumbnails-sidebar"> 135 <div class="smartembed-thumbnails-header"> 136 <span>THUMBNAILS</span> 137 <button class="smartembed-thumbnails-close">â</button> 138 </div> 139 <div class="smartembed-thumbnails-content"></div> 140 </div> 141 142 ${options.toc ? ` 143 <div class="smartembed-toc-sidebar"> 144 <div class="smartembed-toc-header"> 145 <span>TABLE OF CONTENTS</span> 146 <button class="smartembed-toc-close">â</button> 147 </div> 148 <div class="smartembed-toc-content"></div> 149 </div> 150 ` : ''} 151 152 <!-- Navigation Arrows --> 153 <button class="smartembed-nav-arrow smartembed-prev-page" title="Previous Page"> 154 <svg viewBox="0 0 24 24" width="40" height="40" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg> 155 </button> 156 <button class="smartembed-nav-arrow smartembed-next-page" title="Next Page"> 157 <svg viewBox="0 0 24 24" width="40" height="40" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg> 158 </button> 159 160 <!-- Floating Bottom Toolbar --> 161 <div class="smartembed-toolbar-wrapper"> 162 <div class="smartembed-bottom-toolbar"> 163 <div class="smartembed-page-info"> 164 <input type="text" class="smartembed-current-page" value="1" />/<span class="smartembed-page-count">--</span> 165 </div> 166 <button class="smartembed-tool-btn smartembed-toggle-thumbnails" title="Thumbn
166ails"> 167 <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg> 168 </button> 169 ${options.toc ? ` 170 <button class="smartembed-tool-btn smartembed-toggle-toc" title="Table of Contents"> 171 <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><line x1="8" y1="6" x2="21" y2="6"></line><line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" y2="18"></line><line x1="3" y1="6" x2="3.01" y2="6"></line><line x1="3" y1="12" x2="3.01" y2="12"></line><line x1="3" y1="18" x2="3.01" y2="18"></line></svg> 172 </button> 173 ` : ''} 174 <button class="smartembed-tool-btn smartembed-zoom-in" title="Zoom In"><svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg></button> 175 <button class="smartembed-tool-btn smartembed-zoom-out" title="Zoom Out"><svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg></button> 176 ${options.fullscreen ? '<button class="smartembed-tool-btn smartembed-fullscreen-btn" title="Fullscreen"><svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg></button>' : ''} 177 ${options.print ? '<button class="smartembed-tool-btn smartembed-print-btn" title="Print"><svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><polyline points="6 9 6 2 18 2 18 9"></polyline><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path><rect x="6" y="14" width="12" height="8"></rect></svg></button>' : ''} 178 179 ${options.share ? '<div class="smartembed-options-menu-container smartembed-share-container"><button class="smartembed-tool-btn smartembed-share-btn" title="Share"><svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg></button><div class="smartembed-options-dropdown smartembed-share-dropdown" style="right: auto; left: 0; transform: translateX(-40%);"><button class="smartembed-dropdown-item smartembed-share-fb"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg> Facebook</button><button class="smartembed-dropdown-item smartembed-share-tw"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path></svg> Twitter / X</button><button class="smartembed-dropdown-item smartembed-share-wa"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg> WhatsApp</button><button class="smartembed-dropdown-item smartembed-share-li"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"></circle></svg> LinkedIn</button><button class="smartembed-dropdown-item smartembed-share-copy"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg> Copy Link</button></div></div>' : ''} 180 181 <div class="smartembed-options-menu-container"> 182 <button class="smartembed-tool-btn smartembed-options-btn" title="More Options"> 183 <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><circle cx="12" cy="12" r="1"></circle><circle cx="19" cy="12" r="1"></circle><circle cx="5" cy="12" r="1"></circle></svg> 184 </button> 185 <div class="smartembed-options-dropdown"> 186 <!-- Premium Features injected here --> 187 <button class="smartembed-dropdown-item smartembed-toggle-mode"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg> Single Page Mode</button> 188 <button class="smartembed-dropdown-item smartembed-goto-first"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><polyline points="11 17 6 12 11 7"></polyline><polyline points="18 17 13 12 18 7"></polyline></svg> Goto First Page</button> 189 <button class="smartembed-dropdown-item smartembed-goto-last"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><polyline points="13 17 18 12 13 7"></polyline><polyline points="6 17 11 12 6 7"></polyline></svg> Goto Last Page</button> 190 <button class="smartembed-dropdown-item smartembed-tts-btn"><svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path></svg> <span>Turn on/off Sound</span></button> 191 </div> 192 </div> 193 </div> 194 </div> 195 196 ${options.watermark ? '<div class="smartembed-watermark-overlay">' + options.watermarkText + '</div>' : ''} 197 </div> 198 `; 199 200 // ---------- Layout Setup ---------- 201 // Measure the template selector (if rendered) to compute the correct heights. 202 // We do this synchronously right after innerHTML is set so the layout is clean. 203 const templateSelector = container.querySelector('.smartembed-template-selector'); 204 let templateSelectorH = 0; 205 if (templateSelector) { 206 // Force a layout so offsetHeight is accurate 207 templateSelector.style.display = 'block'; 208 templateSelectorH = templateSelector.offsetHeight || 160; 209 } 210 211 // The PDF viewer height = total container height - template selector height 212 const ui = container.querySelector('.smartembed-pdf-viewer-ui'); 213 214 // Ensure the container itself is tall enough: base is options.height (e.g. 750px) 215 // we do NOT add templateSelectorH here; we just let the UI shrink. 216 // The UI uses absolute height = containerHeight - templateSelectorH. 217 const baseViewerH = parseInt(options.height || 750);
218 const totalContainerH = baseViewerH + templateSelectorH; 219 container.style.setProperty('height', totalContainerH + 'px', 'important'); 220 container.style.setProperty('min-height', totalContainerH + 'px', 'important'); 221 222 // Set the viewer UI to fill only the PDF viewer portion 223 ui.style.height = baseViewerH + 'px'; 224 ui.style.minHeight = baseViewerH + 'px'; 225 ui.style.flexShrink = '0'; 226 227 const scrollArea = container.querySelector('.smartembed-pdf-scroll-area'); 228 const pagesContainer = container.querySelector('.smartembed-pdf-pages-container'); 229 const pageNumEl = container.querySelector('.smartembed-current-page'); 230 const pageCountEl = container.querySelector('.smartembed-page-count'); 231 const prevBtn = container.querySelector('.smartembed-prev-page'); 232 const nextBtn = container.querySelector('.smartembed-next-page'); 233 const zoomInBtn = container.querySelector('.smartembed-zoom-in'); 234 const zoomOutBtn = container.querySelector('.smartembed-zoom-out'); 235 const fullscreenBtn = container.querySelector('.smartembed-fullscreen-btn'); 236 const printBtn = container.querySelector('.smartembed-print-btn'); 237 const shareBtn = container.querySelector('.smartembed-share-btn'); 238 const ttsBtn = container.querySelector('.smartembed-tts-btn'); 239 const optionsBtn = container.querySelector('.smartembed-options-btn'); 240 const toggleThumbnailsBtn = container.querySelector('.smartembed-toggle-thumbnails'); 241 const closeThumbnailsBtn = container.querySelector('.smartembed-thumbnails-close'); 242 const thumbnailsSidebar = container.querySelector('.smartembed-thumbnails-sidebar'); 243 const thumbnailsContent = container.querySelector('.smartembed-thumbnails-content'); 244 245 const toggleTocBtn = container.querySelector('.smartembed-toggle-toc'); 246 const closeTocBtn = container.querySelector('.smartembed-toc-close'); 247 const tocSidebar = container.querySelector('.smartembed-toc-sidebar'); 248 const tocContent = container.querySelector('.smartembed-toc-content'); 249 const gotoLastBtn = container.querySelector('.smartembed-goto-last'); 250 const gotoFirstBtn = container.querySelector('.smartembed-goto-first'); 251 const toggleModeBtn = container.querySelector('.smartembed-toggle-mode'); 252 253 if (toggleModeBtn && options.mode === 'continuous') { 254 toggleModeBtn.innerHTML = toggleModeBtn.innerHTML.replace('Single Page Mode', 'Flipbook Mode'); 255 } 256 257 let pdfDoc = null; 258 let numPages = 0; 259 let scale = 1.0; 260 let renderTask = null; 261 let pageRendering = false; 262 let pageNumPending = null; 263 let currentPage = 1; 264 let pageFlipInstance = null; 265 let flipZoom = 1.0; 266 let flipPanX = 0; 267 let flipPanY = 0; 268 let flipPageDivs = []; // hoisted so zoom re-render can access canvases 269 let cachedCanvases = []; 270 let cachedFitScale = 0; 271 let baseFitScale = 1; // the fitScale calculated at init time 272 let soundEnabled = options.enablePaginationSound !== false && options.enablePaginationSound !== 'false' && options.enablePaginationSound !== 0 && options.enablePaginationSound !== '0'; // pagination sound toggle state 273 274 // --- Pagination Sound Setup --- 275 let pageFlipAudio = null; 276 if (window.smartEmbedFront && window.smartEmbedFront.pluginUrl) { 277 pageFlipAudio = new Audio(window.smartEmbedFront.pluginUrl + 'assets/audio/Pagination sounds.mp3'); 278 pageFlipAudio.preload = 'auto'; 279 pageFlipAudio.volume = 0.7; 280 } 281 282 // Apply combined centering + pan + scale to the flipbook container 283 function applyFlipbookTransform(centerXPct) { 284 const coverWrapper = pagesContainer.querySelector('.smartembed-book-cover-3d'); 285 const flipContainer = pagesContainer.querySelector('.smartembed-flipbook-container'); 286 const targetEl = coverWrapper || flipContainer; 287 if (!targetEl) return; 288 if (centerXPct === null || centerXPct === undefined) { 289 centerXPct = 0; 290 } 291 const zoomFactor = (flipZoom && flipZoom !== 1.0) ? flipZoom : 1; 292 targetEl.style.transform = 293 `translate(calc(${centerXPct}px + ${flipPanX}px), ${flipPanY}px) scale(${zoomFactor})`; 294 } 295 296 // Re-render all flipbook canvases at baseFitScale * flipZoom for crisp quality 297 let _reRenderTimer = null; 298 function reRenderFlipbookPages() { 299 if (!pdfDoc || flipPageDivs.length === 0) return; 300 clearTimeout(_reRenderTimer);
301 _reRenderTimer = setTimeout(() => { 302 const renderDpr = Math.max(window.devicePixelRatio || 1, 2.5); // Slightly higher for zoom 303 const renderScale = baseFitScale * flipZoom * renderDpr; 304 flipPageDivs.forEach(({ canvas, index }) => { 305 pdfDoc.getPage(index).then(pdfPage => { 306 const vp = pdfPage.getViewport({ scale: renderScale }); 307 const ctx = canvas.getContext('2d'); 308 canvas.width = vp.width; 309 canvas.height = vp.height; 310 const cssW = Math.round(vp.width / renderDpr); 311 const cssH = Math.round(vp.height / renderDpr); 312 canvas.style.width = cssW + 'px'; 313 canvas.style.height = cssH + 'px'; 314 ctx.setTransform(1, 0, 0, 1, 0, 0); 315 pdfPage.render({ 316 canvasContext: ctx, 317 viewport: vp 318 }); 319 }); 320 }); 321 }, 150); // debounce: wait for rapid zoom clicks to settle 322 } 323 324 // Handle direct page input navigation 325 if (pageNumEl) { 326 pageNumEl.addEventListener('change', () => { 327 const rawVal = pageNumEl.value.split('-')[0].trim(); 328 let val = parseInt(rawVal, 10); 329 if (!val || isNaN(val)) { 330 val = 1; 331 } 332 if (val < 1) val = 1; 333 if (val > numPages) val = numPages; 334 335 currentPage = val; 336 queueRenderPage(currentPage); 337 }); 338 339 pageNumEl.addEventListener('keydown', (e) => { 340 if (e.key === 'Enter') { 341 pageNumEl.blur(); 342 } 343 }); 344 345 pageNumEl.addEventListener('click', () => { 346 pageNumEl.select(); 347 }); 348 } 349 350 // Restore resume page 351 if (options.resume) { 352 const savedPage = localStorage.getItem('smartembed_page_' + options.url); 353 if (savedPage) currentPage = parseInt(savedPage, 10); 354 } 355 356 let imageGallery = null; 357 if (options.images) { 358 imageGallery = options.images.split(',').map(s => s.trim()).filter(s => s); 359 } 360 361 function updateNavigationArrows() { 362 if (options.mode === 'continuous') { 363 if (prevBtn) prevBtn.style.display = 'none'; 364 if (nextBtn) nextBtn.style.display = 'none'; 365 } else { 366 if (prevBtn) prevBtn.style.display = 'flex'; 367 if (nextBtn) nextBtn.style.display = 'flex'; 368 } 369 } 370 371 function initViewerWithDoc(pdf) { 372 pdfDoc = pdf; 373 numPages = pdf.numPages || pdfDoc.numPages; 374 pageCountEl.textContent = numPages; 375 buildThumbnails(); 376 buildToc(); 377 378 updateNavigationArrows(); 379 380 if (options.mode === 'continuous') { 381 scrollArea.classList.remove('flipbook-mode'); 382 renderAllPages(); 383 } else if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 384 scrollArea.classList.add('flipbook-mode'); 385 renderFlipbook(); 386 } else { 387 scrollArea.classList.remove('flipbook-mode'); 388 renderPage(currentPage); 389 } 390 391 // Apply Watermark Overlay 392 if (options.watermark && options.watermarkText) { 393 const watermarkDiv = document.createElement('div'); 394 watermarkDiv.className = 'smartembed-watermark-overlay'; 395 watermarkDiv.textContent = options.watermarkText; 396 watermarkDiv.style.position = 'absolute'; 397 watermarkDiv.style.pointerEvents = 'none'; 398 watermarkDiv.style.zIndex = '9999'; 399 watermarkDiv.style.color = 'rgba(150, 150, 150, 0.3)'; 400 watermarkDiv.style.fontWeight = 'bold'; 401 watermarkDiv.style.whiteSpace = 'nowrap'; 402 403 if (options.watermarkPlacement === 'tiled') { 404 watermarkDiv.style.top = '0'; 405 watermarkDiv.style.left = '0'; 406 watermarkDiv.style.width = '100%'; 407 watermarkDiv.style.height = '100%'; 408 watermarkDiv.style.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='200' width='200'><text x='50%' y='50%' transform='translate(-50%, -50%) rotate(-45, 100, 100)' fill='rgba(150, 150, 150, 0.2)' font-size='24' font-family='Arial' text-anchor='middle'>${encodeURIComponent(options.watermarkText)}</text></svg>")`; 409 watermarkDiv.textContent = ''; // Use SVG background instead 410 } else if (options.watermarkPlacement === 'diagonal') { 411 watermarkDiv.style.top = '50%'; 412 watermarkDiv.style.left = '50%'; 413 watermarkDiv.style.transform = 'translate(-50%, -50%) rotate(-45deg)'; 414 watermarkDiv.style.fontSize = '60px'; 415 } else { 416 // Center 417 watermarkDiv.style.top = '50%'; 418 watermarkDiv.style.left = '50%'; 419 watermarkDiv.style.transform = 'translate(-50%, -50%)'; 420 watermarkDiv.style.fontSize = '40px'; 421 } 422 423 scrollArea.appendChild(watermarkDiv); 424 scrollArea.style.position = 'relative'; 425 } 426 427 // Analytics: Track View 428 if (options.bookId && options.ajaxUrl) { 429 var formData = new FormData(); 430 formData.append('action', 'smartembed_track_event'); 431 formData.append('event_type', 'view'); 432 formData.append('book_id', options.bookId); 433 fetch(options.ajaxUrl, { method: 'POST', body: formData }).catch(e => {}); 434 435 // Analytics: Track Max Page Scrolled 436 let maxPageReached = 1; 437 function updateMaxPage(p) { 438 if (p > maxPageReached) maxPageReached = p; 439 } 440 441 // Hook into pageNumEl changes 442 if (pageNumEl) { 443 const observer = new MutationObserver(() => { 444 let current = parseInt(pageNumEl.value || pageNumEl.textContent); 445 if (!isNaN(current)) updateMaxPage(current); 446 }); 447 observer.observe(pageNumEl, { attributes: true, childList: true, characterData: true, subtree: true });
448 pageNumEl.addEventListener('change', () => { 449 let current = parseInt(pageNumEl.value); 450 if (!isNaN(current)) updateMaxPage(current); 451 }); 452 } 453 454 // Send beacon on unload 455 window.addEventListener('beforeunload', () => { 456 if (maxPageReached > 1) { 457 var fd = new FormData(); 458 fd.append('action', 'smartembed_track_event'); 459 fd.append('event_type', 'scroll'); 460 fd.append('book_id', options.bookId); 461 fd.append('max_page', maxPageReached); 462 fd.append('total_pages', numPages); 463 navigator.sendBeacon(options.ajaxUrl, fd); 464 } 465 }); 466 } 467 } 468 469 if (imageGallery && imageGallery.length > 0) { 470 const mockPdfDoc = { 471 numPages: imageGallery.length, 472 getPage: function(num) { 473 return new Promise((resolve) => { 474 const imgUrl = imageGallery[num - 1]; 475 const img = new Image(); 476 img.src = imgUrl; 477 img.onload = () => { 478 resolve({ 479 getViewport: ({scale}) => ({ width: img.naturalWidth * scale, height: img.naturalHeight * scale, scale }), 480 render: ({canvasContext, viewport}) => { 481 canvasContext.drawImage(img, 0, 0, viewport.width, viewport.height); 482 return { promise: Promise.resolve() }; 483 }, 484 getAnnotations: () => Promise.resolve([]), 485 getTextContent: () => Promise.resolve({ items: [] }), 486 }); 487 }; 488 img.onerror = () => { 489 resolve({ 490 getViewport: ({scale}) => ({ width: 800 * scale, height: 1100 * scale, scale }), 491 render: ({canvasContext, viewport}) => { 492 canvasContext.fillStyle = '#f0f0f0'; 493 canvasContext.fillRect(0, 0, viewport.width, viewport.height); 494 return { promise: Promise.resolve() }; 495 }, 496 getAnnotations: () => Promise.resolve([]), 497 getTextContent: () => Promise.resolve({ items: [] }), 498 }); 499 }; 500 }); 501 }, 502 getOutline: () => Promise.resolve([]), 503 getDestination: () => Promise.resolve(null), 504 getPageIndex: () => Promise.resolve(0) 505 }; 506 initViewerWithDoc(mockPdfDoc); 507 } else { 508 const loadingTask = pdfjsLib.getDocument(options.url); 509 510 loadingTask.onPassword = function(updatePassword, reason) { 511 const password = prompt(reason === pdfjsLib.PasswordResponses.NEED_PASSWORD 512 ? 'Please enter the password to view this PDF:' 513 : 'Incorrect password. Please try again:'); 514 if (password !== null) updatePassword(password); 515 else titleEl.textContent = 'Password required'; 516 }; 517 518 loadingTask.promise.then(pdf => { 519 initViewerWithDoc(pdf); 520 }).catch(err => { 521 console.error('Error loading PDF:', err); 522 container.innerHTML = '<div style="padding:40px; color:#fff; text-align:center;">Failed to load PDF file.</div>'; 523 }); 524 } 525 526 // Save original height for restoring when switching modes 527 const originalHeight = container.style.height || (options.height ? options.height : '750px'); 528 529 // Handle Window Resize to make viewer dynamically adjustable 530 let resizeTimeout; 531 window.addEventListener('resize', () => { 532 clearTimeout(resizeTimeout); 533 resizeTimeout = setTimeout(() => { 534 if (!pdfDoc) return; // Not fully loaded yet 535 536 const wasContinuous = options.mode === 'continuous'; 537
538 // Enforce continuous on smaller screens 539 if (window.innerWidth <= 850) { 540 options.mode = 'continuous'; 541 } 542 543 // If mode changed due to resize 544 if (wasContinuous !== (options.mode === 'continuous')) { 545 if (pageFlipInstance) { 546 try { pageFlipInstance.destroy(); } catch(e){} 547 pageFlipInstance = null; 548 } 549 if (options.mode === 'continuous') { 550 scrollArea.classList.remove('flipbook-mode'); 551 } else { 552 scrollArea.classList.add('flipbook-mode'); 553 } 554 updateNavigationArrows(); 555 556 if (toggleModeBtn) { 557 const currentText = toggleModeBtn.innerHTML; 558 if (options.mode === 'continuous' && currentText.includes('Single Page Mode')) { 559 toggleModeBtn.innerHTML = currentText.replace('Single Page Mode', 'Flipbook Mode'); 560 } else if (options.mode !== 'continuous' && currentText.includes('Flipbook Mode')) { 561 toggleModeBtn.innerHTML = currentText.replace('Flipbook Mode', 'Single Page Mode'); 562 } 563 } 564 } 565 566 // Re-render to adapt to new container dimensions 567 if (options.mode === 'continuous') { 568 scrollArea.classList.remove('flipbook-mode'); 569 renderAllPages(); 570 } else if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 571 scrollArea.classList.add('flipbook-mode'); 572 if (pageFlipInstance) { 573 try { pageFlipInstance.destroy(); } catch(e){} 574 pageFlipInstance = null; 575 } 576 renderFlipbook(); 577 } else { 578 scrollArea.classList.remove('flipbook-mode'); 579 renderPage(currentPage); 580 } 581 }, 300); 582 }); 583 584 function buildThumbnails() { 585 thumbnailsContent.innerHTML = ''; 586 for (let i = 1; i <= numPages; i++) { 587 const thumb = document.createElement('div'); 588 thumb.className = 'smartembed-thumbnail'; 589 const canvas = document.createElement('canvas'); 590 const pageLabel = document.createElement('div'); 591 pageLabel.className = 'smartembed-thumbnail-label'; 592 pageLabel.textContent = i; 593 thumb.appendChild(canvas); 594 thumb.appendChild(pageLabel); 595 thumbnailsContent.appendChild(thumb); 596 597 thumb.addEventListener('click', () => { 598 currentPage = i; 599 queueRenderPage(currentPage); 600 thumbnailsSidebar.classList.remove('open'); 601 }); 602 603 pdfDoc.getPage(i).then(page => { 604 const viewport = page.getViewport({ scale: 0.2 }); 605 canvas.height = viewport.height; 606 canvas.width = viewport.width; 607 const renderTask = page.render({ 608 canvasContext: canvas.getContext('2d'), 609 viewport: viewport 610 }); 611 renderTask.promise.then(() => { 612 document.dispatchEvent(new CustomEvent('smartembed_page_rendered', { 613 detail: { 614 pageIndex: i, 615 pdfPage: page, 616 canvas: canvas, 617 viewport: viewport, 618 pageDiv: null, // No pageDiv in thumbnails 619 container: container, 620 options: options, 621 isThumbnail: true 622 } 623 })); 624 }); 625 }); 626 } 627 } 628 629 function buildToc() { 630 if (!options.toc || !tocContent) return; 631 pdfDoc.getOutline().then(outline => { 632 if (!outline || outline.length === 0) { 633 tocContent.innerHTML = '<div class="smartembed-toc-empty" style="padding: 15px; color: #888;">No Table of Contents available.</div>'; 634 return; 635 } 636 637 function renderOutline(items, containerEl) { 638 const ul = document.createElement('ul'); 639 ul.className = 'smartembed-toc-list'; 640 ul.style.listStyle = 'none'; 641 ul.style.paddingLeft = containerEl === tocContent ? '0' : '15px'; 642
643 items.forEach(item => { 644 const li = document.createElement('li'); 645 li.className = 'smartembed-toc-item'; 646 li.style.marginBottom = '8px'; 647 648 const a = document.createElement('a'); 649 a.textContent = item.title; 650 a.href = '#'; 651 a.style.textDecoration = 'none'; 652 a.style.color = 'inherit'; 653 a.style.fontSize = '14px'; 654 655 a.addEventListener('click', (e) => { 656 e.preventDefault(); 657 if (typeof item.dest === 'string') { 658 pdfDoc.getDestination(item.dest).then(destArray => { 659 if (destArray) { 660 pdfDoc.getPageIndex(destArray[0]).then(pageIndex => { 661 currentPage = pageIndex + 1; 662 queueRenderPage(currentPage); 663 tocSidebar.classList.remove('open'); 664 }); 665 } 666 }); 667 } else if (Array.isArray(item.dest)) { 668 pdfDoc.getPageIndex(item.dest[0]).then(pageIndex => { 669 currentPage = pageIndex + 1; 670 queueRenderPage(currentPage); 671 tocSidebar.classList.remove('open'); 672 }); 673 } 674 }); 675 li.appendChild(a); 676 if (item.items && item.items.length > 0) { 677 renderOutline(item.items, li); 678 } 679 ul.appendChild(li); 680 }); 681 containerEl.appendChild(ul); 682 } 683 tocContent.innerHTML = ''; 684 renderOutline(outline, tocContent); 685 }).catch(err => { 686 console.error('Error loading outline:', err); 687 tocContent.innerHTML = '<div class="smartembed-toc-empty" style="padding: 15px; color: #888;">Failed to load outline.</div>'; 688 }); 689 } 690 691 692 function getFitScale(page) { 693 const containerW = container.offsetWidth || 650; 694 const viewport = page.getViewport({ scale: 1.0 }); 695 const padW = window.innerWidth <= 850 ? 20 : 60; 696 const scaleW = (containerW - padW) / viewport.width; 697 698 if (options.mode !== 'continuous') { 699 const ui = container.querySelector('.smartembed-pdf-viewer-ui'); 700 const containerH = ui ? (ui.offsetHeight || parseInt(ui.style.height) || 750) : 750; 701 const scaleH = (containerH - 80) / viewport.height; 702 return Math.min(scaleW, Math.max(scaleH, 0.1)); 703 } else { 704 // Prevent continuous mode pages from becoming overwhelmingly large on wide screens 705 // 850px is a comfortable maximum reading width for most documents 706 const maxReadingScale = 850 / viewport.width; 707 return Math.min(scaleW, maxReadingScale); 708 } 709 } 710 711 function renderPage(num) { 712 pageRendering = true; 713 pdfDoc.getPage(num).then(page => { 714 const fitScale = getFitScale(page) * scale; 715 const viewport = page.getViewport({ scale: fitScale }); 716 const dpr = Math.max(window.devicePixelRatio || 1, 2); 717 pagesContainer.innerHTML = ''; // clear 718 const canvas = document.createElement('canvas'); 719 const ctx = canvas.getContext('2d'); 720 canvas.width = viewport.width * dpr; 721 canvas.height = viewport.height * dpr; 722 canvas.style.width = viewport.width + 'px'; 723 canvas.style.height = viewport.height + 'px'; 724 ctx.scale(dpr, dpr); 725 canvas.className = 'smartembed-pdf-page-canvas'; 726 pagesContainer.appendChild(canvas); 727 728 const renderContext = { 729 canvasContext: ctx, 730 viewport: viewport 731 }; 732 renderTask = page.render(renderContext); 733 734 renderTask.promise.then(() => { 735 pageRendering = false; 736 document.dispatchEvent(new CustomEvent('smartembed_page_rendered', { 737 detail: { 738 pageIndex: num, 739 pdfPage: page, 740 canvas: canvas, 741 viewport: viewport, 742 pageDiv: null, 743 container: container, 744 options: options, 745 isThumbnail: false 746 } 747 })); 748 if (pageNumPending !== null) { 749 renderPage(pageNumPending); 750 pageNumPending = null; 751 } 752 }); 753 }); 754 if (pageNumEl) pageNumEl.value = num; 755 if (options.resume) localStorage.setItem('smartembed_page_' + options.url, num); 756 } 757 758 function renderAllPages() { 759 pagesContainer.innerHTML = ''; 760 761 if (!document.fullscreenElement) { 762 container.style.setProperty('height', originalHeight, 'important'); 763 container.style.setProperty('min-height', originalHeight, 'important'); 764 } 765 766 pdfDoc.getPage(1).then(firstPage => { 767 const baseScale = getFitScale(firstPage); 768 const finalScale = baseScale * scale; 769 770 for (let i = 1; i <= numPages; i++) { 771 const wrapper = document.createElement('div'); 772 wrapper.className = 'smartembed-page-wrapper'; 773 wrapper.id = `page-${i}`; 774 if (options.pagesGap && !isNaN(options.pagesGap)) { 775 wrapper.style.marginBottom = options.pagesGap + 'px'; 776 } 777 778 const canvas = document.createElement('canvas'); 779 canvas.className = 'smartembed-pdf-page-canvas'; 780 wrapper.appendChild(canvas); 781 pagesContainer.appendChild(wrapper); 782 783 pdfDoc.getPage(i).then(page => { 784 const viewport = page.getViewport({ scale: finalScale }); 785 const dpr = Math.max(window.devicePixelRatio || 1, 2); 786 const ctx = canvas.getContext('2d'); 787 canvas.width = viewport.width * dpr; 788 canvas.height = viewport.height * dpr; 789 canvas.style.width = viewport.width + 'px'; 790 canvas.style.height = viewport.height + 'px'; 791 ctx.scale(dpr, dpr); 792 const renderTask = page.render({ 793 canvasContext: ctx, 794 viewport: viewport 795 }); 796 renderTask.promise.then(() => { 797 document.dispatchEvent(new CustomEvent('smartembed_page_rendered', { 798 detail: { 799 pageIndex: i, 800 pdfPage: page, 801 canvas: canvas, 802 viewport: viewport, 803 pageDiv: wrapper, 804 container: container, 805 options: options, 806 isThumbnail: false 807 } 808 })); 809 }); 810 }); 811 } 812 }); 813 } 814 815 scrollArea.addEventListener('scroll', () => { 816 if (options.mode !== 'continuous') return; 817 const wrappers = pagesContainer.querySelectorAll('.smartembed-page-wrapper'); 818 let current = 1; 819 let minDiff = Infinity; 820 const scrollCenter = scrollArea.scrollTop + scrollArea.offsetHeight / 2; 821
822 wrappers.forEach((wrap, index) => { 823 const diff = Math.abs((wrap.offsetTop + wrap.offsetHeight / 2) - scrollCenter); 824 if (diff < minDiff) { 825 minDiff = diff; 826 current = index + 1; 827 } 828 }); 829 830 if (current !== currentPage) { 831 currentPage = current; 832 if (pageNumEl) pageNumEl.value = currentPage; 833 if (options.resume) localStorage.setItem('smartembed_page_' + options.url, currentPage); 834 } 835 }); 836 837 function renderFlipbook() { 838 // Create the flipbook without the external cover wrapper 839 pagesContainer.innerHTML = `<div class="smartembed-flipbook-container" id="flipbook-wrapper-${options.uid}"></div>`; 840 const flipContainer = pagesContainer.querySelector('.smartembed-flipbook-container'); 841 842 // Get page 1 dimensions to seed PageFlip, then render all pages 843 pdfDoc.getPage(1).then(p1 => { 844 const containerW = container.offsetWidth || 650; 845 const naturalVp = p1.getViewport({ scale: 1.0 }); 846 847 // Use the explicitly set ui height (baseViewerH) for reliable calculations 848 const parentW = containerW; 849 const parentH = ui.offsetHeight || parseInt(ui.style.height) || 750; 850 851 // Always render single page for shortcode flipbook (portrait mode) 852 const padX = 40; // horizontal padding inside grey area 853 const padY = 80; // vertical padding (room for toolbar + nav arrows) 854 855 const availW = parentW - padX; 856 const availH = parentH - padY; 857 858 // Calculate scale to fit page within available space while preserving aspect ratio 859 const scaleW = availW / naturalVp.width; 860 const scaleH = availH / naturalVp.height; 861 // Multiply by 0.88 to shrink the book slightly and guarantee space on all sides 862 let fitScale = Math.min(scaleW, scaleH) * 0.88; 863 864 // Clamp scale to prevent extreme values 865 if (fitScale < 0.1) fitScale = 0.1; 866 if (fitScale > 3.0) fitScale = 3.0; 867 868 const pgW = Math.round(naturalVp.width * fitScale); 869 const pgH = Math.round(naturalVp.height * fitScale); 870 871 // Recalculate container height based on actual page height 872 if (!document.fullscreenElement) { 873 const tplSelectorH = templateSelectorH || 0; 874 const newViewerH = Math.min(pgH + 80, parentH); 875 const newTotalH = newViewerH + tplSelectorH; 876 877 // Update container total height â only shrink if page is smaller than viewer 878 if (pgH + 80 < parentH) { 879 container.style.setProperty('height', newTotalH + 'px', 'important'); 880 container.style.setProperty('min-height', newTotalH + 'px', 'important'); 881 ui.style.height = newViewerH + 'px'; 882 ui.style.minHeight = newViewerH + 'px'; 883 } 884 } 885 886 // Set container width and height explicitly â single page mode keeps book centered 887 // with constrained width so it doesn't fill the entire grey area 888 flipContainer.style.width = pgW + 'px'; 889 flipContainer.style.height = pgH + 'px'; 890 891 if (options.tilt) { 892 flipContainer.classList.add('smartembed-tilt-enabled'); 893 } 894 if (options.rtl) { 895 flipContainer.classList.add('smartembed-rtl-enabled'); 896 } 897 898 baseFitScale = fitScale; // save for zoom re-renders 899 900 // Configure PageFlip based on selected template 901 let useShadow = true; 902 let useCover = true; 903 let tplPortrait = true; // Always portrait for shortcode flipbook (single page at a time) 904 905 if (options.mode === 'flipbook_single') { 906 tplPortrait = true; 907 } 908 909 if (options.currentTemplate === '2d_flipbook') { 910 useShadow = true; 911 useCover = true; 912 flipContainer.classList.add('smartembed-tpl-2d'); 913 } else if (options.currentTemplate === 'hard_cover') { 914 useCover = true; 915 useShadow = true; 916 flipContainer.classList.add('smartembed-tpl-hardcover'); 917 } else if (options.currentTemplate === 'binder') { 918 useCover = true; 919 useShadow = true; 920 flipContainer.classList.add('smartembed-tpl-binder'); 921 } else if (options.currentTemplate === 'calendar') { 922 tplPortrait = true; 923 flipContainer.classList.add('smartembed-tpl-calendar'); 924 } else if (options.currentTemplate === '3d_flipbook') { 925 useCover = true; 926 useShadow = true; 927 flipContainer.classList.add('smartembed-tpl-3d'); 928 } else { 929 // Default Plain Flipbook (Free version) â no spine shadow 930 useCover = true; 931 useShadow = false; 932 } 933 934 // Initialize 935 try { 936 let flipOptions = { 937 width: naturalVp.width, 938 height: naturalVp.height, 939 size: 'stretch', 940 minWidth: 200, 941 maxWidth: 3000, 942 minHeight: 300, 943 maxHeight: 3000, 944 showCover: useCover, 945 usePortrait: tplPortrait, // Portrait mode for mobile or calendar 946 maxShadowOpacity: useShadow ? 0.3 : 0, 947 mobileScrollSupport: true, 948 drawShadow: useShadow, 949 flippingTime: 450, // Faster, snappier animation 950 swipeDistance: 30, // Make it easier to turn pages 951 useMouseEvents: true, 952 clickEventForward: true 953 }; 954 955 if (options.mode === 'flipbook_single') { 956 flipOptions.usePortrait = true; 957 } 958 959 pageFlipInstance = new St.PageFlip(flipContainer, flipOptions); 960 961 if (options.mode === 'flipbook_single') { 962 flipOptions.usePortrait = true; 963 } 964 965 pageFlipInstance = new St.PageFlip(flipContainer, flipOptions); 966 967 } catch(e) { 968 console.error('PageFlip init failed', e); 969 options.mode = 'continuous'; 970 renderAllPages(); 971 return; 972 } 973 974 // Use cached canvases if available and scale is identical 975 let useCache = (cachedCanvases.length === numPages && cachedFitScale === fitScale); 976 977 // Pre-build all page divs with canvases 978 flipPageDivs = []; // save to outer scope for zoom re-renders 979 const dprInit = Math.max(window.devicePixelRatio || 1, 2); 980 const isRTL = options.rtl; 981 for (let i = 1; i <= numPages; i++) { 982 const pageDiv = document.createElement('div');
983 pageDiv.className = 'my-page'; 984 985 // Assign side attribute for LTR vs RTL (for page stacking styles) 986 let side = (i % 2 === 0) ? 'left' : 'right'; 987 if (isRTL) { 988 side = (i % 2 === 0) ? 'right' : 'left'; 989 } 990 pageDiv.setAttribute('data-side', side); 991 992 // Assign Hard Cover density if it's the first or last page 993 if (options.currentTemplate === 'hard_cover' || options.currentTemplate === 'binder') { 994 if (i === 1 || i === numPages) { 995 pageDiv.setAttribute('data-density', 'hard'); 996 } 997 } 998 999 let canvas; 1000 if (useCache) { 1001 canvas = cachedCanvases[i - 1]; 1002 } else { 1003 canvas = document.createElement('canvas'); 1004 // Render at high DPI but display at CSS pixel size 1005 canvas.width = pgW * dprInit; 1006 canvas.height = pgH * dprInit; 1007 } 1008 1009 canvas.style.width = pgW + 'px'; 1010 canvas.style.height = pgH + 'px'; 1011 pageDiv.appendChild(canvas); 1012 flipContainer.appendChild(pageDiv); 1013 flipPageDivs.push({ pageDiv, canvas, index: i }); 1014 1015 if (!useCache) { 1016 cachedCanvases.push(canvas); 1017 } 1018 } 1019 1020 // Load structure into PageFlip immediately so it renders the shell 1021 pageFlipInstance.loadFromHTML(flipContainer.querySelectorAll('.my-p
1021age')); 1022 1023 if (!useCache) { 1024 cachedFitScale = fitScale; 1025 // Now async-render each PDF page into its canvas (correct closure per page) 1026 flipPageDivs.forEach(({ canvas, index }) => { 1027 pdfDoc.getPage(index).then(pdfPage => { 1028 const renderDpr = Math.max(window.devicePixelRatio || 1, 2); 1029 const vp = pdfPage.getViewport({ scale: fitScale * renderDpr }); 1030 const ctx = canvas.getContext('2d'); 1031 canvas.width = vp.width; 1032 canvas.height = vp.height; 1033 canvas.style.width = pgW + 'px'; 1034 canvas.style.height = pgH + 'px'; 1035 ctx.setTransform(1, 0, 0, 1, 0, 0); 1036 const renderTask = pdfPage.render({ 1037 canvasContext: ctx, 1038 viewport: vp 1039 }); 1040 renderTask.promise.then(() => { 1041 document.dispatchEvent(new CustomEvent('smartembed_page_rendered', { 1042 detail: { 1043 pageIndex: index, 1044 pdfPage: pdfPage, 1045 canvas: canvas, 1046 viewport: vp, 1047 pageDiv: canvas.parentElement, 1048 container: container, 1049 options: options, 1050 isThumbnail: false 1051 } 1052 })); 1053 }); 1054 }); 1055 }); 1056 } 1057 1058 // ========================================== 1059 // Custom Hardcover Intro (3D Animation) 1060 // ========================================== 1061 if (options.currentTemplate === 'hard_cover' && currentPage === 1 && !document.fullscreenElement) { 1062 const introEl = document.createElement('div'); 1063 introEl.className = 'smartembed-custom-hardcover-intro'; 1064 1065 const hcBook = document.createElement('div'); 1066 hcBook.className = 'smartembed-hc-book'; 1067 hcBook.style.width = pgW + 'px'; 1068 hcBook.style.height = pgH + 'px'; 1069 1070 const frontCover = document.createElement('div'); 1071 frontCover.className = 'smartembed-hc-part smartembed-hc-front'; 1072 const introCanvas = document.createElement('canvas'); 1073 1074 // Render page 1 to intro canvas 1075 pdfDoc.getPage(1).then(page => { 1076 const vp = page.getViewport({ scale: fitScale * (window.devicePixelRatio || 1) }); 1077 introCanvas.width = vp.width; 1078 introCanvas.height = vp.height; 1079 const ctx = introCanvas.getContext('2d'); 1080 page.render({ canvasContext: ctx, viewport: vp }); 1081 }); 1082 1083 frontCover.appendChild(introCanvas); 1084 1085 const spine = document.createElement('div'); 1086 spine.className = 'smartembed-hc-part smartembed-hc-spine'; 1087 const backCover = document.createElement('div'); 1088 backCover.className = 'smartembed-hc-part smartembed-hc-back'; 1089 1090 const pTop = document.createElement('div'); 1091 pTop.className = 'smartembed-hc-part smartembed-hc-pages-top'; 1092 const pBottom = document.createElement('div'); 1093 pBottom.className = 'smartembed-hc-part smartembed-hc-pages-bottom'; 1094 const pRight = document.createElement('div'); 1095 pRight.className = 'smartembed-hc-part smartembed-hc-pages-right'; 1096 1097 const shadow = document.createElement('div'); 1098 shadow.className = 'smartembed-hc-shadow'; 1099 1100 const hint = document.createElement('div'); 1101 hint.className = 'smartembed-open-hint'; 1102 hint.textContent = 'Click to Open Book'; 1103 1104 hcBook.appendChild(shadow); 1105 hcBook.appendChild(backCover); 1106 hcBook.appendChild(spine); 1107 hcBook.appendChild(pTop); 1108 hcBook.appendChild(pBottom); 1109 hcBook.appendChild(pRight); 1110 hcBook.appendChild(frontCover); 1111 introEl.appendChild(hcBook); 1112 introEl.appendChild(hint); 1113
1114 // Append to scrollArea so the overlay covers the full viewer 1115 scrollArea.style.position = 'relative'; 1116 scrollArea.appendChild(introEl); 1117 1118 // Hide actual flip container while intro is active 1119 flipContainer.style.opacity = '0'; 1120 flipContainer.style.pointerEvents = 'none'; 1121 flipContainer.style.transition = 'opacity 0.8s ease'; 1122 1123 let introOpened = false; 1124 const openBook = () => { 1125 if (introOpened) return; 1126 introOpened = true; 1127 introEl.classList.add('is-opening'); 1128 setTimeout(() => { 1129 introEl.style.transition = 'opacity 0.5s ease'; 1130 introEl.classList.add('hidden'); 1131 flipContainer.style.opacity = '1'; 1132 flipContainer.style.pointerEvents = 'auto'; 1133 if (pageFlipInstance && numPages > 1) { 1134 pageFlipInstance.turnToPage(1); 1135 } 1136 // Remove intro DOM after fade 1137 setTimeout(() => { if (introEl.parentNode) introEl.parentNode.removeChild(introEl); }, 600); 1138 }, 1500); 1139 }; 1140 1141 hcBook.addEventListener('click', openBook); 1142 // Also support touch for mobile 1143 hcBook.addEventListener('touchend', (e) => { e.preventDefault(); openBook(); }, { passive: false }); 1144 } 1145 1146 // Restore resume position 1147 if (currentPage > 1) { 1148 setTimeout(() => pageFlipInstance.turnToPage(currentPage - 1), 100); 1149 } 1150 1151 // Sync orientation class 1152 const updateOrientationClass = () => { 1153 const orient = pageFlipInstance.getOrientation(); 1154 if (orient === 'landscape') { 1155 flipContainer.classList.add('se-orientation-landscape'); 1156 flipContainer.classList.remove('se-orientation-portrait'); 1157 } else { 1158 flipContainer.classList.add('se-orientation-portrait'); 1159 flipContainer.classList.remove('se-orientation-landscape'); 1160 } 1161 }; 1162 updateOrientationClass(); 1163 1164 // Add transition style to targetEl for smooth centering slide 1165 const coverWrapper = pagesContainer.querySelector('.smartembed-book-cover-3d'); 1166 const targetEl = coverWrapper || flipContainer; 1167 if (targetEl) { 1168 targetEl.style.transition = 'transform 0.45s ease-out'; 1169 } 1170 1171 let initialShift = 0; 1172 if (pageFlipInstance.getOrientation() === 'landscape') { 1173 initialShift = -(pgW / 4) - 20; 1174 } 1175 applyFlipbookTransform(initialShift); 1176 1177 // Sync page counter on flip 1178 pageFlipInstance.on('flip', (e) => { 1179 const idx = e.data; 1180 currentPage = idx + 1; 1181 1182 if (pageNumEl) { 1183 if (pageFlipInstance.getOrientation() === 'landscape' && idx > 0 && idx < numPages - 1) { 1184 pageNumEl.value = `${idx + 1}-${idx + 2}`; 1185 } else { 1186 pageNumEl.value = idx + 1; 1187 } 1188 } 1189 1190 let shiftX = 0; 1191 if (pageFlipInstance.getOrientation() === 'landscape') { 1192 if (idx === 0) { 1193 shiftX = -(pgW / 4) - 20; 1194 } else if (idx === numPages - 1 && numPages % 2 === 0) { 1195 shiftX = (pgW / 4) + 20; 1196 } 1197 } 1198 applyFlipbookTransform(shiftX); 1199 1200 if (options.resume) localStorage.setItem('smartembed_page_' + options.url, currentPage); 1201 }); 1202 1203 // Handle orientation changes (e.g. window resize/rotation) 1204 pageFlipInstance.on('changeOrientation', (e) => { 1205 updateOrientationClass(); 1206 applyFlipbookTransform(0); 1207 }); 1208 1209 // Play sound immediately on flip start 1210 pageFlipInstance.on('changeState', (e) => { 1211 if (e.data === 'flipping' && soundEnabled && pageFlipAudio) { 1212 pageFlipAudio.currentTime = 0; 1213 pageFlipAudio.play().catch(() => {}); 1214 } 1215 }); 1216 }); 1217 } 1218 1219 // Navigation 1220 const queueRenderPage = num => { 1221 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1222 if (pageFlipInstance) pageFlipInstance.turnToPage(num - 1); 1223 return; 1224 } 1225 if (options.mode === 'continuous') { 1226 const targetPage = pagesContainer.querySelector(`#page-${num}`); 1227 if (targetPage) { 1228 scrollArea.scrollTo({ 1229 top: targetPage.offsetTop - 10, 1230 behavior: 'smooth' 1231 }); 1232 } 1233 return; 1234 } 1235 if (soundEnabled && pageFlipAudio) { 1236 pageFlipAudio.currentTime = 0; 1237 pageFlipAudio.play().catch(() => {}); 1238 } 1239 if (pageRendering) pageNumPending = num; 1240 else renderPage(num); 1241 }; 1242 1243 const onPrevPage = () => { 1244 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1245 if (pageFlipInstance) pageFlipInstance.flipPrev(); 1246 return; 1247 } 1248 if (currentPage <= 1) return; 1249 currentPage--; 1250 queueRenderPage(currentPage); 1251 }; 1252 1253 const onNextPage = () => { 1254 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1255 if (pageFlipInstance) pageFlipInstance.flipNext(); 1256 return; 1257 } 1258 if (currentPage >= numPages) return; 1259 currentPage++; 1260 queueRenderPage(currentPage); 1261 }; 1262 1263 if (prevBtn) prevBtn.addEventListener('click', onPrevPage); 1264 if (nextBtn) nextBtn.addEventListener('click', onNextPage); 1265 1266 if (toggleThumbnailsBtn) toggleThumbnailsBtn.addEventListener('click', () => { 1267 thumbnailsSidebar.classList.toggle('open'); 1268 if (tocSidebar && tocSidebar.classList.contains('open')) tocSidebar.classList.remove('open'); 1269 }); 1270 if (closeThumbnailsBtn) closeThumbnailsBtn.addEventListener('click', () => { 1271 thumbnailsSidebar.classList.remove('open'); 1272 }); 1273 1274 if (toggleTocBtn) toggleTocBtn.addEventListener('click', () => { 1275 tocSidebar.classList.toggle('open'); 1276 if (thumbnailsSidebar && thumbnailsSidebar.classList.contains('open')) thumbnailsSidebar.classList.remove('open'); 1277 }); 1278 if (closeTocBtn) closeTocBtn.addEventListener('click', () => { 1279 tocSidebar.classList.remove('open'); 1280 }); 1281 1282 if (optionsBtn) { 1283 optionsBtn.addEventListener('click', (e) => { 1284 e.stopPropagation(); 1285 container.querySelectorAll('.smartembed-options-dropdown').forEach(dd => { 1286 if (!dd.classList.contains('smartembed-share-dropdown')) dd.classList.remove('show'); 1287 }); 1288 const menu = container.querySelector('.smartembed-options-menu-container > .smartembed-options-dropdown:not(.smartembed-share-dropdown)'); 1289 if (menu) menu.classList.toggle('show'); 1290 }); 1291 } 1292 1293 document.addEventListener('click', () => { 1294 container.querySelectorAll('.smartembed-options-dropdown').forEach(dd => dd.classList.remove('show')); 1295 }); 1296 1297 if (gotoFirstBtn) gotoFirstBtn.addEventListener('click', () => { 1298 currentPage = 1; queueRenderPage(currentPage); 1299 container.querySelector('.smartembed-options-dropdown').classList.remove('show'); 1300 }); 1301 if (gotoLastBtn) gotoLastBtn.addEventListener('click', () => { 1302 currentPage = numPages; queueRenderPage(currentPage); 1303 container.querySelector('.smartembed-options-dropdown').classList.remove('show'); 1304 }); 1305 if (toggleModeBtn) toggleModeBtn.addEventListener('click', () => { 1306 const isFlip = options.mode === 'flipbook' || options.mode === 'flipbook_single'; 1307 options.mode = isFlip ? 'continuous' : 'flipbook'; 1308 1309 if (pageFlipInstance) { 1310 try { pageFlipInstance.destroy(); } catch(e){} 1311 pageFlipInstance = null; 1312 } 1313 1314 if (options.mode === 'continuous') { 1315 scrollArea.classList.remove('flipbook-mode'); 1316 renderAllPages(); 1317 } else { 1318 scrollArea.classList.add('flipbook-mode'); 1319 renderFlipbook(); 1320 } 1321 1322 updateNavigationArrows(); 1323 1324 toggleModeBtn.innerHTML = options.mode === 'continuous' ? 1325 toggleModeBtn.innerHTML.replace('Single Page Mode', 'Flipbook Mode') : 1326 toggleModeBtn.innerHTML.replace('Flipbook Mode', 'Single Page Mode'); 1327 1328 container.querySelector('.smartembed-options-dropdown').classList.remove('show'); 1329 }); 1330 1331 if (printBtn) { 1332 printBtn.addEventListener('click', () => { 1333 const iframe = document.createElement('iframe'); 1334 iframe.style.display = 'none'; 1335 iframe.src = options.url; 1336 document.body.appendChild(iframe); 1337 iframe.onload = () => { 1338 iframe.contentWindow.print(); 1339 setTimeout(() => iframe.remove(), 5000); 1340 }; 1341 }); 1342 } 1343 1344 if (shareBtn) { 1345 shareBtn.addEventListener('click', (e) => { 1346 e.stopPropagation(); 1347 container.querySelectorAll('.smartembed-options-dropdown').forEach(dd => { 1348 if (!dd.classList.contains('smartembed-share-dropdown')) dd.classList.remove('show'); 1349 }); 1350 const shareMenu = container.querySelector('.smartembed-share-dropdown'); 1351 if (shareMenu) shareMenu.classList.toggle('show'); 1352 }); 1353 1354 const fbShare = container.querySelector('.smartembed-share-fb'); 1355 const twShare = container.querySelector('.smartembed-share-tw'); 1356 const waShare = container.querySelector('.smartembed-share-wa'); 1357 const liShare = container.querySelector('.smartembed-share-li'); 1358 const cpShare = container.querySelector('.smartembed-share-copy'); 1359 1360 if (fbShare) fbShare.addEventListener('click', () => window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(options.url)}`, '_blank')); 1361 if (twShare) twShare.addEventListener('click', () => window.open(`https://twitter.com/intent/tweet?url=${encodeURIComponent(options.url)}&text=${encodeURIComponent(document.title)}`, '_blank')); 1362 if (waShare) waShare.addEventListener('click', () =>
1362 window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(document.title + " " + options.url)}`, '_blank')); 1363 if (liShare) liShare.addEventListener('click', () => window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(options.url)}`, '_blank')); 1364 if (cpShare) cpShare.addEventListener('click', () => { 1365 navigator.clipboard.writeText(options.url); 1366 alert('Link copied to clipboard!'); 1367 }); 1368 } 1369 1370 // Template Selector Click Handler 1371 const templateCards = container.querySelectorAll('.smartembed-tpl-card'); 1372 templateCards.forEach(card => { 1373 card.addEventListener('click', () => { 1374 const tpl = card.getAttribute('data-tpl'); 1375 if (tpl === options.currentTemplate) return; 1376 1377 // Update active UI 1378 templateCards.forEach(c => c.classList.remove('active')); 1379 card.classList.add('active'); 1380 1381 // Set new template and mode 1382 options.currentTemplate = tpl; 1383 if (tpl === 'vertical_reader') options.mode = 'continuous'; 1384 else if (tpl === 'slider') options.mode = 'presentation'; 1385 else options.mode = 'flipbook'; 1386 1387 // Re-render 1388 if (options.mode === 'continuous') { 1389 scrollArea.classList.remove('flipbook-mode'); 1390 renderAllPages(); 1391 } else if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1392 scrollArea.classList.add('flipbook-mode'); 1393 if (pageFlipInstance) { 1394 try { pageFlipInstance.destroy(); } catch(e){} 1395 pageFlipInstance = null; 1396 } 1397 renderFlipbook(); 1398 } else { 1399 scrollArea.classList.remove('flipbook-mode'); 1400 renderPage(currentPage); 1401 } 1402 1403 updateNavigationArrows(); 1404 if (toggleModeBtn) { 1405 toggleModeBtn.innerHTML = options.mode === 'continuous' ? 1406 toggleModeBtn.innerHTML.replace('Single Page Mode', 'Flipbook Mode') : 1407 toggleModeBtn.innerHTML.replace('Flipbook Mode', 'Single Page Mode'); 1408 } 1409 }); 1410 }); 1411 1412 // Zoom 1413 if (zoomInBtn) zoomInBtn.addEventListener('click', () => { 1414 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1415 flipZoom = Math.min(flipZoom + 0.15, 3.0); 1416 applyFlipbookTransform(0); 1417 reRenderFlipbookPages(); 1418 // Enable pan cursor when zoomed in 1419 const cw = pagesContainer.querySelector('.smartembed-book-cover-3d'); 1420 const fc = pagesContainer.querySelector('.smartembed-flipbook-container'); 1421 if (cw) cw.classList.toggle('se-flipbook-zoomed', flipZoom > 1.0); 1422 if (fc) fc.classList.toggle('se-flipbook-zoomed', flipZoom > 1.0); 1423 } else { 1424 scale += 0.25; 1425 if (options.mode === 'continuous') renderAllPages(); 1426 else renderPage(currentPage); 1427 } 1428 }); 1429 if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => { 1430 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1431 flipZoom = Math.max(flipZoom - 0.15, 0.4); 1432 if (flipZoom <= 1.0) { flipPanX = 0; flipPanY = 0; } // reset pan at/below 1x 1433 applyFlipbookTransform(0); 1434 reRenderFlipbookPages(); 1435 const cw = pagesContainer.querySelector('.smartembed-book-cover-3d'); 1436 const fc = pagesContainer.querySelector('.smartembed-flipbook-container'); 1437 if (cw) cw.classList.toggle('se-flipbook-zoomed', flipZoom > 1.0); 1438 if (fc) fc.classList.toggle('se-flipbook-zoomed', flipZoom > 1.0); 1439 } else { 1440 scale -= 0.25; 1441 if (scale < 0.5) scale = 0.5; 1442 if (options.mode === 'continuous') renderAllPages(); 1443 else renderPage(currentPage); 1444 console.log('Images array is empty.'); 1445 } 1446 }); 1447 1448 // ========================================== 1449 // PRO FEATURES (Merged) 1450 // ========================================== 1451 if (options.isPro) { 1452 1453 if (options.download) { 1454 // Find the main toolbar instead of the dropdown 1455 const toolbar = container.querySelector('.smartembed-bottom-toolbar'); 1456 if (toolbar) { 1457 const downloadBtn = document.createElement('button'); 1458 downloadBtn.className = 'smartembed-tool-btn smartembed-download-btn'; 1459 downloadBtn.title = 'Download'; 1460 downloadBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>'; 1461 1462 // Insert before the print button if it exists, otherwise before the share container, or just append 1463 const printBtn = toolbar.querySelector('.smartembed-print-btn'); 1464 const shareContainer = toolbar.querySelector('.smartembed-share-container'); 1465 const optionsMenu = toolbar.querySelector('.smartembed-options-menu-container:last-child'); 1466 1467 if (printBtn) { 1468 toolbar.insertBefore(downloadBtn, printBtn); 1469 } else if (shareContainer) { 1470 toolbar.insertBefore(downloadBtn, shareContainer); 1471 } else if (optionsMenu) { 1472 toolbar.insertBefore(downloadBtn, optionsMenu); 1473 } else { 1474 toolbar.appendChild(downloadBtn); 1475 } 1476 1477 downloadBtn.addEventListener('click', async (evt) => { 1478 evt.preventDefault(); 1479 downloadBtn.style.opacity = '0.5'; 1480 const originalHtml = downloadBtn.innerHTML; 1481 downloadBtn.innerHTML = 'Preparing...'; 1482 1483 try { 1484 if (options.bookId && options.ajaxUrl) { 1485 var formData = new FormData(); 1486 formData.append('action', 'smartembed_track_event'); 1487 formData.append('event_type', 'download'); 1488 formData.append('book_id', options.bookId); 1489 fetch(options.ajaxUrl, { method: 'POST', body: formData }).catch(e => {}); 1490 } 1491 1492 if (options.watermarkLogoUrl && typeof window.PDFLib !== 'undefined') { 1493 const PDFLib = window.PDFLib; 1494 const pdfBytes = await fetch(options.url).then(res => res.arrayBuffer()); 1495 const pdfDocLib = await PDFLib.PDFDocument.load(pdfBytes); 1496 1497 const logoBytes = await fetch(options.watermarkLogoUrl).then(res => res.arrayBuffer()); 1498 let logoImage; 1499 if (options.watermarkLogoUrl.toLowerCase().endsWith('.png')) { 1500 logoImage = await pdfDocLib.embedPng(logoBytes); 1501 } else { 1502 logoImage = await pdfDocLib.embedJpg(logoBytes); 1503 } 1504 1505 const pages = pdfDocLib.getPages(); 1506 const maxLogoWidth = 200; 1507 const scaleFactor = Math.min(1, maxLogoWidth / logoImage.width); 1508 const logoWidth = logoImage.width * scaleFactor; 1509 const logoHeight = logoImage.height * scaleFactor; 1510 1511 for (const page of pages) { 1512 const { width: pageWidth, height: pageHeight } = page.getSize(); 1513 page.drawImage(logoImage, { 1514 x: pageWidth / 2 - logoWidth / 2, 1515 y: pageHeight / 2 - logoHeight / 2, 1516 width: logoWidth, 1517 height: logoHeight, 1518 opacity: 0.3, 1519 }); 1520 } 1521 1522 const modifiedPdfBytes = await pdfDocLib.save(); 1523 const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' }); 1524 const url = URL.createObjectURL(blob); 1525 const a = document.createElement('a'); 1526 a.href = url; 1527 a.download = options.url.split('/').pop() || 'download.pdf'; 1528 a.click(); 1529 URL.revokeObjectURL(url); 1530 } else { 1531 const a = document.createElement('a'); 1532 a.href = options.url; 1533 a.download = options.url.split('/').pop() || 'download.pdf'; 1534 a.click(); 1535 } 1536 } catch (err) { 1537 console.error('Download error', err); 1538 alert('Error downloading PDF.'); 1539 } 1540 1541 downloadBtn.style.opacity = '1'; 1542 downloadBtn.innerHTML = originalHtml; 1543 container.querySelectorAll('.smartembed-options-dropdown').forEach(dd => dd.classList.remove('show')); 1544 }); 1545 } // end if dropdown 1546 } // end if download 1547 1548 // Zoom Scroll 1549 container.addEventListener('wheel', (e) => { 1550 if (e.ctrlKey || e.metaKey) { 1551 e.preventDefault(); 1552 if (e.deltaY < 0) { 1553 const zoomInBtn = container.querySelector('.smartembed-zoom-in'); 1554 if (zoomInBtn) zoomInBtn.click(); 1555 } else { 1556 const zoomOutBtn = container.querySelector('.smartembed-zoom-out'); 1557 if (zoomOutBtn) zoomOutBtn.click(); 1558 } 1559 } 1560 }, { passive: false }); 1561 } // end if options.isPro 1562 // PDF Links & Text Search 1563 document.addEventListener('smartembed_page_rendered', async function(e) { 1564 const { pdfPage, canvas, viewport, pageDiv, isThumbnail, options } = e.detail; 1565 if (!options.isPro) return; 1566 if (isThumbnail || !pageDiv) return; 1567 1568 pageDiv.style.position = 'relative'; 1569 1570 const pdfjsViewer = window.pdfjsViewer; 1571 if (!pdfjsViewer) return; 1572 1573 // --- TextLayer (for Search / Selection) --- 1574 let textLayerDiv = pageDiv.querySelector('.textLayer'); 1575 if (textLayerDiv) textLayerDiv.remove(); 1576 1577 textLayerDiv = document.createElement('div'); 1578 textLayerDiv.className = 'textLayer'; 1579 textLayerDiv.style.position = 'absolute'; 1580 textLayerDiv.style.top = '0'; 1581 textLayerDiv.style.left = '0'; 1582 textLayerDiv.style.right = '0'; 1583 textLayerDiv.style.bottom = '0'; 1584 textLayerDiv.style.overflow = 'hidden'; 1585 textLayerDiv.style.lineHeight = '1.0'; 1586 1587 // Ensure --scale-factor is set for pdf_viewer.css 1588 textLayerDiv.style.setProperty('--scale-factor', viewport.scale); 1589 1590 pageDiv.appendChild(textLayerDiv); 1591 1592 try { 1593 const textContent = await pdfPage.getTextContent(); 1594 pdfjsViewer.renderTextLayer({ 1595 textContentSource: textContent, 1596 container: textLayerDiv, 1597 viewport: viewport, 1598 textDivs: [] 1599 }); 1600 } catch (err) { 1601 console.error('Error rendering text layer', err); 1602 } 1603 1604 // --- AnnotationLayer (for PDF Links) --- 1605 if (options.showAnnotations !== false) { 1606 let annoLayerDiv = pageDiv.querySelector('.annotationLayer'); 1607 if (annoLayerDiv) annoLayerDiv.remove(); 1608 1609 annoLayerDiv = document.createElement('div'); 1610 annoLayerDiv.className = 'annotationLayer'; 1611 annoLayerDiv.style.position = 'absolute'; 1612 annoLayerDiv.style.top = '0'; 1613 annoLayerDiv.style.left = '0'; 1614 annoLayerDiv.style.right = '0'; 1615 annoLayerDiv.style.bottom = '0'; 1616 1617 // Ensure --scale-factor is set for pdf_viewer.css 1618 annoLayerDiv.style.setProperty('--scale-factor', viewport.scale); 1619 1620 pageDiv.appendChild(annoLayerDiv); 1621 1622 try { 1623 const annotations = await pdfPage.getAnnotations(); 1624 if (annotations.length > 0) { 1625 pdfjsViewer.AnnotationLayer.render({ 1626 viewport: viewport, 1627 div: annoLayerDiv, 1628 annotations: annotations, 1629 page: pdfPage, 1630 linkService: new pdfjsViewer.SimpleLinkService(), 1631 downloadManager: null 1632 }); 1633 } 1634 } catch (err) { 1635 console.error('Error rendering annotation layer', err); 1636 } 1637 } 1638 });
1639 1640 // --- Flipbook Pan/Drag (click-and-hold to move when zoomed) --- 1641 if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1642 let _isDragging = false; 1643 let _dragStartX = 0; 1644 let _dragStartY = 0; 1645 let _panAtDragStart = { x: 0, y: 0 }; 1646 1647 const onDragStart = (clientX, clientY) => { 1648 if (flipZoom <= 1.0) return; 1649 _isDragging = true; 1650 _dragStartX = clientX; 1651 _dragStartY = clientY; 1652 _panAtDragStart = { x: flipPanX, y: flipPanY }; 1653 const cw = pagesContainer.querySelector('.smartembed-book-cover-3d'); 1654 const fc = pagesContainer.querySelector('.smartembed-flipbook-container'); 1655 if (cw) cw.classList.add('se-flipbook-dragging'); 1656 if (fc) fc.classList.add('se-flipbook-dragging'); 1657 // Disable PageFlip mouse events while panning 1658 if (pageFlipInstance) pageFlipInstance.getSettings().useMouseEvents = false; 1659 }; 1660 1661 const onDragMove = (clientX, clientY) => { 1662 if (!_isDragging) return; 1663 flipPanX = _panAtDragStart.x + (clientX - _dragStartX); 1664 flipPanY = _panAtDragStart.y + (clientY - _dragStartY); 1665 applyFlipbookTransform(null); 1666 }; 1667 1668 const onDragEnd = () => { 1669 if (!_isDragging) return; 1670 _isDragging = false; 1671 const cw = pagesContainer.querySelector('.smartembed-book-cover-3d'); 1672 const fc = pagesContainer.querySelector('.smartembed-flipbook-container'); 1673 if (cw) cw.classList.remove('se-flipbook-dragging'); 1674 if (fc) fc.classList.remove('se-flipbook-dragging'); 1675 // Re-enable PageFlip mouse events after panning 1676 if (pageFlipInstance) pageFlipInstance.getSettings().useMouseEvents = true; 1677 }; 1678 1679 // Mouse events 1680 scrollArea.addEventListener('mousedown', (e) => { 1681 if (e.button !== 0) return; 1682 onDragStart(e.clientX, e.clientY); 1683 }); 1684 window.addEventListener('mousemove', (e) => onDragMove(e.clientX, e.clientY)); 1685 window.addEventListener('mouseup', onDragEnd); 1686 1687 // Touch events 1688 scrollArea.addEventListener('touchstart', (e) => { 1689 if (e.touches.length !== 1) return; 1690 onDragStart(e.touches[0].clientX, e.touches[0].clientY); 1691 }, { passive: true }); 1692 scrollArea.addEventListener('touchmove', (e) => { 1693 if (e.touches.length !== 1) return; 1694 onDragMove(e.touches[0].clientX, e.touches[0].clientY); 1695 }, { passive: true }); 1696 scrollArea.addEventListener('touchend', onDragEnd); 1697 } 1698 1699 1700 // Fullscreen 1701 if (fullscreenBtn) { 1702 fullscreenBtn.addEventListener('click', () => { 1703 if (!document.fullscreenElement) { 1704 container.requestFullscreen().catch(err => { 1705 console.error(`Error attempting to enable full-screen mode: ${err.message}`); 1706 }); 1707 } else { 1708 document.exitFullscreen(); 1709 } 1710 }); 1711 } 1712 1713 // Re-render content when entering/exiting fullscreen so the book fills the screen 1714 container.addEventListener('fullscreenchange', () => { 1715 if (!pdfDoc) return; 1716 const isFs = !!document.fullscreenElement; 1717 1718 if (isFs) { 1719 setTimeout(() => { 1720 const fsW = window.innerWidth; 1721 const fsH = window.innerHeight; 1722 1723 // Expand container to full screen 1724 container.style.setProperty('width', '100vw', 'important'); 1725 container.style.setProperty('height', '100vh', 'important'); 1726 container.style.setProperty('max-width', '100vw', 'important'); 1727 container.style.setProperty('min-height', '100vh', 'important'); 1728 container.style.setProperty('border-radius','0', 'important'); 1729 1730 // CRITICAL: update ui height so renderFlipbook calculates at new size 1731 const templateSelectorEl = container.querySelector('.smartembed-template-selector'); 1732 const currentTemplateSelectorH = templateSelectorEl ? templateSelectorEl.offsetHeight : 0; 1733 const uiH = fsH - currentTemplateSelectorH; 1734 ui.style.height = uiH + 'px'; 1735 ui.style.minHeight = uiH + 'px'; 1736 1737 // Reset zoom/pan so book is centred 1738 flipZoom = 1.0; 1739 flipPanX = 0; 1740 flipPanY = 0; 1741 1742 if (pageFlipInstance) { 1743 try { pageFlipInstance.destroy(); } catch(e){} 1744 pageFlipInstance = null; 1745 } 1746 cachedCanvases = []; 1747 cachedFitScale = 0; 1748 1749 if (options.mode === 'continuous') { 1750 renderAllPages(); 1751 } else if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1752 scrollArea.classList.add('flipbook-mode'); 1753 renderFlipbook(); 1754 } else { 1755 renderPage(currentPage); 1756 } 1757 }, 200); 1758 } else { 1759 // Restore original container + ui dimensions 1760 container.style.setProperty('height', originalHeight, 'important'); 1761 container.style.setProperty('min-height', originalHeight, 'important'); 1762 container.style.removeProperty('width'); 1763 container.style.removeProperty('max-width'); 1764 container.style.removeProperty('border-radius'); 1765
1766 ui.style.height = baseViewerH + 'px'; 1767 ui.style.minHeight = baseViewerH + 'px'; 1768 1769 flipZoom = 1.0; 1770 flipPanX = 0; 1771 flipPanY = 0; 1772 1773 if (pageFlipInstance) { 1774 try { pageFlipInstance.destroy(); } catch(e){} 1775 pageFlipInstance = null; 1776 } 1777 cachedCanvases = []; 1778 cachedFitScale = 0; 1779 1780 if (options.mode === 'continuous') { 1781 renderAllPages(); 1782 } else if (options.mode === 'flipbook' || options.mode === 'flipbook_single') { 1783 scrollArea.classList.add('flipbook-mode'); 1784 renderFlipbook(); 1785 } else { 1786 renderPage(currentPage); 1787 } 1788 } 1789 }); 1790 1791 // TTS (Text to Speech) 1792 if (ttsBtn) { 1793 let speaking = false; 1794 ttsBtn.addEventListener('click', () => { 1795 if (speaking) { 1796 window.speechSynthesis.cancel(); 1797 speaking = false; 1798 ttsBtn.querySelector('span').textContent = 'Turn on/off Sound'; 1799 return; 1800 } 1801 pdfDoc.getPage(currentPage).then(page => { 1802 page.getTextContent().then(textContent => { 1803 let text = textContent.items.map(item => item.str).join(' '); 1804 const utterance = new SpeechSynthesisUtterance(text); 1805 window.speechSynthesis.speak(utterance); 1806 speaking = true; 1807 ttsBtn.querySelector('span').textContent = 'Stop Sound'; 1808 utterance.onend = () => { 1809 speaking = false; 1810 ttsBtn.querySelector('span').textContent = 'Turn on/off Sound'; 1811 }; 1812 }); 1813 }); 1814 }); 1815 } 1816 } 1817 1818 window.smartEmbedInitAll = function() { 1819 if (typeof pdfjsLib === 'undefined') { 1820 if (!document.getElementById('smartembed-pdfjs-fallback')) { 1821 const script = document.createElement('script'); 1822 script.id = 'smartembed-pdfjs-fallback'; 1823 script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; 1824 document.head.appendChild(script); 1825 } 1826 setTimeout(window.smartEmbedInitAll, 500); 1827 return; 1828 } 1829 1830 if (typeof St === 'undefined' && typeof PageFlip === 'undefined') { 1831 if (!document.getElementById('smartembed-pageflip-fallback')) { 1832 const script = document.createElement('script'); 1833 script.id = 'smartembed-pageflip-fallback'; 1834 script.src = 'https://cdn.jsdelivr.net/npm/page-flip/dist/js/page-flip.browser.min.js'; 1835 document.head.appendChild(script); 1836 } 1837 setTimeout(window.smartEmbedInitAll, 500); 1838 return; 1839 } 1840 1841 if (!pdfjsLib.GlobalWorkerOptions.workerSrc) { 1842 pdfjsLib.GlobalWorkerOptions.workerSrc = (window.smartEmbedFront && window.smartEmbedFront.workerSrc) 1843 ? window.smartEmbedFront.workerSrc 1844 : 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; 1845 } 1846 1847 document.querySelectorAll('.smartembed-pdf-app-container:not(.smartembed-initialized)').forEach(container => { 1848 container.classList.add('smartembed-initialized'); 1849 initSmartEmbedViewer(container); 1850 }); 1851 1852 // Dispatch event for Pro addon to hook into 1853 document.dispatchEvent(new CustomEvent('smartembed_viewer_initialized')); 1854 }; 1855 1856 document.addEventListener('DOMContentLoaded', window.smartEmbedInitAll); 1857 1858 // Support for Elementor Editor 1859 if (typeof window.jQuery !== 'undefined') { 1860 window.jQuery(window).on('elementor/frontend/init', function() { 1861 if (window.elementorFrontend && window.elementorFrontend.hooks) { 1862 window.elementorFrontend.hooks.addAction('frontend/element_ready/widget', function() { 1863 window.smartEmbedInitAll(); 1864 }); 1865 } 1866 }); 1867 } 1868 1869 // Fallback for other dynamic content loads (e.g., AJAX, Gutenberg preview) 1870 const observer = new MutationObserver((mutations) => { 1871 for (let mutation of mutations) { 1872 if (mutation.addedNodes.length) { 1873 const hasViewer = Array.from(mutation.addedNodes).some(node => 1874 node.nodeType === 1 && (node.classList.contains('smartembed-pdf-app-container') || node.querySelector('.smartembed-pdf-app-container')) 1875 ); 1876 if (hasViewer) { 1877 window.smartEmbedInitAll(); 1878 } 1879 } 1880 } 1881 });
1882 observer.observe(document.body, { childList: true, subtree: true }); 1883 1884 // Execute immediately in case script was loaded dynamically after DOMContentLoaded 1885 window.smartEmbedInitAll(); 1886 1887})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.