Merge pull request #1320 from TriliumNext/porting_js

chore: port more js to ts
This commit is contained in:
Elian Doran 2025-03-03 23:04:14 +02:00 committed by GitHub
commit 5289f94553
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 381 additions and 237 deletions

View File

@ -22,7 +22,6 @@ import type LoadResults from "../services/load_results.js";
import type { Attribute } from "../services/attribute_parser.js"; import type { Attribute } from "../services/attribute_parser.js";
import type NoteTreeWidget from "../widgets/note_tree.js"; import type NoteTreeWidget from "../widgets/note_tree.js";
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.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"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
interface Layout { interface Layout {
@ -56,8 +55,8 @@ export interface ContextMenuCommandData extends CommandData {
} }
export interface NoteCommandData extends CommandData { export interface NoteCommandData extends CommandData {
notePath?: string; notePath?: string | null;
hoistedNoteId?: string; hoistedNoteId?: string | null;
viewScope?: ViewScope; viewScope?: ViewScope;
} }
@ -172,9 +171,9 @@ export type CommandMappings = {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void; callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
}; };
executeWithTextEditor: CommandData & executeWithTextEditor: CommandData &
ExecuteCommandData<TextEditor> & { ExecuteCommandData<TextEditor> & {
callback?: GetTextEditorCallback; callback?: GetTextEditorCallback;
}; };
executeWithCodeEditor: CommandData & ExecuteCommandData<null>; executeWithCodeEditor: CommandData & ExecuteCommandData<null>;
/** /**
* Called upon when attempting to retrieve the content element of a {@link NoteContext}. * Called upon when attempting to retrieve the content element of a {@link NoteContext}.
@ -326,7 +325,7 @@ type EventMappings = {
ntxId: string | null; ntxId: string | null;
}; };
contextsReopenedEvent: { contextsReopenedEvent: {
mainNtxId: string; mainNtxId: string | null;
tabPosition: number; tabPosition: number;
}; };
noteDetailRefreshed: { noteDetailRefreshed: {
@ -340,7 +339,7 @@ type EventMappings = {
newNoteContextCreated: { newNoteContextCreated: {
noteContext: NoteContext; noteContext: NoteContext;
}; };
noteContextRemovedEvent: { noteContextRemoved: {
ntxIds: string[]; ntxIds: string[];
}; };
exportSvg: { exportSvg: {
@ -361,6 +360,7 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined }; relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined }; relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined }; relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChangedEvent: {};
}; };
export type EventListener<T extends EventNames> = { export type EventListener<T extends EventNames> = {

View File

@ -66,12 +66,13 @@ export default class Entrypoints extends Component {
} }
async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) { async toggleNoteHoistingCommand({ noteId = appContext.tabManager.getActiveContextNoteId() }) {
if (!noteId) { const activeNoteContext = appContext.tabManager.getActiveContext();
if (!activeNoteContext || !noteId) {
return; return;
} }
const noteToHoist = await froca.getNote(noteId); const noteToHoist = await froca.getNote(noteId);
const activeNoteContext = appContext.tabManager.getActiveContext();
if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) { if (noteToHoist?.noteId === activeNoteContext.hoistedNoteId) {
await activeNoteContext.unhoist(); await activeNoteContext.unhoist();
@ -83,6 +84,11 @@ export default class Entrypoints extends Component {
async hoistNoteCommand({ noteId }: { noteId: string }) { async hoistNoteCommand({ noteId }: { noteId: string }) {
const noteContext = appContext.tabManager.getActiveContext(); const noteContext = appContext.tabManager.getActiveContext();
if (!noteContext) {
logError("hoistNoteCommand: noteContext is null");
return;
}
if (noteContext.hoistedNoteId !== noteId) { if (noteContext.hoistedNoteId !== noteId) {
await noteContext.setHoistedNoteId(noteId); await noteContext.setHoistedNoteId(noteId);
} }
@ -174,7 +180,11 @@ export default class Entrypoints extends Component {
} }
async runActiveNoteCommand() { 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 // ctrl+enter is also used elsewhere, so make sure we're running only when appropriate
if (!note || note.type !== "code") { if (!note || note.type !== "code") {

View File

@ -17,7 +17,7 @@ export default class ShortcutComponent extends Component implements EventListene
} }
bindNoteShortcutHandler(labelOrRow: AttributeRow) { bindNoteShortcutHandler(labelOrRow: AttributeRow) {
const handler = () => appContext.tabManager.getActiveContext().setNote(labelOrRow.noteId); const handler = () => appContext.tabManager.getActiveContext()?.setNote(labelOrRow.noteId);
const namespace = labelOrRow.attributeId; const namespace = labelOrRow.attributeId;
if (labelOrRow.isDeleted) { if (labelOrRow.isDeleted) {

View File

@ -4,23 +4,40 @@ import server from "../services/server.js";
import options from "../services/options.js"; import options from "../services/options.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import treeService from "../services/tree.js"; import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import NoteContext from "./note_context.js"; import NoteContext from "./note_context.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import Mutex from "../utils/mutex.js"; import Mutex from "../utils/mutex.js";
import linkService from "../services/link.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 { export default class TabManager extends Component {
public children: NoteContext[];
public mutex: Mutex;
public activeNtxId: string | null;
public recentlyClosedTabs: TabState[];
public tabsUpdate: SpacedUpdate;
constructor() { constructor() {
super(); super();
/** @property {NoteContext[]} */
this.children = []; this.children = [];
this.mutex = new Mutex(); this.mutex = new Mutex();
this.activeNtxId = null; 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.recentlyClosedTabs = [];
this.tabsUpdate = new SpacedUpdate(async () => { this.tabsUpdate = new SpacedUpdate(async () => {
@ -28,7 +45,9 @@ export default class TabManager extends Component {
return; 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", { await server.put("options", {
openNoteContexts: JSON.stringify(openNoteContexts) openNoteContexts: JSON.stringify(openNoteContexts)
@ -38,13 +57,11 @@ export default class TabManager extends Component {
appContext.addBeforeUnloadListener(this); appContext.addBeforeUnloadListener(this);
} }
/** @returns {NoteContext[]} */ get noteContexts(): NoteContext[] {
get noteContexts() {
return this.children; return this.children;
} }
/** @type {NoteContext[]} */ get mainNoteContexts(): NoteContext[] {
get mainNoteContexts() {
return this.noteContexts.filter((nc) => !nc.mainNtxId); return this.noteContexts.filter((nc) => !nc.mainNtxId);
} }
@ -53,11 +70,12 @@ export default class TabManager extends Component {
const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || []; const noteContextsToOpen = (appContext.isMainWindow && options.getJson("openNoteContexts")) || [];
// preload all notes at once // 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); 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 // note doesn't exist so don't try to open tab for it
return false; return false;
} }
@ -80,9 +98,10 @@ export default class TabManager extends Component {
ntxId: parsedFromUrl.ntxId, ntxId: parsedFromUrl.ntxId,
active: true, active: true,
hoistedNoteId: parsedFromUrl.hoistedNoteId || "root", 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; 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 // 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) // (useful, for e.g., opening clipped notes from clipper or opening link in an extra window)
if (parsedFromUrl.notePath) { 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) { } else if (parsedFromUrl.searchString) {
await appContext.triggerCommand("searchNotes", { await appContext.triggerCommand("searchNotes", {
searchString: parsedFromUrl.searchString searchString: parsedFromUrl.searchString
}); });
} }
} catch (e) { } catch (e: unknown) {
logError(`Loading note contexts '${options.get("openNoteContexts")}' failed: ${e.message} ${e.stack}`); 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 // try to recover
await this.openEmptyTab(); await this.openEmptyTab();
} }
} }
noteSwitchedEvent({ noteContext }) { noteSwitchedEvent({ noteContext }: EventData<"noteSwitched">) {
if (noteContext.isActive()) { if (noteContext.isActive()) {
this.setCurrentNavigationStateToHash(); this.setCurrentNavigationStateToHash();
} }
@ -135,10 +163,10 @@ export default class TabManager extends Component {
const activeNoteContext = this.getActiveContext(); const activeNoteContext = this.getActiveContext();
this.updateDocumentTitle(activeNoteContext); 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(); const activeNoteContext = this.getActiveContext();
if (!activeNoteContext) { if (!activeNoteContext) {
return ""; return "";
@ -152,21 +180,15 @@ export default class TabManager extends Component {
}); });
} }
/** @returns {NoteContext[]} */ getNoteContexts(): NoteContext[] {
getNoteContexts() {
return this.noteContexts; return this.noteContexts;
} }
/** getMainNoteContexts(): NoteContext[] {
* Main context is essentially a tab (children are splits), so this returns tabs.
* @returns {NoteContext[]}
*/
getMainNoteContexts() {
return this.noteContexts.filter((nc) => nc.isMainContext()); return this.noteContexts.filter((nc) => nc.isMainContext());
} }
/** @returns {NoteContext} */ getNoteContextById(ntxId: string | null): NoteContext {
getNoteContextById(ntxId) {
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId); const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId);
if (!noteContext) { if (!noteContext) {
@ -176,58 +198,47 @@ export default class TabManager extends Component {
return noteContext; return noteContext;
} }
/** getActiveContext(): NoteContext | null {
* Get active context which represents the visible split with focus. Active context can, but doesn't have to be "main".
*
* @returns {NoteContext}
*/
getActiveContext() {
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null; return this.activeNtxId ? this.getNoteContextById(this.activeNtxId) : null;
} }
/** getActiveMainContext(): NoteContext | null {
* Get active main context which corresponds to the active tab.
*
* @returns {NoteContext}
*/
getActiveMainContext() {
return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null; return this.activeNtxId ? this.getNoteContextById(this.activeNtxId).getMainContext() : null;
} }
/** @returns {string|null} */ getActiveContextNotePath(): string | null {
getActiveContextNotePath() {
const activeContext = this.getActiveContext(); const activeContext = this.getActiveContext();
return activeContext ? activeContext.notePath : null; return activeContext?.notePath ?? null;
} }
/** @returns {FNote} */ getActiveContextNote(): FNote | null {
getActiveContextNote() {
const activeContext = this.getActiveContext(); const activeContext = this.getActiveContext();
return activeContext ? activeContext.note : null; return activeContext ? activeContext.note : null;
} }
/** @returns {string|null} */ getActiveContextNoteId(): string | null {
getActiveContextNoteId() {
const activeNote = this.getActiveContextNote(); const activeNote = this.getActiveContextNote();
return activeNote ? activeNote.noteId : null; return activeNote ? activeNote.noteId : null;
} }
/** @returns {string|null} */ getActiveContextNoteType(): string | null {
getActiveContextNoteType() {
const activeNote = this.getActiveContextNote(); const activeNote = this.getActiveContextNote();
return activeNote ? activeNote.type : null; 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; return activeNote ? activeNote.mime : null;
} }
async switchToNoteContext(ntxId, notePath, viewScope = {}, hoistedNoteId = null) { async switchToNoteContext(
const noteContext = this.noteContexts.find((nc) => nc.ntxId === ntxId) || (await this.openEmptyTab()); 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); await this.activateNoteContext(noteContext.ntxId);
@ -242,20 +253,21 @@ export default class TabManager extends Component {
async openAndActivateEmptyTab() { async openAndActivateEmptyTab() {
const noteContext = await this.openEmptyTab(); const noteContext = await this.openEmptyTab();
await this.activateNoteContext(noteContext.ntxId); await this.activateNoteContext(noteContext.ntxId);
noteContext.setEmpty();
await noteContext.setEmpty();
} }
async openEmptyTab(ntxId = null, hoistedNoteId = "root", mainNtxId = null) { async openEmptyTab(
ntxId: string | null = null,
hoistedNoteId: string = "root",
mainNtxId: string | null = null
): Promise<NoteContext> {
const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId); const noteContext = new NoteContext(ntxId, hoistedNoteId, mainNtxId);
const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId); const existingNoteContext = this.children.find((nc) => nc.ntxId === noteContext.ntxId);
if (existingNoteContext) { if (existingNoteContext) {
await existingNoteContext.setHoistedNoteId(hoistedNoteId); await existingNoteContext.setHoistedNoteId(hoistedNoteId);
return existingNoteContext; return existingNoteContext;
} }
@ -266,29 +278,40 @@ export default class TabManager extends Component {
return noteContext; return noteContext;
} }
async openInNewTab(targetNoteId, hoistedNoteId = null) { async openInNewTab(targetNoteId: string, hoistedNoteId: string | null = null) {
const noteContext = await this.openEmptyTab(null, hoistedNoteId || this.getActiveContext().hoistedNoteId); const noteContext = await this.openEmptyTab(
null,
hoistedNoteId || this.getActiveContext()?.hoistedNoteId
);
await noteContext.setNote(targetNoteId); await noteContext.setNote(targetNoteId);
} }
async openInSameTab(targetNoteId, hoistedNoteId = null) { async openInSameTab(targetNoteId: string, hoistedNoteId: string | null = null) {
const activeContext = this.getActiveContext(); const activeContext = this.getActiveContext();
if (!activeContext) return;
await activeContext.setHoistedNoteId(hoistedNoteId || activeContext.hoistedNoteId); await activeContext.setHoistedNoteId(hoistedNoteId || activeContext.hoistedNoteId);
await activeContext.setNote(targetNoteId); await activeContext.setNote(targetNoteId);
} }
/** async openTabWithNoteWithHoisting(
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab. notePath: string,
*/ opts: {
async openTabWithNoteWithHoisting(notePath, opts = {}) { activate?: boolean | null;
ntxId?: string | null;
mainNtxId?: string | null;
hoistedNoteId?: string | null;
viewScope?: Record<string, any> | null;
} = {}
): Promise<NoteContext> {
const noteContext = this.getActiveContext(); const noteContext = this.getActiveContext();
let hoistedNoteId = "root"; let hoistedNoteId = "root";
if (noteContext) { if (noteContext) {
const resolvedNotePath = await treeService.resolveNotePath(notePath, noteContext.hoistedNoteId); 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; hoistedNoteId = noteContext.hoistedNoteId;
} }
} }
@ -298,7 +321,16 @@ export default class TabManager extends Component {
return this.openContextWithNote(notePath, opts); 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<string, any> | null;
} = {}
): Promise<NoteContext> {
const activate = !!opts.activate; const activate = !!opts.activate;
const ntxId = opts.ntxId || null; const ntxId = opts.ntxId || null;
const mainNtxId = opts.mainNtxId || 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); this.activateNoteContext(noteContext.ntxId, false);
await this.triggerEvent("noteSwitchedAndActivated", { await this.triggerEvent("noteSwitchedAndActivatedEvent", {
noteContext, noteContext,
notePath: noteContext.notePath // resolved note path notePath: noteContext.notePath // resolved note path
}); });
@ -327,21 +359,24 @@ export default class TabManager extends Component {
return noteContext; return noteContext;
} }
async activateOrOpenNote(noteId) { async activateOrOpenNote(noteId: string) {
for (const noteContext of this.getNoteContexts()) { for (const noteContext of this.getNoteContexts()) {
if (noteContext.note && noteContext.note.noteId === noteId) { if (noteContext.note && noteContext.note.noteId === noteId) {
this.activateNoteContext(noteContext.ntxId); this.activateNoteContext(noteContext.ntxId);
return; return;
} }
} }
// if no tab with this note has been found we'll create new tab // if no tab with this note has been found we'll create new tab
await this.openContextWithNote(noteId, { activate: true }); 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) { if (ntxId === this.activeNtxId) {
return; return;
} }
@ -359,11 +394,7 @@ export default class TabManager extends Component {
this.setCurrentNavigationStateToHash(); this.setCurrentNavigationStateToHash();
} }
/** async removeNoteContext(ntxId: string | null) {
* @param ntxId
* @returns {Promise<boolean>} true if note context has been removed, false otherwise
*/
async removeNoteContext(ntxId) {
// removing note context is an async process which can take some time, if users presses CTRL-W quickly, two // 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. // close events could interleave which would then lead to attempting to activate already removed context.
return await this.mutex.runExclusively(async () => { return await this.mutex.runExclusively(async () => {
@ -373,7 +404,7 @@ export default class TabManager extends Component {
noteContextToRemove = this.getNoteContextById(ntxId); noteContextToRemove = this.getNoteContextById(ntxId);
} catch { } catch {
// note context not found // note context not found
return false; return;
} }
if (noteContextToRemove.isMainContext()) { if (noteContextToRemove.isMainContext()) {
@ -383,7 +414,7 @@ export default class TabManager extends Component {
if (noteContextToRemove.isEmpty()) { if (noteContextToRemove.isEmpty()) {
// this is already the empty note context, no point in closing it and replacing with another // this is already the empty note context, no point in closing it and replacing with another
// empty tab // empty tab
return false; return;
} }
await this.openEmptyTab(); await this.openEmptyTab();
@ -399,7 +430,7 @@ export default class TabManager extends Component {
const noteContextsToRemove = noteContextToRemove.getSubContexts(); const noteContextsToRemove = noteContextToRemove.getSubContexts();
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); 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()) { if (!noteContextToRemove.isMainContext()) {
const siblings = noteContextToRemove.getMainContext().getSubContexts(); const siblings = noteContextToRemove.getMainContext().getSubContexts();
@ -421,12 +452,10 @@ export default class TabManager extends Component {
} }
this.removeNoteContexts(noteContextsToRemove); this.removeNoteContexts(noteContextsToRemove);
return true;
}); });
} }
removeNoteContexts(noteContextsToRemove) { removeNoteContexts(noteContextsToRemove: NoteContext[]) {
const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId); const ntxIdsToRemove = noteContextsToRemove.map((nc) => nc.ntxId);
const position = this.noteContexts.findIndex((nc) => ntxIdsToRemove.includes(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.addToRecentlyClosedTabs(noteContextsToRemove, position);
this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove }); this.triggerEvent("noteContextRemoved", { ntxIds: ntxIdsToRemove.filter((id) => id !== null) });
this.tabsUpdate.scheduleUpdate(); this.tabsUpdate.scheduleUpdate();
} }
addToRecentlyClosedTabs(noteContexts, position) { addToRecentlyClosedTabs(noteContexts: NoteContext[], position: number) {
if (noteContexts.length === 1 && noteContexts[0].isEmpty()) { if (noteContexts.length === 1 && noteContexts[0].isEmpty()) {
return; return;
} }
@ -448,26 +477,42 @@ export default class TabManager extends Component {
this.recentlyClosedTabs.push({ contexts: noteContexts, position: position }); this.recentlyClosedTabs.push({ contexts: noteContexts, position: position });
} }
tabReorderEvent({ ntxIdsInOrder }) { tabReorderEvent({ ntxIdsInOrder }: { ntxIdsInOrder: string[] }) {
const order = {}; const order: Record<string, number> = {};
let i = 0; let i = 0;
for (const ntxId of ntxIdsInOrder) { for (const ntxId of ntxIdsInOrder) {
for (const noteContext of this.getNoteContextById(ntxId).getSubContexts()) { 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(); 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])); 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) { if (oldMainNtxId && newMainNtxId) {
this.children.forEach((c) => { this.children.forEach((c) => {
@ -485,7 +530,8 @@ export default class TabManager extends Component {
} }
async activateNextTabCommand() { 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 oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === this.mainNoteContexts.length - 1 ? 0 : oldIdx + 1].ntxId; 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() { 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 oldIdx = this.mainNoteContexts.findIndex((nc) => nc.ntxId === activeMainNtxId);
const newActiveNtxId = this.mainNoteContexts[oldIdx === 0 ? this.mainNoteContexts.length - 1 : oldIdx - 1].ntxId; 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() { async closeActiveTabCommand() {
await this.removeNoteContext(this.activeNtxId); if (this.activeNtxId) {
await this.removeNoteContext(this.activeNtxId);
}
} }
beforeUnloadEvent() { beforeUnloadEvent(): boolean {
this.tabsUpdate.updateNowIfNecessary(); this.tabsUpdate.updateNowIfNecessary();
return true; // don't block closing the tab, this metadata is not that important 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() { async closeAllTabsCommand() {
for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) { for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) {
await this.removeNoteContext(ntxIdToRemove); if (ntxIdToRemove) {
}
}
async closeOtherTabsCommand({ ntxId }) {
for (const ntxIdToRemove of this.mainNoteContexts.map((nc) => nc.ntxId)) {
if (ntxIdToRemove !== ntxId) {
await this.removeNoteContext(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 ntxIds = this.mainNoteContexts.map((nc) => nc.ntxId);
const index = ntxIds.indexOf(ntxId); const index = ntxIds.indexOf(ntxId);
if (index !== -1) { if (index !== -1) {
const idsToRemove = ntxIds.slice(index + 1); const idsToRemove = ntxIds.slice(index + 1);
for (const ntxIdToRemove of idsToRemove) { 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); await this.removeNoteContext(ntxId);
} }
async moveTabToNewWindowCommand({ ntxId }) { async moveTabToNewWindowCommand({ ntxId }: { ntxId: string }) {
const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId); const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
const removed = await this.removeNoteContext(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); const { notePath, hoistedNoteId } = this.getNoteContextById(ntxId);
this.triggerCommand("openInWindow", { notePath, hoistedNoteId }); this.triggerCommand("openInWindow", { notePath, hoistedNoteId });
} }
async reopenLastTabCommand() { async reopenLastTabCommand() {
let closeLastEmptyTab = null; const closeLastEmptyTab: NoteContext | undefined = await this.mutex.runExclusively(async () => {
let closeLastEmptyTab
await this.mutex.runExclusively(async () => {
if (this.recentlyClosedTabs.length === 0) { if (this.recentlyClosedTabs.length === 0) {
return; return closeLastEmptyTab;
} }
if (this.noteContexts.length === 1 && this.noteContexts[0].isEmpty()) { if (this.noteContexts.length === 1 && this.noteContexts[0].isEmpty()) {
@ -575,6 +626,8 @@ export default class TabManager extends Component {
} }
const lastClosedTab = this.recentlyClosedTabs.pop(); const lastClosedTab = this.recentlyClosedTabs.pop();
if (!lastClosedTab) return closeLastEmptyTab;
const noteContexts = lastClosedTab.contexts; const noteContexts = lastClosedTab.contexts;
for (const noteContext of noteContexts) { for (const noteContext of noteContexts) {
@ -589,25 +642,26 @@ export default class TabManager extends Component {
...this.noteContexts.slice(-noteContexts.length), ...this.noteContexts.slice(-noteContexts.length),
...this.noteContexts.slice(lastClosedTab.position, -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()); let mainNtx = noteContexts.find((nc) => nc.isMainContext());
if (mainNtx) { if (mainNtx) {
// reopened a tab, need to reorder new tab widget in tab row // reopened a tab, need to reorder new tab widget in tab row
await this.triggerEvent("contextsReopened", { await this.triggerEvent("contextsReopenedEvent", {
mainNtxId: mainNtx.ntxId, mainNtxId: mainNtx.ntxId,
tabPosition: ntxsInOrder.filter((nc) => nc.isMainContext()).findIndex((nc) => nc.ntxId === mainNtx.ntxId) tabPosition: ntxsInOrder.filter((nc) => nc.isMainContext()).findIndex((nc) => nc.ntxId === mainNtx.ntxId)
}); });
} else { } else {
// reopened a single split, need to reorder the pane widget in split note container // reopened a single split, need to reorder the pane widget in split note container
await this.triggerEvent("contextsReopened", { await this.triggerEvent("contextsReopenedEvent", {
ntxId: ntxsInOrder[lastClosedTab.position].ntxId, mainNtxId: ntxsInOrder[lastClosedTab.position].ntxId,
// this is safe since lastClosedTab.position can never be 0 in this case // 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()); const noteContextToActivate = noteContexts.length === 1 ? noteContexts[0] : noteContexts.find((nc) => nc.isMainContext());
if (!noteContextToActivate) return closeLastEmptyTab;
await this.activateNoteContext(noteContextToActivate.ntxId); await this.activateNoteContext(noteContextToActivate.ntxId);
@ -615,6 +669,7 @@ export default class TabManager extends Component {
noteContext: noteContextToActivate, noteContext: noteContextToActivate,
notePath: noteContextToActivate.notePath notePath: noteContextToActivate.notePath
}); });
return closeLastEmptyTab;
}); });
if (closeLastEmptyTab) { if (closeLastEmptyTab) {
@ -626,7 +681,9 @@ export default class TabManager extends Component {
this.tabsUpdate.scheduleUpdate(); this.tabsUpdate.scheduleUpdate();
} }
async updateDocumentTitle(activeNoteContext) { async updateDocumentTitle(activeNoteContext: NoteContext | null) {
if (!activeNoteContext) return;
const titleFragments = [ const titleFragments = [
// it helps to navigate in history if note title is included in the title // it helps to navigate in history if note title is included in the title
await activeNoteContext.getNavigationTitle(), await activeNoteContext.getNavigationTitle(),
@ -636,7 +693,7 @@ export default class TabManager extends Component {
document.title = titleFragments.join(" - "); document.title = titleFragments.join(" - ");
} }
async entitiesReloadedEvent({ loadResults }) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const activeContext = this.getActiveContext(); const activeContext = this.getActiveContext();
if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) { if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) {
@ -646,7 +703,6 @@ export default class TabManager extends Component {
async frocaReloadedEvent() { async frocaReloadedEvent() {
const activeContext = this.getActiveContext(); const activeContext = this.getActiveContext();
if (activeContext) { if (activeContext) {
await this.updateDocumentTitle(activeContext); await this.updateDocumentTitle(activeContext);
} }

View File

@ -22,13 +22,19 @@ function getItems(): MenuItem<CommandNames>[] {
function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) { function handleLinkContextMenuItem(command: string | undefined, notePath: string, viewScope = {}, hoistedNoteId: string | null = null) {
if (!hoistedNoteId) { if (!hoistedNoteId) {
hoistedNoteId = appContext.tabManager.getActiveContext().hoistedNoteId; hoistedNoteId = appContext.tabManager.getActiveContext()?.hoistedNoteId ?? null;
} }
if (command === "openNoteInNewTab") { if (command === "openNoteInNewTab") {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope }); appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} else if (command === "openNoteInNewSplit") { } 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]; const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope }); appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath, hoistedNoteId, viewScope });

View File

@ -44,7 +44,7 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null; const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const branch = froca.getBranch(this.node.data.branchId); const branch = froca.getBranch(this.node.data.branchId);
const isNotRoot = note?.noteId !== "root"; const isNotRoot = note?.noteId !== "root";
const isHoisted = note?.noteId === appContext.tabManager.getActiveContext().hoistedNoteId; const isHoisted = note?.noteId === appContext.tabManager.getActiveContext()?.hoistedNoteId;
const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null; const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null;
// some actions don't support multi-note, so they are disabled when notes are selected, // some actions don't support multi-note, so they are disabled when notes are selected,
@ -226,8 +226,8 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
templateNoteId: templateNoteId templateNoteId: templateNoteId
}); });
} else if (command === "openNoteInSplit") { } else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1]; const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath }); this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "convertNoteToAttachment") { } else if (command === "convertNoteToAttachment") {

View File

@ -152,10 +152,10 @@ async function deleteNotes(branchIdsToDelete: string[], forceDeleteAllClones = f
async function activateParentNotePath() { async function activateParentNotePath() {
// this is not perfect, maybe we should find the next/previous sibling, but that's more complex // this is not perfect, maybe we should find the next/previous sibling, but that's more complex
const activeContext = appContext.tabManager.getActiveContext(); const activeContext = appContext.tabManager.getActiveContext();
const parentNotePathArr = activeContext.notePathArray.slice(0, -1); const parentNotePathArr = activeContext?.notePathArray.slice(0, -1);
if (parentNotePathArr.length > 0) { if (parentNotePathArr && parentNotePathArr.length > 0) {
activeContext.setNote(parentNotePathArr.join("/")); activeContext?.setNote(parentNotePathArr.join("/"));
} }
} }

View File

@ -457,13 +457,13 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.BasicWidget = BasicWidget; this.BasicWidget = BasicWidget;
this.activateNote = async (notePath) => { this.activateNote = async (notePath) => {
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
}; };
this.activateNewNote = async (notePath) => { this.activateNewNote = async (notePath) => {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(notePath); await appContext.tabManager.getActiveContext()?.setNote(notePath);
await appContext.triggerEvent("focusAndSelectTitle", {}); await appContext.triggerEvent("focusAndSelectTitle", {});
}; };
@ -480,8 +480,8 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
this.openSplitWithNote = async (notePath, activate) => { this.openSplitWithNote = async (notePath, activate) => {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts[subContexts.length - 1]; const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
await appContext.triggerCommand("openNewNoteSplit", { ntxId, notePath }); 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.addTextToActiveContextEditor = (text) => appContext.triggerCommand("addTextToActiveEditor", { text });
this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); this.getActiveContextNote = (): FNote => {
this.getActiveContext = () => appContext.tabManager.getActiveContext(); const note = appContext.tabManager.getActiveContextNote();
this.getActiveMainContext = () => appContext.tabManager.getActiveMainContext(); 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.getNoteContexts = () => appContext.tabManager.getNoteContexts();
this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts(); this.getMainNoteContexts = () => appContext.tabManager.getMainNoteContexts();
this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); this.getActiveContextTextEditor = () => {
this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); 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.getActiveNoteDetailWidget = () => new Promise((resolve) => appContext.triggerCommand("executeInActiveNoteDetailWidget", { callback: resolve }));
this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath();
@ -665,5 +698,5 @@ function FrontendScriptApi(this: Api, startNote: FNote, currentNote: FNote, orig
} }
export default FrontendScriptApi as any as { export default FrontendScriptApi as any as {
new (startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api; new(startNote: FNote, currentNote: FNote, originEntity: Entity | null, $container: JQuery<HTMLElement> | null): Api;
}; };

View File

@ -80,7 +80,7 @@ ws.subscribeToMessages(async (message) => {
toastService.showPersistent(toast); toastService.showPersistent(toast);
if (message.result.importedNoteId) { 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); toastService.showPersistent(toast);
if (message.result.parentNoteId) { if (message.result.parentNoteId) {
await appContext.tabManager.getActiveContext().setNote(message.result.importedNoteId, { await appContext.tabManager.getActiveContext()?.setNote(message.result.importedNoteId, {
viewScope: { viewScope: {
viewMode: "attachments" viewMode: "attachments"
} }

View File

@ -288,11 +288,15 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext(); const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
noteContext.setNote(notePath, { viewScope }).then(() => { if (noteContext) {
if (noteContext !== appContext.tabManager.getActiveContext()) { noteContext.setNote(notePath, { viewScope }).then(() => {
appContext.tabManager.activateNoteContext(noteContext.ntxId); if (noteContext !== appContext.tabManager.getActiveContext()) {
} appContext.tabManager.activateNoteContext(noteContext.ntxId);
}); }
});
} else {
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
}
} }
} else if (hrefLink) { } else if (hrefLink) {
const withinEditLink = $link?.hasClass("ck-link-actions__preview"); const withinEditLink = $link?.hasClass("ck-link-actions__preview");

View File

@ -86,8 +86,8 @@ async function createNote(parentNotePath: string | undefined, options: CreateNot
await ws.waitForMaxKnownEntityChangeId(); 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}`); await activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
if (options.focus === "title") { if (options.focus === "title") {
@ -152,8 +152,7 @@ async function duplicateSubtree(noteId: string, parentNotePath: string) {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
const activeNoteContext = appContext.tabManager.getActiveContext(); appContext.tabManager.getActiveContext()?.setNote(`${parentNotePath}/${note.noteId}`);
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
const origNote = await froca.getNote(noteId); const origNote = await froca.getNote(noteId);
toastService.showMessage(t("note_create.duplicated", { title: origNote?.title })); toastService.showMessage(t("note_create.duplicated", { title: origNote?.title }));

View File

@ -138,7 +138,7 @@ function getParentProtectedStatus(node: Fancytree.FancytreeNode) {
return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected; return hoistedNoteService.isHoistedNode(node) ? false : node.getParent().data.isProtected;
} }
function getNoteIdFromUrl(urlOrNotePath: string | undefined) { function getNoteIdFromUrl(urlOrNotePath: string | null | undefined) {
if (!urlOrNotePath) { if (!urlOrNotePath) {
return null; return null;
} }

View File

@ -16,7 +16,7 @@ export default class Mutex {
return newPromise; return newPromise;
} }
async runExclusively(cb: () => Promise<void>) { async runExclusively(cb: () => Promise<any>) {
const unlock = await this.lock(); const unlock = await this.lock();
try { try {

View File

@ -171,7 +171,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
const { note: newNote } = await server.post<ReturnType<typeof attachmentsApiRoute.convertAttachmentToNote>>(`attachments/${this.attachmentId}/convert-to-note`); const { note: newNote } = await server.post<ReturnType<typeof attachmentsApiRoute.convertAttachmentToNote>>(`attachments/${this.attachmentId}/convert-to-note`);
toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title })); toastService.showMessage(t("attachments_actions.convert_success", { title: this.attachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newNote.noteId); await appContext.tabManager.getActiveContext()?.setNote(newNote.noteId);
} }
async renameAttachmentCommand() { async renameAttachmentCommand() {

View File

@ -43,8 +43,8 @@ const DROPDOWN_TPL = `
data-calendar-input="month"></button> data-calendar-input="month"></button>
<ul class="dropdown-menu" data-calendar-input="month-list"> <ul class="dropdown-menu" data-calendar-input="month-list">
${Object.entries(MONTHS) ${Object.entries(MONTHS)
.map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`) .map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`)
.join("")} .join("")}
</ul> </ul>
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button> <button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button>
@ -149,7 +149,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
const note = await dateNoteService.getDayNote(date); const note = await dateNoteService.getDayNote(date);
if (note) { if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId); appContext.tabManager.getActiveContext()?.setNote(note.noteId);
this.dropdown?.hide(); this.dropdown?.hide();
} else { } else {
toastService.showError(t("calendar.cannot_find_day_note")); toastService.showError(t("calendar.cannot_find_day_note"));
@ -189,10 +189,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
async dropdownShown() { async dropdownShown() {
await libraryLoader.requireLibrary(libraryLoader.CALENDAR_WIDGET); await libraryLoader.requireLibrary(libraryLoader.CALENDAR_WIDGET);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
const activeNote = appContext.tabManager.getActiveContextNote();
this.init(activeNote?.getOwnedLabelValue("dateNote"));
} }
init(activeDate: string | null) { init(activeDate: string | null) {

View File

@ -78,7 +78,7 @@ export default class NoteLauncher extends AbstractLauncher {
} }
getHoistedNoteId() { getHoistedNoteId() {
return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext().hoistedNoteId; return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
} }
getTitle() { getTitle() {

View File

@ -10,6 +10,6 @@ export default class TodayLauncher extends NoteLauncher {
} }
getHoistedNoteId() { getHoistedNoteId() {
return appContext.tabManager.getActiveContext().hoistedNoteId; return appContext.tabManager.getActiveContext()?.hoistedNoteId;
} }
} }

View File

@ -228,7 +228,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, { await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, {
viewScope: { viewScope: {
viewMode: "attachments", viewMode: "attachments",
attachmentId: newAttachment.attachmentId attachmentId: newAttachment.attachmentId

View File

@ -24,8 +24,7 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
if (visible) { if (visible) {
this.triggerEvent("focusTree", {}); this.triggerEvent("focusTree", {});
} else { } else {
const activeNoteContext = appContext.tabManager.getActiveContext(); this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId });
this.triggerEvent("focusOnDetail", { ntxId: activeNoteContext.ntxId });
} }
} }
} }

View File

@ -1,8 +1,29 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import appContext from "../../components/app_context.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 { interface NoteContextEvent {
constructor(widgetFactory) { noteContext: NoteContext;
}
interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean;
ntxId?: string;
}
type WidgetFactory = () => SplitNoteWidget;
interface Widgets {
[key: string]: SplitNoteWidget;
}
export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
private widgetFactory: WidgetFactory;
private widgets: Widgets;
constructor(widgetFactory: WidgetFactory) {
super("row"); super("row");
this.widgetFactory = widgetFactory; this.widgetFactory = widgetFactory;
@ -13,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer {
this.collapsible(); this.collapsible();
} }
async newNoteContextCreatedEvent({ noteContext }) { async newNoteContextCreatedEvent({ noteContext }: NoteContextEvent) {
const widget = this.widgetFactory(); const widget = this.widgetFactory();
const $renderedWidget = widget.render(); const $renderedWidget = widget.render();
@ -23,19 +44,31 @@ export default class SplitNoteContainer extends FlexContainer {
this.$widget.append($renderedWidget); this.$widget.append($renderedWidget);
widget.handleEvent("initialRenderComplete"); widget.handleEvent("initialRenderComplete", {});
widget.toggleExt(false); widget.toggleExt(false);
this.widgets[noteContext.ntxId] = widget; if (noteContext.ntxId) {
this.widgets[noteContext.ntxId] = widget;
}
await widget.handleEvent("setNoteContext", { noteContext }); await widget.handleEvent("setNoteContext", { noteContext });
this.child(widget); this.child(widget);
} }
async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }) { async openNewNoteSplitEvent({ ntxId, notePath, hoistedNoteId, viewScope }: {
const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId; ntxId: string;
notePath?: string;
hoistedNoteId?: string;
viewScope?: any;
}) {
const mainNtxId = appContext.tabManager.getActiveMainContext()?.ntxId;
if (!mainNtxId) {
logError("empty mainNtxId!");
return;
}
if (!ntxId) { if (!ntxId) {
logError("empty ntxId!"); logError("empty ntxId!");
@ -43,7 +76,7 @@ export default class SplitNoteContainer extends FlexContainer {
ntxId = mainNtxId; ntxId = mainNtxId;
} }
hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext().hoistedNoteId; hoistedNoteId = hoistedNoteId || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const noteContext = await appContext.tabManager.openEmptyTab(null, hoistedNoteId, mainNtxId); 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 // insert the note context after the originating note context
ntxIds.splice(ntxIds.indexOf(ntxId) + 1, 0, noteContext.ntxId); 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 // 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}"]`)); 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); appContext.tabManager.removeNoteContext(ntxId);
} }
async moveThisNoteSplitCommand({ ntxId, isMovingLeft }) { async moveThisNoteSplitCommand({ ntxId, isMovingLeft }: { ntxId: string; isMovingLeft: boolean }): Promise<void> {
if (!ntxId) { if (!ntxId) {
logError("empty ntxId!"); logError("empty ntxId!");
return; 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 newNtxIds = [...ntxIds.slice(0, leftIndex), ntxIds[leftIndex + 1], ntxIds[leftIndex], ...ntxIds.slice(leftIndex + 2)];
const isChangingMainContext = !contexts[leftIndex].mainNtxId; const isChangingMainContext = !contexts[leftIndex].mainNtxId;
this.triggerCommand("noteContextReorder", { this.triggerCommand("noteContextReorder" as keyof CommandMappings, {
ntxIdsInOrder: newNtxIds, ntxIdsInOrder: newNtxIds,
oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null, oldMainNtxId: isChangingMainContext ? ntxIds[leftIndex] : null,
newMainNtxId: isChangingMainContext ? ntxIds[leftIndex + 1] : 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]); await appContext.tabManager.activateNoteContext(isMovingLeft ? ntxIds[leftIndex + 1] : ntxIds[leftIndex]);
} }
activeContextChangedEvent() { activeContextChangedEvent(): void {
this.refresh(); this.refresh();
} }
noteSwitchedAndActivatedEvent() { noteSwitchedAndActivatedEvent(): void {
this.refresh(); this.refresh();
} }
noteContextRemovedEvent({ ntxIds }) { noteContextRemovedEvent({ ntxIds }: { ntxIds: string[] }): void {
this.children = this.children.filter((c) => !ntxIds.includes(c.ntxId)); this.children = this.children.filter((c) => c.ntxId && !ntxIds.includes(c.ntxId));
for (const ntxId of ntxIds) { for (const ntxId of ntxIds) {
this.$widget.find(`[data-ntx-id="${ntxId}"]`).remove(); 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) { if (ntxId === undefined || afterNtxId === undefined) {
// no single split reopened // no single split reopened
return; 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}"]`)); this.$widget.find(`[data-ntx-id="${ntxId}"]`).insertAfter(this.$widget.find(`[data-ntx-id="${afterNtxId}"]`));
} }
async refresh() { async refresh(): Promise<void> {
this.toggleExt(true); this.toggleExt(true);
} }
toggleInt(show) {} // not needed toggleExt(show: boolean): void {
toggleExt(show) {
const activeMainContext = appContext.tabManager.getActiveMainContext(); const activeMainContext = appContext.tabManager.getActiveMainContext();
const activeNtxId = activeMainContext ? activeMainContext.ntxId : null; const activeNtxId = activeMainContext ? activeMainContext.ntxId : null;
@ -149,7 +180,7 @@ export default class SplitNoteContainer extends FlexContainer {
const noteContext = appContext.tabManager.getNoteContextById(ntxId); const noteContext = appContext.tabManager.getNoteContextById(ntxId);
const widget = this.widgets[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 * 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. * activation, further note switches are always propagated to the tabs.
*/ */
handleEventInChildren(name, data) { handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>): Promise<any> | null {
if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) { if (["noteSwitched", "noteSwitchedAndActivated"].includes(name)) {
// this event is propagated only to the widgets of a particular tab // 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) { if (!widget) {
return Promise.resolve(); 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; 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 { } else {
return Promise.resolve(); return Promise.resolve();
} }
} }
if (name === "activeContextChanged") { if (name === "activeContextChanged") {
return this.refreshNotShown(data); return this.refreshNotShown(data as NoteContextEvent);
} else { } else {
return super.handleEventInChildren(name, data); return super.handleEventInChildren(name, data);
} }
} }
refreshNotShown(data) { private refreshNotShown(data: NoteContextEvent): Promise<any> {
const promises = []; const promises: Promise<any>[] = [];
for (const subContext of data.noteContext.getMainContext().getSubContexts()) { for (const subContext of data.noteContext.getMainContext().getSubContexts()) {
if (!subContext.ntxId) {
continue;
}
const widget = this.widgets[subContext.ntxId]; const widget = this.widgets[subContext.ntxId];
if (!widget.hasBeenAlreadyShown) { if (!widget.hasBeenAlreadyShown) {
widget.hasBeenAlreadyShown = true; widget.hasBeenAlreadyShown = true;
promises.push(widget.handleEvent("activeContextChanged", { noteContext: subContext })); const eventPromise = widget.handleEvent("activeContextChanged", { noteContext: subContext });
promises.push(eventPromise || Promise.resolve());
} }
} }

View File

@ -65,7 +65,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); 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) })); toastService.showMessage(t("code_buttons.sql_console_saved_message", { notePath: await treeService.getNotePathTitle(notePath) }));
}); });

View File

@ -60,15 +60,15 @@ export default class ContextualHelpButton extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.on("click", () => { this.$widget.on("click", () => {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const targetNote = `_help_${this.helpNoteIdToOpen}`; 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 = { const viewScope: ViewScope = {
viewMode: "contextual-help" viewMode: "contextual-help"
}; };
if (!helpSubcontext) { if (!helpSubcontext) {
// The help is not already open, open a new split with it. // 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", { this.triggerCommand("openNewNoteSplit", {
ntxId, ntxId,
notePath: targetNote, notePath: targetNote,

View File

@ -28,8 +28,8 @@ class MobileDetailMenuWidget extends BasicWidget {
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
items: [ items: [
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note.type !== "search" }, { 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.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" }
], ],
selectMenuItemHandler: async ({ command }) => { selectMenuItemHandler: async ({ command }) => {
if (command === "insertChildNote") { if (command === "insertChildNote") {

View File

@ -322,7 +322,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
.warmupTicks(30) .warmupTicks(30)
.onNodeClick((node) => { .onNodeClick((node) => {
if (node.id) { if (node.id) {
appContext.tabManager.getActiveContext().setNote((node as Node).id); appContext.tabManager.getActiveContext()?.setNote((node as Node).id);
} }
}) })
.onNodeRightClick((node, e) => { .onNodeRightClick((node, e) => {
@ -371,7 +371,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
if (mapRootNoteId === "hoisted") { if (mapRootNoteId === "hoisted") {
mapRootNoteId = hoistedNoteService.getHoistedNoteId(); mapRootNoteId = hoistedNoteService.getHoistedNoteId();
} else if (!mapRootNoteId) { } else if (!mapRootNoteId) {
mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId; mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId;
} }
return mapRootNoteId ?? ""; return mapRootNoteId ?? "";

View File

@ -424,10 +424,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const activeNoteContext = appContext.tabManager.getActiveContext(); const activeNoteContext = appContext.tabManager.getActiveContext();
const opts: SetNoteOpts = {}; const opts: SetNoteOpts = {};
if (activeNoteContext.viewScope?.viewMode === "contextual-help") { if (activeNoteContext?.viewScope?.viewMode === "contextual-help") {
opts.viewScope = activeNoteContext.viewScope; opts.viewScope = activeNoteContext.viewScope;
} }
await activeNoteContext.setNote(notePath, opts); await activeNoteContext?.setNote(notePath, opts);
}, },
expand: (event, data) => this.setExpanded(data.node.data.branchId, true), expand: (event, data) => this.setExpanded(data.node.data.branchId, true),
collapse: (event, data) => this.setExpanded(data.node.data.branchId, false), 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 // TODO: Deduplicate with server's notes.ts#getAndValidateParent
if (!["search", "launcher"].includes(note.type) if (!["search", "launcher"].includes(note.type)
&& !note.isOptions() && !note.isOptions()
&& !note.isLaunchBarConfig() && !note.isLaunchBarConfig()
&& !note.noteId.startsWith("_help") && !note.noteId.startsWith("_help")
) { ) {
const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on( const $createChildNoteButton = $(`<span class="tree-item-button add-note-button bx bx-plus" title="${t("note_tree.create-child-note")}"></span>`).on(
"click", "click",
cancelClickPropagation cancelClickPropagation
@ -1758,6 +1758,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
appContext.tabManager.getActiveContext().setNote(resp.note.noteId); appContext.tabManager.getActiveContext()?.setNote(resp.note.noteId);
} }
} }

View File

@ -246,7 +246,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
await ws.waitForMaxKnownEntityChangeId(); 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 // Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape // See https://www.i18next.com/translation-function/interpolation#unescape
toastService.showMessage(t("search_definition.search_note_saved", { notePathTitle: await treeService.getNotePathTitle(notePath) })); toastService.showMessage(t("search_definition.search_note_saved", { notePathTitle: await treeService.getNotePathTitle(notePath) }));

View File

@ -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 { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js"; import contextMenu from "../menus/context_menu.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import keyboardActionService from "../services/keyboard_actions.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 froca from "../services/froca.js";
import attributeService from "../services/attributes.js"; import attributeService from "../services/attributes.js";
import type NoteContext from "../components/note_context.js"; import type NoteContext from "../components/note_context.js";
@ -419,13 +419,13 @@ export default class TabRowWidget extends BasicWidget {
closeActiveTabCommand({ $el }: CommandListenerData<"closeActiveTab">) { closeActiveTabCommand({ $el }: CommandListenerData<"closeActiveTab">) {
const ntxId = $el.closest(".note-tab").attr("data-ntx-id"); const ntxId = $el.closest(".note-tab").attr("data-ntx-id");
appContext.tabManager.removeNoteContext(ntxId); appContext.tabManager.removeNoteContext(ntxId ?? null);
} }
setTabCloseEvent($tab: JQuery<HTMLElement>) { setTabCloseEvent($tab: JQuery<HTMLElement>) {
$tab.on("mousedown", (e) => { $tab.on("mousedown", (e) => {
if (e.which === 2) { 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 return true; // event has been handled
} }
@ -494,7 +494,7 @@ export default class TabRowWidget extends BasicWidget {
return $tab.attr("data-ntx-id"); return $tab.attr("data-ntx-id");
} }
noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemovedEvent">) { noteContextRemovedEvent({ ntxIds }: EventData<"noteContextRemoved">) {
for (const ntxId of ntxIds) { for (const ntxId of ntxIds) {
this.removeTab(ntxId); this.removeTab(ntxId);
} }
@ -516,7 +516,7 @@ export default class TabRowWidget extends BasicWidget {
this.draggabillyDragging.element.style.transform = ""; this.draggabillyDragging.element.style.transform = "";
this.draggabillyDragging.dragEnd(); this.draggabillyDragging.dragEnd();
this.draggabillyDragging.isDragging = false; 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.destroy();
this.draggabillyDragging = null; this.draggabillyDragging = null;
} }
@ -650,7 +650,7 @@ export default class TabRowWidget extends BasicWidget {
} }
contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopenedEvent">) { contextsReopenedEvent({ mainNtxId, tabPosition }: EventData<"contextsReopenedEvent">) {
if (mainNtxId === undefined || tabPosition === undefined) { if (!mainNtxId || !tabPosition) {
// no tab reopened // no tab reopened
return; return;
} }
@ -748,7 +748,7 @@ export default class TabRowWidget extends BasicWidget {
hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) { hoistedNoteChangedEvent({ ntxId }: EventData<"hoistedNoteChanged">) {
const $tab = this.getTabById(ntxId); const $tab = this.getTabById(ntxId);
if ($tab) { if ($tab && ntxId) {
const noteContext = appContext.tabManager.getNoteContextById(ntxId); const noteContext = appContext.tabManager.getNoteContextById(ntxId);
this.updateTab($tab, noteContext); this.updateTab($tab, noteContext);

View File

@ -155,7 +155,7 @@ export default class CalendarView extends ViewMode {
const note = await date_notes.getDayNote(e.dateStr); const note = await date_notes.getDayNote(e.dateStr);
if (note) { if (note) {
appContext.tabManager.getActiveContext().setNote(note.noteId); appContext.tabManager.getActiveContext()?.setNote(note.noteId);
} }
} }
}); });