From 9eae6620d00e1606a53bc3ca57dec2f527a5dda3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 20 Sep 2025 09:44:36 +0300 Subject: [PATCH] chore(react/type_widget): basic editable code --- apps/client/src/widgets/NoteDetail.tsx | 7 ++- apps/client/src/widgets/note_detail.ts.bak | 26 -------- apps/client/src/widgets/react/hooks.tsx | 25 ++++++++ .../src/widgets/type_widgets/code/Code.tsx | 62 +++++++++++++++++++ .../widgets/type_widgets/code/CodeMirror.tsx | 11 +++- .../type_widgets/code/ReadOnlyCode.tsx | 29 --------- .../src/widgets/type_widgets/code/code.css | 16 ++++- .../src/widgets/type_widgets/type_widget.ts | 1 + .../widgets/type_widgets_old/editable_code.ts | 26 +------- 9 files changed, 118 insertions(+), 85 deletions(-) create mode 100644 apps/client/src/widgets/type_widgets/code/Code.tsx delete mode 100644 apps/client/src/widgets/type_widgets/code/ReadOnlyCode.tsx diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 533ee2f9b..2496a94f1 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -2,7 +2,7 @@ import { NoteType } from "@triliumnext/commons"; import { useNoteContext } from "./react/hooks" import FNote from "../entities/fnote"; import protected_session_holder from "../services/protected_session_holder"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useMemo, useState } from "preact/hooks"; import NoteContext from "../components/note_context"; import Empty from "./type_widgets/Empty"; import { VNode } from "preact"; @@ -15,7 +15,9 @@ import WebView from "./type_widgets/WebView"; import "./NoteDetail.css"; import File from "./type_widgets/File"; import Image from "./type_widgets/Image"; -import ReadOnlyCode from "./type_widgets/code/ReadOnlyCode"; +import { ReadOnlyCode, EditableCode } from "./type_widgets/code/Code"; +import SpacedUpdate from "../services/spaced_update"; +import server from "../services/server"; /** * A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one, @@ -74,6 +76,7 @@ function getCorrespondingWidget(noteType: ExtendedNoteType | undefined, props: T case "file": return case "image": return case "readOnlyCode": return + case "editableCode": return default: break; } } diff --git a/apps/client/src/widgets/note_detail.ts.bak b/apps/client/src/widgets/note_detail.ts.bak index 46f149588..a907cb43a 100644 --- a/apps/client/src/widgets/note_detail.ts.bak +++ b/apps/client/src/widgets/note_detail.ts.bak @@ -74,32 +74,6 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { this.typeWidgets = {}; - this.spacedUpdate = new SpacedUpdate(async () => { - if (!this.noteContext) { - return; - } - - const { note } = this.noteContext; - if (!note) { - return; - } - - const { noteId } = note; - - const data = await this.getTypeWidget().getData(); - - // for read only notes - if (data === undefined) { - return; - } - - protectedSessionHolder.touchProtectedSessionIfNecessary(note); - - await server.put(`notes/${noteId}/data`, data, this.componentId); - - this.getTypeWidget().dataSaved(); - }); - appContext.addBeforeUnloadListener(this); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 6a4b4ab15..85e43ab09 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -19,6 +19,8 @@ import Mark from "mark.js"; import { DragData } from "../note_tree"; import Component from "../../components/component"; import toast, { ToastOptions } from "../../services/toast"; +import protected_session_holder from "../../services/protected_session_holder"; +import server from "../../services/server"; export function useTriliumEvent(eventName: T, handler: (data: EventData) => void) { const parentComponent = useContext(ParentComponent); @@ -73,6 +75,29 @@ export function useSpacedUpdate(callback: () => void | Promise, interval = return spacedUpdateRef.current; } +export function useEditorSpacedUpdate({ note, getData, dataSaved }: { + note: FNote, + getData: () => Promise | object | undefined, + dataSaved?: () => void +}) { + const parentComponent = useContext(ParentComponent); + const callback = useMemo(() => { + return async () => { + const data = await getData(); + + // for read only notes + if (data === undefined) return; + + protected_session_holder.touchProtectedSessionIfNecessary(note); + await server.put(`notes/${note.noteId}/data`, data, parentComponent?.componentId); + + dataSaved?.(); + } + }, [ note, getData, dataSaved ]) + const spacedUpdate = useSpacedUpdate(callback); + return spacedUpdate; +} + /** * Allows a React component to read and write a Trilium option, while also watching for external changes. * diff --git a/apps/client/src/widgets/type_widgets/code/Code.tsx b/apps/client/src/widgets/type_widgets/code/Code.tsx new file mode 100644 index 000000000..24899b67e --- /dev/null +++ b/apps/client/src/widgets/type_widgets/code/Code.tsx @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { default as VanillaCodeMirror } from "@triliumnext/codemirror"; +import { TypeWidgetProps } from "../type_widget"; +import "./code.css"; +import CodeMirror from "./CodeMirror"; +import utils from "../../../services/utils"; +import { useEditorSpacedUpdate, useNoteBlob } from "../../react/hooks"; + +export function ReadOnlyCode({ note, viewScope, ntxId }: TypeWidgetProps) { + const [ content, setContent ] = useState(""); + const blob = useNoteBlob(note); + + useEffect(() => { + if (!blob) return; + const isFormattable = note.type === "text" && viewScope?.viewMode === "source"; + setContent(isFormattable ? utils.formatHtml(blob.content) : blob.content); + }, [ blob ]); + + return ( +
+ +
+ ) +} + +export function EditableCode({ note, ntxId }: TypeWidgetProps) { + const editorRef = useRef(null); + const blob = useNoteBlob(note); + const spacedUpdate = useEditorSpacedUpdate({ + note, + getData: () => ({ content: editorRef.current?.getText() }) + }); + + useEffect(() => { + spacedUpdate.allowUpdateWithoutChange(() => { + const codeEditor = editorRef.current; + if (!codeEditor) return; + codeEditor.setText(blob?.content ?? ""); + codeEditor.setMimeType(note.mime); + codeEditor.clearHistory(); + }); + }, [ blob ]); + + return ( +
+ { + spacedUpdate.scheduleUpdate(); + }} + /> +
+ ) +} diff --git a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx index 7a22d58f7..b1d48c32e 100644 --- a/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx +++ b/apps/client/src/widgets/type_widgets/code/CodeMirror.tsx @@ -1,18 +1,20 @@ import { useEffect, useRef } from "preact/hooks"; import { EditorConfig, default as VanillaCodeMirror } from "@triliumnext/codemirror"; -import { useTriliumEvent, useTriliumOptionBool } from "../../react/hooks"; +import { useSyncedRef, useTriliumEvent, useTriliumOptionBool } from "../../react/hooks"; import { refToJQuerySelector } from "../../react/react_utils"; +import { RefObject } from "preact"; interface CodeMirrorProps extends Omit { content: string; mime: string; className?: string; ntxId: string | null | undefined; + editorRef?: RefObject; } -export default function CodeMirror({ className, content, mime, ntxId, ...extraOpts }: CodeMirrorProps) { +export default function CodeMirror({ className, content, mime, ntxId, editorRef: externalEditorRef, ...extraOpts }: CodeMirrorProps) { const parentRef = useRef(null); - const codeEditorRef = useRef(null); + const codeEditorRef = useRef(); const [ codeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled"); const initialized = $.Deferred(); @@ -39,6 +41,9 @@ export default function CodeMirror({ className, content, mime, ntxId, ...extraOp ...extraOpts }); codeEditorRef.current = codeEditor; + if (externalEditorRef) { + externalEditorRef.current = codeEditor; + } initialized.resolve(); return () => codeEditor.destroy(); diff --git a/apps/client/src/widgets/type_widgets/code/ReadOnlyCode.tsx b/apps/client/src/widgets/type_widgets/code/ReadOnlyCode.tsx deleted file mode 100644 index bdbb71ad5..000000000 --- a/apps/client/src/widgets/type_widgets/code/ReadOnlyCode.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect, useState } from "preact/hooks"; -import { TypeWidgetProps } from "../type_widget"; -import "./code.css"; -import CodeMirror from "./CodeMirror"; -import utils from "../../../services/utils"; -import { useNoteBlob } from "../../react/hooks"; - -export default function ReadOnlyCode({ note, viewScope, ntxId }: TypeWidgetProps) { - const [ content, setContent ] = useState(""); - const blob = useNoteBlob(note); - - useEffect(() => { - if (!blob) return; - const isFormattable = note.type === "text" && viewScope?.viewMode === "source"; - setContent(isFormattable ? utils.formatHtml(blob.content) : blob.content); - }, [ blob ]); - - return ( -
- -
- ) -} diff --git a/apps/client/src/widgets/type_widgets/code/code.css b/apps/client/src/widgets/type_widgets/code/code.css index 1af75176a..5ee6953fd 100644 --- a/apps/client/src/widgets/type_widgets/code/code.css +++ b/apps/client/src/widgets/type_widgets/code/code.css @@ -1,4 +1,18 @@ +/* #region Read-only code */ .note-detail-readonly-code { min-height: 50px; position: relative; -} \ No newline at end of file +} +/* #endregion */ + +/* #region Editable code */ +.note-detail-code { + position: relative; + height: 100%; +} + +.note-detail-code-editor { + min-height: 50px; + height: 100%; +} +/* #endregion */ diff --git a/apps/client/src/widgets/type_widgets/type_widget.ts b/apps/client/src/widgets/type_widgets/type_widget.ts index 68c307002..146d6efb0 100644 --- a/apps/client/src/widgets/type_widgets/type_widget.ts +++ b/apps/client/src/widgets/type_widgets/type_widget.ts @@ -1,5 +1,6 @@ import FNote from "../../entities/fnote"; import { ViewScope } from "../../services/link"; +import SpacedUpdate from "../../services/spaced_update"; export interface TypeWidgetProps { note: FNote; diff --git a/apps/client/src/widgets/type_widgets_old/editable_code.ts b/apps/client/src/widgets/type_widgets_old/editable_code.ts index cdf4b912e..9290c8492 100644 --- a/apps/client/src/widgets/type_widgets_old/editable_code.ts +++ b/apps/client/src/widgets/type_widgets_old/editable_code.ts @@ -10,21 +10,7 @@ import { hasTouchBar } from "../../services/utils.js"; import type { EditorConfig } from "@triliumnext/codemirror"; const TPL = /*html*/` -
- - -
-
`; +`; export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { @@ -60,8 +46,6 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { if (this.debounceUpdate) { this.spacedUpdate.resetUpdateTimer(); } - - this.spacedUpdate.scheduleUpdate(); }, tabIndex: 300 } @@ -71,7 +55,7 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { const blob = await this.note?.getBlob(); await this.spacedUpdate.allowUpdateWithoutChange(() => { - this._update(note, blob?.content ?? ""); + this._update(note, blob?.content ?? ""); }); this.show(); @@ -81,12 +65,6 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget { } } - getData() { - return { - content: this.codeEditor.getText() - }; - } - buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) { const items: TouchBarItem[] = []; const note = this.note;