diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 09fda7b843..d507c7e962 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -2,7 +2,7 @@ import { CKTextEditor } from "@triliumnext/ckeditor5"; import { FilterLabelsByType, KeyboardActionNames, NoteType, OptionNames, RelationNames } from "@triliumnext/commons"; import { Tooltip } from "bootstrap"; import Mark from "mark.js"; -import { RefObject, VNode } from "preact"; +import { Ref, RefObject, VNode } from "preact"; import { CSSProperties, useSyncExternalStore } from "preact/compat"; import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; @@ -964,11 +964,13 @@ export function useLegacyImperativeHandlers(handlers: Record) }, [ handlers ]); } -export function useSyncedRef(externalRef?: RefObject, initialValue: T | null = null): RefObject { +export function useSyncedRef(externalRef?: Ref, initialValue: T | null = null): RefObject { const ref = useRef(initialValue); useEffect(() => { - if (externalRef) { + if (typeof externalRef === "function") { + externalRef(ref.current); + } else if (externalRef) { externalRef.current = ref.current; } }, [ ref, externalRef ]); diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx index 0f99d37e07..951ebcdc19 100644 --- a/apps/client/src/widgets/type_widgets/code/Code.tsx +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -2,7 +2,7 @@ import "./code.css"; import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror"; import { NoteType } from "@triliumnext/commons"; -import { RefObject } from "preact"; +import { Ref } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; import appContext, { CommandListenerData } from "../../../components/app_context"; @@ -33,7 +33,7 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit void; placeholder?: string; /** Optional external ref to the underlying CodeMirror `EditorView`. Populated once the editor has initialized. */ - editorRef?: RefObject; + editorRef?: Ref; } export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWidgetProps) { @@ -85,9 +85,13 @@ function formatViewSource(note: FNote, content: string) { } export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, placeholder, editorRef: externalEditorRef, ...editorProps }: EditableCodeProps) { - const internalEditorRef = useRef(null); - const editorRef = externalEditorRef ?? internalEditorRef; + const editorRef = useRef(null); const containerRef = useRef(null); + const combinedEditorRef = (view: VanillaCodeMirror | null) => { + editorRef.current = view; + if (typeof externalEditorRef === "function") externalEditorRef(view); + else if (externalEditorRef) externalEditorRef.current = view; + }; const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled"); const [ noteTabWidth ] = useNoteLabelInt(note, "tabWidth"); const [ noteUseTabs ] = useNoteLabelOptionalBool(note, "indentWithTabs"); @@ -126,7 +130,7 @@ export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentC <> { - if (externalContainerRef && containerRef.current) { - externalContainerRef.current = containerRef.current; + if (containerRef.current) { + if (typeof externalContainerRef === "function") externalContainerRef(containerRef.current); + else if (externalContainerRef) externalContainerRef.current = containerRef.current; } - if (externalEditorRef && codeEditorRef.current) { - externalEditorRef.current = codeEditorRef.current; + if (codeEditorRef.current) { + if (typeof externalEditorRef === "function") externalEditorRef(codeEditorRef.current); + else if (externalEditorRef) externalEditorRef.current = codeEditorRef.current; } initialized.current.resolve(); onInitialized?.(); diff --git a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx index c52c8e7676..e80a0202a2 100644 --- a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx +++ b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx @@ -1,14 +1,14 @@ import { useEffect, useRef } from "preact/hooks"; import { EditorConfig, default as VanillaCodeMirror } from "@triliumnext/codemirror"; import { useSyncedRef } from "../../react/hooks"; -import { RefObject } from "preact"; +import { Ref } from "preact"; export interface CodeMirrorProps extends Omit { content?: string; mime: string; className?: string; - editorRef?: RefObject; - containerRef?: RefObject; + editorRef?: Ref; + containerRef?: Ref; onInitialized?: () => void; } @@ -25,9 +25,8 @@ export default function CodeMirror({ className, content, mime, editorRef: extern ...extraOpts }); codeEditorRef.current = codeEditor; - if (externalEditorRef) { - externalEditorRef.current = codeEditor; - } + if (typeof externalEditorRef === "function") externalEditorRef(codeEditor); + else if (externalEditorRef) externalEditorRef.current = codeEditor; onInitialized?.(); return () => codeEditor.destroy(); diff --git a/apps/client/src/widgets/type_widgets/code/Markdown.tsx b/apps/client/src/widgets/type_widgets/code/Markdown.tsx index dec85ae6e1..586baa35b2 100644 --- a/apps/client/src/widgets/type_widgets/code/Markdown.tsx +++ b/apps/client/src/widgets/type_widgets/code/Markdown.tsx @@ -4,8 +4,8 @@ import VanillaCodeMirror from "@triliumnext/codemirror"; import { renderToHtml } from "@triliumnext/commons"; import DOMPurify from "dompurify"; import { Marked } from "marked"; -import { RefObject } from "preact"; -import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { createContext } from "preact"; +import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import SplitEditor from "../helpers/SplitEditor"; import { ReadOnlyTextContent } from "../text/ReadOnlyText"; @@ -13,29 +13,55 @@ import { TypeWidgetProps } from "../type_widget"; const marked = new Marked({ breaks: true, gfm: true }); +interface MarkdownContextValue { + html: string; + setEditorView: (view: VanillaCodeMirror | null) => void; + setPreviewEl: (el: HTMLDivElement | null) => void; +} + +const MarkdownContext = createContext(null); + +function useMarkdownContext() { + const ctx = useContext(MarkdownContext); + if (!ctx) throw new Error("useMarkdownContext must be used within a Markdown component"); + return ctx; +} + export default function Markdown(props: TypeWidgetProps) { const [ content, setContent ] = useState(""); + const [ editorView, setEditorView ] = useState(null); + const [ previewEl, setPreviewEl ] = useState(null); const html = useMemo(() => renderWithSourceLines(content), [ content ]); - const previewRef = useRef(null); - const editorRef = useRef(null); - useSyncedScrolling(editorRef, previewRef); - useSyncedHighlight(editorRef, previewRef, html); + useSyncedScrolling(editorView, previewEl); + useSyncedHighlight(editorView, previewEl, html); + + const ctx = useMemo( + () => ({ html, setEditorView, setPreviewEl }), + [ html ] + ); return ( - - )} + + } + /> + + ); +} + +function MarkdownPreview({ ntxId }: { ntxId: TypeWidgetProps["ntxId"] }) { + const { html, setPreviewEl } = useMarkdownContext(); + return ( + ); } @@ -47,10 +73,8 @@ export default function Markdown(props: TypeWidgetProps) { * preview so the block tagged with that line is at the top — interpolating to * the next block for smoothness. */ -function useSyncedScrolling(editorRef: RefObject, previewRef: RefObject) { +function useSyncedScrolling(view: VanillaCodeMirror | null, preview: HTMLDivElement | null) { useEffect(() => { - const view = editorRef.current; - const preview = previewRef.current; if (!view || !preview) return; const scroller = view.scrollDOM; @@ -86,7 +110,7 @@ function useSyncedScrolling(editorRef: RefObject, previewRef: scroller.addEventListener("scroll", onScroll, { passive: true }); return () => scroller.removeEventListener("scroll", onScroll); - }, [ editorRef, previewRef ]); + }, [ view, preview ]); } /** @@ -94,10 +118,8 @@ function useSyncedScrolling(editorRef: RefObject, previewRef: * matching the built-in `cm-activeLine` behavior. Re-runs when the rendered * HTML changes so newly inserted blocks pick up the current cursor position. */ -function useSyncedHighlight(editorRef: RefObject, previewRef: RefObject, html: string) { +function useSyncedHighlight(view: VanillaCodeMirror | null, preview: HTMLDivElement | null, html: string) { useEffect(() => { - const view = editorRef.current; - const preview = previewRef.current; if (!view || !preview) return; let current: HTMLElement | null = null; @@ -124,7 +146,7 @@ function useSyncedHighlight(editorRef: RefObject, previewRef: if (v.selectionSet || v.docChanged) update(); }); return unsubscribe; - }, [ editorRef, previewRef, html ]); + }, [ view, preview, html ]); } /** Token types the parser emits but which don't produce top-level block HTML. */ diff --git a/apps/client/src/widgets/type_widgets/text/ReadOnlyText.tsx b/apps/client/src/widgets/type_widgets/text/ReadOnlyText.tsx index 49249e1753..da7e08ebab 100644 --- a/apps/client/src/widgets/type_widgets/text/ReadOnlyText.tsx +++ b/apps/client/src/widgets/type_widgets/text/ReadOnlyText.tsx @@ -5,7 +5,7 @@ import "./ReadOnlyText.css"; import "@triliumnext/ckeditor5"; import clsx from "clsx"; -import { RefObject } from "preact"; +import { Ref } from "preact"; import { useEffect, useLayoutEffect, useMemo } from "preact/hooks"; import appContext from "../../../components/app_context"; @@ -58,7 +58,7 @@ interface ReadOnlyTextContentProps { /** Extra classes appended to the content div. */ className?: string; /** Optional external ref to the rendered content div (e.g. to drive scroll sync). */ - contentRef?: RefObject; + contentRef?: Ref; } /**