refactor(markdown): use different mechanism for syncing based on init

This commit is contained in:
Elian Doran
2026-04-17 20:34:25 +03:00
parent 79ea95cb39
commit e2d6fdb09a
5 changed files with 76 additions and 47 deletions

View File

@@ -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<string, Function>)
}, [ handlers ]);
}
export function useSyncedRef<T>(externalRef?: RefObject<T>, initialValue: T | null = null): RefObject<T> {
export function useSyncedRef<T>(externalRef?: Ref<T>, initialValue: T | null = null): RefObject<T> {
const ref = useRef<T>(initialValue);
useEffect(() => {
if (externalRef) {
if (typeof externalRef === "function") {
externalRef(ref.current);
} else if (externalRef) {
externalRef.current = ref.current;
}
}, [ ref, externalRef ]);

View File

@@ -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<CodeEditorProps
dataSaved?: () => void;
placeholder?: string;
/** Optional external ref to the underlying CodeMirror `EditorView`. Populated once the editor has initialized. */
editorRef?: RefObject<VanillaCodeMirror>;
editorRef?: Ref<VanillaCodeMirror>;
}
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<VanillaCodeMirror>(null);
const editorRef = externalEditorRef ?? internalEditorRef;
const editorRef = useRef<VanillaCodeMirror>(null);
const containerRef = useRef<HTMLPreElement>(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
<>
<CodeEditor
ntxId={ntxId} parentComponent={parentComponent}
editorRef={editorRef} containerRef={containerRef}
editorRef={combinedEditorRef} containerRef={containerRef}
mime={mime ?? "text/plain"}
className="note-detail-code-editor"
placeholder={placeholder ?? t("editable_code.placeholder")}
@@ -221,11 +225,13 @@ export function CodeEditor({ parentComponent, ntxId, containerRef: externalConta
indentSize={editorProps.indentSize ?? (parseInt(codeNoteTabWidth) || 4)}
useTabs={editorProps.useTabs ?? codeNoteIndentWithTabs}
onInitialized={() => {
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?.();

View File

@@ -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<EditorConfig, "parent"> {
content?: string;
mime: string;
className?: string;
editorRef?: RefObject<VanillaCodeMirror>;
containerRef?: RefObject<HTMLPreElement>;
editorRef?: Ref<VanillaCodeMirror>;
containerRef?: Ref<HTMLPreElement>;
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();

View File

@@ -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<MarkdownContextValue | null>(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<VanillaCodeMirror | null>(null);
const [ previewEl, setPreviewEl ] = useState<HTMLDivElement | null>(null);
const html = useMemo(() => renderWithSourceLines(content), [ content ]);
const previewRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<VanillaCodeMirror>(null);
useSyncedScrolling(editorRef, previewRef);
useSyncedHighlight(editorRef, previewRef, html);
useSyncedScrolling(editorView, previewEl);
useSyncedHighlight(editorView, previewEl, html);
const ctx = useMemo<MarkdownContextValue>(
() => ({ html, setEditorView, setPreviewEl }),
[ html ]
);
return (
<SplitEditor
noteType="code"
{...props}
editorRef={editorRef}
onContentChanged={setContent}
previewContent={(
<ReadOnlyTextContent
html={html}
ntxId={props.ntxId}
className="markdown-preview"
contentRef={previewRef}
/>
)}
<MarkdownContext.Provider value={ctx}>
<SplitEditor
noteType="code"
{...props}
editorRef={setEditorView}
onContentChanged={setContent}
previewContent={<MarkdownPreview ntxId={props.ntxId} />}
/>
</MarkdownContext.Provider>
);
}
function MarkdownPreview({ ntxId }: { ntxId: TypeWidgetProps["ntxId"] }) {
const { html, setPreviewEl } = useMarkdownContext();
return (
<ReadOnlyTextContent
html={html}
ntxId={ntxId}
className="markdown-preview"
contentRef={setPreviewEl}
/>
);
}
@@ -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<VanillaCodeMirror>, previewRef: RefObject<HTMLDivElement>) {
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<VanillaCodeMirror>, previewRef:
scroller.addEventListener("scroll", onScroll, { passive: true });
return () => scroller.removeEventListener("scroll", onScroll);
}, [ editorRef, previewRef ]);
}, [ view, preview ]);
}
/**
@@ -94,10 +118,8 @@ function useSyncedScrolling(editorRef: RefObject<VanillaCodeMirror>, 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<VanillaCodeMirror>, previewRef: RefObject<HTMLDivElement>, 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<VanillaCodeMirror>, 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. */

View File

@@ -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<HTMLDivElement>;
contentRef?: Ref<HTMLDivElement>;
}
/**