- 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
192 lines
4.9 KiB
TypeScript
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;
|
|
}
|
|
`;
|
|
}
|