chore(react/type_widget): basic editable code

This commit is contained in:
Elian Doran
2025-09-20 09:44:36 +03:00
parent 6517dd1190
commit 9eae6620d0
9 changed files with 118 additions and 85 deletions

View File

@@ -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 <File {...props} />
case "image": return <Image {...props} />
case "readOnlyCode": return <ReadOnlyCode {...props} />
case "editableCode": return <EditableCode {...props} />
default: break;
}
}

View File

@@ -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);
}

View File

@@ -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<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent);
@@ -73,6 +75,29 @@ export function useSpacedUpdate(callback: () => void | Promise<void>, interval =
return spacedUpdateRef.current;
}
export function useEditorSpacedUpdate({ note, getData, dataSaved }: {
note: FNote,
getData: () => Promise<object | undefined> | 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.
*

View File

@@ -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 (
<div className="note-detail-readonly-code note-detail-printable">
<CodeMirror
className="note-detail-readonly-code-content"
content={content}
mime={note.mime}
readOnly
ntxId={ntxId}
/>
</div>
)
}
export function EditableCode({ note, ntxId }: TypeWidgetProps) {
const editorRef = useRef<VanillaCodeMirror>(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 (
<div className="note-detail-code note-detail-printable">
<CodeMirror
editorRef={editorRef}
className="note-detail-code-editor"
ntxId={ntxId}
onContentChanged={() => {
spacedUpdate.scheduleUpdate();
}}
/>
</div>
)
}

View File

@@ -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<EditorConfig, "parent"> {
content: string;
mime: string;
className?: string;
ntxId: string | null | undefined;
editorRef?: RefObject<VanillaCodeMirror>;
}
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<HTMLPreElement>(null);
const codeEditorRef = useRef<VanillaCodeMirror>(null);
const codeEditorRef = useRef<VanillaCodeMirror>();
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();

View File

@@ -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 (
<div class="note-detail-readonly-code note-detail-printable">
<CodeMirror
className="note-detail-readonly-code-content"
content={content}
mime={note.mime}
readOnly
ntxId={ntxId}
/>
</div>
)
}

View File

@@ -1,4 +1,18 @@
/* #region Read-only code */
.note-detail-readonly-code {
min-height: 50px;
position: relative;
}
/* #endregion */
/* #region Editable code */
.note-detail-code {
position: relative;
height: 100%;
}
.note-detail-code-editor {
min-height: 50px;
height: 100%;
}
/* #endregion */

View File

@@ -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;

View File

@@ -10,21 +10,7 @@ import { hasTouchBar } from "../../services/utils.js";
import type { EditorConfig } from "@triliumnext/codemirror";
const TPL = /*html*/`
<div class="note-detail-code note-detail-printable">
<style>
.note-detail-code {
position: relative;
height: 100%;
}
.note-detail-code-editor {
min-height: 50px;
height: 100%;
}
</style>
<div class="note-detail-code-editor"></div>
</div>`;
`;
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
}
@@ -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;