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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user