mirror of
https://github.com/zadam/trilium.git
synced 2025-10-30 18:05:55 +01:00
chore(react/type_widget): basic editable code
This commit is contained in:
@@ -2,7 +2,7 @@ import { NoteType } from "@triliumnext/commons";
|
|||||||
import { useNoteContext } from "./react/hooks"
|
import { useNoteContext } from "./react/hooks"
|
||||||
import FNote from "../entities/fnote";
|
import FNote from "../entities/fnote";
|
||||||
import protected_session_holder from "../services/protected_session_holder";
|
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 NoteContext from "../components/note_context";
|
||||||
import Empty from "./type_widgets/Empty";
|
import Empty from "./type_widgets/Empty";
|
||||||
import { VNode } from "preact";
|
import { VNode } from "preact";
|
||||||
@@ -15,7 +15,9 @@ import WebView from "./type_widgets/WebView";
|
|||||||
import "./NoteDetail.css";
|
import "./NoteDetail.css";
|
||||||
import File from "./type_widgets/File";
|
import File from "./type_widgets/File";
|
||||||
import Image from "./type_widgets/Image";
|
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,
|
* 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 "file": return <File {...props} />
|
||||||
case "image": return <Image {...props} />
|
case "image": return <Image {...props} />
|
||||||
case "readOnlyCode": return <ReadOnlyCode {...props} />
|
case "readOnlyCode": return <ReadOnlyCode {...props} />
|
||||||
|
case "editableCode": return <EditableCode {...props} />
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,32 +74,6 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
this.typeWidgets = {};
|
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);
|
appContext.addBeforeUnloadListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import Mark from "mark.js";
|
|||||||
import { DragData } from "../note_tree";
|
import { DragData } from "../note_tree";
|
||||||
import Component from "../../components/component";
|
import Component from "../../components/component";
|
||||||
import toast, { ToastOptions } from "../../services/toast";
|
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) {
|
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
@@ -73,6 +75,29 @@ export function useSpacedUpdate(callback: () => void | Promise<void>, interval =
|
|||||||
return spacedUpdateRef.current;
|
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.
|
* Allows a React component to read and write a Trilium option, while also watching for external changes.
|
||||||
*
|
*
|
||||||
|
|||||||
62
apps/client/src/widgets/type_widgets/code/Code.tsx
Normal file
62
apps/client/src/widgets/type_widgets/code/Code.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,18 +1,20 @@
|
|||||||
import { useEffect, useRef } from "preact/hooks";
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
import { EditorConfig, default as VanillaCodeMirror } from "@triliumnext/codemirror";
|
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 { refToJQuerySelector } from "../../react/react_utils";
|
||||||
|
import { RefObject } from "preact";
|
||||||
|
|
||||||
interface CodeMirrorProps extends Omit<EditorConfig, "parent"> {
|
interface CodeMirrorProps extends Omit<EditorConfig, "parent"> {
|
||||||
content: string;
|
content: string;
|
||||||
mime: string;
|
mime: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
ntxId: string | null | undefined;
|
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 parentRef = useRef<HTMLPreElement>(null);
|
||||||
const codeEditorRef = useRef<VanillaCodeMirror>(null);
|
const codeEditorRef = useRef<VanillaCodeMirror>();
|
||||||
const [ codeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled");
|
const [ codeLineWrapEnabled ] = useTriliumOptionBool("codeLineWrapEnabled");
|
||||||
const initialized = $.Deferred();
|
const initialized = $.Deferred();
|
||||||
|
|
||||||
@@ -39,6 +41,9 @@ export default function CodeMirror({ className, content, mime, ntxId, ...extraOp
|
|||||||
...extraOpts
|
...extraOpts
|
||||||
});
|
});
|
||||||
codeEditorRef.current = codeEditor;
|
codeEditorRef.current = codeEditor;
|
||||||
|
if (externalEditorRef) {
|
||||||
|
externalEditorRef.current = codeEditor;
|
||||||
|
}
|
||||||
initialized.resolve();
|
initialized.resolve();
|
||||||
|
|
||||||
return () => codeEditor.destroy();
|
return () => codeEditor.destroy();
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,18 @@
|
|||||||
|
/* #region Read-only code */
|
||||||
.note-detail-readonly-code {
|
.note-detail-readonly-code {
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region Editable code */
|
||||||
|
.note-detail-code {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-detail-code-editor {
|
||||||
|
min-height: 50px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import { ViewScope } from "../../services/link";
|
import { ViewScope } from "../../services/link";
|
||||||
|
import SpacedUpdate from "../../services/spaced_update";
|
||||||
|
|
||||||
export interface TypeWidgetProps {
|
export interface TypeWidgetProps {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
|
|||||||
@@ -10,21 +10,7 @@ import { hasTouchBar } from "../../services/utils.js";
|
|||||||
import type { EditorConfig } from "@triliumnext/codemirror";
|
import type { EditorConfig } from "@triliumnext/codemirror";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
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 {
|
export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||||
|
|
||||||
@@ -60,8 +46,6 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
|||||||
if (this.debounceUpdate) {
|
if (this.debounceUpdate) {
|
||||||
this.spacedUpdate.resetUpdateTimer();
|
this.spacedUpdate.resetUpdateTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.spacedUpdate.scheduleUpdate();
|
|
||||||
},
|
},
|
||||||
tabIndex: 300
|
tabIndex: 300
|
||||||
}
|
}
|
||||||
@@ -71,7 +55,7 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
|||||||
const blob = await this.note?.getBlob();
|
const blob = await this.note?.getBlob();
|
||||||
|
|
||||||
await this.spacedUpdate.allowUpdateWithoutChange(() => {
|
await this.spacedUpdate.allowUpdateWithoutChange(() => {
|
||||||
this._update(note, blob?.content ?? "");
|
this._update(note, blob?.content ?? "");
|
||||||
});
|
});
|
||||||
|
|
||||||
this.show();
|
this.show();
|
||||||
@@ -81,12 +65,6 @@ export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getData() {
|
|
||||||
return {
|
|
||||||
content: this.codeEditor.getText()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
|
buildTouchBarCommand({ TouchBar, buildIcon }: CommandListenerData<"buildTouchBar">) {
|
||||||
const items: TouchBarItem[] = [];
|
const items: TouchBarItem[] = [];
|
||||||
const note = this.note;
|
const note = this.note;
|
||||||
|
|||||||
Reference in New Issue
Block a user