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 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; | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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. | ||||
|  * | ||||
|   | ||||
							
								
								
									
										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 { 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(); | ||||
|   | ||||
| @@ -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 { | ||||
|     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 */ | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|         } | ||||
| @@ -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; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user