diff --git a/src/public/app/components/app_context.ts b/src/public/app/components/app_context.ts index 3357cacef..e06ee5dd7 100644 --- a/src/public/app/components/app_context.ts +++ b/src/public/app/components/app_context.ts @@ -22,7 +22,6 @@ import type LoadResults from "../services/load_results.js"; import type { Attribute } from "../services/attribute_parser.js"; import type NoteTreeWidget from "../widgets/note_tree.js"; import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js"; -import type { ContextMenuEvent } from "../menus/context_menu.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js"; interface Layout { @@ -56,8 +55,8 @@ export interface ContextMenuCommandData extends CommandData { } export interface NoteCommandData extends CommandData { - notePath?: string; - hoistedNoteId?: string; + notePath?: string | null; + hoistedNoteId?: string | null; viewScope?: ViewScope; } @@ -172,9 +171,9 @@ export type CommandMappings = { callback: (value: NoteDetailWidget | PromiseLike) => void; }; executeWithTextEditor: CommandData & - ExecuteCommandData & { - callback?: GetTextEditorCallback; - }; + ExecuteCommandData & { + callback?: GetTextEditorCallback; + }; executeWithCodeEditor: CommandData & ExecuteCommandData; /** * Called upon when attempting to retrieve the content element of a {@link NoteContext}. @@ -326,7 +325,7 @@ type EventMappings = { ntxId: string | null; }; contextsReopenedEvent: { - mainNtxId: string; + mainNtxId: string | null; tabPosition: number; }; noteDetailRefreshed: { @@ -340,7 +339,7 @@ type EventMappings = { newNoteContextCreated: { noteContext: NoteContext; }; - noteContextRemovedEvent: { + noteContextRemoved: { ntxIds: string[]; }; exportSvg: { @@ -361,6 +360,7 @@ type EventMappings = { relationMapResetPanZoom: { ntxId: string | null | undefined }; relationMapResetZoomIn: { ntxId: string | null | undefined }; relationMapResetZoomOut: { ntxId: string | null | undefined }; + activeNoteChangedEvent: {}; }; export type EventListener = { diff --git a/src/public/app/components/entrypoints.ts b/src/public/app/components/entrypoints.ts index 69651de17..21761ba16 100644 --- a/src/public/app/components/entrypoints.ts +++ b/src/public/app/components/entrypoints.ts @@ -66,12 +66,13 @@ export default class Entrypoints extends Component { } async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) { - if (!noteId) { + const activeNoteContext = appContext.tabManager.getActiveContext(); + + if (!activeNoteContext || !noteId) { return; } const noteToHoist = await froca.getNote(noteId); - const activeNoteContext = appContext.tabManager.getActiveContext(); if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) { await activeNoteContext.unhoist(); @@ -83,6 +84,11 @@ export default class Entrypoints extends Component { async hoistNoteCommand({ noteId }: { noteId: string }) { const noteContext = appContext.tabManager.getActiveContext(); + if (!noteContext) { + logError("hoistNoteCommand: noteContext is null"); + return; + } + if (noteContext.hoistedNoteId !== noteId) { await noteContext.setHoistedNoteId(noteId); } @@ -174,7 +180,11 @@ export default class Entrypoints extends Component { } async runActiveNoteCommand() { - const { ntxId, note } = appContext.tabManager.getActiveContext(); + const noteContext = appContext.tabManager.getActiveContext(); + if (!noteContext) { + return; + } + const { ntxId, note } = noteContext; // ctrl+enter is also used elsewhere, so make sure we're running only when appropriate if (!note || note.type !== "code") { diff --git a/src/public/app/components/shortcut_component.ts b/src/public/app/components/shortcut_component.ts index 11c20342a..de2bded97 100644 --- a/src/public/app/components/shortcut_component.ts +++ b/src/public/app/components/shortcut_component.ts @@ -17,7 +17,7 @@ export default class ShortcutComponent extends Component implements EventListene } bindNoteShortcutHandler(labelOrRow: AttributeRow) { - const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId); + const handler = () => appContext.tabManager.getActiveContext()?.setNote(labelOrRow.noteId); const namespace = labelOrRow.attributeId; if (labelOrRow.isDeleted) { diff --git a/src/public/app/components/tab_manager.js b/src/public/app/components/tab_manager.ts similarity index 71% rename from src/public/app/components/tab_manager.js rename to src/public/app/components/tab_manager.ts index 7a7b2f2cf..b441c7819 100644 --- a/src/public/app/components/tab_manager.js +++ b/src/public/app/components/tab_manager.ts @@ -4,23 +4,40 @@ 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 utils from "../services/utils.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; +} export default class TabManager extends Component { + public children: NoteContext[]; + public mutex: Mutex; + public activeNtxId: string | null; + public recentlyClosedTabs: TabState[]; + public tabsUpdate: SpacedUpdate; + constructor() { super(); - /** @property {NoteContext[]} */ this.children = []; this.mutex = new Mutex(); - this.activeNtxId = null; - - // elements are arrays of {contexts, position}, storing note contexts for each tab (one main context + subcontexts [splits]), and the original position of the tab this.recentlyClosedTabs = []; this.tabsUpdate = new SpacedUpdate(async () => { @@ -28,7 +45,9 @@ export default class TabManager extends Component { return; } - const openNoteContexts = this.noteContexts.map((nc) => nc.getPojoState()).filter((t) => !!t); + const openNoteContexts = this.noteContexts + .map((nc) => nc.getPojoState()) + .filter((t) => !!t); await server.put("options", { openNoteContexts: JSON.stringify(openNoteContexts) @@ -38,13 +57,11 @@ export default class TabManager extends Component { appContext.addBeforeUnloadListener(this); } - /** @returns {NoteContext[]} */ - get noteContexts() { + get noteContexts(): NoteContext[] { return this.children; } - /** @type {NoteContext[]} */ - get mainNoteContexts() { + get mainNoteContexts(): NoteContext[] { return this.noteContexts.filter((nc) => !nc.mainNtxId); } @@ -53,11 +70,12 @@ export default class TabManager extends Component { const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; // preload all notes at once - await froca.getNotes([...noteContextsToOpen.flatMap((tab) => [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true); + await froca.getNotes([...noteContextsToOpen.flatMap((tab: NoteContextState) => + [treeService.getNoteIdFromUrl(tab.notePath), tab.hoistedNoteId])], true); - const filteredNoteContexts = noteContextsToOpen.filter((openTab) => { + const filteredNoteContexts = noteContextsToOpen.filter((openTab: NoteContextState) => { const noteId = treeService.getNoteIdFromUrl(openTab.notePath); - if (!(noteId in froca.notes)) { + if (noteId && !(noteId in froca.notes)) { // note doesn't exist so don't try to open tab for it return false; } @@ -80,9 +98,10 @@ export default class TabManager extends Component { ntxId: parsedFromUrl.ntxId, active: true, hoistedNoteId: parsedFromUrl.hoistedNoteId || "root", - viewScope: parsedFromUrl.viewScope || {} + viewScope: parsedFromUrl.viewScope || {}, + mainNtxId: null }); - } else if (!filteredNoteContexts.find((tab) => tab.active)) { + } else if (!filteredNoteContexts.find((tab: NoteContextState) => tab.active)) { filteredNoteContexts[0].active = true; } @@ -101,21 +120,30 @@ export default class TabManager extends Component { // 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); + 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) { - logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`); + } 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 }) { + noteSwitchedEvent({ noteContext }: EventData<"noteSwitched">) { if (noteContext.isActive()) { this.setCurrentNavigationStateToHash(); } @@ -135,10 +163,10 @@ export default class TabManager extends Component { const activeNoteContext = this.getActiveContext(); this.updateDocumentTitle(activeNoteContext); - this.triggerEvent("activeNoteChanged"); // trigger this even in on popstate event + this.triggerEvent("activeNoteChangedEvent", {}); // trigger this even in on popstate event } - calculateHash() { + calculateHash(): string { const activeNoteContext = this.getActiveContext(); if (!activeNoteContext) { return ""; @@ -152,21 +180,15 @@ export default class TabManager extends Component { }); } - /** @returns {NoteContext[]} */ - getNoteContexts() { + getNoteContexts(): NoteContext[] { return this.noteContexts; } - /** - * Main context is essentially a tab (children are splits), so this returns tabs. - * @returns {NoteContext[]} - */ - getMainNoteContexts() { + getMainNoteContexts(): NoteContext[] { return this.noteContexts.filter((nc) => nc.isMainContext()); } - /** @returns {NoteContext} */ - getNoteContextById(ntxId) { + getNoteContextById(ntxId: string | null): NoteContext { const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId); if (!noteContext) { @@ -176,58 +198,47 @@ export default class TabManager extends Component { return noteContext; } - /** - * Get active context which represents the visible split with focus. Active context can, but doesn't have to be "main". - * - * @returns {NoteContext} - */ - getActiveContext() { + getActiveContext(): NoteContext | null { return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null; } - /** - * Get active main context which corresponds to the active tab. - * - * @returns {NoteContext} - */ - getActiveMainContext() { + getActiveMainContext(): NoteContext | null { return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null; } - /** @returns {string|null} */ - getActiveContextNotePath() { + getActiveContextNotePath(): string | null { const activeContext = this.getActiveContext(); - return activeContext ? activeContext.notePath : null; + return activeContext?.notePath ?? null; } - /** @returns {FNote} */ - getActiveContextNote() { + getActiveContextNote(): FNote | null { const activeContext = this.getActiveContext(); return activeContext ? activeContext.note : null; } - /** @returns {string|null} */ - getActiveContextNoteId() { + getActiveContextNoteId(): string | null { const activeNote = this.getActiveContextNote(); - return activeNote ? activeNote.noteId : null; } - /** @returns {string|null} */ - getActiveContextNoteType() { + getActiveContextNoteType(): string | null { const activeNote = this.getActiveContextNote(); - return activeNote ? activeNote.type : null; } - /** @returns {string|null} */ - getActiveContextNoteMime() { - const activeNote = this.getActiveContextNote(); + getActiveContextNoteMime(): string | null { + const activeNote = this.getActiveContextNote(); return activeNote ? activeNote.mime : null; } - async switchToNoteContext(ntxId, notePath, viewScope = {}, hoistedNoteId = null) { - const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || (await this.openEmptyTab()); + async switchToNoteContext( + ntxId: string | null, + notePath: string, + viewScope: Record = {}, + hoistedNoteId: string | null = null + ) { + const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || + await this.openEmptyTab(); await this.activateNoteContext(noteContext.ntxId); @@ -242,20 +253,21 @@ export default class TabManager extends Component { async openAndActivateEmptyTab() { const noteContext = await this.openEmptyTab(); - await this.activateNoteContext(noteContext.ntxId); - - await noteContext.setEmpty(); + noteContext.setEmpty(); } - async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId = null) { + async openEmptyTab( + ntxId: string | null = null, + hoistedNoteId: string = "root", + mainNtxId: string | null = null + ): Promise { 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; } @@ -266,29 +278,40 @@ export default class TabManager extends Component { return noteContext; } - async openInNewTab(targetNoteId, hoistedNoteId = null) { - const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext().hoistedNoteId); + 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, hoistedNoteId = null) { + 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); } - /** - * If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab. - */ - async openTabWithNoteWithHoisting(notePath, opts = {}) { + async openTabWithNoteWithHoisting( + notePath: string, + opts: { + activate?: boolean | null; + ntxId?: string | null; + mainNtxId?: string | null; + hoistedNoteId?: string | null; + viewScope?: Record | null; + } = {} + ): Promise { 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")) { + if (resolvedNotePath?.includes(noteContext.hoistedNoteId) || resolvedNotePath?.includes("_hidden")) { hoistedNoteId = noteContext.hoistedNoteId; } } @@ -298,7 +321,16 @@ export default class TabManager extends Component { return this.openContextWithNote(notePath, opts); } - async openContextWithNote(notePath, opts = {}) { + async openContextWithNote( + notePath: string | null, + opts: { + activate?: boolean | null; + ntxId?: string | null; + mainNtxId?: string | null; + hoistedNoteId?: string | null; + viewScope?: Record | null; + } = {} + ): Promise { const activate = !!opts.activate; const ntxId = opts.ntxId || null; const mainNtxId = opts.mainNtxId || null; @@ -315,10 +347,10 @@ export default class TabManager extends Component { }); } - if (activate) { + if (activate && noteContext.notePath) { this.activateNoteContext(noteContext.ntxId, false); - await this.triggerEvent("noteSwitchedAndActivated", { + await this.triggerEvent("noteSwitchedAndActivatedEvent", { noteContext, notePath: noteContext.notePath // resolved note path }); @@ -327,21 +359,24 @@ export default class TabManager extends Component { return noteContext; } - async activateOrOpenNote(noteId) { + 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, triggerEvent = true) { + async activateNoteContext(ntxId: string | null, triggerEvent: boolean = true) { + if (!ntxId) { + logError("activateNoteContext: ntxId is null"); + return; + } + if (ntxId === this.activeNtxId) { return; } @@ -359,11 +394,7 @@ export default class TabManager extends Component { this.setCurrentNavigationStateToHash(); } - /** - * @param ntxId - * @returns {Promise} true if note context has been removed, false otherwise - */ - async removeNoteContext(ntxId) { + async removeNoteContext(ntxId: string | null) { // 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 () => { @@ -373,7 +404,7 @@ export default class TabManager extends Component { noteContextToRemove = this.getNoteContextById(ntxId); } catch { // note context not found - return false; + return; } if (noteContextToRemove.isMainContext()) { @@ -383,7 +414,7 @@ export default class TabManager extends Component { if (noteContextToRemove.isEmpty()) { // this is already the empty note context, no point in closing it and replacing with another // empty tab - return false; + return; } await this.openEmptyTab(); @@ -399,7 +430,7 @@ export default class TabManager extends Component { const noteContextsToRemove = noteContextToRemove.getSubContexts(); const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); - await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove }); + await this.triggerEvent("beforeNoteContextRemove", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) }); if (!noteContextToRemove.isMainContext()) { const siblings = noteContextToRemove.getMainContext().getSubContexts(); @@ -421,12 +452,10 @@ export default class TabManager extends Component { } this.removeNoteContexts(noteContextsToRemove); - - return true; }); } - removeNoteContexts(noteContextsToRemove) { + removeNoteContexts(noteContextsToRemove: NoteContext[]) { const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(nc.ntxId)); @@ -435,12 +464,12 @@ export default class TabManager extends Component { this.addToRecentlyClosedTabs(noteContextsToRemove, position); - this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove }); + this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) }); this.tabsUpdate.scheduleUpdate(); } - addToRecentlyClosedTabs(noteContexts, position) { + addToRecentlyClosedTabs(noteContexts: NoteContext[], position: number) { if (noteContexts.length === 1 && noteContexts[0].isEmpty()) { return; } @@ -448,26 +477,42 @@ export default class TabManager extends Component { this.recentlyClosedTabs.push({ contexts: noteContexts, position: position }); } - tabReorderEvent({ ntxIdsInOrder }) { - const order = {}; + tabReorderEvent({ ntxIdsInOrder }: { ntxIdsInOrder: string[] }) { + const order: Record = {}; let i = 0; for (const ntxId of ntxIdsInOrder) { for (const noteContext of this.getNoteContextById(ntxId).getSubContexts()) { - order[noteContext.ntxId] = i++; + if (noteContext.ntxId) { + order[noteContext.ntxId] = i++; + } } } - this.children.sort((a, b) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1)); + 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 }) { + 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) => (order[a.ntxId] < order[b.ntxId] ? -1 : 1)); + 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) => { @@ -485,7 +530,8 @@ export default class TabManager extends Component { } async activateNextTabCommand() { - const activeMainNtxId = this.getActiveMainContext().ntxId; + 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; @@ -494,7 +540,8 @@ export default class TabManager extends Component { } async activatePreviousTabCommand() { - const activeMainNtxId = this.getActiveMainContext().ntxId; + 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; @@ -503,12 +550,13 @@ export default class TabManager extends Component { } async closeActiveTabCommand() { - await this.removeNoteContext(this.activeNtxId); + if (this.activeNtxId) { + await this.removeNoteContext(this.activeNtxId); + } } - beforeUnloadEvent() { + beforeUnloadEvent(): boolean { this.tabsUpdate.updateNowIfNecessary(); - return true; // don't block closing the tab, this metadata is not that important } @@ -518,35 +566,39 @@ export default class TabManager extends Component { async closeAllTabsCommand() { for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { - await this.removeNoteContext(ntxIdToRemove); - } - } - - async closeOtherTabsCommand({ ntxId }) { - for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { - if (ntxIdToRemove !== ntxId) { + if (ntxIdToRemove) { await this.removeNoteContext(ntxIdToRemove); } } } - async closeRightTabsCommand({ ntxId }) { + async closeOtherTabsCommand({ ntxId }: { ntxId: string }) { + for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { + if (ntxIdToRemove && 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); + if (ntxIdToRemove) { + await this.removeNoteContext(ntxIdToRemove); + } } } } - async closeTabCommand({ ntxId }) { + async closeTabCommand({ ntxId }: { ntxId: string }) { await this.removeNoteContext(ntxId); } - async moveTabToNewWindowCommand({ ntxId }) { + async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) { const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); const removed = await this.removeNoteContext(ntxId); @@ -556,17 +608,16 @@ export default class TabManager extends Component { } } - async copyTabToNewWindowCommand({ ntxId }) { + async copyTabToNewWindowCommand({ ntxId }: { ntxId: string }) { const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); } async reopenLastTabCommand() { - let closeLastEmptyTab = null; - - await this.mutex.runExclusively(async () => { + const closeLastEmptyTab: NoteContext | undefined = await this.mutex.runExclusively(async () => { + let closeLastEmptyTab if (this.recentlyClosedTabs.length === 0) { - return; + return closeLastEmptyTab; } if (this.noteContexts.length === 1 && this.noteContexts[0].isEmpty()) { @@ -575,6 +626,8 @@ export default class TabManager extends Component { } const lastClosedTab = this.recentlyClosedTabs.pop(); + if (!lastClosedTab) return closeLastEmptyTab; + const noteContexts = lastClosedTab.contexts; for (const noteContext of noteContexts) { @@ -589,25 +642,26 @@ export default class TabManager extends Component { ...this.noteContexts.slice(-noteContexts.length), ...this.noteContexts.slice(lastClosedTab.position, -noteContexts.length) ]; - await this.noteContextReorderEvent({ ntxIdsInOrder: ntxsInOrder.map((nc) => nc.ntxId) }); + 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", { + await this.triggerEvent("contextsReopenedEvent", { 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", { - ntxId: ntxsInOrder[lastClosedTab.position].ntxId, + await this.triggerEvent("contextsReopenedEvent", { + mainNtxId: ntxsInOrder[lastClosedTab.position].ntxId, // this is safe since lastClosedTab.position can never be 0 in this case - afterNtxId: ntxsInOrder[lastClosedTab.position - 1].ntxId + 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); @@ -615,6 +669,7 @@ export default class TabManager extends Component { noteContext: noteContextToActivate, notePath: noteContextToActivate.notePath }); + return closeLastEmptyTab; }); if (closeLastEmptyTab) { @@ -626,7 +681,9 @@ export default class TabManager extends Component { this.tabsUpdate.scheduleUpdate(); } - async updateDocumentTitle(activeNoteContext) { + 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(), @@ -636,7 +693,7 @@ export default class TabManager extends Component { document.title = titleFragments.join(" - "); } - async entitiesReloadedEvent({ loadResults }) { + async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { const activeContext = this.getActiveContext(); if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) { @@ -646,7 +703,6 @@ export default class TabManager extends Component { async frocaReloadedEvent() { const activeContext = this.getActiveContext(); - if (activeContext) { await this.updateDocumentTitle(activeContext); } diff --git a/src/public/app/menus/link_context_menu.ts b/src/public/app/menus/link_context_menu.ts index 6456c6519..cef67b7da 100644 --- a/src/public/app/menus/link_context_menu.ts +++ b/src/public/app/menus/link_context_menu.ts @@ -22,13 +22,19 @@ function getItems(): MenuItem[] { function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) { if (!hoistedNoteId) { - hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; + hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null; } if (command === "openNoteInNewTab") { appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); } else if (command === "openNoteInNewSplit") { - const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); + const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts(); + + if (!subContexts) { + logError("subContexts is null"); + return; + } + const { ntxId } = subContexts[subContexts.length - 1]; appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); diff --git a/src/public/app/menus/tree_context_menu.ts b/src/public/app/menus/tree_context_menu.ts index 5c2c74fe9..4cc094e0b 100644 --- a/src/public/app/menus/tree_context_menu.ts +++ b/src/public/app/menus/tree_context_menu.ts @@ -44,7 +44,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener 0) { - activeContext.setNote(parentNotePathArr.join("/")); + if (parentNotePathArr && parentNotePathArr.length > 0) { + activeContext?.setNote(parentNotePathArr.join("/")); } } diff --git a/src/public/app/services/frontend_script_api.ts b/src/public/app/services/frontend_script_api.ts index 86a793c9f..bd30a58cc 100644 --- a/src/public/app/services/frontend_script_api.ts +++ b/src/public/app/services/frontend_script_api.ts @@ -457,13 +457,13 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig this.BasicWidget = BasicWidget; this.activateNote = async (notePath) => { - await appContext.tabManager.getActiveContext().setNote(notePath); + await appContext.tabManager.getActiveContext()?.setNote(notePath); }; this.activateNewNote = async (notePath) => { await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext().setNote(notePath); + await appContext.tabManager.getActiveContext()?.setNote(notePath); await appContext.triggerEvent("focusAndSelectTitle", {}); }; @@ -480,8 +480,8 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig this.openSplitWithNote = async (notePath, activate) => { await ws.waitForMaxKnownEntityChangeId(); - const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); - const { ntxId } = subContexts[subContexts.length - 1]; + const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts(); + const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath }); @@ -591,15 +591,48 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig this.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text }); - this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); - this.getActiveContext = () => appContext.tabManager.getActiveContext(); - this.getActiveMainContext = () => appContext.tabManager.getActiveMainContext(); + this.getActiveContextNote = (): FNote => { + const note = appContext.tabManager.getActiveContextNote(); + if (!note) { + throw new Error("No active context note found"); + } + return note; + }; + + this.getActiveContext = (): NoteContext => { + const context = appContext.tabManager.getActiveContext(); + if (!context) { + throw new Error("No active context found"); + } + return context; + }; + + this.getActiveMainContext = (): NoteContext => { + const context = appContext.tabManager.getActiveMainContext(); + if (!context) { + throw new Error("No active main context found"); + } + return context; + }; this.getNoteContexts = () => appContext.tabManager.getNoteContexts(); this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts(); - this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); - this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); + this.getActiveContextTextEditor = () => { + const context = appContext.tabManager.getActiveContext(); + if (!context) { + throw new Error("No active context found"); + } + return context.getTextEditor(); + }; + + this.getActiveContextCodeEditor = () => { + const context = appContext.tabManager.getActiveContext(); + if (!context) { + throw new Error("No active context found"); + } + return context.getCodeEditor(); + }; this.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve })); this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); @@ -665,5 +698,5 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig } export default FrontendScriptApi as any as { - new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery | null): Api; + new(startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery | null): Api; }; diff --git a/src/public/app/services/import.ts b/src/public/app/services/import.ts index 97cc40c94..0c6c25f11 100644 --- a/src/public/app/services/import.ts +++ b/src/public/app/services/import.ts @@ -80,7 +80,7 @@ ws.subscribeToMessages(async (message) => { toastService.showPersistent(toast); if (message.result.importedNoteId) { - await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId); + await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId); } } }); @@ -102,7 +102,7 @@ ws.subscribeToMessages(async (message) => { toastService.showPersistent(toast); if (message.result.parentNoteId) { - await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId, { + await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId, { viewScope: { viewMode: "attachments" } diff --git a/src/public/app/services/link.ts b/src/public/app/services/link.ts index 8cda26fa5..7cd0a861d 100644 --- a/src/public/app/services/link.ts +++ b/src/public/app/services/link.ts @@ -288,11 +288,15 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext(); - noteContext.setNote(notePath, { viewScope }).then(() => { - if (noteContext !== appContext.tabManager.getActiveContext()) { - appContext.tabManager.activateNoteContext(noteContext.ntxId); - } - }); + if (noteContext) { + noteContext.setNote(notePath, { viewScope }).then(() => { + if (noteContext !== appContext.tabManager.getActiveContext()) { + appContext.tabManager.activateNoteContext(noteContext.ntxId); + } + }); + } else { + appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true }); + } } } else if (hrefLink) { const withinEditLink = $link?.hasClass("ck-link-actions__preview"); diff --git a/src/public/app/services/note_create.ts b/src/public/app/services/note_create.ts index 90dd94d8f..47cf02bb6 100644 --- a/src/public/app/services/note_create.ts +++ b/src/public/app/services/note_create.ts @@ -86,8 +86,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot await ws.waitForMaxKnownEntityChangeId(); - if (options.activate) { - const activeNoteContext = appContext.tabManager.getActiveContext(); + const activeNoteContext = appContext.tabManager.getActiveContext(); + if (activeNoteContext && options.activate) { await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`); if (options.focus === "title") { @@ -152,8 +152,7 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) { await ws.waitForMaxKnownEntityChangeId(); - const activeNoteContext = appContext.tabManager.getActiveContext(); - activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`); + appContext.tabManager.getActiveContext()?.setNote(`${parentNotePath}/${note.noteId}`); const origNote = await froca.getNote(noteId); toastService.showMessage(t("note_create.duplicated", { title: origNote?.title })); diff --git a/src/public/app/services/tree.ts b/src/public/app/services/tree.ts index e7c94ca0e..ff4c34210 100644 --- a/src/public/app/services/tree.ts +++ b/src/public/app/services/tree.ts @@ -138,7 +138,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) { return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected; } -function getNoteIdFromUrl(urlOrNotePath: string | undefined) { +function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) { if (!urlOrNotePath) { return null; } diff --git a/src/public/app/utils/mutex.ts b/src/public/app/utils/mutex.ts index 76ea57b0d..197e2f134 100644 --- a/src/public/app/utils/mutex.ts +++ b/src/public/app/utils/mutex.ts @@ -16,7 +16,7 @@ export default class Mutex { return newPromise; } - async runExclusively(cb: () => Promise) { + async runExclusively(cb: () => Promise) { const unlock = await this.lock(); try { diff --git a/src/public/app/widgets/buttons/attachments_actions.ts b/src/public/app/widgets/buttons/attachments_actions.ts index d2c0841b7..e7b82b28f 100644 --- a/src/public/app/widgets/buttons/attachments_actions.ts +++ b/src/public/app/widgets/buttons/attachments_actions.ts @@ -171,7 +171,7 @@ export default class AttachmentActionsWidget extends BasicWidget { const { note: newNote } = await server.post>(`attachments/${this.attachmentId}/convert-to-note`); toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext().setNote(newNote.noteId); + await appContext.tabManager.getActiveContext()?.setNote(newNote.noteId); } async renameAttachmentCommand() { diff --git a/src/public/app/widgets/buttons/calendar.ts b/src/public/app/widgets/buttons/calendar.ts index b279e7689..131d176bf 100644 --- a/src/public/app/widgets/buttons/calendar.ts +++ b/src/public/app/widgets/buttons/calendar.ts @@ -43,8 +43,8 @@ const DROPDOWN_TPL = ` data-calendar-input="month"> @@ -149,7 +149,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { const note = await dateNoteService.getDayNote(date); if (note) { - appContext.tabManager.getActiveContext().setNote(note.noteId); + appContext.tabManager.getActiveContext()?.setNote(note.noteId); this.dropdown?.hide(); } else { toastService.showError(t("calendar.cannot_find_day_note")); @@ -189,10 +189,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget { async dropdownShown() { await libraryLoader.requireLibrary(libraryLoader.CALENDAR_WIDGET); - - const activeNote = appContext.tabManager.getActiveContextNote(); - - this.init(activeNote?.getOwnedLabelValue("dateNote")); + this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null); } init(activeDate: string | null) { diff --git a/src/public/app/widgets/buttons/launcher/note_launcher.ts b/src/public/app/widgets/buttons/launcher/note_launcher.ts index 950021091..7622df401 100644 --- a/src/public/app/widgets/buttons/launcher/note_launcher.ts +++ b/src/public/app/widgets/buttons/launcher/note_launcher.ts @@ -78,7 +78,7 @@ export default class NoteLauncher extends AbstractLauncher { } getHoistedNoteId() { - return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext().hoistedNoteId; + return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId; } getTitle() { diff --git a/src/public/app/widgets/buttons/launcher/today_launcher.ts b/src/public/app/widgets/buttons/launcher/today_launcher.ts index 838fa41c2..7e203bb7b 100644 --- a/src/public/app/widgets/buttons/launcher/today_launcher.ts +++ b/src/public/app/widgets/buttons/launcher/today_launcher.ts @@ -10,6 +10,6 @@ export default class TodayLauncher extends NoteLauncher { } getHoistedNoteId() { - return appContext.tabManager.getActiveContext().hoistedNoteId; + return appContext.tabManager.getActiveContext()?.hoistedNoteId; } } diff --git a/src/public/app/widgets/buttons/note_actions.ts b/src/public/app/widgets/buttons/note_actions.ts index dc332b541..a1ecd45d9 100644 --- a/src/public/app/widgets/buttons/note_actions.ts +++ b/src/public/app/widgets/buttons/note_actions.ts @@ -228,7 +228,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget { toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, { + await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { viewScope: { viewMode: "attachments", attachmentId: newAttachment.attachmentId diff --git a/src/public/app/widgets/containers/left_pane_container.ts b/src/public/app/widgets/containers/left_pane_container.ts index bbfedaa2a..21d4b280e 100644 --- a/src/public/app/widgets/containers/left_pane_container.ts +++ b/src/public/app/widgets/containers/left_pane_container.ts @@ -24,8 +24,7 @@ export default class LeftPaneContainer extends FlexContainer { if (visible) { this.triggerEvent("focusTree", {}); } else { - const activeNoteContext = appContext.tabManager.getActiveContext(); - this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId }); + this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId }); } } } diff --git a/src/public/app/widgets/containers/split_note_container.js b/src/public/app/widgets/containers/split_note_container.ts similarity index 64% rename from src/public/app/widgets/containers/split_note_container.js rename to src/public/app/widgets/containers/split_note_container.ts index 28f71369e..9b828120b 100644 --- a/src/public/app/widgets/containers/split_note_container.js +++ b/src/public/app/widgets/containers/split_note_container.ts @@ -1,8 +1,29 @@ import FlexContainer from "./flex_container.js"; import appContext from "../../components/app_context.js"; +import NoteContext from "../../components/note_context.js"; +import type { CommandMappings, EventNames, EventData } from "../../components/app_context.js"; +import type BasicWidget from "../basic_widget.js"; -export default class SplitNoteContainer extends FlexContainer { - constructor(widgetFactory) { +interface NoteContextEvent { + noteContext: NoteContext; +} + +interface SplitNoteWidget extends BasicWidget { + hasBeenAlreadyShown?: boolean; + ntxId?: string; +} + +type WidgetFactory = () => SplitNoteWidget; + +interface Widgets { + [key: string]: SplitNoteWidget; +} + +export default class SplitNoteContainer extends FlexContainer { + private widgetFactory: WidgetFactory; + private widgets: Widgets; + + constructor(widgetFactory: WidgetFactory) { super("row"); this.widgetFactory = widgetFactory; @@ -13,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer { this.collapsible(); } - async newNoteContextCreatedEvent({ noteContext }) { + async newNoteContextCreatedEvent({ noteContext }: NoteContextEvent) { const widget = this.widgetFactory(); const $renderedWidget = widget.render(); @@ -23,19 +44,31 @@ export default class SplitNoteContainer extends FlexContainer { this.$widget.append($renderedWidget); - widget.handleEvent("initialRenderComplete"); + widget.handleEvent("initialRenderComplete", {}); widget.toggleExt(false); - this.widgets[noteContext.ntxId] = widget; + if (noteContext.ntxId) { + this.widgets[noteContext.ntxId] = widget; + } await widget.handleEvent("setNoteContext", { noteContext }); this.child(widget); } - async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }) { - const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId; + async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: { + ntxId: string; + notePath?: string; + hoistedNoteId?: string; + viewScope?: any; + }) { + const mainNtxId = appContext.tabManager.getActiveMainContext()?.ntxId; + + if (!mainNtxId) { + logError("empty mainNtxId!"); + return; + } if (!ntxId) { logError("empty ntxId!"); @@ -43,7 +76,7 @@ export default class SplitNoteContainer extends FlexContainer { ntxId = mainNtxId; } - hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext().hoistedNoteId; + hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId; const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId); @@ -53,7 +86,7 @@ export default class SplitNoteContainer extends FlexContainer { // insert the note context after the originating note context ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId); - this.triggerCommand("noteContextReorder", { ntxIdsInOrder: ntxIds }); + this.triggerCommand("noteContextReorder" as keyof CommandMappings, { ntxIdsInOrder: ntxIds }); // move the note context rendered widget after the originating widget this.$widget.find(`[data-ntx-id="${noteContext.ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${ntxId}"]`)); @@ -67,11 +100,11 @@ export default class SplitNoteContainer extends FlexContainer { } } - closeThisNoteSplitCommand({ ntxId }) { + closeThisNoteSplitCommand({ ntxId }: { ntxId: string }): void { appContext.tabManager.removeNoteContext(ntxId); } - async moveThisNoteSplitCommand({ ntxId, isMovingLeft }) { + async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: { ntxId: string; isMovingLeft: boolean }): Promise { if (!ntxId) { logError("empty ntxId!"); return; @@ -96,7 +129,7 @@ export default class SplitNoteContainer extends FlexContainer { const newNtxIds = [...ntxIds.slice(0, leftIndex), ntxIds[leftIndex + 1], ntxIds[leftIndex], ...ntxIds.slice(leftIndex + 2)]; const isChangingMainContext = !contexts[leftIndex].mainNtxId; - this.triggerCommand("noteContextReorder", { + this.triggerCommand("noteContextReorder" as keyof CommandMappings, { ntxIdsInOrder: newNtxIds, oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null, newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1] : null @@ -109,16 +142,16 @@ export default class SplitNoteContainer extends FlexContainer { await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]); } - activeContextChangedEvent() { + activeContextChangedEvent(): void { this.refresh(); } - noteSwitchedAndActivatedEvent() { + noteSwitchedAndActivatedEvent(): void { this.refresh(); } - noteContextRemovedEvent({ ntxIds }) { - this.children = this.children.filter((c) => !ntxIds.includes(c.ntxId)); + noteContextRemovedEvent({ ntxIds }: { ntxIds: string[] }): void { + this.children = this.children.filter((c) => c.ntxId && !ntxIds.includes(c.ntxId)); for (const ntxId of ntxIds) { this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove(); @@ -127,7 +160,7 @@ export default class SplitNoteContainer extends FlexContainer { } } - contextsReopenedEvent({ ntxId, afterNtxId }) { + contextsReopenedEvent({ ntxId, afterNtxId }: { ntxId?: string; afterNtxId?: string }): void { if (ntxId === undefined || afterNtxId === undefined) { // no single split reopened return; @@ -135,13 +168,11 @@ export default class SplitNoteContainer extends FlexContainer { this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`)); } - async refresh() { + async refresh(): Promise { this.toggleExt(true); } - toggleInt(show) {} // not needed - - toggleExt(show) { + toggleExt(show: boolean): void { const activeMainContext = appContext.tabManager.getActiveMainContext(); const activeNtxId = activeMainContext ? activeMainContext.ntxId : null; @@ -149,7 +180,7 @@ export default class SplitNoteContainer extends FlexContainer { const noteContext = appContext.tabManager.getNoteContextById(ntxId); const widget = this.widgets[ntxId]; - widget.toggleExt(show && activeNtxId && [noteContext.ntxId, noteContext.mainNtxId].includes(activeNtxId)); + widget.toggleExt(show && activeNtxId !== null && [noteContext.ntxId, noteContext.mainNtxId].includes(activeNtxId)); } } @@ -158,41 +189,50 @@ export default class SplitNoteContainer extends FlexContainer { * are not executed, we're waiting for the first tab activation, and then we update the tab. After this initial * activation, further note switches are always propagated to the tabs. */ - handleEventInChildren(name, data) { + handleEventInChildren(name: T, data: EventData): Promise | null { if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) { // this event is propagated only to the widgets of a particular tab - const widget = this.widgets[data.noteContext.ntxId]; + const noteContext = (data as NoteContextEvent).noteContext; + const widget = noteContext.ntxId ? this.widgets[noteContext.ntxId] : undefined; if (!widget) { return Promise.resolve(); } - if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivated" || appContext.tabManager.getActiveMainContext() === data.noteContext.getMainContext()) { + if (widget.hasBeenAlreadyShown || name === "noteSwitchedAndActivatedEvent" || appContext.tabManager.getActiveMainContext() === noteContext.getMainContext()) { widget.hasBeenAlreadyShown = true; - return [widget.handleEvent("noteSwitched", data), this.refreshNotShown(data)]; + return Promise.all([ + widget.handleEvent("noteSwitched", { noteContext, notePath: noteContext.notePath }), + this.refreshNotShown({ noteContext }) + ]); } else { return Promise.resolve(); } } if (name === "activeContextChanged") { - return this.refreshNotShown(data); + return this.refreshNotShown(data as NoteContextEvent); } else { return super.handleEventInChildren(name, data); } } - refreshNotShown(data) { - const promises = []; + private refreshNotShown(data: NoteContextEvent): Promise { + const promises: Promise[] = []; for (const subContext of data.noteContext.getMainContext().getSubContexts()) { + if (!subContext.ntxId) { + continue; + } + const widget = this.widgets[subContext.ntxId]; if (!widget.hasBeenAlreadyShown) { widget.hasBeenAlreadyShown = true; - promises.push(widget.handleEvent("activeContextChanged", { noteContext: subContext })); + const eventPromise = widget.handleEvent("activeContextChanged", { noteContext: subContext }); + promises.push(eventPromise || Promise.resolve()); } } diff --git a/src/public/app/widgets/floating_buttons/code_buttons.ts b/src/public/app/widgets/floating_buttons/code_buttons.ts index 35c7e0af2..ca6491f76 100644 --- a/src/public/app/widgets/floating_buttons/code_buttons.ts +++ b/src/public/app/widgets/floating_buttons/code_buttons.ts @@ -65,7 +65,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget { await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext().setNote(notePath); + await appContext.tabManager.getActiveContext()?.setNote(notePath); toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) })); }); diff --git a/src/public/app/widgets/floating_buttons/help_button.ts b/src/public/app/widgets/floating_buttons/help_button.ts index 6794ddf38..d4ec6e1a3 100644 --- a/src/public/app/widgets/floating_buttons/help_button.ts +++ b/src/public/app/widgets/floating_buttons/help_button.ts @@ -60,15 +60,15 @@ export default class ContextualHelpButton extends NoteContextAwareWidget { doRender() { this.$widget = $(TPL); this.$widget.on("click", () => { - const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); + const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts(); const targetNote = `_help_${this.helpNoteIdToOpen}`; - const helpSubcontext = subContexts.find((s) => s.viewScope?.viewMode === "contextual-help"); + const helpSubcontext = subContexts?.find((s) => s.viewScope?.viewMode === "contextual-help"); const viewScope: ViewScope = { viewMode: "contextual-help" }; if (!helpSubcontext) { // The help is not already open, open a new split with it. - const { ntxId } = subContexts[subContexts.length - 1]; + const { ntxId } = subContexts?.[subContexts.length - 1] ?? {}; this.triggerCommand("openNewNoteSplit", { ntxId, notePath: targetNote, diff --git a/src/public/app/widgets/mobile_widgets/mobile_detail_menu.ts b/src/public/app/widgets/mobile_widgets/mobile_detail_menu.ts index 7d4fc3ddd..58039f910 100644 --- a/src/public/app/widgets/mobile_widgets/mobile_detail_menu.ts +++ b/src/public/app/widgets/mobile_widgets/mobile_detail_menu.ts @@ -28,8 +28,8 @@ class MobileDetailMenuWidget extends BasicWidget { x: e.pageX, y: e.pageY, items: [ - { title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note.type !== "search" }, - { title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note.noteId !== "root" } + { title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" }, + { title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" } ], selectMenuItemHandler: async ({ command }) => { if (command === "insertChildNote") { diff --git a/src/public/app/widgets/note_map.ts b/src/public/app/widgets/note_map.ts index 00855958e..45cefa1d9 100644 --- a/src/public/app/widgets/note_map.ts +++ b/src/public/app/widgets/note_map.ts @@ -322,7 +322,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { .warmupTicks(30) .onNodeClick((node) => { if (node.id) { - appContext.tabManager.getActiveContext().setNote((node as Node).id); + appContext.tabManager.getActiveContext()?.setNote((node as Node).id); } }) .onNodeRightClick((node, e) => { @@ -371,7 +371,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { if (mapRootNoteId === "hoisted") { mapRootNoteId = hoistedNoteService.getHoistedNoteId(); } else if (!mapRootNoteId) { - mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId; + mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId; } return mapRootNoteId ?? ""; diff --git a/src/public/app/widgets/note_tree.ts b/src/public/app/widgets/note_tree.ts index 5d2635c76..d1ec85611 100644 --- a/src/public/app/widgets/note_tree.ts +++ b/src/public/app/widgets/note_tree.ts @@ -424,10 +424,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const activeNoteContext = appContext.tabManager.getActiveContext(); const opts: SetNoteOpts = {}; - if (activeNoteContext.viewScope?.viewMode === "contextual-help") { + if (activeNoteContext?.viewScope?.viewMode === "contextual-help") { opts.viewScope = activeNoteContext.viewScope; } - await activeNoteContext.setNote(notePath, opts); + await activeNoteContext?.setNote(notePath, opts); }, expand: (event, data) => this.setExpanded(data.node.data.branchId, true), collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), @@ -619,10 +619,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { // TODO: Deduplicate with server's notes.ts#getAndValidateParent if (!["search", "launcher"].includes(note.type) - && !note.isOptions() - && !note.isLaunchBarConfig() - && !note.noteId.startsWith("_help") - ) { + && !note.isOptions() + && !note.isLaunchBarConfig() + && !note.noteId.startsWith("_help") + ) { const $createChildNoteButton = $(``).on( "click", cancelClickPropagation @@ -1758,6 +1758,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { await ws.waitForMaxKnownEntityChangeId(); - appContext.tabManager.getActiveContext().setNote(resp.note.noteId); + appContext.tabManager.getActiveContext()?.setNote(resp.note.noteId); } } diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.ts b/src/public/app/widgets/ribbon_widgets/search_definition.ts index 2328f996f..08df15426 100644 --- a/src/public/app/widgets/ribbon_widgets/search_definition.ts +++ b/src/public/app/widgets/ribbon_widgets/search_definition.ts @@ -246,7 +246,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget { await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext().setNote(notePath); + await appContext.tabManager.getActiveContext()?.setNote(notePath); // Note the {{- notePathTitle}} in json file is not typo, it's unescaping // See https://www.i18next.com/translation-function/interpolation#unescape toastService.showMessage(t("search_definition.search_note_saved", { notePathTitle: await treeService.getNotePathTitle(notePath) })); diff --git a/src/public/app/widgets/tab_row.ts b/src/public/app/widgets/tab_row.ts index f0af54493..c726cc528 100644 --- a/src/public/app/widgets/tab_row.ts +++ b/src/public/app/widgets/tab_row.ts @@ -1,10 +1,10 @@ -import Draggabilly, { type DraggabillyCallback, type MoveVector } from "draggabilly"; +import Draggabilly, { type MoveVector } from "draggabilly"; import { t } from "../services/i18n.js"; import BasicWidget from "./basic_widget.js"; import contextMenu from "../menus/context_menu.js"; import utils from "../services/utils.js"; import keyboardActionService from "../services/keyboard_actions.js"; -import appContext, { type CommandData, type CommandListenerData, type EventData } from "../components/app_context.js"; +import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js"; import froca from "../services/froca.js"; import attributeService from "../services/attributes.js"; import type NoteContext from "../components/note_context.js"; @@ -419,13 +419,13 @@ export default class TabRowWidget extends BasicWidget { closeActiveTabCommand({ $el }: CommandListenerData<"closeActiveTab">) { const ntxId = $el.closest(".note-tab").attr("data-ntx-id"); - appContext.tabManager.removeNoteContext(ntxId); + appContext.tabManager.removeNoteContext(ntxId ?? null); } setTabCloseEvent($tab: JQuery) { $tab.on("mousedown", (e) => { if (e.which === 2) { - appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id")); + appContext.tabManager.removeNoteContext($tab.attr("data-ntx-id") ?? null); return true; // event has been handled } @@ -494,7 +494,7 @@ export default class TabRowWidget extends BasicWidget { return $tab.attr("data-ntx-id"); } - noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemovedEvent">) { + noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemoved">) { for (const ntxId of ntxIds) { this.removeTab(ntxId); } @@ -516,7 +516,7 @@ export default class TabRowWidget extends BasicWidget { this.draggabillyDragging.element.style.transform = ""; this.draggabillyDragging.dragEnd(); this.draggabillyDragging.isDragging = false; - this.draggabillyDragging.positionDrag = () => {}; // Prevent Draggabilly from updating tabEl.style.transform in later frames + this.draggabillyDragging.positionDrag = () => { }; // Prevent Draggabilly from updating tabEl.style.transform in later frames this.draggabillyDragging.destroy(); this.draggabillyDragging = null; } @@ -650,7 +650,7 @@ export default class TabRowWidget extends BasicWidget { } contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopenedEvent">) { - if (mainNtxId === undefined || tabPosition === undefined) { + if (!mainNtxId || !tabPosition) { // no tab reopened return; } @@ -748,7 +748,7 @@ export default class TabRowWidget extends BasicWidget { hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) { const $tab = this.getTabById(ntxId); - if ($tab) { + if ($tab && ntxId) { const noteContext = appContext.tabManager.getNoteContextById(ntxId); this.updateTab($tab, noteContext); diff --git a/src/public/app/widgets/view_widgets/calendar_view.ts b/src/public/app/widgets/view_widgets/calendar_view.ts index f34469d7e..ebec32a2e 100644 --- a/src/public/app/widgets/view_widgets/calendar_view.ts +++ b/src/public/app/widgets/view_widgets/calendar_view.ts @@ -155,7 +155,7 @@ export default class CalendarView extends ViewMode { const note = await date_notes.getDayNote(e.dateStr); if (note) { - appContext.tabManager.getActiveContext().setNote(note.noteId); + appContext.tabManager.getActiveContext()?.setNote(note.noteId); } } });