From 64ca04ad0758b28a1605e824d6b331b48f244a1e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Dec 2025 21:55:47 +0200 Subject: [PATCH] feat(client/right_pane): jump to heading --- .../src/widgets/sidebar/TableOfContents.tsx | 7 +-- .../src/widgets/type_widgets/file/Pdf.tsx | 51 ++++++++++----- packages/pdfjs-viewer/src/custom.ts | 63 ++++++++++++++++--- 3 files changed, 92 insertions(+), 29 deletions(-) diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index 1a5b96ceb..16ccd325b 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -22,7 +22,7 @@ interface HeadingsWithNesting extends RawHeading { } export interface HeadingContext { - // scrollToHeading(heading: RawHeading): void; + scrollToHeading(heading: RawHeading): void; headings: RawHeading[]; } @@ -43,14 +43,11 @@ export default function TableOfContents() { function PdfTableOfContents() { const data = useGetContextData("toc"); - console.log("Rendering with data", data); return ( { - - }} + scrollToHeading={data?.scrollToHeading || (() => {})} /> ); } diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx index 5d68d403f..e6b7467b0 100644 --- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx +++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx @@ -6,7 +6,7 @@ import FBlob from "../../../entities/fblob"; import FNote from "../../../entities/fnote"; import server from "../../../services/server"; import { useViewModeConfig } from "../../collections/NoteList"; -import { useSetContextData, useTriliumOption } from "../../react/hooks"; +import { useTriliumOption } from "../../react/hooks"; const VARIABLE_WHITELIST = new Set([ "root-background", @@ -40,12 +40,22 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: { if (event.data.type === "pdfjs-viewer-toc") { if (event.data.data) { // Convert PDF outline to HeadingContext format + const headings = convertPdfOutlineToHeadings(event.data.data); noteContext.setContextData("toc", { - headings: convertPdfOutlineToHeadings(event.data.data) + headings, + scrollToHeading: (heading) => { + iframeRef.current?.contentWindow?.postMessage({ + type: "trilium-scroll-to-heading", + headingId: heading.id + }, "*"); + } }); } else { // No ToC available, use empty headings - noteContext.setContextData("toc", { headings: [] }); + noteContext.setContextData("toc", { + headings: [], + scrollToHeading: () => {} + }); } } } @@ -135,25 +145,36 @@ function cssVarsToString(vars) { interface PdfOutlineItem { title: string; level: number; - dest: any; + dest: unknown; + id: string; items: PdfOutlineItem[]; } -function convertPdfOutlineToHeadings(outline: PdfOutlineItem[], parentLevel = 0) { - const headings: any[] = []; +interface PdfHeading { + level: number; + text: string; + id: string; + element: null; +} - for (const item of outline) { - headings.push({ - level: parentLevel + 1, - text: item.title, - id: `pdf-outline-${headings.length}`, - element: null // PDFs don't have DOM elements - }); +function convertPdfOutlineToHeadings(outline: PdfOutlineItem[]): PdfHeading[] { + const headings: PdfHeading[] = []; - if (item.items && item.items.length > 0) { - headings.push(...convertPdfOutlineToHeadings(item.items, parentLevel + 1)); + function flatten(items: PdfOutlineItem[]) { + for (const item of items) { + headings.push({ + level: item.level + 1, + text: item.title, + id: item.id, + element: null // PDFs don't have DOM elements + }); + + if (item.items && item.items.length > 0) { + flatten(item.items); + } } } + flatten(outline); return headings; } diff --git a/packages/pdfjs-viewer/src/custom.ts b/packages/pdfjs-viewer/src/custom.ts index 911ece35b..1f3d922eb 100644 --- a/packages/pdfjs-viewer/src/custom.ts +++ b/packages/pdfjs-viewer/src/custom.ts @@ -18,6 +18,7 @@ async function main() { app.eventBus.on("documentloaded", () => { manageSave(); extractAndSendToc(); + setupScrollToHeading(); }); await app.initializedPromise; }; @@ -91,27 +92,71 @@ async function extractAndSendToc() { return; } - // Convert PDF.js outline format to a simpler structure - const toc = convertOutlineToToc(outline); + // Store outline items with their destinations for later scrolling + const outlineMap = new Map(); + const toc = convertOutlineToToc(outline, 0, outlineMap); + + // Store the map globally so setupScrollToHeading can access it + (window as any).TRILIUM_OUTLINE_MAP = outlineMap; window.parent.postMessage({ type: "pdfjs-viewer-toc", data: toc }, "*"); } catch (error) { + window.parent.postMessage({ + type: "pdfjs-viewer-toc", data: null }, "*"); } } -function convertOutlineToToc(outline: any[], level = 0): any[] { - return outline.map(item => ({ - title: item.title, - level: level, - dest: item.dest, - items: item.items && item.items.length > 0 ? convertOutlineToToc(item.items, level + 1) : [] - })); +function convertOutlineToToc(outline: any[], level = 0, outlineMap?: Map, parentId = ""): any[] { + return outline.map((item, index) => { + const id = parentId ? `${parentId}-${index}` : `pdf-outline-${index}`; + + if (outlineMap) { + outlineMap.set(id, item); + } + + return { + title: item.title, + level: level, + dest: item.dest, + id: id, + items: item.items && item.items.length > 0 ? convertOutlineToToc(item.items, level + 1, outlineMap, id) : [] + }; + }); } main(); console.log("Custom script loaded"); + +function setupScrollToHeading() { + window.addEventListener("message", async (event) => { + if (event.data?.type === "trilium-scroll-to-heading") { + const headingId = event.data.headingId; + const outlineMap = (window as any).TRILIUM_OUTLINE_MAP as Map; + + if (!outlineMap) return; + + const outlineItem = outlineMap.get(headingId); + if (!outlineItem || !outlineItem.dest) return; + + const app = window.PDFViewerApplication; + + // Navigate to the destination + try { + const dest = typeof outlineItem.dest === 'string' + ? await app.pdfDocument.getDestination(outlineItem.dest) + : outlineItem.dest; + + if (dest) { + app.pdfLinkService.goToDestination(dest); + } + } catch (error) { + console.error("Error navigating to heading:", error); + } + } + }); +}