|
|
<!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> |