From ee96bb9f1b087779bb8755d48d28e1e7b8d4ea2b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Mon, 5 Jan 2026 10:39:20 +0000 Subject: [PATCH] tabs: add clearURL method, loading tabs clears previous qps Navigatable has clearURL, which for Search clears "search" qp, and invites clears "invite" qp. Tab interfaces optionally include "contentObject: AsTab", and show/hide funcs are passed the contentObject of the previously loaded tab if one is available, so that they can call it's clearURL method. This means searches you typed for the accounts tab won't pop up when switching to activity. --- ts/admin.ts | 9 +++++++-- ts/modules/accounts.ts | 4 ++++ ts/modules/activity.ts | 4 ++++ ts/modules/invites.ts | 8 ++++++++ ts/modules/search.ts | 4 ++++ ts/modules/tabs.ts | 16 ++++++++++------ ts/typings/d.ts | 16 +++++++++++++--- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/ts/admin.ts b/ts/admin.ts index 4dcf854..202e216 100644 --- a/ts/admin.ts +++ b/ts/admin.ts @@ -152,15 +152,19 @@ let navigated = false; // load tabs const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = []; [window.invites, accounts, activity, settings].forEach((p: AsTab) => { - let t: { id: string; url: string; reloader: () => void; unloader?: () => void } = { + let t: { id: string; url: string; reloader: (previous?: AsTab) => void; unloader?: () => void } = { id: p.tabName, url: p.pagePath, - reloader: () => { + reloader: (previous: AsTab) => { if (isPageEventBindable(p)) p.bindPageEvents(); if (!navigated && isNavigatable(p) && p.isURL()) { navigated = true; p.navigate(); } else { + if (navigated && previous && isNavigatable(previous)) { + // Clear the query param, as it was likely for a different page + previous.clearURL(); + } p.reload(() => {}); } }, @@ -170,6 +174,7 @@ const tabs: { id: string; url: string; reloader: () => void; unloader?: () => vo window.tabs.addTab( t.id, window.pages.Base + window.pages.Admin + "/" + t.url, + p, null, t.reloader, t.unloader || null, diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index a43868e..b6fa83f 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -2516,6 +2516,10 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab { if (details) this.details(details); }); }; + + clearURL() { + this._search.clearURL(); + } } // An alternate view showing accounts in sub-lists grouped by group/label. diff --git a/ts/modules/activity.ts b/ts/modules/activity.ts index 4284239..5bfd9af 100644 --- a/ts/modules/activity.ts +++ b/ts/modules/activity.ts @@ -708,4 +708,8 @@ export class activityList extends PaginatedList implements Navigatable, AsTab { } this._search.navigate(urlParams.toString()); }; + + clearURL() { + this._search.clearURL(); + } } diff --git a/ts/modules/invites.ts b/ts/modules/invites.ts index c9f85d5..017c95a 100644 --- a/ts/modules/invites.ts +++ b/ts/modules/invites.ts @@ -818,6 +818,14 @@ export class DOMInviteList implements InviteList { this.focusInvite(inviteCode, window.lang.notif("errorInviteNotFound")); }; + clearURL() { + const url = new URL(window.location.href); + if (!url.searchParams.has("invite")) return; + url.searchParams.delete("invite"); + console.log("pushing", url.toString()); + window.history.pushState(null, "", url.toString()); + } + constructor() { this._list = document.getElementById("invites") as HTMLDivElement; this.empty = true; diff --git a/ts/modules/search.ts b/ts/modules/search.ts index eb076d7..9d9c7fc 100644 --- a/ts/modules/search.ts +++ b/ts/modules/search.ts @@ -731,6 +731,10 @@ export class Search implements Navigatable { this.setQueryParam(""); }; + clearURL() { + this.clearQueryParam(); + } + // setQueryParam sets the ?search query param to the current searchbox content, // or value if given. If everything is set up correctly, this should trigger a search when it is // set to a new value. diff --git a/ts/modules/tabs.ts b/ts/modules/tabs.ts index efe2b0c..74c67ab 100644 --- a/ts/modules/tabs.ts +++ b/ts/modules/tabs.ts @@ -1,4 +1,4 @@ -import { PageManager } from "../modules/pages.js"; +import { PageManager } from "./pages"; export function isPageEventBindable(object: any): object is PageEventBindable { return "bindPageEvents" in object; @@ -12,7 +12,7 @@ export class TabManager implements TabManager { private _current: string = ""; private _baseOffset = -1; tabs: Map; - pages: PageManager; + pages: Pages; constructor() { this.tabs = new Map(); @@ -26,14 +26,16 @@ export class TabManager implements TabManager { addTab = ( tabID: string, url: string, - preFunc = () => void {}, - postFunc = () => void {}, + contentObject: AsTab | null, + preFunc: (previous?: AsTab) => void = (_?: AsTab) => void {}, + postFunc: (previous?: AsTab) => void = (_?: AsTab) => void {}, unloadFunc = () => void {}, ) => { let tab: Tab = { page: null, tabEl: document.getElementById("tab-" + tabID) as HTMLDivElement, buttonEl: document.getElementById("button-tab-" + tabID) as HTMLButtonElement, + contentObject: contentObject, preFunc: preFunc, postFunc: postFunc, }; @@ -91,14 +93,16 @@ export class TabManager implements TabManager { [t] = this.tabs.values(); } + const prev = this.tabs.get(this.current); + this._current = t.page.name; if (t.preFunc && !noRun) { - t.preFunc(); + t.preFunc(prev?.contentObject); } this.pages.load(tabID); if (t.postFunc && !noRun) { - t.postFunc(); + t.postFunc(prev?.contentObject); } }; } diff --git a/ts/typings/d.ts b/ts/typings/d.ts index 685c155..3eb29a9 100644 --- a/ts/typings/d.ts +++ b/ts/typings/d.ts @@ -75,6 +75,8 @@ declare interface AsTab { declare interface Navigatable { // isURL will return whether the given url (or the current page url if not passed) is a valid link to some resource(s) in the class. isURL(url?: string): boolean; + // clearURL will remove related query params from the current URL. It will likely be called when switching pages. + clearURL(): void; // navigate will load and focus the resource(s) in the class referenced by the given url (or current page url if not passed). navigate(url?: string): void; } @@ -196,14 +198,22 @@ declare interface Tab { page: Page; tabEl: HTMLDivElement; buttonEl: HTMLSpanElement; - preFunc?: () => void; - postFunc?: () => void; + contentObject?: AsTab; + preFunc?: (previous?: AsTab) => void; + postFunc?: (previous?: AsTab) => void; } declare interface Tabs { tabs: Map; pages: Pages; - addTab(tabID: string, url: string, preFunc: () => void, postFunc: () => void, unloadFunc: () => void): void; + addTab( + tabID: string, + url: string, + contentObject: AsTab | null, + preFunc: () => void, + postFunc: () => void, + unloadFunc: () => void, + ): void; current: string; switch(tabID: string, noRun?: boolean): void; }