Add comprehensive PDF reader enhancements

- pdf-page-sizes.ts: Dynamic page sizing and layout detection
- pdf-rotation.ts: Page rotation with orientation detection
- pdf-minimap.ts: Thumbnail minimap for navigation
- pdf-dual-page.ts: Dual page spread support
- pdf-links.ts: Clickable link handling
- pdf-clipbooard.ts: Copy to clipboard functionality
- pdf-bookmarks.ts: Bookmark management
- pdf-outline.ts: Document outline/TOC integration
- pdf-text-selection.ts: Text selection and highlighting
- page-cache.ts: PDF page caching system
- pdf-search.ts: Full-text search within PDF
- pdf-navigation.ts: Navigation controls and history
- annotation-layer.ts: PDF annotation rendering
- text-layer-renderer.ts: Text layer overlay rendering
- pdfjs-wrapper.ts: PDF.js wrapper with utilities
This commit is contained in:
2026-04-04 01:00:49 -04:00
parent d60469a757
commit c94679ec1e
15 changed files with 2121 additions and 0 deletions
+216
View File
@@ -0,0 +1,216 @@
// PDF navigation: page turning, zoom, fit modes
// Procedural style: Functions, not classes
type PageFitMode = "fit-width" | "fit-page" | "fit-height" | "none";
interface PDFNavigationState {
currentPage: number;
totalPages: number;
currentScale: number;
fitMode: PageFitMode;
scrollContainer: HTMLElement | null;
}
let navState: PDFNavigationState = {
currentPage: 1,
totalPages: 0,
currentScale: 1.0,
fitMode: "fit-width",
scrollContainer: null,
};
// ============================================================
// Initialization
// ============================================================
export function initializePDFNavigation(
container: HTMLElement,
onPageChange: (pageNumber: number) => void,
onZoomChange: (scale: number) => void,
): void {
navState.scrollContainer =
container.querySelector(".pdf-scroll-container") || container;
setupPDFKeyboardNav(onPageChange);
setupPDFScrollTracking(onPageChange);
}
export function setPDFTotalPages(totalPages: number): void {
navState.totalPages = totalPages;
}
// ============================================================
// Page Navigation
// ============================================================
export function goToPDFPage(pageNumber: number): void {
if (pageNumber < 1 || pageNumber > navState.totalPages) return;
navState.currentPage = pageNumber;
const callback = (window as any).pdfOnPageChange;
if (callback) callback(pageNumber);
scrollToPDFPage(pageNumber);
}
export function nextPDFPage(): void {
if (navState.currentPage < navState.totalPages) {
goToPDFPage(navState.currentPage + 1);
}
}
export function previousPDFPage(): void {
if (navState.currentPage > 1) {
goToPDFPage(navState.currentPage - 1);
}
}
function scrollToPDFPage(pageNumber: number): void {
if (!navState.scrollContainer) return;
const pageElement = navState.scrollContainer.querySelector(
`[data-page-number="${pageNumber}"]`,
);
if (pageElement) {
pageElement.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
// ============================================================
// Zoom Controls
// ============================================================
export function setPDFZoom(scale: number): void {
navState.currentScale = scale;
navState.fitMode = "none";
const callback = (window as any).pdfOnZoomChange;
if (callback) callback(scale);
updatePDFZoom();
}
export function setPDFFitMode(mode: PageFitMode): void {
navState.fitMode = mode;
updatePDFZoom();
}
export function zoomPDFIn(): void {
setPDFZoom(navState.currentScale * 1.2);
}
export function zoomPDFOut(): void {
setPDFZoom(navState.currentScale / 1.2);
}
function updatePDFZoom(): void {
if (!navState.scrollContainer) return;
const pages = navState.scrollContainer.querySelectorAll(
".pdf-page-container",
);
pages.forEach((page: Element) => {
(page as HTMLElement).style.transform = `scale(${navState.currentScale})`;
(page as HTMLElement).style.transformOrigin = "top center";
});
}
// ============================================================
// Keyboard Navigation
// ============================================================
function setupPDFKeyboardNav(onPageChange: (pageNumber: number) => void): void {
document.addEventListener("keydown", handlePDFKeyDown);
}
function handlePDFKeyDown(e: KeyboardEvent): void {
switch (e.key) {
case "ArrowRight":
case "ArrowDown":
e.preventDefault();
nextPDFPage();
break;
case "ArrowLeft":
case "ArrowUp":
e.preventDefault();
previousPDFPage();
break;
case "Home":
e.preventDefault();
goToPDFPage(1);
break;
case "End":
e.preventDefault();
goToPDFPage(navState.totalPages);
break;
}
}
// ============================================================
// Scroll Tracking
// ============================================================
function setupPDFScrollTracking(
onPageChange: (pageNumber: number) => void,
): void {
if (!navState.scrollContainer) return;
let scrollTimeout: NodeJS.Timeout;
navState.scrollContainer.addEventListener("scroll", () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
updateCurrentPageFromScroll(onPageChange);
}, 100);
});
}
function updateCurrentPageFromScroll(
onPageChange: (pageNumber: number) => void,
): void {
if (!navState.scrollContainer) return;
const scrollTop = navState.scrollContainer.scrollTop;
const containerHeight = navState.scrollContainer.clientHeight;
const pages = navState.scrollContainer.querySelectorAll("[data-page-number]");
let maxVisibility = 0;
let mostVisiblePage = navState.currentPage;
pages.forEach((page) => {
const element = page as HTMLElement;
const pageTop = element.offsetTop;
const pageBottom = pageTop + element.offsetHeight;
const visibleTop = Math.max(scrollTop, pageTop);
const visibleBottom = Math.min(scrollTop + containerHeight, pageBottom);
const visibleHeight = Math.max(0, visibleBottom - visibleTop);
if (visibleHeight > maxVisibility) {
maxVisibility = visibleHeight;
mostVisiblePage = parseInt(element.dataset.pageNumber || "1");
}
});
if (mostVisiblePage !== navState.currentPage) {
navState.currentPage = mostVisiblePage;
onPageChange(mostVisiblePage);
}
}
// ============================================================
// Getters
// ============================================================
export function getCurrentPDFPage(): number {
return navState.currentPage;
}
export function getTotalPDFPages(): number {
return navState.totalPages;
}
export function getPDFScale(): number {
return navState.currentScale;
}