mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	chore(monorepo/client): move client source files
This commit is contained in:
		
							
								
								
									
										700
									
								
								apps/client/src/components/tab_manager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										700
									
								
								apps/client/src/components/tab_manager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,700 @@ | ||||
| import Component from "./component.js"; | ||||
| import SpacedUpdate from "../services/spaced_update.js"; | ||||
| import server from "../services/server.js"; | ||||
| import options from "../services/options.js"; | ||||
| import froca from "../services/froca.js"; | ||||
| import treeService from "../services/tree.js"; | ||||
| import NoteContext from "./note_context.js"; | ||||
| import appContext from "./app_context.js"; | ||||
| import Mutex from "../utils/mutex.js"; | ||||
| import linkService from "../services/link.js"; | ||||
| import type { EventData } from "./app_context.js"; | ||||
| import type FNote from "../entities/fnote.js"; | ||||
|  | ||||
| interface TabState { | ||||
|     contexts: NoteContext[]; | ||||
|     position: number; | ||||
| } | ||||
|  | ||||
| interface NoteContextState { | ||||
|     ntxId: string; | ||||
|     mainNtxId: string | null; | ||||
|     notePath: string | null; | ||||
|     hoistedNoteId: string; | ||||
|     active: boolean; | ||||
|     viewScope: Record<string, any>; | ||||
| } | ||||
|  | ||||
| export default class TabManager extends Component { | ||||
|     public children: NoteContext[]; | ||||
|     public mutex: Mutex; | ||||
|     public activeNtxId: string | null; | ||||
|     public recentlyClosedTabs: TabState[]; | ||||
|     public tabsUpdate: SpacedUpdate; | ||||
|  | ||||
|     constructor() { | ||||
|         super(); | ||||
|  | ||||
|         this.children = []; | ||||
|         this.mutex = new Mutex(); | ||||
|         this.activeNtxId = null; | ||||
|         this.recentlyClosedTabs = []; | ||||
|  | ||||
|         this.tabsUpdate = new SpacedUpdate(async () => { | ||||
|             if (!appContext.isMainWindow) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const openNoteContexts = this.noteContexts | ||||
|                 .map((nc) => nc.getPojoState()) | ||||
|                 .filter((t) => !!t); | ||||
|  | ||||
|             await server.put("options", { | ||||
|                 openNoteContexts: JSON.stringify(openNoteContexts) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         appContext.addBeforeUnloadListener(this); | ||||
|     } | ||||
|  | ||||
|     get noteContexts(): NoteContext[] { | ||||
|         return this.children; | ||||
|     } | ||||
|  | ||||
|     get mainNoteContexts(): NoteContext[] { | ||||
|         return this.noteContexts.filter((nc) => !nc.mainNtxId); | ||||
|     } | ||||
|  | ||||
|     async loadTabs() { | ||||
|         try { | ||||
|             const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; | ||||
|  | ||||
|             // preload all notes at once | ||||
|             await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) => | ||||
|                 [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true); | ||||
|  | ||||
|             const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => { | ||||
|                 const noteId = treeService.getNoteIdFromUrl(openTab.notePath); | ||||
|                 if (!noteId || !(noteId in froca.notes)) { | ||||
|                     // note doesn't exist so don't try to open tab for it | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 if (!(openTab.hoistedNoteId in froca.notes)) { | ||||
|                     openTab.hoistedNoteId = "root"; | ||||
|                 } | ||||
|  | ||||
|                 return true; | ||||
|             }); | ||||
|  | ||||
|             // resolve before opened tabs can change this | ||||
|             const parsedFromUrl = linkService.parseNavigationStateFromUrl(window.location.href); | ||||
|  | ||||
|             if (filteredNoteContexts.length === 0) { | ||||
|                 parsedFromUrl.ntxId = parsedFromUrl.ntxId || NoteContext.generateNtxId(); // generate already here, so that we later know which one to activate | ||||
|  | ||||
|                 filteredNoteContexts.push({ | ||||
|                     notePath: parsedFromUrl.notePath || "root", | ||||
|                     ntxId: parsedFromUrl.ntxId, | ||||
|                     active: true, | ||||
|                     hoistedNoteId: parsedFromUrl.hoistedNoteId || "root", | ||||
|                     viewScope: parsedFromUrl.viewScope || {} | ||||
|                 }); | ||||
|             } else if (!filteredNoteContexts.find((tab: NoteContextState) => tab.active)) { | ||||
|                 filteredNoteContexts[0].active = true; | ||||
|             } | ||||
|  | ||||
|             await this.tabsUpdate.allowUpdateWithoutChange(async () => { | ||||
|                 for (const tab of filteredNoteContexts) { | ||||
|                     await this.openContextWithNote(tab.notePath, { | ||||
|                         activate: tab.active, | ||||
|                         ntxId: tab.ntxId, | ||||
|                         mainNtxId: tab.mainNtxId, | ||||
|                         hoistedNoteId: tab.hoistedNoteId, | ||||
|                         viewScope: tab.viewScope | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             // if there's a notePath in the URL, make sure it's open and active | ||||
|             // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window) | ||||
|             if (parsedFromUrl.notePath) { | ||||
|                 await appContext.tabManager.switchToNoteContext( | ||||
|                     parsedFromUrl.ntxId, | ||||
|                     parsedFromUrl.notePath, | ||||
|                     parsedFromUrl.viewScope, | ||||
|                     parsedFromUrl.hoistedNoteId | ||||
|                 ); | ||||
|             } else if (parsedFromUrl.searchString) { | ||||
|                 await appContext.triggerCommand("searchNotes", { | ||||
|                     searchString: parsedFromUrl.searchString | ||||
|                 }); | ||||
|             } | ||||
|         } catch (e: unknown) { | ||||
|             if (e instanceof Error) { | ||||
|                 logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`); | ||||
|             } else { | ||||
|                 logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${String(e)}`); | ||||
|             } | ||||
|  | ||||
|             // try to recover | ||||
|             await this.openEmptyTab(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     noteSwitchedEvent({ noteContext }: EventData<"noteSwitched">) { | ||||
|         if (noteContext.isActive()) { | ||||
|             this.setCurrentNavigationStateToHash(); | ||||
|         } | ||||
|  | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     setCurrentNavigationStateToHash() { | ||||
|         const calculatedHash = this.calculateHash(); | ||||
|  | ||||
|         // update if it's the first history entry or there has been a change | ||||
|         if (window.history.length === 0 || calculatedHash !== window.location?.hash) { | ||||
|             // using pushState instead of directly modifying document.location because it does not trigger hashchange | ||||
|             window.history.pushState(null, "", calculatedHash); | ||||
|         } | ||||
|  | ||||
|         const activeNoteContext = this.getActiveContext(); | ||||
|         this.updateDocumentTitle(activeNoteContext); | ||||
|  | ||||
|         this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event | ||||
|     } | ||||
|  | ||||
|     calculateHash(): string { | ||||
|         const activeNoteContext = this.getActiveContext(); | ||||
|         if (!activeNoteContext) { | ||||
|             return ""; | ||||
|         } | ||||
|  | ||||
|         return linkService.calculateHash({ | ||||
|             notePath: activeNoteContext.notePath, | ||||
|             ntxId: activeNoteContext.ntxId, | ||||
|             hoistedNoteId: activeNoteContext.hoistedNoteId, | ||||
|             viewScope: activeNoteContext.viewScope | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     getNoteContexts(): NoteContext[] { | ||||
|         return this.noteContexts; | ||||
|     } | ||||
|  | ||||
|     getMainNoteContexts(): NoteContext[] { | ||||
|         return this.noteContexts.filter((nc) => nc.isMainContext()); | ||||
|     } | ||||
|  | ||||
|     getNoteContextById(ntxId: string | null): NoteContext { | ||||
|         const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId); | ||||
|  | ||||
|         if (!noteContext) { | ||||
|             throw new Error(`Cannot find noteContext id='${ntxId}'`); | ||||
|         } | ||||
|  | ||||
|         return noteContext; | ||||
|     } | ||||
|  | ||||
|     getActiveContext(): NoteContext | null { | ||||
|         return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null; | ||||
|     } | ||||
|  | ||||
|     getActiveMainContext(): NoteContext | null { | ||||
|         return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null; | ||||
|     } | ||||
|  | ||||
|     getActiveContextNotePath(): string | null { | ||||
|         const activeContext = this.getActiveContext(); | ||||
|         return activeContext?.notePath ?? null; | ||||
|     } | ||||
|  | ||||
|     getActiveContextNote(): FNote | null { | ||||
|         const activeContext = this.getActiveContext(); | ||||
|         return activeContext ? activeContext.note : null; | ||||
|     } | ||||
|  | ||||
|     getActiveContextNoteId(): string | null { | ||||
|         const activeNote = this.getActiveContextNote(); | ||||
|         return activeNote ? activeNote.noteId : null; | ||||
|     } | ||||
|  | ||||
|     getActiveContextNoteType(): string | null { | ||||
|         const activeNote = this.getActiveContextNote(); | ||||
|         return activeNote ? activeNote.type : null; | ||||
|     } | ||||
|  | ||||
|     getActiveContextNoteMime(): string | null { | ||||
|         const activeNote = this.getActiveContextNote(); | ||||
|         return activeNote ? activeNote.mime : null; | ||||
|     } | ||||
|  | ||||
|     async switchToNoteContext( | ||||
|         ntxId: string | null, | ||||
|         notePath: string, | ||||
|         viewScope: Record<string, any> = {}, | ||||
|         hoistedNoteId: string | null = null | ||||
|     ) { | ||||
|         const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || | ||||
|             await this.openEmptyTab(); | ||||
|  | ||||
|         await this.activateNoteContext(noteContext.ntxId); | ||||
|  | ||||
|         if (hoistedNoteId) { | ||||
|             await noteContext.setHoistedNoteId(hoistedNoteId); | ||||
|         } | ||||
|  | ||||
|         if (notePath) { | ||||
|             await noteContext.setNote(notePath, { viewScope }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async openAndActivateEmptyTab() { | ||||
|         const noteContext = await this.openEmptyTab(); | ||||
|         await this.activateNoteContext(noteContext.ntxId); | ||||
|         noteContext.setEmpty(); | ||||
|     } | ||||
|  | ||||
|     async openEmptyTab( | ||||
|         ntxId: string | null = null, | ||||
|         hoistedNoteId: string = "root", | ||||
|         mainNtxId: string | null = null | ||||
|     ): Promise<NoteContext> { | ||||
|         const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId); | ||||
|  | ||||
|         const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId); | ||||
|  | ||||
|         if (existingNoteContext) { | ||||
|             await existingNoteContext.setHoistedNoteId(hoistedNoteId); | ||||
|             return existingNoteContext; | ||||
|         } | ||||
|  | ||||
|         this.child(noteContext); | ||||
|  | ||||
|         await this.triggerEvent("newNoteContextCreated", { noteContext }); | ||||
|  | ||||
|         return noteContext; | ||||
|     } | ||||
|  | ||||
|     async openInNewTab(targetNoteId: string, hoistedNoteId: string | null = null) { | ||||
|         const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext()?.hoistedNoteId); | ||||
|  | ||||
|         await noteContext.setNote(targetNoteId); | ||||
|     } | ||||
|  | ||||
|     async openInSameTab(targetNoteId: string, hoistedNoteId: string | null = null) { | ||||
|         const activeContext = this.getActiveContext(); | ||||
|         if (!activeContext) return; | ||||
|  | ||||
|         await activeContext.setHoistedNoteId(hoistedNoteId || activeContext.hoistedNoteId); | ||||
|         await activeContext.setNote(targetNoteId); | ||||
|     } | ||||
|  | ||||
|     async openTabWithNoteWithHoisting( | ||||
|         notePath: string, | ||||
|         opts: { | ||||
|             activate?: boolean | null; | ||||
|             ntxId?: string | null; | ||||
|             mainNtxId?: string | null; | ||||
|             hoistedNoteId?: string | null; | ||||
|             viewScope?: Record<string, any> | null; | ||||
|         } = {} | ||||
|     ): Promise<NoteContext> { | ||||
|         const noteContext = this.getActiveContext(); | ||||
|         let hoistedNoteId = "root"; | ||||
|  | ||||
|         if (noteContext) { | ||||
|             const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId); | ||||
|  | ||||
|             if (resolvedNotePath?.includes(noteContext.hoistedNoteId) || resolvedNotePath?.includes("_hidden")) { | ||||
|                 hoistedNoteId = noteContext.hoistedNoteId; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         opts.hoistedNoteId = hoistedNoteId; | ||||
|  | ||||
|         return this.openContextWithNote(notePath, opts); | ||||
|     } | ||||
|  | ||||
|     async openContextWithNote( | ||||
|         notePath: string | null, | ||||
|         opts: { | ||||
|             activate?: boolean | null; | ||||
|             ntxId?: string | null; | ||||
|             mainNtxId?: string | null; | ||||
|             hoistedNoteId?: string | null; | ||||
|             viewScope?: Record<string, any> | null; | ||||
|         } = {} | ||||
|     ): Promise<NoteContext> { | ||||
|         const activate = !!opts.activate; | ||||
|         const ntxId = opts.ntxId || null; | ||||
|         const mainNtxId = opts.mainNtxId || null; | ||||
|         const hoistedNoteId = opts.hoistedNoteId || "root"; | ||||
|         const viewScope = opts.viewScope || { viewMode: "default" }; | ||||
|  | ||||
|         const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId); | ||||
|         if (notePath) { | ||||
|             await noteContext.setNote(notePath, { | ||||
|                 // if activate is false, then send normal noteSwitched event | ||||
|                 triggerSwitchEvent: !activate, | ||||
|                 viewScope: viewScope | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (activate && noteContext.notePath) { | ||||
|             this.activateNoteContext(noteContext.ntxId, false); | ||||
|  | ||||
|             await this.triggerEvent("noteSwitchedAndActivated", { | ||||
|                 noteContext, | ||||
|                 notePath: noteContext.notePath // resolved note path | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         return noteContext; | ||||
|     } | ||||
|  | ||||
|     async activateOrOpenNote(noteId: string) { | ||||
|         for (const noteContext of this.getNoteContexts()) { | ||||
|             if (noteContext.note && noteContext.note.noteId === noteId) { | ||||
|                 this.activateNoteContext(noteContext.ntxId); | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // if no tab with this note has been found we'll create new tab | ||||
|         await this.openContextWithNote(noteId, { activate: true }); | ||||
|     } | ||||
|  | ||||
|     async activateNoteContext(ntxId: string | null, triggerEvent: boolean = true) { | ||||
|         if (!ntxId) { | ||||
|             logError("activateNoteContext: ntxId is null"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (ntxId === this.activeNtxId) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.activeNtxId = ntxId; | ||||
|  | ||||
|         if (triggerEvent) { | ||||
|             await this.triggerEvent("activeContextChanged", { | ||||
|                 noteContext: this.getNoteContextById(ntxId) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|  | ||||
|         this.setCurrentNavigationStateToHash(); | ||||
|     } | ||||
|  | ||||
|     async removeNoteContext(ntxId: string | null): Promise<boolean> { | ||||
|         // removing note context is an async process which can take some time, if users presses CTRL-W quickly, two | ||||
|         // close events could interleave which would then lead to attempting to activate already removed context. | ||||
|         return await this.mutex.runExclusively(async (): Promise<boolean> => { | ||||
|             let noteContextToRemove; | ||||
|  | ||||
|             try { | ||||
|                 noteContextToRemove = this.getNoteContextById(ntxId); | ||||
|             } catch { | ||||
|                 // note context not found | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             if (noteContextToRemove.isMainContext()) { | ||||
|                 const mainNoteContexts = this.getNoteContexts().filter((nc) => nc.isMainContext()); | ||||
|  | ||||
|                 if (mainNoteContexts.length === 1) { | ||||
|                     if (noteContextToRemove.isEmpty()) { | ||||
|                         // this is already the empty note context, no point in closing it and replacing with another | ||||
|                         // empty tab | ||||
|                         return false; | ||||
|                     } | ||||
|  | ||||
|                     await this.openEmptyTab(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // close dangling autocompletes after closing the tab | ||||
|             const $autocompleteEl = $(".aa-input"); | ||||
|             if ("autocomplete" in $autocompleteEl) { | ||||
|                 $autocompleteEl.autocomplete("close"); | ||||
|             } | ||||
|  | ||||
|             const noteContextsToRemove = noteContextToRemove.getSubContexts(); | ||||
|             const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); | ||||
|  | ||||
|             await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) }); | ||||
|  | ||||
|             if (!noteContextToRemove.isMainContext()) { | ||||
|                 const siblings = noteContextToRemove.getMainContext().getSubContexts(); | ||||
|                 const idx = siblings.findIndex((nc) => nc.ntxId === noteContextToRemove.ntxId); | ||||
|                 const contextToActivateIdx = idx === siblings.length - 1 ? idx - 1 : idx + 1; | ||||
|                 const contextToActivate = siblings[contextToActivateIdx]; | ||||
|  | ||||
|                 await this.activateNoteContext(contextToActivate.ntxId); | ||||
|             } else if (this.mainNoteContexts.length <= 1) { | ||||
|                 await this.openAndActivateEmptyTab(); | ||||
|             } else if (ntxIdsToRemove.includes(this.activeNtxId)) { | ||||
|                 const idx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === noteContextToRemove.ntxId); | ||||
|  | ||||
|                 if (idx === this.mainNoteContexts.length - 1) { | ||||
|                     await this.activatePreviousTabCommand(); | ||||
|                 } else { | ||||
|                     await this.activateNextTabCommand(); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.removeNoteContexts(noteContextsToRemove); | ||||
|             return true; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     removeNoteContexts(noteContextsToRemove: NoteContext[]) { | ||||
|         const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); | ||||
|  | ||||
|         const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(nc.ntxId)); | ||||
|  | ||||
|         this.children = this.children.filter((nc) => !ntxIdsToRemove.includes(nc.ntxId)); | ||||
|  | ||||
|         this.addToRecentlyClosedTabs(noteContextsToRemove, position); | ||||
|  | ||||
|         this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) }); | ||||
|  | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     addToRecentlyClosedTabs(noteContexts: NoteContext[], position: number) { | ||||
|         if (noteContexts.length === 1 && noteContexts[0].isEmpty()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.recentlyClosedTabs.push({ contexts: noteContexts, position: position }); | ||||
|     } | ||||
|  | ||||
|     tabReorderEvent({ ntxIdsInOrder }: { ntxIdsInOrder: string[] }) { | ||||
|         const order: Record<string, number> = {}; | ||||
|  | ||||
|         let i = 0; | ||||
|  | ||||
|         for (const ntxId of ntxIdsInOrder) { | ||||
|             for (const noteContext of this.getNoteContextById(ntxId).getSubContexts()) { | ||||
|                 if (noteContext.ntxId) { | ||||
|                     order[noteContext.ntxId] = i++; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.children.sort((a, b) => { | ||||
|             if (!a.ntxId || !b.ntxId) return 0; | ||||
|             return (order[a.ntxId] ?? 0) < (order[b.ntxId] ?? 0) ? -1 : 1; | ||||
|         }); | ||||
|  | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     noteContextReorderEvent({ | ||||
|         ntxIdsInOrder, | ||||
|         oldMainNtxId, | ||||
|         newMainNtxId | ||||
|     }: { | ||||
|         ntxIdsInOrder: string[]; | ||||
|         oldMainNtxId?: string; | ||||
|         newMainNtxId?: string; | ||||
|     }) { | ||||
|         const order = Object.fromEntries(ntxIdsInOrder.map((v, i) => [v, i])); | ||||
|  | ||||
|         this.children.sort((a, b) => { | ||||
|             if (!a.ntxId || !b.ntxId) return 0; | ||||
|             return (order[a.ntxId] ?? 0) < (order[b.ntxId] ?? 0) ? -1 : 1; | ||||
|         }); | ||||
|  | ||||
|         if (oldMainNtxId && newMainNtxId) { | ||||
|             this.children.forEach((c) => { | ||||
|                 if (c.ntxId === newMainNtxId) { | ||||
|                     // new main context has null mainNtxId | ||||
|                     c.mainNtxId = null; | ||||
|                 } else if (c.ntxId === oldMainNtxId || c.mainNtxId === oldMainNtxId) { | ||||
|                     // old main context or subcontexts all have the new mainNtxId | ||||
|                     c.mainNtxId = newMainNtxId; | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     async activateNextTabCommand() { | ||||
|         const activeMainNtxId = this.getActiveMainContext()?.ntxId; | ||||
|         if (!activeMainNtxId) return; | ||||
|  | ||||
|         const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId); | ||||
|         const newActiveNtxId = this.mainNoteContexts[oldIdx === this.mainNoteContexts.length - 1 ? 0 : oldIdx + 1].ntxId; | ||||
|  | ||||
|         await this.activateNoteContext(newActiveNtxId); | ||||
|     } | ||||
|  | ||||
|     async activatePreviousTabCommand() { | ||||
|         const activeMainNtxId = this.getActiveMainContext()?.ntxId; | ||||
|         if (!activeMainNtxId) return; | ||||
|  | ||||
|         const oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId); | ||||
|         const newActiveNtxId = this.mainNoteContexts[oldIdx === 0 ? this.mainNoteContexts.length - 1 : oldIdx - 1].ntxId; | ||||
|  | ||||
|         await this.activateNoteContext(newActiveNtxId); | ||||
|     } | ||||
|  | ||||
|     async closeActiveTabCommand() { | ||||
|         await this.removeNoteContext(this.activeNtxId); | ||||
|     } | ||||
|  | ||||
|     beforeUnloadEvent(): boolean { | ||||
|         this.tabsUpdate.updateNowIfNecessary(); | ||||
|         return true; // don't block closing the tab, this metadata is not that important | ||||
|     } | ||||
|  | ||||
|     openNewTabCommand() { | ||||
|         this.openAndActivateEmptyTab(); | ||||
|     } | ||||
|  | ||||
|     async closeAllTabsCommand() { | ||||
|         for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { | ||||
|             await this.removeNoteContext(ntxIdToRemove); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async closeOtherTabsCommand({ ntxId }: { ntxId: string }) { | ||||
|         for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { | ||||
|             if (ntxIdToRemove !== ntxId) { | ||||
|                 await this.removeNoteContext(ntxIdToRemove); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async closeRightTabsCommand({ ntxId }: { ntxId: string }) { | ||||
|         const ntxIds = this.mainNoteContexts.map((nc) => nc.ntxId); | ||||
|         const index = ntxIds.indexOf(ntxId); | ||||
|  | ||||
|         if (index !== -1) { | ||||
|             const idsToRemove = ntxIds.slice(index + 1); | ||||
|             for (const ntxIdToRemove of idsToRemove) { | ||||
|                 await this.removeNoteContext(ntxIdToRemove); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async closeTabCommand({ ntxId }: { ntxId: string }) { | ||||
|         await this.removeNoteContext(ntxId); | ||||
|     } | ||||
|  | ||||
|     async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) { | ||||
|         const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); | ||||
|  | ||||
|         const removed = await this.removeNoteContext(ntxId); | ||||
|  | ||||
|         if (removed) { | ||||
|             this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) { | ||||
|         const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); | ||||
|         this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); | ||||
|     } | ||||
|  | ||||
|     async reopenLastTabCommand() { | ||||
|         const closeLastEmptyTab: NoteContext | undefined = await this.mutex.runExclusively(async () => { | ||||
|             let closeLastEmptyTab | ||||
|             if (this.recentlyClosedTabs.length === 0) { | ||||
|                 return closeLastEmptyTab; | ||||
|             } | ||||
|  | ||||
|             if (this.noteContexts.length === 1 && this.noteContexts[0].isEmpty()) { | ||||
|                 // new empty tab is created after closing the last tab, this reverses the empty tab creation | ||||
|                 closeLastEmptyTab = this.noteContexts[0]; | ||||
|             } | ||||
|  | ||||
|             const lastClosedTab = this.recentlyClosedTabs.pop(); | ||||
|             if (!lastClosedTab) return closeLastEmptyTab; | ||||
|  | ||||
|             const noteContexts = lastClosedTab.contexts; | ||||
|  | ||||
|             for (const noteContext of noteContexts) { | ||||
|                 this.child(noteContext); | ||||
|  | ||||
|                 await this.triggerEvent("newNoteContextCreated", { noteContext }); | ||||
|             } | ||||
|  | ||||
|             //  restore last position of contexts stored in tab manager | ||||
|             const ntxsInOrder = [ | ||||
|                 ...this.noteContexts.slice(0, lastClosedTab.position), | ||||
|                 ...this.noteContexts.slice(-noteContexts.length), | ||||
|                 ...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length) | ||||
|             ]; | ||||
|             this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId).filter((id) => id !== null) }); | ||||
|  | ||||
|             let mainNtx = noteContexts.find((nc) => nc.isMainContext()); | ||||
|             if (mainNtx) { | ||||
|                 // reopened a tab, need to reorder new tab widget in tab row | ||||
|                 await this.triggerEvent("contextsReopened", { | ||||
|                     mainNtxId: mainNtx.ntxId, | ||||
|                     tabPosition: ntxsInOrder.filter((nc) => nc.isMainContext()).findIndex((nc) => nc.ntxId === mainNtx.ntxId) | ||||
|                 }); | ||||
|             } else { | ||||
|                 // reopened a single split, need to reorder the pane widget in split note container | ||||
|                 await this.triggerEvent("contextsReopened", { | ||||
|                     mainNtxId: ntxsInOrder[lastClosedTab.position].ntxId, | ||||
|                     // this is safe since lastClosedTab.position can never be 0 in this case | ||||
|                     tabPosition: lastClosedTab.position - 1 | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             const noteContextToActivate = noteContexts.length === 1 ? noteContexts[0] : noteContexts.find((nc) => nc.isMainContext()); | ||||
|             if (!noteContextToActivate) return closeLastEmptyTab; | ||||
|  | ||||
|             await this.activateNoteContext(noteContextToActivate.ntxId); | ||||
|  | ||||
|             await this.triggerEvent("noteSwitched", { | ||||
|                 noteContext: noteContextToActivate, | ||||
|                 notePath: noteContextToActivate.notePath | ||||
|             }); | ||||
|             return closeLastEmptyTab; | ||||
|         }); | ||||
|  | ||||
|         if (closeLastEmptyTab) { | ||||
|             await this.removeNoteContext(closeLastEmptyTab.ntxId); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     hoistedNoteChangedEvent() { | ||||
|         this.tabsUpdate.scheduleUpdate(); | ||||
|     } | ||||
|  | ||||
|     async updateDocumentTitle(activeNoteContext: NoteContext | null) { | ||||
|         if (!activeNoteContext) return; | ||||
|  | ||||
|         const titleFragments = [ | ||||
|             // it helps to navigate in history if note title is included in the title | ||||
|             await activeNoteContext.getNavigationTitle(), | ||||
|             "TriliumNext Notes" | ||||
|         ].filter(Boolean); | ||||
|  | ||||
|         document.title = titleFragments.join(" - "); | ||||
|     } | ||||
|  | ||||
|     async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { | ||||
|         const activeContext = this.getActiveContext(); | ||||
|  | ||||
|         if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) { | ||||
|             await this.updateDocumentTitle(activeContext); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async frocaReloadedEvent() { | ||||
|         const activeContext = this.getActiveContext(); | ||||
|         if (activeContext) { | ||||
|             await this.updateDocumentTitle(activeContext); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user