Files
bookhoard/web/src/reader/pdf/annotation-layer.ts
T
john-okeefe 43eee2170d style(reader): apply code formatting to existing format files
Run code formatting (prettier/eslint) on existing reader format files that
were not migrated to the new structure. This ensures consistent code style
across the codebase.

## Changes

### Formatting Applied
- Added newlines at end of files
- Fixed line length and indentation
- Updated import statements for consistency
- Applied code style rules uniformly

### Files Affected
- **Comic**: background-color, chapter-markers, page-order, panel-gap
- **Ebook**: font-loader, search, view-modes
- **Manga**: vertical-scroll-mode
- **PDF**: annotation-layer, pdf-navigation, pdf-text-selection

## Purpose

These files will be removed in subsequent commits as they are replaced by
the new modular structure. The formatting ensures consistency during the
transition period and maintains code quality standards.

Note: These are non-functional formatting changes only. No logic or behavior
was modified in this commit.
2026-04-09 14:54:16 -04:00

124 lines
3.2 KiB
TypeScript

// Annotation layer for rendering highlights and notes on PDFs
// Feature Registration Pattern implementation
import { ReaderContext } from "../core/reader-context";
export function init(context: ReaderContext): void {
const highlights = new Map<string, HTMLElement>();
context.events.on(
"pdf:highlights:render",
(detail: { container: HTMLElement; highlights: any[] }) => {
clearPDFHighlights(detail.container);
for (const highlight of detail.highlights) {
renderSinglePDFHighlight(detail.container, highlight, highlights);
}
},
);
context.events.on(
"pdf:highlights:clear",
(detail: { container: HTMLElement }) => {
clearPDFHighlights(detail.container);
},
);
context.events.on(
"pdf:highlight:remove",
(detail: { highlightId: string }) => {
removePDFHighlight(detail.highlightId, highlights);
},
);
context.events.on("reader:unload", () => {
highlights.forEach((element) => element.remove());
highlights.clear();
});
}
interface PDFHighlight {
id: string;
pageNumber: number;
rects: DOMRect[];
text: string;
color: string;
noteId?: string;
}
function renderSinglePDFHighlight(
container: HTMLElement,
highlight: PDFHighlight,
highlights: Map<string, HTMLElement>,
): void {
const overlay = document.createElement("div");
overlay.className = "pdf-highlight-annotation";
overlay.dataset.highlightId = highlight.id;
overlay.style.backgroundColor = parseColor(highlight.color);
for (const rect of highlight.rects) {
const rectDiv = document.createElement("div");
rectDiv.className = "pdf-highlight-rect";
rectDiv.style.left = `${rect.left}px`;
rectDiv.style.top = `${rect.top}px`;
rectDiv.style.width = `${rect.width}px`;
rectDiv.style.height = `${rect.height}px`;
overlay.appendChild(rectDiv);
}
if (highlight.noteId) {
overlay.style.cursor = "pointer";
overlay.addEventListener("click", () => {
showNotePopup(highlight);
});
}
overlay.addEventListener("mouseenter", () => {
overlay.style.opacity = "0.8";
});
overlay.addEventListener("mouseleave", () => {
overlay.style.opacity = "0.5";
});
container.appendChild(overlay);
highlights.set(highlight.id, overlay);
}
function parseColor(color: string): string {
if (color.startsWith("#")) {
const hex = color.slice(1);
const r = parseInt(hex.slice(0, 2), 16);
const g = parseInt(hex.slice(2, 4), 16);
const b = parseInt(hex.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, 0.4)`;
}
return color;
}
function showNotePopup(highlight: PDFHighlight): void {
console.log("Show note for highlight:", highlight.id);
const event = new CustomEvent("pdf:note-show", {
detail: { highlightId: highlight.id },
});
window.dispatchEvent(event);
}
export function clearPDFHighlights(container: HTMLElement): void {
const highlights = container.querySelectorAll(".pdf-highlight-annotation");
Array.from(highlights).forEach((element) => element.remove());
}
export function removePDFHighlight(
highlightId: string,
highlights: Map<string, HTMLElement>,
): void {
const element = highlights.get(highlightId);
if (element) {
element.remove();
highlights.delete(highlightId);
}
}