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.
This commit is contained in:
Harvey Tindall
2026-01-05 10:39:20 +00:00
parent 721b209e1f
commit ee96bb9f1b
7 changed files with 50 additions and 11 deletions

View File

@@ -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,

View File

@@ -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.

View File

@@ -708,4 +708,8 @@ export class activityList extends PaginatedList implements Navigatable, AsTab {
}
this._search.navigate(urlParams.toString());
};
clearURL() {
this._search.clearURL();
}
}

View File

@@ -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;

View File

@@ -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.

View File

@@ -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<string, Tab>;
pages: PageManager;
pages: Pages;
constructor() {
this.tabs = new Map<string, Tab>();
@@ -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);
}
};
}

View File

@@ -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<string, Tab>;
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;
}