1/** 2 * IndicePdfView â leichter Canvas-PDF-Viewer auf Basis von PDF.js. 3 * 4 * Für Browser ohne eingebauten PDF-Viewer (insb. iOS Safari / Android Chrome), 5 * die PDFs im iframe nur als erste Seite oder gar nicht darstellen. 6 * 7 * Verwendung: 8 * if (IndicePdfView.needsCanvasViewer()) { 9 * IndicePdfView.render(containerElement, '/api/.../download?...&preview=1'); 10 * } 11 * 12 * PDF.js (lokal: /js/lib/pdfjs/) wird erst beim ersten render() lazy nachgeladen. 13 */ 14(function () { 15 'use strict'; 16 17 if (window.IndicePdfView) return; 18 19 var PDFJS_SRC = '/js/lib/pdfjs/pdf.min.js'; 20 var PDFJS_WORKER = '/js/lib/pdfjs/pdf.worker.min.js'; 21 var PAGE_GAP = 12; // grauer Abstand um die Seiten (px) 22 var pdfjsPromise = null; 23 var stylesInjected = false; 24 25 /* ---------------------------- Styles ---------------------------- */ 26 27 function injectStyles() { 28 if (stylesInjected) return; 29 stylesInjected = true; 30 var css = '' 31 + '.ipdfv-container{position:relative;background:#8d949c;-webkit-overflow-scrolling:touch;}' 32 + '.ipdfv-pagebar{position:sticky;top:8px;z-index:5;display:table;margin:0 auto;' 33 + 'background:rgba(0,0,0,0.65);color:#fff;font-size:12px;line-height:1;' 34 + 'padding:6px 12px;border-radius:999px;pointer-events:none;font-family:sans-serif;}' 35 + '.ipdfv-page{margin:' + PAGE_GAP + 'px auto;background:#fff;' 36 + 'box-shadow:0 2px 8px rgba(0,0,0,0.35);position:relative;}' 37 + '.ipdfv-page canvas{display:block;}' 38 + '.ipdfv-page:not([data-rendered])::after{content:"";position:absolute;top:50%;left:50%;' 39 + 'width:24px;height:24px;margin:-12px 0 0 -12px;border:3px solid #e2e2e2;' 40 + 'border-top-color:#8d949c;border-radius:50%;animation:ipdfv-spin .8s linear infinite;}' 41 + '.ipdfv-status{display:flex;flex-direction:column;align-items:center;justify-content:center;' 42 + 'gap:12px;padding:48px 16px;color:#fff;font-family:sans-serif;font-size:14px;text-align:center;}' 43 + '.ipdfv-spinner{width:32px;height:32px;border:3px solid rgba(255,255,255,0.35);' 44 + 'border-top-color:#fff;border-radius:50%;animation:ipdfv-spin .8s linear infinite;}' 45 + '@keyframes ipdfv-spin{to{transform:rotate(360deg);}}'; 46 var style = document.createElement('style'); 47 style.setAttribute('data-ipdfv', '1'); 48 style.textContent = css; 49 document.head.appendChild(style); 50 } 51 52 /* ---------------------------- PDF.js lazy ---------------------------- */ 53 54 function loadPdfJs() { 55 if (window.pdfjsLib) { 56 window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER; 57 return Promise.resolve(window.pdfjsLib); 58 } 59 if (pdfjsPromise) return pdfjsPromise; 60 pdfjsPromise = new Promise(function (resolve, reject) { 61 var s = document.createElement('script'); 62 s.src = PDFJS_SRC; 63 s.onload = function () { 64 if (window.pdfjsLib) { 65 window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_WORKER; 66 resolve(window.pdfjsLib); 67 } else { 68 pdfjsPromise = null; 69 reject(new Error('pdfjsLib nicht verfuegbar')); 70 } 71 }; 72 s.onerror = function () { 73 pdfjsPromise = null; 74 reject(new Error('PDF.js konnte nicht geladen werden')); 75 }; 76 document.head.appendChild(s); 77 }); 78 return pdfjsPromise; 79 } 80 81 /* ---------------------------- Rendering ---------------------------- */ 82 83 function availWidth(container) { 84 return Math.max(100, container.clientWidth - PAGE_GAP * 2); 85 } 86 87 function buildPages(container, pdf) { 88 var numPages = pdf.numPages; 89 container.innerHTML = ''; 90 91 // Sticky-Seitenanzeige "Seite X / Y" 92 var pagebar = document.createElement('div'); 93 pagebar.className = 'ipdfv-pagebar'; 94 pagebar.textContent = 'Seite 1 / ' + numPages; 95 container.appendChild(pagebar); 96 97 // Placeholder fuer alle Seiten sofort anlegen (DIN-A4-Verhaeltnis als Startwert) 98 var w = availWidth(container); 99 var holders = []; 100 for (var i = 1; i <= numPages; i++) { 101 var holder = document.createElement('div'); 102 holder.className = 'ipdfv-page'; 103 holder.setAttribute('data-page', String(i)); 104 holder.style.width = w + 'px'; 105 holder.style.height = Math.round(w * 1.414) + 'px'; 106 container.appendChild(holder); 107 holders.push(holder); 108 } 109 110 // Exakte Hoehen aus den Seiten-Metadaten nachziehen (rendert nichts) 111 (function fixHeights(idx) { 112 if (idx > numPages) return; 113 pdf.getPage(idx).then(function (page) { 114 var holder = holders[idx - 1]; 115 if (holder && !holder.hasAttribute('data-rendered')) { 116 var vp = page.getViewport({ scale: 1 }); 117 holder.style.height = Math.round(availWidth(container) * (vp.height / vp.width)) + 'px'; 118 } 119 fixHeights(idx + 1); 120 }).catch(function () { fixHeights(idx + 1); }); 121 })(1); 122 123 // Lazy Rendering pro Seite via IntersectionObserver 124 var started = {}; 125 function renderPage(holder) { 126 var num = parseInt(holder.getAttribute('data-page'), 10); 127 if (started[num]) return; 128 started[num] = true; 129 pdf.getPage(num).then(function (page) { 130 var base = page.getViewport({ scale: 1 }); 131 var scale = availWidth(container) / base.width; 132 var vp = page.getViewport({ scale: scale }); 133 var dpr = window.devicePixelRatio || 1; 134 135 var canvas = document.createElement('canvas'); 136 canvas.width = Math.floor(vp.width * dpr); 137 canvas.height = Math.floor(vp.height * dpr); 138 canvas.style.width = Math.floor(vp.width) + 'px'; 139 canvas.style.height = Math.floor(vp.height) + 'px'; 140 141 var task = page.render({ 142 canvasContext: canvas.getContext('2d'), 143 viewport: vp, 144 transform: dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : null 145 }); 146 return task.promise.then(function () { 147 holder.style.width = Math.floor(vp.width) + 'px'; 148 holder.style.height = Math.floor(vp.height) + 'px'; 149 holder.innerHTML = ''; 150 holder.appendChild(canvas); 151 holder.setAttribute('data-rendered', '1'); 152 }); 153 }).catch(function (e) { 154 if (window.console) console.error('[IndicePdfView] Seite ' + num, e); 155 }); 156 } 157 158 var observer = new IntersectionObserver(function (entries) {
159 entries.forEach(function (entry) { 160 if (entry.isIntersecting) { 161 observer.unobserve(entry.target); 162 renderPage(entry.target); 163 } 164 }); 165 }, { root: container, rootMargin: '600px' }); 166 holders.forEach(function (h) { observer.observe(h); }); 167 168 // Seitenanzeige beim Scrollen aktualisieren 169 function updatePagebar() { 170 var mark = container.scrollTop + container.clientHeight * 0.4; 171 var current = 1; 172 for (var j = 0; j < holders.length; j++) { 173 if (holders[j].offsetTop <= mark) current = j + 1; 174 else break; 175 } 176 pagebar.textContent = 'Seite ' + current + ' / ' + numPages; 177 } 178 container.addEventListener('scroll', updatePagebar, { passive: true }); 179 } 180 181 /* ---------------------------- Public API ---------------------------- */ 182 183 window.IndicePdfView = { 184 185 /** 186 * Soll PDF.js statt iframe genutzt werden? 187 */ 188 needsCanvasViewer: function () { 189 if (navigator.pdfViewerEnabled === false) return true; // Browser sagt explizit: kein PDF-Plugin 190 if (navigator.pdfViewerEnabled === true) return false; 191 // Fallback-Heuristik fuer aeltere Browser: Touch-Geraet 192 return ('ontouchstart' in window) && /Android|iPhone|iPad|iPod/i.test(navigator.userAgent); 193 }, 194 195 /** 196 * Rendert das PDF seitenweise als Canvas in den Container 197 * (Seiten untereinander, Container scrollt vertikal). 198 */ 199 render: function (container, url) { 200 injectStyles(); 201 container.classList.add('ipdfv-container'); 202 container.style.overflowY = 'auto'; 203 container.style.display = 'block'; // ggf. Flex-Layout des Aufrufers neutralisieren 204 container.innerHTML = '<div class="ipdfv-status"><div class="ipdfv-spinner"></div>' 205 + '<span>PDF wird geladen...</span></div>'; 206 207 function start() { 208 loadPdfJs().then(function (pdfjsLib) { 209 return pdfjsLib.getDocument({ url: url, withCredentials: true }).promise; 210 }).then(function (pdf) { 211 buildPages(container, pdf); 212 }).catch(function (e) { 213 if (window.console) console.error('[IndicePdfView]', e); 214 container.innerHTML = '<div class="ipdfv-status">' 215 + '<span>Das PDF konnte nicht geladen werden.</span></div>'; 216 }); 217 } 218 219 // Falls der Container noch nicht gelayoutet ist (clientWidth 0), 220 // kurz auf das Layout warten (max. ~10 Frames). 221 var tries = 0; 222 (function waitForLayout() { 223 if (container.clientWidth > 0 || tries >= 10) { start(); return; } 224 tries++; 225 requestAnimationFrame(waitForLayout); 226 })(); 227 } 228 }; 229})();
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.