Replace PDF.js canvas with native browser iframe PDF viewer
- Browser renders PDF exactly as-is using its built-in viewer (Chrome/Edge/Firefox) - No external dependencies - just a blob URL in an <iframe> - Page navigation via #page=N fragment syncs PDF view with playback position - Fixed flex layout so iframe fills remaining height correctly - Removed PDF.js script tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
00af54c95a
commit
7afbe3a22a
3 changed files with 51 additions and 67 deletions
|
|
@ -21,9 +21,8 @@ const audioCache = new Map();
|
||||||
let allModels = {};
|
let allModels = {};
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
|
|
||||||
// PDF.js state
|
// PDF blob URL for native browser viewer
|
||||||
let pdfDoc = null;
|
let pdfBlobUrl = null;
|
||||||
const pageCanvases = []; // canvas elements per page
|
|
||||||
|
|
||||||
// ── DOM ────────────────────────────────────────────────────────────
|
// ── DOM ────────────────────────────────────────────────────────────
|
||||||
const $ = id => document.getElementById(id);
|
const $ = id => document.getElementById(id);
|
||||||
|
|
@ -184,70 +183,45 @@ function renderText(sentences) {
|
||||||
// ── VIEW SWITCHING ─────────────────────────────────────────────────
|
// ── VIEW SWITCHING ─────────────────────────────────────────────────
|
||||||
function setView(mode) {
|
function setView(mode) {
|
||||||
state.currentView = mode;
|
state.currentView = mode;
|
||||||
const textViewer = $('text-viewer');
|
|
||||||
const pdfViewer = $('pdf-viewer');
|
|
||||||
const contentArea = $('content-area');
|
const contentArea = $('content-area');
|
||||||
const btnText = $('btn-view-text');
|
|
||||||
const btnPdf = $('btn-view-pdf');
|
|
||||||
|
|
||||||
if (mode === 'pdf') {
|
if (mode === 'pdf') {
|
||||||
textViewer.style.display = 'none';
|
$('text-viewer').style.display = 'none';
|
||||||
pdfViewer.style.display = '';
|
$('pdf-viewer').style.display = 'flex';
|
||||||
contentArea.classList.add('pdf-mode');
|
contentArea.classList.add('pdf-mode');
|
||||||
btnPdf.classList.add('active');
|
$('btn-view-pdf').classList.add('active');
|
||||||
btnText.classList.remove('active');
|
$('btn-view-text').classList.remove('active');
|
||||||
$('doc-hint').textContent = 'Viewing original PDF · switch to Text to click sentences';
|
$('doc-hint').textContent = 'Viewing original PDF · switch to Text to click sentences';
|
||||||
} else {
|
} else {
|
||||||
textViewer.style.display = '';
|
$('text-viewer').style.display = '';
|
||||||
pdfViewer.style.display = 'none';
|
$('pdf-viewer').style.display = 'none';
|
||||||
contentArea.classList.remove('pdf-mode');
|
contentArea.classList.remove('pdf-mode');
|
||||||
btnText.classList.add('active');
|
$('btn-view-text').classList.add('active');
|
||||||
btnPdf.classList.remove('active');
|
$('btn-view-pdf').classList.remove('active');
|
||||||
$('doc-hint').textContent = 'Click any sentence to jump there · Space to play/pause · ← → to skip';
|
$('doc-hint').textContent = 'Click any sentence to jump there · Space to play/pause · ← → to skip';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
window.setView = setView;
|
window.setView = setView;
|
||||||
|
|
||||||
// ── PDF.js RENDERING ───────────────────────────────────────────────
|
// ── PDF NATIVE RENDERING ───────────────────────────────────────────
|
||||||
async function renderPDF(b64) {
|
function renderPDF(b64) {
|
||||||
if (typeof pdfjsLib === 'undefined') return;
|
// Revoke previous blob to free memory
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc =
|
if (pdfBlobUrl) { URL.revokeObjectURL(pdfBlobUrl); pdfBlobUrl = null; }
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
|
|
||||||
|
|
||||||
const pdfPages = $('pdf-pages');
|
|
||||||
pdfPages.innerHTML = '';
|
|
||||||
pageCanvases.length = 0;
|
|
||||||
|
|
||||||
const binary = atob(b64);
|
const binary = atob(b64);
|
||||||
const bytes = new Uint8Array(binary.length);
|
const bytes = new Uint8Array(binary.length);
|
||||||
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
||||||
|
const blob = new Blob([bytes], { type: 'application/pdf' });
|
||||||
|
pdfBlobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
pdfDoc = await pdfjsLib.getDocument({ data: bytes }).promise;
|
const pdfPages = $('pdf-pages');
|
||||||
|
pdfPages.innerHTML = '';
|
||||||
|
|
||||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
const iframe = document.createElement('iframe');
|
||||||
const page = await pdfDoc.getPage(i);
|
iframe.id = 'pdf-iframe';
|
||||||
const vp = page.getViewport({ scale: 1.5 });
|
iframe.src = pdfBlobUrl;
|
||||||
|
iframe.style.cssText = 'width:100%;height:100%;border:none;display:block;';
|
||||||
const wrap = document.createElement('div');
|
pdfPages.appendChild(iframe);
|
||||||
wrap.className = 'pdf-page-wrap';
|
|
||||||
wrap.dataset.page = i - 1;
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = vp.width;
|
|
||||||
canvas.height = vp.height;
|
|
||||||
canvas.style.width = Math.min(vp.width, 800) + 'px';
|
|
||||||
canvas.style.height = (Math.min(vp.width, 800) / vp.width * vp.height) + 'px';
|
|
||||||
|
|
||||||
await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
|
|
||||||
wrap.appendChild(canvas);
|
|
||||||
|
|
||||||
const lbl = document.createElement('div');
|
|
||||||
lbl.className = 'pdf-page-num';
|
|
||||||
lbl.textContent = `Page ${i}`;
|
|
||||||
pdfPages.appendChild(wrap);
|
|
||||||
pdfPages.appendChild(lbl);
|
|
||||||
pageCanvases.push(wrap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSentencePageMap(pageTexts, sentences) {
|
function buildSentencePageMap(pageTexts, sentences) {
|
||||||
|
|
@ -269,8 +243,12 @@ function buildSentencePageMap(pageTexts, sentences) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollPDFToPage(pageIdx) {
|
function scrollPDFToPage(pageIdx) {
|
||||||
if (pageIdx < 0 || pageIdx >= pageCanvases.length) return;
|
if (!pdfBlobUrl) return;
|
||||||
pageCanvases[pageIdx].scrollIntoView({ behavior: 'smooth', block: 'start' });
|
const iframe = $('pdf-iframe');
|
||||||
|
if (!iframe) return;
|
||||||
|
// #page= fragment navigates inside Chrome/Edge native PDF viewer
|
||||||
|
const target = pdfBlobUrl + '#page=' + (pageIdx + 1);
|
||||||
|
if (iframe.src !== target) iframe.src = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LOAD DOCUMENT ──────────────────────────────────────────────────
|
// ── LOAD DOCUMENT ──────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,6 @@
|
||||||
<p id="loading-text">Loading…</p>
|
<p id="loading-text">Loading…</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -201,8 +201,12 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
|
||||||
.content-area {
|
.content-area {
|
||||||
flex: 1; overflow-y: auto; padding: 44px 56px;
|
flex: 1; overflow-y: auto; padding: 44px 56px;
|
||||||
display: flex; flex-direction: column;
|
display: flex; flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.content-area.pdf-mode {
|
||||||
|
padding: 20px 20px 0;
|
||||||
|
overflow: hidden; /* iframe fills remaining height */
|
||||||
}
|
}
|
||||||
.content-area.pdf-mode { padding: 24px 24px 0; overflow: hidden; }
|
|
||||||
.document-header { margin-bottom: 28px; }
|
.document-header { margin-bottom: 28px; }
|
||||||
.doc-title-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px; }
|
.doc-title-row { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 6px; }
|
||||||
.document-title { font-size: 22px; font-weight: 700; flex: 1; }
|
.document-title { font-size: 22px; font-weight: 700; flex: 1; }
|
||||||
|
|
@ -281,22 +285,25 @@ textarea:focus, .text-field:focus { border-color: var(--accent); }
|
||||||
|
|
||||||
/* ── PDF VIEWER ──────────────────────────── */
|
/* ── PDF VIEWER ──────────────────────────── */
|
||||||
.pdf-viewer {
|
.pdf-viewer {
|
||||||
flex: 1; overflow-y: auto; padding: 24px; background: #1a1a1a;
|
flex: 1;
|
||||||
display: flex; flex-direction: column; align-items: center; gap: 16px;
|
min-height: 0; /* essential: lets flex child shrink below content size */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
#pdf-pages { display: flex; flex-direction: column; gap: 16px; align-items: center; width: 100%; }
|
#pdf-pages {
|
||||||
.pdf-page-wrap {
|
flex: 1;
|
||||||
position: relative; box-shadow: 0 4px 24px rgba(0,0,0,.5);
|
min-height: 0;
|
||||||
border-radius: 4px; overflow: hidden; max-width: 100%;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.pdf-page-wrap canvas { display: block; max-width: 100%; height: auto; }
|
#pdf-iframe {
|
||||||
.pdf-page-highlight {
|
flex: 1;
|
||||||
position: absolute; border-radius: 2px;
|
min-height: 0;
|
||||||
background: rgba(124,58,237,.35);
|
width: 100%;
|
||||||
pointer-events: none; transition: opacity .2s;
|
border: none;
|
||||||
}
|
display: block;
|
||||||
.pdf-page-num {
|
|
||||||
text-align: center; font-size: 11px; color: var(--text-d); margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── SETTINGS SIDEBAR ────────────────────── */
|
/* ── SETTINGS SIDEBAR ────────────────────── */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue