// 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(); 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, ): 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, ): void { const element = highlights.get(highlightId); if (element) { element.remove(); highlights.delete(highlightId); } }