Files
bookhoard/web/src/reader/pdf/pdf-minimap.ts
T
john-okeefe c94679ec1e 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
2026-04-04 01:00:49 -04:00

192 lines
4.9 KiB
TypeScript

// Mini-map navigation for PDF pages
// Procedural implementation (no OOP)
interface PDFMiniMapState {
miniMap: HTMLElement;
currentPage: number;
totalPages: number;
thumbnails: Map<number, HTMLCanvasElement>;
onPageNavigate: (pageNumber: number) => void;
}
function createPDFMiniMap(
container: HTMLElement,
onPageNavigate: (pageNumber: number) => void,
): PDFMiniMapState {
const miniMap = createMiniMapElement(container);
container.appendChild(miniMap);
return {
miniMap,
currentPage: 1,
totalPages: 0,
thumbnails: new Map(),
onPageNavigate,
};
}
function createMiniMapElement(container: HTMLElement): HTMLElement {
const miniMap = document.createElement("div");
miniMap.className = "pdf-minimap";
miniMap.innerHTML = `
<div class="pdf-minimap-header">Pages</div>
<div class="pdf-minimap-thumbnails"></div>
<div class="pdf-minimap-indicator"></div>
`;
const style = document.createElement("style");
style.textContent = getMiniMapStyles();
miniMap.appendChild(style);
return miniMap;
}
async function initializePDFMiniMap(
state: PDFMiniMapState,
totalPages: number,
renderThumbnail: (page: number) => Promise<HTMLCanvasElement>,
): Promise<PDFMiniMapState> {
const newState = { ...state, totalPages };
await generateMiniMapThumbnails(newState, renderThumbnail);
setupMiniMapEventListeners(newState);
return newState;
}
async function generateMiniMapThumbnails(
state: PDFMiniMapState,
renderThumbnail: (page: number) => Promise<HTMLCanvasElement>,
): Promise<void> {
const container = state.miniMap.querySelector(
".pdf-minimap-thumbnails",
) as HTMLElement;
container.innerHTML = "";
for (let page = 1; page <= state.totalPages; page++) {
try {
const thumbnail = await renderThumbnail(page);
thumbnail.className = "pdf-minimap-thumbnail";
thumbnail.dataset.pageNumber = page.toString();
thumbnail.style.width = "80px";
thumbnail.style.height = "auto";
thumbnail.style.cursor = "pointer";
thumbnail.style.marginBottom = "4px";
container.appendChild(thumbnail);
state.thumbnails.set(page, thumbnail);
} catch (error) {
console.error(`Failed to generate thumbnail for page ${page}:`, error);
}
}
}
function setupMiniMapEventListeners(state: PDFMiniMapState): void {
const container = state.miniMap.querySelector(".pdf-minimap-thumbnails");
container?.addEventListener("click", (e) => {
const thumbnail = (e.target as HTMLElement).closest(
".pdf-minimap-thumbnail",
) as HTMLElement;
if (thumbnail) {
const pageNumber = parseInt(thumbnail.dataset.pageNumber || "1");
state.onPageNavigate(pageNumber);
}
});
}
function updateMiniMapCurrentPage(
state: PDFMiniMapState,
pageNumber: number,
): PDFMiniMapState {
const indicator = state.miniMap.querySelector(
".pdf-minimap-indicator",
) as HTMLElement;
const thumbnail = state.thumbnails.get(pageNumber);
if (thumbnail && indicator) {
const rect = thumbnail.getBoundingClientRect();
indicator.style.top = `${thumbnail.offsetTop}px`;
indicator.style.height = `${rect.height}px`;
}
state.thumbnails.forEach((thumb, page) => {
if (page === pageNumber) {
thumb.style.outline = "2px solid var(--accent)";
thumb.style.opacity = "1";
} else {
thumb.style.outline = "none";
thumb.style.opacity = "0.7";
}
});
return { ...state, currentPage: pageNumber };
}
function showMiniMap(state: PDFMiniMapState): void {
state.miniMap.style.display = "block";
}
function hideMiniMap(state: PDFMiniMapState): void {
state.miniMap.style.display = "none";
}
function toggleMiniMap(state: PDFMiniMapState): void {
const isVisible = state.miniMap.style.display !== "none";
state.miniMap.style.display = isVisible ? "none" : "block";
}
function getMiniMapStyles(): string {
return `
.pdf-minimap {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 100px;
max-height: 80vh;
background: var(--bg-primary);
border: 1px solid var(--text-secondary);
border-radius: 8px;
padding: 8px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.pdf-minimap-header {
font-size: 12px;
font-weight: bold;
text-align: center;
margin-bottom: 8px;
color: var(--text-primary);
}
.pdf-minimap-thumbnails {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.pdf-minimap-thumbnail {
transition: outline 0.2s, opacity 0.2s;
border-radius: 2px;
}
.pdf-minimap-thumbnail:hover {
opacity: 1 !important;
outline: 1px solid var(--text-secondary) !important;
}
.pdf-minimap-indicator {
position: absolute;
left: 0;
right: 0;
border-left: 3px solid var(--accent);
pointer-events: none;
transition: top 0.3s ease-out;
}
`;
}