mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +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 | ||||||
|         } |         } | ||||||
| @@ -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