diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts index 15791c741..5295007a7 100644 --- a/apps/client/src/components/note_context.ts +++ b/apps/client/src/components/note_context.ts @@ -390,7 +390,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded"> * If no content could be determined `null` is returned instead. */ async getContentElement() { - return this.timeout>( + return this.timeout | null>( new Promise((resolve) => appContext.triggerCommand("executeWithContentElement", { resolve, diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index b3bd6cf29..5ec68a42d 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1115,3 +1115,19 @@ export function useTextEditor(noteContext: NoteContext | null | undefined) { return textEditor; } + +export function useContentElement(noteContext: NoteContext | null | undefined) { + const [ contentElement, setContentElement ] = useState(null); + const requestIdRef = useRef(0); + + useEffect(() => { + const requestId = ++requestIdRef.current; + noteContext?.getContentElement().then(contentElement => { + // Prevent stale async. + if (requestId !== requestIdRef.current) return; + setContentElement(contentElement?.[0] ?? null); + }); + }, [ noteContext ]); + + return contentElement; +} diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index 55178811c..64249bd27 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -5,7 +5,7 @@ import clsx from "clsx"; import { useEffect, useState } from "preact/hooks"; import { t } from "../../services/i18n"; -import { useActiveNoteContext, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks"; +import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks"; import Icon from "../react/Icon"; import RightPanelWidget from "./RightPanelWidget"; @@ -27,7 +27,9 @@ export default function TableOfContents() { return ( - {noteType === "text" && !isReadOnly && } + {noteType === "text" && ( + isReadOnly ? : + )} ); } @@ -159,3 +161,26 @@ function extractTocFromTextEditor(editor: CKTextEditor) { return headings; } //#endregion + +function ReadOnlyTextTableOfContents() { + const { noteContext } = useActiveNoteContext(); + const contentEl = useContentElement(noteContext); + const headings = extractTocFromStaticHtml(contentEl); + + return ; +} + +function extractTocFromStaticHtml(el: HTMLElement | null) { + if (!el) return []; + + const headings: RawHeading[] = []; + for (const headingEl of el.querySelectorAll("h1,h2,h3,h4,h5,h6")) { + headings.push({ + id: crypto.randomUUID(), + level: parseInt(headingEl.tagName.substring(1), 10), + text: headingEl.textContent + }); + } + + return headings; +}