mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 17:47:43 +02:00
refactor(markdown): use different mechanism for syncing based on init
This commit is contained in:
@@ -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 ]);
|
||||
|
||||
@@ -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?.();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user