mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	chore(monorepo): relocate client files
This commit is contained in:
		
							
								
								
									
										388
									
								
								apps/client/src/components/note_context.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								apps/client/src/components/note_context.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,388 @@ | ||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||
| import server from "../services/server.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| import appContext, { type EventData, type EventListener } from "./app_context.js"; | ||||
| import treeService from "../services/tree.js"; | ||||
| import Component from "./component.js"; | ||||
| import froca from "../services/froca.js"; | ||||
| import hoistedNoteService from "../services/hoisted_note.js"; | ||||
| import options from "../services/options.js"; | ||||
| import type { ViewScope } from "../services/link.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
| import type TypeWidget from "../widgets/type_widgets/type_widget.js"; | ||||
|  | ||||
| export interface SetNoteOpts { | ||||
|     triggerSwitchEvent?: unknown; | ||||
|     viewScope?: ViewScope; | ||||
| } | ||||
|  | ||||
| export type GetTextEditorCallback = (editor: TextEditor) => void; | ||||
|  | ||||
| class NoteContext extends Component implements EventListener<"entitiesReloaded"> { | ||||
|     ntxId: string | null; | ||||
|     hoistedNoteId: string; | ||||
|     mainNtxId: string | null; | ||||
|  | ||||
|     notePath?: string | null; | ||||
|     noteId?: string | null; | ||||
|     parentNoteId?: string | null; | ||||
|     viewScope?: ViewScope; | ||||
|  | ||||
|     constructor(ntxId: string | null = null, hoistedNoteId: string = "root", mainNtxId: string | null = null) { | ||||
|         super(); | ||||
|  | ||||
|         this.ntxId = ntxId || NoteContext.generateNtxId(); | ||||
|         this.hoistedNoteId = hoistedNoteId; | ||||
|         this.mainNtxId = mainNtxId; | ||||
|  | ||||
|         this.resetViewScope(); | ||||
|     } | ||||
|  | ||||
|     static generateNtxId() { | ||||
|         return utils.randomString(6); | ||||
|     } | ||||
|  | ||||
|     setEmpty() { | ||||
|         this.notePath = null; | ||||
|         this.noteId = null; | ||||
|         this.parentNoteId = null; | ||||
|         // hoisted note is kept intentionally | ||||
|  | ||||
|         this.triggerEvent("noteSwitched", { | ||||
|             noteContext: this, | ||||
|             notePath: this.notePath | ||||
|         }); | ||||
|  | ||||
|         this.resetViewScope(); | ||||
|     } | ||||
|  | ||||
|     isEmpty() { | ||||
|         return !this.noteId; | ||||
|     } | ||||
|  | ||||
|     async setNote(inputNotePath: string | undefined, opts: SetNoteOpts = {}) { | ||||
|         opts.triggerSwitchEvent = opts.triggerSwitchEvent !== undefined ? opts.triggerSwitchEvent : true; | ||||
|         opts.viewScope = opts.viewScope || {}; | ||||
|         opts.viewScope.viewMode = opts.viewScope.viewMode || "default"; | ||||
|  | ||||
|         if (!inputNotePath) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const resolvedNotePath = await this.getResolvedNotePath(inputNotePath); | ||||
|  | ||||
|         if (!resolvedNotePath) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (this.notePath === resolvedNotePath && utils.areObjectsEqual(this.viewScope, opts.viewScope)) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await this.triggerEvent("beforeNoteSwitch", { noteContext: this }); | ||||
|  | ||||
|         utils.closeActiveDialog(); | ||||
|  | ||||
|         this.notePath = resolvedNotePath; | ||||
|         this.viewScope = opts.viewScope; | ||||
|         ({ noteId: this.noteId, parentNoteId: this.parentNoteId } = treeService.getNoteIdAndParentIdFromUrl(resolvedNotePath)); | ||||
|  | ||||
|         this.saveToRecentNotes(resolvedNotePath); | ||||
|  | ||||
|         protectedSessionHolder.touchProtectedSessionIfNecessary(this.note); | ||||
|  | ||||
|         if (opts.triggerSwitchEvent) { | ||||
|             await this.triggerEvent("noteSwitched", { | ||||
|                 noteContext: this, | ||||
|                 notePath: this.notePath | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         await this.setHoistedNoteIfNeeded(); | ||||
|  | ||||
|         if (utils.isMobile()) { | ||||
|             this.triggerCommand("setActiveScreen", { screen: "detail" }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async setHoistedNoteIfNeeded() { | ||||
|         if (this.hoistedNoteId === "root" && this.notePath?.startsWith("root/_hidden") && !this.note?.isLabelTruthy("keepCurrentHoisting")) { | ||||
|             // hidden subtree displays only when hoisted, so it doesn't make sense to keep root as hoisted note | ||||
|  | ||||
|             let hoistedNoteId = "_hidden"; | ||||
|  | ||||
|             if (this.note?.isLaunchBarConfig()) { | ||||
|                 hoistedNoteId = "_lbRoot"; | ||||
|             } else if (this.note?.isOptions()) { | ||||
|                 hoistedNoteId = "_options"; | ||||
|             } | ||||
|  | ||||
|             await this.setHoistedNoteId(hoistedNoteId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     getSubContexts() { | ||||
|         return appContext.tabManager.noteContexts.filter((nc) => nc.ntxId === this.ntxId || nc.mainNtxId === this.ntxId); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A main context represents a tab and also the first split. Further splits are the children contexts of the main context. | ||||
|      * Imagine you have a tab with 3 splits, each showing notes A, B, C (in this order). | ||||
|      * In such a scenario, A context is the main context (also representing the tab as a whole), and B, C are the children | ||||
|      * of context A. | ||||
|      * | ||||
|      * @returns {boolean} true if the context is main (= tab) | ||||
|      */ | ||||
|     isMainContext() { | ||||
|         // if null, then this is a main context | ||||
|         return !this.mainNtxId; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * See docs for isMainContext() for better explanation. | ||||
|      * | ||||
|      * @returns {NoteContext} | ||||
|      */ | ||||
|     getMainContext() { | ||||
|         if (this.mainNtxId) { | ||||
|             try { | ||||
|                 return appContext.tabManager.getNoteContextById(this.mainNtxId); | ||||
|             } catch (e) { | ||||
|                 this.mainNtxId = null; | ||||
|                 return this; | ||||
|             } | ||||
|         } else { | ||||
|             return this; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     saveToRecentNotes(resolvedNotePath: string) { | ||||
|         setTimeout(async () => { | ||||
|             // we include the note in the recent list only if the user stayed on the note at least 5 seconds | ||||
|             if (resolvedNotePath && resolvedNotePath === this.notePath) { | ||||
|                 await server.post("recent-notes", { | ||||
|                     noteId: this.note?.noteId, | ||||
|                     notePath: this.notePath | ||||
|                 }); | ||||
|                 utils.reloadTray(); | ||||
|             } | ||||
|         }, 5000); | ||||
|     } | ||||
|  | ||||
|     async getResolvedNotePath(inputNotePath: string) { | ||||
|         const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId); | ||||
|  | ||||
|         if (!resolvedNotePath) { | ||||
|             logError(`Cannot resolve note path ${inputNotePath}`); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if ((await hoistedNoteService.checkNoteAccess(resolvedNotePath, this)) === false) { | ||||
|             return; // note is outside of hoisted subtree and user chose not to unhoist | ||||
|         } | ||||
|  | ||||
|         return resolvedNotePath; | ||||
|     } | ||||
|  | ||||
|     get note(): FNote | null { | ||||
|         if (!this.noteId || !(this.noteId in froca.notes)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         return froca.notes[this.noteId]; | ||||
|     } | ||||
|  | ||||
|     /** @returns {string[]} */ | ||||
|     get notePathArray() { | ||||
|         return this.notePath ? this.notePath.split("/") : []; | ||||
|     } | ||||
|  | ||||
|     isActive() { | ||||
|         return appContext.tabManager.activeNtxId === this.ntxId; | ||||
|     } | ||||
|  | ||||
|     getPojoState() { | ||||
|         if (this.hoistedNoteId !== "root") { | ||||
|             // keeping empty hoisted tab is esp. important for mobile (e.g. opened launcher config) | ||||
|  | ||||
|             if (!this.notePath && this.getSubContexts().length === 0) { | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return { | ||||
|             ntxId: this.ntxId, | ||||
|             mainNtxId: this.mainNtxId, | ||||
|             notePath: this.notePath, | ||||
|             hoistedNoteId: this.hoistedNoteId, | ||||
|             active: this.isActive(), | ||||
|             viewScope: this.viewScope | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     async unhoist() { | ||||
|         await this.setHoistedNoteId("root"); | ||||
|     } | ||||
|  | ||||
|     async setHoistedNoteId(noteIdToHoist: string) { | ||||
|         if (this.hoistedNoteId === noteIdToHoist) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.hoistedNoteId = noteIdToHoist; | ||||
|  | ||||
|         if (!this.notePathArray?.includes(noteIdToHoist)) { | ||||
|             await this.setNote(noteIdToHoist); | ||||
|         } | ||||
|  | ||||
|         await this.triggerEvent("hoistedNoteChanged", { | ||||
|             noteId: noteIdToHoist, | ||||
|             ntxId: this.ntxId | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** @returns {Promise<boolean>} */ | ||||
|     async isReadOnly() { | ||||
|         if (this?.viewScope?.readOnlyTemporarilyDisabled) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         // "readOnly" is a state valid only for text/code notes | ||||
|         if (!this.note || (this.note.type !== "text" && this.note.type !== "code")) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (this.note.isLabelTruthy("readOnly")) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         if (this.viewScope?.viewMode === "source") { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         const blob = await this.note.getBlob(); | ||||
|         if (!blob) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         const sizeLimit = this.note.type === "text" ? options.getInt("autoReadonlySizeText") : options.getInt("autoReadonlySizeCode"); | ||||
|  | ||||
|         return sizeLimit && blob.contentLength > sizeLimit && !this.note.isLabelTruthy("autoReadOnlyDisabled"); | ||||
|     } | ||||
|  | ||||
|     async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         if (this.noteId && loadResults.isNoteReloaded(this.noteId)) { | ||||
|             const noteRow = loadResults.getEntityRow("notes", this.noteId); | ||||
|  | ||||
|             if (noteRow.isDeleted) { | ||||
|                 this.noteId = null; | ||||
|                 this.notePath = null; | ||||
|  | ||||
|                 this.triggerEvent("noteSwitched", { | ||||
|                     noteContext: this, | ||||
|                     notePath: this.notePath | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     hasNoteList() { | ||||
|         return ( | ||||
|             this.note && | ||||
|             ["default", "contextual-help"].includes(this.viewScope?.viewMode ?? "") && | ||||
|             (this.note.hasChildren() || this.note.getLabelValue("viewType") === "calendar") && | ||||
|             ["book", "text", "code"].includes(this.note.type) && | ||||
|             this.note.mime !== "text/x-sqlite;schema=trilium" && | ||||
|             !this.note.isLabelTruthy("hideChildrenOverview") | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     async getTextEditor(callback?: GetTextEditorCallback) { | ||||
|         return this.timeout<TextEditor>( | ||||
|             new Promise((resolve) => | ||||
|                 appContext.triggerCommand("executeWithTextEditor", { | ||||
|                     callback, | ||||
|                     resolve, | ||||
|                     ntxId: this.ntxId | ||||
|                 }) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     async getCodeEditor() { | ||||
|         return this.timeout( | ||||
|             new Promise<CodeMirrorInstance>((resolve) => | ||||
|                 appContext.triggerCommand("executeWithCodeEditor", { | ||||
|                     resolve, | ||||
|                     ntxId: this.ntxId | ||||
|                 }) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a promise which will retrieve the JQuery element of the content of this note context. | ||||
|      * | ||||
|      * Do note that retrieving the content element needs to be handled by the type widget, which is the one which | ||||
|      * provides the content element by listening to the `executeWithContentElement` event. Not all note types support | ||||
|      * this. | ||||
|      * | ||||
|      * If no content could be determined `null` is returned instead. | ||||
|      */ | ||||
|     async getContentElement() { | ||||
|         return this.timeout<JQuery<HTMLElement>>( | ||||
|             new Promise((resolve) => | ||||
|                 appContext.triggerCommand("executeWithContentElement", { | ||||
|                     resolve, | ||||
|                     ntxId: this.ntxId | ||||
|                 }) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     async getTypeWidget() { | ||||
|         return this.timeout( | ||||
|             new Promise<TypeWidget | null>((resolve) => | ||||
|                 appContext.triggerCommand("executeWithTypeWidget", { | ||||
|                     resolve, | ||||
|                     ntxId: this.ntxId | ||||
|                 }) | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     timeout<T>(promise: Promise<T | null>) { | ||||
|         return Promise.race([promise, new Promise((res) => setTimeout(() => res(null), 200))]) as Promise<T>; | ||||
|     } | ||||
|  | ||||
|     resetViewScope() { | ||||
|         // view scope contains data specific to one note context and one "view". | ||||
|         // it is used to e.g., make read-only note temporarily editable or to hide TOC | ||||
|         // this is reset after navigating to a different note | ||||
|         this.viewScope = {}; | ||||
|     } | ||||
|  | ||||
|     async getNavigationTitle() { | ||||
|         if (!this.note) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         const { note, viewScope } = this; | ||||
|  | ||||
|         const isNormalView = (viewScope?.viewMode === "default" || viewScope?.viewMode === "contextual-help"); | ||||
|         let title = (isNormalView ? note.title : `${note.title}: ${viewScope?.viewMode}`); | ||||
|  | ||||
|         if (viewScope?.attachmentId) { | ||||
|             // assuming the attachment has been already loaded | ||||
|             const attachment = await note.getAttachmentById(viewScope.attachmentId); | ||||
|  | ||||
|             if (attachment) { | ||||
|                 title += `: ${attachment.title}`; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return title; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default NoteContext; | ||||
		Reference in New Issue
	
	Block a user