| <!DOCTYPE html> |
| <html lang="vi"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>PDF Viewer</title> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| background: #2c3e50; |
| height: 100vh; |
| overflow: hidden; |
| } |
| |
| .container { |
| height: 100vh; |
| display: flex; |
| flex-direction: column; |
| background: white; |
| } |
| |
| |
| .menubar { |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| padding: 8px 15px; |
| display: flex; |
| align-items: center; |
| gap: 15px; |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); |
| flex-shrink: 0; |
| } |
| |
| .menubar h1 { |
| font-size: 1.1em; |
| color: white; |
| margin: 0; |
| font-weight: 600; |
| } |
| |
| .menubar-spacer { |
| flex: 1; |
| } |
| |
| .btn { |
| padding: 6px 14px; |
| background: rgba(255,255,255,0.2); |
| color: white; |
| border: 1px solid rgba(255,255,255,0.3); |
| border-radius: 5px; |
| cursor: pointer; |
| font-weight: 500; |
| font-size: 0.85em; |
| transition: all 0.2s ease; |
| white-space: nowrap; |
| } |
| |
| .btn:hover:not(:disabled) { |
| background: rgba(255,255,255,0.3); |
| transform: translateY(-1px); |
| } |
| |
| .btn:disabled { |
| opacity: 0.4; |
| cursor: not-allowed; |
| } |
| |
| .btn-primary { |
| background: white; |
| color: #667eea; |
| border-color: white; |
| font-weight: 600; |
| } |
| |
| .btn-primary:hover { |
| background: #f8f9fa; |
| } |
| |
| #fileInput { |
| display: none; |
| } |
| |
| .file-count { |
| padding: 6px 12px; |
| background: rgba(255,255,255,0.9); |
| color: #667eea; |
| border-radius: 15px; |
| font-weight: 600; |
| font-size: 0.85em; |
| } |
| |
| .page-info { |
| color: white; |
| font-size: 0.9em; |
| font-weight: 500; |
| } |
| |
| .zoom-controls { |
| display: flex; |
| gap: 8px; |
| align-items: center; |
| } |
| |
| .zoom-level { |
| color: white; |
| font-size: 0.85em; |
| min-width: 50px; |
| text-align: center; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| background: #f8f9fa; |
| padding: 0 10px; |
| overflow-x: auto; |
| border-bottom: 1px solid #dee2e6; |
| gap: 2px; |
| flex-shrink: 0; |
| } |
| |
| .tabs::-webkit-scrollbar { |
| height: 5px; |
| } |
| |
| .tabs::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| } |
| |
| .tabs::-webkit-scrollbar-thumb { |
| background: #667eea; |
| border-radius: 3px; |
| } |
| |
| .tab { |
| padding: 8px 12px; |
| cursor: pointer; |
| border: none; |
| background: transparent; |
| color: #6c757d; |
| font-weight: 500; |
| font-size: 0.8em; |
| transition: all 0.2s ease; |
| white-space: nowrap; |
| position: relative; |
| border-radius: 6px 6px 0 0; |
| margin-top: 2px; |
| display: flex; |
| align-items: center; |
| gap: 5px; |
| max-width: 200px; |
| overflow: hidden; |
| text-overflow: ellipsis; |
| } |
| |
| .tab:hover { |
| background: rgba(102, 126, 234, 0.1); |
| color: #667eea; |
| } |
| |
| .tab.active { |
| background: white; |
| color: #667eea; |
| } |
| |
| .tab.active::after { |
| content: ''; |
| position: absolute; |
| bottom: -1px; |
| left: 0; |
| right: 0; |
| height: 2px; |
| background: #667eea; |
| } |
| |
| .close-tab { |
| margin-left: auto; |
| color: #dc3545; |
| font-weight: bold; |
| font-size: 1em; |
| opacity: 0.6; |
| padding: 0 4px; |
| } |
| |
| .close-tab:hover { |
| opacity: 1; |
| } |
| |
| |
| .content { |
| flex: 1; |
| overflow: hidden; |
| display: flex; |
| flex-direction: column; |
| background: #525252; |
| } |
| |
| .tab-content { |
| display: none; |
| flex: 1; |
| overflow: hidden; |
| } |
| |
| .tab-content.active { |
| display: flex; |
| flex-direction: column; |
| } |
| |
| .pdf-viewer-container { |
| flex: 1; |
| overflow: auto; |
| background: #525252; |
| padding: 20px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 20px; |
| } |
| |
| .pdf-viewer-container::-webkit-scrollbar { |
| width: 10px; |
| } |
| |
| .pdf-viewer-container::-webkit-scrollbar-track { |
| background: #3a3a3a; |
| } |
| |
| .pdf-viewer-container::-webkit-scrollbar-thumb { |
| background: #667eea; |
| border-radius: 5px; |
| } |
| |
| .pdf-viewer-container::-webkit-scrollbar-thumb:hover { |
| background: #5a6fd8; |
| } |
| |
| .pdf-page { |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); |
| background: white; |
| transition: opacity 0.3s ease; |
| } |
| |
| .pdf-page.loading { |
| opacity: 0.5; |
| } |
| |
| .page-placeholder { |
| background: #ddd; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: #666; |
| font-size: 1.2em; |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); |
| } |
| |
| .empty-state { |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| color: #aaa; |
| padding: 50px; |
| pointer-events: none; |
| } |
| |
| .content:not(:has(.tab-content.active)) .empty-state { |
| display: flex; |
| } |
| |
| .content:has(.tab-content.active) .empty-state { |
| display: none; |
| } |
| |
| .empty-state svg { |
| width: 100px; |
| height: 100px; |
| margin-bottom: 20px; |
| opacity: 0.5; |
| } |
| |
| .empty-state h2 { |
| font-size: 1.5em; |
| margin-bottom: 10px; |
| } |
| |
| .loading { |
| text-align: center; |
| padding: 50px; |
| color: white; |
| font-size: 1.2em; |
| font-weight: bold; |
| } |
| |
| |
| .drop-overlay { |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: rgba(102, 126, 234, 0.95); |
| display: none; |
| align-items: center; |
| justify-content: center; |
| z-index: 9999; |
| } |
| |
| .drop-overlay.active { |
| display: flex; |
| } |
| |
| .drop-message { |
| color: white; |
| font-size: 2em; |
| font-weight: bold; |
| text-align: center; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="drop-overlay" id="dropOverlay"> |
| <div class="drop-message">📄 Thả file PDF vào đây</div> |
| </div> |
|
|
| <div class="container"> |
| |
| <div class="menubar"> |
| <h1>📄 PDF Viewer</h1> |
| |
| <label for="fileInput" class="btn btn-primary"> |
| 📁 Mở file |
| </label> |
| <input type="file" id="fileInput" accept=".pdf" multiple> |
| |
| <div class="file-count" id="fileCount" style="display: none;">0 file</div> |
| |
| <div class="menubar-spacer"></div> |
|
|
| <div class="zoom-controls" id="zoomControls" style="display: none;"> |
| <button class="btn" id="zoomOut">-</button> |
| <span class="zoom-level" id="zoomLevel">100%</span> |
| <button class="btn" id="zoomIn">+</button> |
| </div> |
| |
| <div class="page-info" id="pageInfo" style="display: none;"> |
| Trang <span id="currentPage">1</span> / <span id="totalPages">1</span> |
| </div> |
| |
| <button class="btn" id="prevPage" style="display: none;">◀</button> |
| <button class="btn" id="nextPage" style="display: none;">▶</button> |
| </div> |
|
|
| |
| <div class="tabs" id="tabs"></div> |
|
|
| |
| <div class="content" id="content"> |
| <div class="empty-state"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| </svg> |
| <h2>Chưa có PDF nào</h2> |
| <p>Nhấn "Mở file" hoặc kéo thả file PDF vào đây</p> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; |
| |
| const fileInput = document.getElementById('fileInput'); |
| const tabsContainer = document.getElementById('tabs'); |
| const contentContainer = document.getElementById('content'); |
| const fileCountEl = document.getElementById('fileCount'); |
| const pageInfo = document.getElementById('pageInfo'); |
| const currentPageEl = document.getElementById('currentPage'); |
| const totalPagesEl = document.getElementById('totalPages'); |
| const prevPageBtn = document.getElementById('prevPage'); |
| const nextPageBtn = document.getElementById('nextPage'); |
| const dropOverlay = document.getElementById('dropOverlay'); |
| const zoomControls = document.getElementById('zoomControls'); |
| const zoomLevelEl = document.getElementById('zoomLevel'); |
| const zoomInBtn = document.getElementById('zoomIn'); |
| const zoomOutBtn = document.getElementById('zoomOut'); |
| |
| let pdfFiles = []; |
| let pdfDocs = []; |
| let currentPages = []; |
| let zoomLevels = []; |
| let scrollPositions = []; |
| let activeIndex = -1; |
| let renderQueue = []; |
| let isRendering = false; |
| |
| |
| function loadSavedState() { |
| try { |
| const savedState = localStorage.getItem('pdfViewerState'); |
| if (savedState) { |
| const state = JSON.parse(savedState); |
| currentPages = state.currentPages || []; |
| zoomLevels = state.zoomLevels || []; |
| scrollPositions = state.scrollPositions || []; |
| activeIndex = state.activeIndex || -1; |
| } |
| } catch (e) { |
| console.log('No saved state found'); |
| } |
| } |
| |
| |
| function saveState() { |
| try { |
| const state = { |
| currentPages, |
| zoomLevels, |
| scrollPositions, |
| activeIndex, |
| fileNames: pdfFiles.map(f => f.name) |
| }; |
| localStorage.setItem('pdfViewerState', JSON.stringify(state)); |
| } catch (e) { |
| console.error('Failed to save state:', e); |
| } |
| } |
| |
| |
| setInterval(saveState, 2000); |
| |
| |
| window.addEventListener('beforeunload', saveState); |
| |
| |
| loadSavedState(); |
| |
| |
| fileInput.addEventListener('change', (e) => { |
| handleFiles(e.target.files); |
| }); |
| |
| |
| let dragCounter = 0; |
| |
| document.body.addEventListener('dragenter', (e) => { |
| e.preventDefault(); |
| dragCounter++; |
| dropOverlay.classList.add('active'); |
| }); |
| |
| document.body.addEventListener('dragleave', (e) => { |
| e.preventDefault(); |
| dragCounter--; |
| if (dragCounter === 0) { |
| dropOverlay.classList.remove('active'); |
| } |
| }); |
| |
| document.body.addEventListener('dragover', (e) => { |
| e.preventDefault(); |
| }); |
| |
| document.body.addEventListener('drop', (e) => { |
| e.preventDefault(); |
| dragCounter = 0; |
| dropOverlay.classList.remove('active'); |
| const files = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf'); |
| if (files.length > 0) { |
| handleFiles(files); |
| } |
| }); |
| |
| |
| prevPageBtn.addEventListener('click', () => scrollToPrevPage()); |
| nextPageBtn.addEventListener('click', () => scrollToNextPage()); |
| |
| |
| zoomInBtn.addEventListener('click', () => changeZoom(0.25)); |
| zoomOutBtn.addEventListener('click', () => changeZoom(-0.25)); |
| |
| function handleFiles(files) { |
| if (files.length === 0) return; |
| |
| const newFiles = Array.from(files); |
| const startIndex = pdfFiles.length; |
| pdfFiles = [...pdfFiles, ...newFiles]; |
| |
| fileCountEl.style.display = 'inline-block'; |
| fileCountEl.textContent = `${pdfFiles.length} file`; |
| |
| newFiles.forEach((file, idx) => { |
| const fileIndex = startIndex + idx; |
| |
| if (!zoomLevels[fileIndex]) { |
| zoomLevels[fileIndex] = 1.5; |
| } |
| if (!currentPages[fileIndex]) { |
| currentPages[fileIndex] = 1; |
| } |
| if (!scrollPositions[fileIndex]) { |
| scrollPositions[fileIndex] = 0; |
| } |
| addTab(file, fileIndex); |
| }); |
| |
| if (activeIndex === -1) { |
| switchTab(startIndex); |
| } |
| |
| saveState(); |
| } |
| |
| function addTab(file, index) { |
| const tab = document.createElement('button'); |
| tab.className = 'tab'; |
| tab.innerHTML = ` |
| <span>📄</span> |
| <span>${file.name}</span> |
| <span class="close-tab">×</span> |
| `; |
| |
| tab.querySelector('.close-tab').addEventListener('click', (e) => { |
| e.stopPropagation(); |
| closeTab(index); |
| }); |
| |
| tab.addEventListener('click', () => switchTab(index)); |
| tab.dataset.index = index; |
| tab.title = file.name; |
| tabsContainer.appendChild(tab); |
| |
| const tabContent = document.createElement('div'); |
| tabContent.className = 'tab-content'; |
| tabContent.dataset.index = index; |
| tabContent.innerHTML = '<div class="loading">⏳ Đang tải PDF...</div>'; |
| |
| contentContainer.appendChild(tabContent); |
| |
| loadPDF(file, index, tabContent); |
| } |
| |
| async function loadPDF(file, index, container) { |
| try { |
| const arrayBuffer = await file.arrayBuffer(); |
| const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise; |
| |
| pdfDocs[index] = pdf; |
| currentPages[index] = 1; |
| |
| const viewerContainer = document.createElement('div'); |
| viewerContainer.className = 'pdf-viewer-container'; |
| viewerContainer.dataset.index = index; |
| |
| |
| for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { |
| const page = await pdf.getPage(pageNum); |
| const viewport = page.getViewport({scale: zoomLevels[index]}); |
| |
| const canvas = document.createElement('canvas'); |
| canvas.className = 'pdf-page'; |
| canvas.height = viewport.height; |
| canvas.width = viewport.width; |
| canvas.dataset.page = pageNum; |
| canvas.dataset.rendered = 'false'; |
| |
| viewerContainer.appendChild(canvas); |
| } |
| |
| container.innerHTML = ''; |
| container.appendChild(viewerContainer); |
| |
| |
| const emptyState = contentContainer.querySelector('.empty-state'); |
| if (emptyState) { |
| emptyState.style.display = 'none'; |
| } |
| |
| |
| let scrollTimeout; |
| viewerContainer.addEventListener('scroll', () => { |
| |
| scrollPositions[index] = viewerContainer.scrollTop; |
| |
| clearTimeout(scrollTimeout); |
| scrollTimeout = setTimeout(() => { |
| updateCurrentPage(index, viewerContainer); |
| renderVisiblePages(index, viewerContainer); |
| saveState(); |
| }, 100); |
| }); |
| |
| |
| if (scrollPositions[index]) { |
| setTimeout(() => { |
| viewerContainer.scrollTop = scrollPositions[index]; |
| }, 100); |
| } |
| |
| |
| renderVisiblePages(index, viewerContainer); |
| |
| } catch (error) { |
| container.innerHTML = ` |
| <div style="text-align: center; padding: 50px; color: #dc3545;"> |
| <h2>❌ Lỗi khi tải PDF</h2> |
| <p>${error.message}</p> |
| </div> |
| `; |
| } |
| } |
| |
| async function renderVisiblePages(index, container) { |
| const pages = container.querySelectorAll('.pdf-page'); |
| const containerRect = container.getBoundingClientRect(); |
| |
| pages.forEach((canvas, idx) => { |
| const pageNum = parseInt(canvas.dataset.page); |
| const rect = canvas.getBoundingClientRect(); |
| |
| |
| const isVisible = rect.top < containerRect.bottom + 500 && |
| rect.bottom > containerRect.top - 500; |
| |
| if (isVisible && canvas.dataset.rendered === 'false') { |
| renderQueue.push({index, pageNum, canvas}); |
| } |
| }); |
| |
| processRenderQueue(); |
| } |
| |
| async function processRenderQueue() { |
| if (isRendering || renderQueue.length === 0) return; |
| |
| isRendering = true; |
| |
| while (renderQueue.length > 0) { |
| const {index, pageNum, canvas} = renderQueue.shift(); |
| |
| try { |
| const pdf = pdfDocs[index]; |
| const page = await pdf.getPage(pageNum); |
| const viewport = page.getViewport({scale: zoomLevels[index]}); |
| |
| canvas.height = viewport.height; |
| canvas.width = viewport.width; |
| |
| const context = canvas.getContext('2d'); |
| await page.render({ |
| canvasContext: context, |
| viewport: viewport |
| }).promise; |
| |
| canvas.dataset.rendered = 'true'; |
| } catch (error) { |
| console.error('Render error:', error); |
| } |
| } |
| |
| isRendering = false; |
| } |
| |
| function updateCurrentPage(index, container) { |
| if (index !== activeIndex) return; |
| |
| const pages = container.querySelectorAll('.pdf-page'); |
| const containerRect = container.getBoundingClientRect(); |
| const containerCenter = containerRect.top + containerRect.height / 2; |
| |
| let closestPage = 1; |
| let closestDistance = Infinity; |
| |
| pages.forEach((page, idx) => { |
| const pageRect = page.getBoundingClientRect(); |
| const pageCenter = pageRect.top + pageRect.height / 2; |
| const distance = Math.abs(pageCenter - containerCenter); |
| |
| if (distance < closestDistance) { |
| closestDistance = distance; |
| closestPage = idx + 1; |
| } |
| }); |
| |
| currentPages[index] = closestPage; |
| currentPageEl.textContent = closestPage; |
| saveState(); |
| } |
| |
| function scrollToPrevPage() { |
| const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`); |
| if (!container) return; |
| |
| const targetPage = Math.max(1, currentPages[activeIndex] - 1); |
| scrollToPage(container, targetPage); |
| } |
| |
| function scrollToNextPage() { |
| const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`); |
| if (!container) return; |
| |
| const pdf = pdfDocs[activeIndex]; |
| const targetPage = Math.min(pdf.numPages, currentPages[activeIndex] + 1); |
| scrollToPage(container, targetPage); |
| } |
| |
| function scrollToPage(container, pageNum) { |
| const page = container.querySelector(`[data-page="${pageNum}"]`); |
| if (page) { |
| page.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| } |
| } |
| |
| function changeZoom(delta) { |
| if (activeIndex === -1) return; |
| |
| zoomLevels[activeIndex] = Math.max(0.5, Math.min(3, zoomLevels[activeIndex] + delta)); |
| zoomLevelEl.textContent = Math.round(zoomLevels[activeIndex] * 100) + '%'; |
| |
| saveState(); |
| |
| |
| const tabContent = contentContainer.querySelector(`.tab-content[data-index="${activeIndex}"]`); |
| const file = pdfFiles[activeIndex]; |
| |
| if (tabContent && file) { |
| const currentScroll = tabContent.querySelector('.pdf-viewer-container')?.scrollTop || 0; |
| loadPDF(file, activeIndex, tabContent).then(() => { |
| const container = tabContent.querySelector('.pdf-viewer-container'); |
| if (container) { |
| container.scrollTop = currentScroll * (zoomLevels[activeIndex] / (zoomLevels[activeIndex] - delta)); |
| scrollPositions[activeIndex] = container.scrollTop; |
| } |
| }); |
| } |
| } |
| |
| function switchTab(index) { |
| activeIndex = index; |
| |
| const tabs = tabsContainer.querySelectorAll('.tab'); |
| tabs.forEach((tab) => { |
| tab.classList.toggle('active', parseInt(tab.dataset.index) === index); |
| }); |
| |
| const contents = contentContainer.querySelectorAll('.tab-content'); |
| contents.forEach((content) => { |
| content.classList.toggle('active', parseInt(content.dataset.index) === index); |
| }); |
| |
| |
| if (pdfDocs[index]) { |
| pageInfo.style.display = 'block'; |
| prevPageBtn.style.display = 'inline-block'; |
| nextPageBtn.style.display = 'inline-block'; |
| zoomControls.style.display = 'flex'; |
| totalPagesEl.textContent = pdfDocs[index].numPages; |
| currentPageEl.textContent = currentPages[index] || 1; |
| zoomLevelEl.textContent = Math.round(zoomLevels[index] * 100) + '%'; |
| |
| |
| const container = contentContainer.querySelector(`.tab-content.active .pdf-viewer-container`); |
| if (container) { |
| setTimeout(() => renderVisiblePages(index, container), 100); |
| } |
| } |
| |
| saveState(); |
| } |
| |
| function closeTab(index) { |
| pdfFiles.splice(index, 1); |
| pdfDocs.splice(index, 1); |
| currentPages.splice(index, 1); |
| zoomLevels.splice(index, 1); |
| scrollPositions.splice(index, 1); |
| |
| const tab = tabsContainer.querySelector(`[data-index="${index}"]`); |
| if (tab) tab.remove(); |
| |
| const content = contentContainer.querySelector(`.tab-content[data-index="${index}"]`); |
| if (content) content.remove(); |
| |
| |
| tabsContainer.querySelectorAll('.tab').forEach((tab) => { |
| const idx = parseInt(tab.dataset.index); |
| if (idx > index) { |
| tab.dataset.index = idx - 1; |
| } |
| }); |
| |
| contentContainer.querySelectorAll('.tab-content').forEach((content) => { |
| const idx = parseInt(content.dataset.index); |
| if (idx > index) { |
| content.dataset.index = idx - 1; |
| } |
| }); |
| |
| if (pdfFiles.length === 0) { |
| fileCountEl.style.display = 'none'; |
| pageInfo.style.display = 'none'; |
| prevPageBtn.style.display = 'none'; |
| nextPageBtn.style.display = 'none'; |
| zoomControls.style.display = 'none'; |
| activeIndex = -1; |
| |
| |
| tabsContainer.innerHTML = ''; |
| contentContainer.innerHTML = ` |
| <div class="empty-state"> |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <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> |
| </svg> |
| <h2>Chưa có PDF nào</h2> |
| <p>Nhấn "Mở file" hoặc kéo thả file PDF vào đây</p> |
| </div> |
| `; |
| localStorage.removeItem('pdfViewerState'); |
| } else { |
| fileCountEl.textContent = `${pdfFiles.length} file`; |
| if (activeIndex >= pdfFiles.length || activeIndex === index) { |
| switchTab(Math.max(0, Math.min(index, pdfFiles.length - 1))); |
| } |
| } |
| |
| saveState(); |
| } |
| |
| |
| document.addEventListener('keydown', (e) => { |
| if (activeIndex === -1) return; |
| |
| if (e.key === 'ArrowUp' || e.key === 'PageUp') { |
| e.preventDefault(); |
| scrollToPrevPage(); |
| } else if (e.key === 'ArrowDown' || e.key === 'PageDown') { |
| e.preventDefault(); |
| scrollToNextPage(); |
| } else if (e.key === '+' || e.key === '=') { |
| e.preventDefault(); |
| changeZoom(0.25); |
| } else if (e.key === '-') { |
| e.preventDefault(); |
| changeZoom(-0.25); |
| } |
| }); |
| </script> |
| </body> |
| </html> |