From 704dcd011e6096df241cdacfc6cff3c6cd16c0f2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 18 Dec 2025 11:46:13 +0200 Subject: [PATCH] feat(toc): basic support for docs --- apps/client/src/components/app_context.ts | 74 +++++++++---------- apps/client/src/widgets/react/hooks.tsx | 9 +++ .../src/widgets/sidebar/TableOfContents.tsx | 5 +- apps/client/src/widgets/type_widgets/Doc.tsx | 16 ++-- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/apps/client/src/components/app_context.ts b/apps/client/src/components/app_context.ts index b5ad30003..e0b8c651b 100644 --- a/apps/client/src/components/app_context.ts +++ b/apps/client/src/components/app_context.ts @@ -1,40 +1,41 @@ -import froca from "../services/froca.js"; -import RootCommandExecutor from "./root_command_executor.js"; -import Entrypoints from "./entrypoints.js"; -import options from "../services/options.js"; -import utils, { hasTouchBar } from "../services/utils.js"; -import zoomComponent from "./zoom.js"; -import TabManager from "./tab_manager.js"; -import Component from "./component.js"; -import keyboardActionsService from "../services/keyboard_actions.js"; -import linkService, { type ViewScope } from "../services/link.js"; -import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js"; -import MainTreeExecutors from "./main_tree_executors.js"; -import toast from "../services/toast.js"; -import ShortcutComponent from "./shortcut_component.js"; -import { t, initLocale } from "../services/i18n.js"; -import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; -import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; -import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; -import type LoadResults from "../services/load_results.js"; -import type { Attribute } from "../services/attribute_parser.js"; -import type NoteTreeWidget from "../widgets/note_tree.js"; -import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; -import type { NativeImage, TouchBar } from "electron"; -import TouchBarComponent from "./touch_bar.js"; import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type CodeMirror from "@triliumnext/codemirror"; -import { StartupChecks } from "./startup_checks.js"; -import type { CreateNoteOpts } from "../services/note_create.js"; -import { ColumnComponent } from "tabulator-tables"; -import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx"; -import type RootContainer from "../widgets/containers/root_container.js"; import { SqlExecuteResults } from "@triliumnext/commons"; -import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx"; -import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx"; +import type { NativeImage, TouchBar } from "electron"; +import { ColumnComponent } from "tabulator-tables"; + +import type { Attribute } from "../services/attribute_parser.js"; +import froca from "../services/froca.js"; +import { initLocale,t } from "../services/i18n.js"; +import keyboardActionsService from "../services/keyboard_actions.js"; +import linkService, { type ViewScope } from "../services/link.js"; +import type LoadResults from "../services/load_results.js"; +import type { CreateNoteOpts } from "../services/note_create.js"; +import options from "../services/options.js"; +import toast from "../services/toast.js"; +import utils, { hasTouchBar } from "../services/utils.js"; import { ReactWrappedWidget } from "../widgets/basic_widget.js"; -import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx"; +import type RootContainer from "../widgets/containers/root_container.js"; +import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx"; +import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js"; +import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; +import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx"; import type { InfoProps } from "../widgets/dialogs/info.jsx"; +import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx"; +import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx"; +import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js"; +import type NoteTreeWidget from "../widgets/note_tree.js"; +import Component from "./component.js"; +import Entrypoints from "./entrypoints.js"; +import MainTreeExecutors from "./main_tree_executors.js"; +import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js"; +import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; +import RootCommandExecutor from "./root_command_executor.js"; +import ShortcutComponent from "./shortcut_component.js"; +import { StartupChecks } from "./startup_checks.js"; +import TabManager from "./tab_manager.js"; +import TouchBarComponent from "./touch_bar.js"; +import zoomComponent from "./zoom.js"; interface Layout { getRootWidget: (appContext: AppContext) => RootContainer; @@ -447,6 +448,7 @@ type EventMappings = { }; searchRefreshed: { ntxId?: string | null }; textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor }; + contentElRefreshed: { ntxId?: string | null, contentEl: HTMLElement }; hoistedNoteChanged: { noteId: string; ntxId: string | null; @@ -695,10 +697,8 @@ $(window).on("beforeunload", () => { console.log(`Component ${component.componentId} is not finished saving its state.`); allSaved = false; } - } else { - if (!listener()) { - allSaved = false; - } + } else if (!listener()) { + allSaved = false; } } @@ -708,7 +708,7 @@ $(window).on("beforeunload", () => { } }); -$(window).on("hashchange", function () { +$(window).on("hashchange", () => { const { notePath, ntxId, viewScope, searchString } = linkService.parseNavigationStateFromUrl(window.location.href); if (notePath || ntxId) { diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 5ec68a42d..73fbfc101 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -1119,6 +1119,7 @@ export function useTextEditor(noteContext: NoteContext | null | undefined) { export function useContentElement(noteContext: NoteContext | null | undefined) { const [ contentElement, setContentElement ] = useState(null); const requestIdRef = useRef(0); + const [, forceUpdate] = useState(0); useEffect(() => { const requestId = ++requestIdRef.current; @@ -1126,8 +1127,16 @@ export function useContentElement(noteContext: NoteContext | null | undefined) { // Prevent stale async. if (requestId !== requestIdRef.current) return; setContentElement(contentElement?.[0] ?? null); + forceUpdate(v => v + 1); }); }, [ noteContext ]); + // React to content changes initializing. + useTriliumEvent("contentElRefreshed", ({ ntxId: eventNtxId, contentEl }) => { + if (eventNtxId !== noteContext?.ntxId) return; + setContentElement(contentEl); + forceUpdate(v => v + 1); + }); + return contentElement; } diff --git a/apps/client/src/widgets/sidebar/TableOfContents.tsx b/apps/client/src/widgets/sidebar/TableOfContents.tsx index 64249bd27..696e37e56 100644 --- a/apps/client/src/widgets/sidebar/TableOfContents.tsx +++ b/apps/client/src/widgets/sidebar/TableOfContents.tsx @@ -27,9 +27,8 @@ export default function TableOfContents() { return ( - {noteType === "text" && ( - isReadOnly ? : - )} + {((noteType === "text" && isReadOnly) || (noteType === "doc")) && } + {noteType === "text" && !isReadOnly && } ); } diff --git a/apps/client/src/widgets/type_widgets/Doc.tsx b/apps/client/src/widgets/type_widgets/Doc.tsx index 5c7a31890..93929c7ba 100644 --- a/apps/client/src/widgets/type_widgets/Doc.tsx +++ b/apps/client/src/widgets/type_widgets/Doc.tsx @@ -1,10 +1,12 @@ -import { useEffect, useRef, useState } from "preact/hooks"; -import { RawHtmlBlock } from "../react/RawHtml"; -import renderDoc from "../../services/doc_renderer"; import "./Doc.css"; -import { TypeWidgetProps } from "./type_widget"; + +import { useEffect, useRef } from "preact/hooks"; + +import appContext from "../../components/app_context"; +import renderDoc from "../../services/doc_renderer"; import { useTriliumEvent } from "../react/hooks"; import { refToJQuerySelector } from "../react/react_utils"; +import { TypeWidgetProps } from "./type_widget"; export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) { const initialized = useRef | null>(null); @@ -14,9 +16,11 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) { if (!note) return; initialized.current = renderDoc(note).then($content => { - containerRef.current?.replaceChildren(...$content); + if (!containerRef.current) return; + containerRef.current.replaceChildren(...$content); + appContext.triggerEvent("contentElRefreshed", { ntxId, contentEl: containerRef.current }); }); - }, [ note ]); + }, [ note, ntxId ]); useTriliumEvent("executeWithContentElement", async ({ resolve, ntxId: eventNtxId}) => { if (eventNtxId !== ntxId) return;