refactor(markdown): get rid of DOM queries

This commit is contained in:
Elian Doran
2026-04-17 06:51:31 +03:00
parent dc57d6131a
commit 0dfbfaa61c
3 changed files with 25 additions and 37 deletions

View File

@@ -2,6 +2,7 @@ import "./code.css";
import { default as VanillaCodeMirror, getThemeById } from "@triliumnext/codemirror";
import { NoteType } from "@triliumnext/commons";
import { RefObject } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
import appContext, { CommandListenerData } from "../../../components/app_context";
@@ -31,6 +32,8 @@ export interface EditableCodeProps extends TypeWidgetProps, Omit<CodeEditorProps
/** Invoked after the content of the note has been uploaded to the server, using a spaced update. */
dataSaved?: () => void;
placeholder?: string;
/** Optional external ref to the underlying CodeMirror `EditorView`. Populated once the editor has initialized. */
editorRef?: RefObject<VanillaCodeMirror>;
}
export function ReadOnlyCode({ note, viewScope, ntxId, parentComponent }: TypeWidgetProps) {
@@ -81,8 +84,9 @@ function formatViewSource(note: FNote, content: string) {
return content;
}
export function EditableCode({ note, ntxId, noteContext, debounceUpdate, parentComponent, updateInterval, noteType = "code", onContentChanged, dataSaved, placeholder, ...editorProps }: EditableCodeProps) {
const editorRef = useRef<VanillaCodeMirror>(null);
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 containerRef = useRef<HTMLPreElement>(null);
const [ vimKeymapEnabled ] = useTriliumOptionBool("vimKeymapEnabled");
const [ noteTabWidth ] = useNoteLabelInt(note, "tabWidth");

View File

@@ -3,12 +3,11 @@
padding-block: 0.25em;
}
.note-detail-split-preview {
.markdown-preview {
box-sizing: border-box;
height: 100%;
overflow: auto;
.markdown-preview {
padding: 0.5em;
}
padding: 0.5em;
}
}

View File

@@ -15,13 +15,15 @@ export default function Markdown(props: TypeWidgetProps) {
const [ content, setContent ] = useState("");
const html = useMemo(() => DOMPurify.sanitize(renderWithSourceLines(content), { ADD_ATTR: [ "data-source-line" ] }), [ content ]);
const previewRef = useRef<HTMLDivElement>(null);
const editorRef = useRef<VanillaCodeMirror>(null);
useSyncedScrolling(previewRef);
useSyncedScrolling(editorRef, previewRef);
return (
<SplitEditor
noteType="code"
{...props}
editorRef={editorRef}
onContentChanged={setContent}
previewContent={(
<div
@@ -42,22 +44,20 @@ 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(previewRef: RefObject<HTMLDivElement>) {
function useSyncedScrolling(editorRef: RefObject<VanillaCodeMirror>, previewRef: RefObject<HTMLDivElement>) {
useEffect(() => {
let rafId = 0;
let scroller: HTMLElement | null = null;
let cmEditor: HTMLElement | null = null;
let preview: HTMLElement | null = null;
const view = editorRef.current;
const preview = previewRef.current;
if (!view || !preview) return;
const scroller = view.scrollDOM;
function onScroll() {
if (!scroller || !cmEditor || !preview) return;
const view = VanillaCodeMirror.findFromDOM(cmEditor);
if (!view) return;
if (!view || !preview) return;
const topLine = view.state.doc.lineAt(view.lineBlockAtHeight(scroller.scrollTop).from).number;
const blocks = previewRef.current?.querySelectorAll<HTMLElement>("[data-source-line]");
if (!blocks?.length) return;
const blocks = preview.querySelectorAll<HTMLElement>("[data-source-line]");
if (!blocks.length) return;
let before: HTMLElement | null = null;
let after: HTMLElement | null = null;
@@ -81,24 +81,9 @@ function useSyncedScrolling(previewRef: RefObject<HTMLDivElement>) {
preview.scrollTop = beforeOffset + (afterOffset - beforeOffset) * ratio;
}
function tryAttach() {
const split = previewRef.current?.closest(".note-detail-split");
scroller = split?.querySelector<HTMLElement>(".cm-scroller") ?? null;
cmEditor = split?.querySelector<HTMLElement>(".cm-editor") ?? null;
preview = split?.querySelector<HTMLElement>(".note-detail-split-preview") ?? null;
if (!scroller || !cmEditor || !preview) {
rafId = requestAnimationFrame(tryAttach);
return;
}
scroller.addEventListener("scroll", onScroll, { passive: true });
}
tryAttach();
return () => {
cancelAnimationFrame(rafId);
scroller?.removeEventListener("scroll", onScroll);
};
}, [ previewRef ]);
scroller.addEventListener("scroll", onScroll, { passive: true });
return () => scroller.removeEventListener("scroll", onScroll);
}, [ editorRef, previewRef ]);
}
/**