// PDF navigation: page turning, zoom, fit modes // Feature Registration Pattern implementation import { ReaderContext } from "../core/reader-context"; export function init(context: ReaderContext): void { let navState: PDFNavigationState | null = null; context.events.on( "reader:loaded", (detail: { container: HTMLElement; totalPages: number }) => { navState = { currentPage: 1, totalPages: detail.totalPages, currentScale: 1.0, fitMode: "fit-width", scrollContainer: detail.container.querySelector(".pdf-scroll-container") || detail.container, }; setupPDFKeyboardNav(context, navState); setupPDFScrollTracking(context, navState); }, ); context.events.on("pdf:navigate:to-page", (detail: { page: number }) => { if (navState) { goToPDFPage(navState, detail.page, context); } }); context.events.on("pdf:navigate:next", () => { if (navState) { nextPDFPage(navState, context); } }); context.events.on("pdf:navigate:previous", () => { if (navState) { previousPDFPage(navState, context); } }); context.events.on("pdf:zoom:set", (detail: { scale: number }) => { if (navState) { setPDFZoom(navState, detail.scale, context); } }); context.events.on("pdf:zoom:in", () => { if (navState) { zoomPDFIn(navState, context); } }); context.events.on("pdf:zoom:out", () => { if (navState) { zoomPDFOut(navState, context); } }); context.events.on( "pdf:fit:set", (detail: { mode: "fit-width" | "fit-page" | "fit-height" | "none" }) => { if (navState) { setPDFFitMode(navState, detail.mode, context); } }, ); context.events.on("reader:unload", () => { navState = null; }); } type PageFitMode = "fit-width" | "fit-page" | "fit-height" | "none"; interface PDFNavigationState { currentPage: number; totalPages: number; currentScale: number; fitMode: PageFitMode; scrollContainer: HTMLElement | null; } function goToPDFPage( state: PDFNavigationState, pageNumber: number, context: ReaderContext, ): void { if (pageNumber < 1 || pageNumber > state.totalPages) return; state.currentPage = pageNumber; scrollToPDFPage(state, pageNumber); context.events.emit("pdf:page-changed", { page: pageNumber }); } function nextPDFPage(state: PDFNavigationState, context: ReaderContext): void { if (state.currentPage < state.totalPages) { goToPDFPage(state, state.currentPage + 1, context); } } function previousPDFPage( state: PDFNavigationState, context: ReaderContext, ): void { if (state.currentPage > 1) { goToPDFPage(state, state.currentPage - 1, context); } } function scrollToPDFPage(state: PDFNavigationState, pageNumber: number): void { if (!state.scrollContainer) return; const pageElement = state.scrollContainer.querySelector( `[data-page-number="${pageNumber}"]`, ); if (pageElement) { pageElement.scrollIntoView({ behavior: "smooth", block: "start" }); } } function setPDFZoom( state: PDFNavigationState, scale: number, context: ReaderContext, ): void { state.currentScale = scale; state.fitMode = "none"; updatePDFZoom(state); context.events.emit("pdf:zoom-changed", { scale }); } function setPDFFitMode( state: PDFNavigationState, mode: PageFitMode, context: ReaderContext, ): void { state.fitMode = mode; updatePDFZoom(state); context.events.emit("pdf:fit-changed", { mode }); } function zoomPDFIn(state: PDFNavigationState, context: ReaderContext): void { setPDFZoom(state, state.currentScale * 1.2, context); } function zoomPDFOut(state: PDFNavigationState, context: ReaderContext): void { setPDFZoom(state, state.currentScale / 1.2, context); } function updatePDFZoom(state: PDFNavigationState): void { const event = new CustomEvent("pdf-update-zoom", { detail: { scale: state.currentScale, fitMode: state.fitMode, }, }); window.dispatchEvent(event); } function setupPDFKeyboardNav( context: ReaderContext, state: PDFNavigationState, ): void { document.addEventListener("keydown", (e) => { if (e.key === "ArrowRight" || e.key === "ArrowDown") { nextPDFPage(state, context); } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { previousPDFPage(state, context); } else if (e.key === "+" || e.key === "=") { zoomPDFIn(state, context); } else if (e.key === "-" || e.key === "_") { zoomPDFOut(state, context); } }); } function setupPDFScrollTracking( context: ReaderContext, state: PDFNavigationState, ): void { if (!state.scrollContainer) return; state.scrollContainer.addEventListener("scroll", () => { const currentPage = getCurrentPDFPage(state); if (currentPage !== state.currentPage) { state.currentPage = currentPage; context.events.emit("pdf:page-changed", { page: currentPage }); } }); } function getCurrentPDFPage(state: PDFNavigationState): number { if (!state.scrollContainer) return state.currentPage; const containerRect = state.scrollContainer.getBoundingClientRect(); const viewportMiddle = containerRect.top + containerRect.height / 2; for (let i = 1; i <= state.totalPages; i++) { const pageElement = state.scrollContainer.querySelector( `[data-page-number="${i}"]`, ); if (pageElement) { const rect = pageElement.getBoundingClientRect(); if (rect.top <= viewportMiddle && rect.bottom >= viewportMiddle) { return i; } } } return state.currentPage; }