list/search/accounts: fully(?) page-based search

searches are triggered by setting the search QP, and ran by a listener
set on PageManager. same with details. Works surprisingly well, but i'm
sure theres bugs.
This commit is contained in:
Harvey Tindall
2025-12-25 13:04:03 +00:00
parent 3308739619
commit c10f1e3b36
8 changed files with 201 additions and 114 deletions

View File

@@ -1,7 +1,7 @@
import { ThemeManager } from "./modules/theme.js"; import { ThemeManager } from "./modules/theme.js";
import { lang, LangFile, loadLangSelector } from "./modules/lang.js"; import { lang, LangFile, loadLangSelector } from "./modules/lang.js";
import { Modal } from "./modules/modal.js"; import { Modal } from "./modules/modal.js";
import { Tabs, Tab, isPageEventBindable, isNavigatable } from "./modules/tabs.js"; import { TabManager, isPageEventBindable, isNavigatable } from "./modules/tabs.js";
import { DOMInviteList, createInvite } from "./modules/invites.js"; import { DOMInviteList, createInvite } from "./modules/invites.js";
import { accountsList } from "./modules/accounts.js"; import { accountsList } from "./modules/accounts.js";
import { settingsList } from "./modules/settings.js"; import { settingsList } from "./modules/settings.js";
@@ -129,6 +129,9 @@ window.availableProfiles = window.availableProfiles || [];
}); });
});*/ });*/
// tab content objects will register with this independently, so initialise now
window.tabs = new TabManager();
var inviteCreator = new createInvite(); var inviteCreator = new createInvite();
var accounts = new accountsList(); var accounts = new accountsList();
@@ -148,7 +151,6 @@ let navigated = false;
// load tabs // load tabs
const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = []; const tabs: { id: string; url: string; reloader: () => void; unloader?: () => void }[] = [];
window.tabs = new Tabs();
[window.invites, accounts, activity, settings].forEach((p: AsTab) => { [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: () => void; unloader?: () => void } = {
id: p.tabName, id: p.tabName,

View File

@@ -940,7 +940,7 @@ class User extends TableRow implements UserDTO, SearchableItem {
}; };
} }
interface UsersDTO extends paginatedDTO { interface UsersDTO extends PaginatedDTO {
users: UserDTO[]; users: UserDTO[];
} }
@@ -1054,9 +1054,8 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
}; };
private _inDetails: boolean = false; private _inDetails: boolean = false;
details(username: string, jfId: string) { details(jfId?: string) {
this.unbindPageEvents(); if (!jfId) jfId = this.users.keys().next().value;
console.debug("Loading details for ", username, jfId);
this._details.load( this._details.load(
this.users.get(jfId), this.users.get(jfId),
() => { () => {
@@ -1066,17 +1065,28 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
this.processSelectedAccounts(); this.processSelectedAccounts();
this._table.classList.add("unfocused"); this._table.classList.add("unfocused");
this._details.hidden = false; this._details.hidden = false;
const url = new URL(window.location.href);
url.searchParams.set("details", jfId);
window.history.pushState(null, "", url.toString());
}, },
() => { this.closeDetails,
this._inDetails = false;
this.processSelectedAccounts();
this._details.hidden = true;
this._table.classList.remove("unfocused");
this.bindPageEvents();
},
); );
} }
closeDetails = () => {
if (!this._inDetails) return;
this._inDetails = false;
this.processSelectedAccounts();
this._details.hidden = true;
this._table.classList.remove("unfocused");
this.bindPageEvents();
const url = new URL(window.location.href);
url.searchParams.delete("details");
window.history.pushState(null, "", url.toString());
};
constructor() { constructor() {
super({ super({
loader: document.getElementById("accounts-loader"), loader: document.getElementById("accounts-loader"),
@@ -1095,7 +1105,7 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
getPageEndpoint: "/users", getPageEndpoint: "/users",
itemsPerPage: 40, itemsPerPage: 40,
maxItemsLoadedForSearch: 200, maxItemsLoadedForSearch: 200,
appendNewItems: (resp: paginatedDTO) => { appendNewItems: (resp: PaginatedDTO) => {
for (let u of (resp as UsersDTO).users || []) { for (let u of (resp as UsersDTO).users || []) {
if (this.users.has(u.id)) { if (this.users.has(u.id)) {
this.users.get(u.id).update(u); this.users.get(u.id).update(u);
@@ -1110,7 +1120,7 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
this._search.ascending, this._search.ascending,
); );
}, },
replaceWithNewItems: (resp: paginatedDTO) => { replaceWithNewItems: (resp: PaginatedDTO) => {
let accountsOnDOM = new Map<string, boolean>(); let accountsOnDOM = new Map<string, boolean>();
for (let id of this.users.keys()) { for (let id of this.users.keys()) {
@@ -1161,6 +1171,7 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
clearSearchButtonSelector: ".accounts-search-clear", clearSearchButtonSelector: ".accounts-search-clear",
serverSearchButtonSelector: ".accounts-search-server", serverSearchButtonSelector: ".accounts-search-server",
onSearchCallback: (_0: boolean, _1: boolean) => { onSearchCallback: (_0: boolean, _1: boolean) => {
this.closeDetails();
this.processSelectedAccounts(); this.processSelectedAccounts();
}, },
searchServer: null, searchServer: null,
@@ -1418,10 +1429,14 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
this._search.showHideSearchOptionsHeader(); this._search.showHideSearchOptionsHeader();
this.registerURLListener(); this.registerURLListener();
// FIXME: registerParamListener once PageManager is global
//
this._details = new UserInfo(document.getElementsByClassName("accounts-details")[0] as HTMLElement); this._details = new UserInfo(document.getElementsByClassName("accounts-details")[0] as HTMLElement);
document.addEventListener("accounts-show-details", (ev: ShowDetailsEvent) => { document.addEventListener("accounts-show-details", (ev: ShowDetailsEvent) => {
this.details(ev.detail.username, ev.detail.id); const url = new URL(window.location.href);
url.searchParams.set("details", ev.detail.id);
window.history.pushState(null, "", url.toString());
// this.details(ev.detail.id);
}); });
// Get rid of nasty CSS // Get rid of nasty CSS
@@ -1431,16 +1446,16 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
}; };
} }
reload = (callback?: (resp: paginatedDTO) => void) => { reload = (callback?: (resp: PaginatedDTO) => void) => {
this._reload(callback); this._reload(callback);
this.loadTemplates(); this.loadTemplates();
}; };
loadMore = (loadAll: boolean = false, callback?: (resp?: paginatedDTO) => void) => { loadMore = (loadAll: boolean = false, callback?: (resp?: PaginatedDTO) => void) => {
this._loadMore(loadAll, callback); this._loadMore(loadAll, callback);
}; };
loadAll = (callback?: (resp?: paginatedDTO) => void) => { loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
this._loadAll(callback); this._loadAll(callback);
}; };
@@ -1498,7 +1513,7 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
} }
if (next == SelectAllState.All) { if (next == SelectAllState.All) {
this.loadAll((_: paginatedDTO) => { this.loadAll((_: PaginatedDTO) => {
if (!this.lastPage) { if (!this.lastPage) {
// Pretend to live-select elements as they load. // Pretend to live-select elements as they load.
this._counter.selected = this._counter.shown; this._counter.selected = this._counter.shown;
@@ -2457,35 +2472,47 @@ export class accountsList extends PaginatedList implements Navigatable, AsTab {
focusAccount = (userID: string) => { focusAccount = (userID: string) => {
console.debug("focusing user", userID); console.debug("focusing user", userID);
this._c.searchBox.value = `id:"${userID}"`; this._search.setQueryParam(`id:"${userID}"`);
this._search.onSearchBoxChange();
this._search.onServerSearch();
if (userID in this.users) this.users.get(userID).focus(); if (userID in this.users) this.users.get(userID).focus();
}; };
public static readonly _accountURLEvent = "account-url"; public static readonly _accountURLEvent = "account-url";
registerURLListener = () => registerURLListener = () => {
document.addEventListener(accountsList._accountURLEvent, (event: CustomEvent) => { document.addEventListener(accountsList._accountURLEvent, (event: CustomEvent) => {
this.focusAccount(event.detail); this.focusAccount(event.detail);
}); });
window.tabs.pages.registerParamListener(
this.tabName,
(_: URLSearchParams) => {
this.navigate();
},
"user",
"details",
"search",
);
};
isURL = (url?: string) => { isURL = (url?: string) => {
const urlParams = new URLSearchParams(url || window.location.search); const urlParams = new URLSearchParams(url || window.location.search);
const userID = urlParams.get("user"); const userID = urlParams.get("user");
return Boolean(userID) || this._search.isURL(url); const details = urlParams.get("details");
return Boolean(userID) || Boolean(details) || this._search.isURL(url);
}; };
navigate = (url?: string) => { navigate = (url?: string) => {
const urlParams = new URLSearchParams(url || window.location.search); const urlParams = new URLSearchParams(url || window.location.search);
const userID = urlParams.get("user"); const details = urlParams.get("details");
const userID = details || urlParams.get("user");
let search = urlParams.get("search") || ""; let search = urlParams.get("search") || "";
if (userID) { if (userID) {
search = `id:${userID}" ` + search; search = `id:"${userID}"`;
urlParams.set("search", search); urlParams.set("search", search);
// Get rid of it, as it'll now be included in the "search" param anyway // Get rid of it, as it'll now be included in the "search" param anyway
urlParams.delete("user"); urlParams.delete("user");
} }
this._search.navigate(urlParams.toString()); this._search.navigate(urlParams.toString(), () => {
if (details) this.details(details);
});
}; };
} }
@@ -2856,10 +2883,12 @@ class UserInfo extends PaginatedList {
card.classList.add("unfocused"); card.classList.add("unfocused");
card.innerHTML = ` card.innerHTML = `
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<table class="table text-base leading-5"> <div class="overflow-x-scroll">
<thead class="user-info-table-header"></thead> <table class="table text-base leading-5">
<tbody class="user-info-row"></tbody> <thead class="user-info-table-header"></thead>
</table> <tbody class="user-info-row"></tbody>
</table>
</div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<div class="jf-activity-source flex flex-row justify-between gap-2"> <div class="jf-activity-source flex flex-row justify-between gap-2">
<h3 class="heading text-lg">${window.lang.strings("activityFromJF")}</h3> <h3 class="heading text-lg">${window.lang.strings("activityFromJF")}</h3>
@@ -2907,7 +2936,7 @@ class UserInfo extends PaginatedList {
maxItemsLoadedForSearch: 200, maxItemsLoadedForSearch: 200,
disableSearch: true, disableSearch: true,
hideButtonsOnLastPage: true, hideButtonsOnLastPage: true,
appendNewItems: (resp: paginatedDTO) => { appendNewItems: (resp: PaginatedDTO) => {
for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) { for (let entry of (resp as PaginatedActivityLogEntriesDTO).entries || []) {
if (this.entries.has("" + entry.Id)) { if (this.entries.has("" + entry.Id)) {
this.entries.get("" + entry.Id).update(null, entry); this.entries.get("" + entry.Id).update(null, entry);
@@ -2918,7 +2947,7 @@ class UserInfo extends PaginatedList {
this._search.setOrdering(Array.from(this.entries.keys()), "Date", true); this._search.setOrdering(Array.from(this.entries.keys()), "Date", true);
}, },
replaceWithNewItems: (resp: paginatedDTO) => { replaceWithNewItems: (resp: PaginatedDTO) => {
let entriesOnDOM = new Map<string, boolean>(); let entriesOnDOM = new Map<string, boolean>();
for (let id of this.entries.keys()) { for (let id of this.entries.keys()) {
@@ -2973,15 +3002,15 @@ class UserInfo extends PaginatedList {
this.entries.set("" + entry.Id, new ActivityLogEntry(this.username, entry)); this.entries.set("" + entry.Id, new ActivityLogEntry(this.username, entry));
}; };
loadMore = (loadAll: boolean = false, callback?: (resp?: paginatedDTO) => void) => { loadMore = (loadAll: boolean = false, callback?: (resp?: PaginatedDTO) => void) => {
this._loadMore(loadAll, callback); this._loadMore(loadAll, callback);
}; };
loadAll = (callback?: (resp?: paginatedDTO) => void) => { loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
this._loadAll(callback); this._loadAll(callback);
}; };
reload = (callback?: (resp: paginatedDTO) => void) => { reload = (callback?: (resp: PaginatedDTO) => void) => {
this._reload(callback); this._reload(callback);
}; };

View File

@@ -554,7 +554,7 @@ interface ActivitiesReqDTO extends PaginatedReqDTO {
type: string[]; type: string[];
} }
interface ActivitiesDTO extends paginatedDTO { interface ActivitiesDTO extends PaginatedDTO {
activities: activity[]; activities: activity[];
} }
@@ -589,7 +589,7 @@ export class activityList extends PaginatedList implements Navigatable, AsTab {
getPageEndpoint: "/activity", getPageEndpoint: "/activity",
itemsPerPage: 20, itemsPerPage: 20,
maxItemsLoadedForSearch: 200, maxItemsLoadedForSearch: 200,
appendNewItems: (resp: paginatedDTO) => { appendNewItems: (resp: PaginatedDTO) => {
let ordering: string[] = this._search.ordering; let ordering: string[] = this._search.ordering;
for (let act of (resp as ActivitiesDTO).activities || []) { for (let act of (resp as ActivitiesDTO).activities || []) {
this.activities.set(act.id, new Activity(act)); this.activities.set(act.id, new Activity(act));
@@ -597,7 +597,7 @@ export class activityList extends PaginatedList implements Navigatable, AsTab {
} }
this._search.setOrdering(ordering, this._c.defaultSortField, this.ascending); this._search.setOrdering(ordering, this._c.defaultSortField, this.ascending);
}, },
replaceWithNewItems: (resp: paginatedDTO) => { replaceWithNewItems: (resp: PaginatedDTO) => {
// FIXME: Implement updates to existing elements, rather than just wiping each time. // FIXME: Implement updates to existing elements, rather than just wiping each time.
// Remove existing items // Remove existing items
@@ -645,7 +645,7 @@ export class activityList extends PaginatedList implements Navigatable, AsTab {
this._sortDirection.addEventListener("click", () => (this.ascending = !this.ascending)); this._sortDirection.addEventListener("click", () => (this.ascending = !this.ascending));
} }
reload = (callback?: (resp: paginatedDTO) => void) => { reload = (callback?: (resp: PaginatedDTO) => void) => {
this._reload(callback); this._reload(callback);
}; };
@@ -653,7 +653,7 @@ export class activityList extends PaginatedList implements Navigatable, AsTab {
this._loadMore(loadAll, callback); this._loadMore(loadAll, callback);
}; };
loadAll = (callback?: (resp?: paginatedDTO) => void) => { loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
this._loadAll(callback); this._loadAll(callback);
}; };

View File

@@ -101,8 +101,8 @@ export interface PaginatedListConfig {
getPageEndpoint: string | (() => string); getPageEndpoint: string | (() => string);
itemsPerPage: number; itemsPerPage: number;
maxItemsLoadedForSearch: number; maxItemsLoadedForSearch: number;
appendNewItems: (resp: paginatedDTO) => void; appendNewItems: (resp: PaginatedDTO) => void;
replaceWithNewItems: (resp: paginatedDTO) => void; replaceWithNewItems: (resp: PaginatedDTO) => void;
defaultSortField?: string; defaultSortField?: string;
defaultSortAscending?: boolean; defaultSortAscending?: boolean;
pageLoadCallback?: (req: XMLHttpRequest) => void; pageLoadCallback?: (req: XMLHttpRequest) => void;
@@ -231,7 +231,7 @@ export abstract class PaginatedList implements PageEventBindable {
searchConfig.onSearchCallback = ( searchConfig.onSearchCallback = (
newItems: boolean, newItems: boolean,
loadAll: boolean, loadAll: boolean,
callback?: (resp: paginatedDTO) => void, callback?: (resp: PaginatedDTO) => void,
) => { ) => {
// if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused"); // if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused");
// else this._c.loadAllButton.classList.add("unfocused"); // else this._c.loadAllButton.classList.add("unfocused");
@@ -258,12 +258,12 @@ export abstract class PaginatedList implements PageEventBindable {
if (previousCallback) previousCallback(newItems, loadAll); if (previousCallback) previousCallback(newItems, loadAll);
}; };
const previousServerSearch = searchConfig.searchServer; const previousServerSearch = searchConfig.searchServer;
searchConfig.searchServer = (params: PaginatedReqDTO, newSearch: boolean) => { searchConfig.searchServer = (params: PaginatedReqDTO, newSearch: boolean, then?: () => void) => {
this._searchParams = params; this._searchParams = params;
if (newSearch) this.reload(); if (newSearch) this.reload(then);
else this.loadMore(false); else this.loadMore(false, then);
if (previousServerSearch) previousServerSearch(params, newSearch); if (previousServerSearch) previousServerSearch(params, newSearch, then);
}; };
searchConfig.clearServerSearch = () => { searchConfig.clearServerSearch = () => {
console.trace("Clearing server search"); console.trace("Clearing server search");
@@ -362,9 +362,9 @@ export abstract class PaginatedList implements PageEventBindable {
private _load = ( private _load = (
itemLimit: number, itemLimit: number,
page: number, page: number,
appendFunc: (resp: paginatedDTO) => void, // Function to append/put items in storage. appendFunc: (resp: PaginatedDTO) => void, // Function to append/put items in storage.
pre?: (resp: paginatedDTO) => void, pre?: (resp: PaginatedDTO) => void,
post?: (resp: paginatedDTO) => void, post?: (resp: PaginatedDTO) => void,
failCallback?: (req: XMLHttpRequest) => void, failCallback?: (req: XMLHttpRequest) => void,
) => { ) => {
this._lastLoad = Date.now(); this._lastLoad = Date.now();
@@ -388,7 +388,7 @@ export abstract class PaginatedList implements PageEventBindable {
} }
this._hasLoaded = true; this._hasLoaded = true;
let resp = req.response as paginatedDTO; let resp = req.response as PaginatedDTO;
if (pre) pre(resp); if (pre) pre(resp);
this.lastPage = resp.last_page; this.lastPage = resp.last_page;
@@ -406,8 +406,8 @@ export abstract class PaginatedList implements PageEventBindable {
}; };
// Removes all elements, and reloads the first page. // Removes all elements, and reloads the first page.
public abstract reload: (callback?: (resp: paginatedDTO) => void) => void; public abstract reload: (callback?: (resp: PaginatedDTO) => void) => void;
protected _reload = (callback?: (resp: paginatedDTO) => void) => { protected _reload = (callback?: (resp: PaginatedDTO) => void) => {
this.lastPage = false; this.lastPage = false;
this._counter.reset(); this._counter.reset();
this._counter.getTotal( this._counter.getTotal(
@@ -422,14 +422,14 @@ export abstract class PaginatedList implements PageEventBindable {
limit, limit,
0, 0,
this._c.replaceWithNewItems, this._c.replaceWithNewItems,
(_0: paginatedDTO) => { (_0: PaginatedDTO) => {
// Allow refreshes every 15s // Allow refreshes every 15s
if (this._c.refreshButton) { if (this._c.refreshButton) {
this._c.refreshButton.disabled = true; this._c.refreshButton.disabled = true;
setTimeout(() => (this._c.refreshButton.disabled = false), 15000); setTimeout(() => (this._c.refreshButton.disabled = false), 15000);
} }
}, },
(resp: paginatedDTO) => { (resp: PaginatedDTO) => {
this._search.onSearchBoxChange(true, false, false); this._search.onSearchBoxChange(true, false, false);
if (this._search.inSearch) { if (this._search.inSearch) {
// this._c.loadAllButton.classList.remove("unfocused"); // this._c.loadAllButton.classList.remove("unfocused");
@@ -444,8 +444,8 @@ export abstract class PaginatedList implements PageEventBindable {
}; };
// Loads the next page. If "loadAll", all pages will be loaded until the last is reached. // Loads the next page. If "loadAll", all pages will be loaded until the last is reached.
public abstract loadMore: (loadAll?: boolean, callback?: (resp?: paginatedDTO) => void) => void; public abstract loadMore: (loadAll?: boolean, callback?: (resp?: PaginatedDTO) => void) => void;
protected _loadMore = (loadAll: boolean = false, callback?: (resp: paginatedDTO) => void) => { protected _loadMore = (loadAll: boolean = false, callback?: (resp: PaginatedDTO) => void) => {
this._c.loadMoreButtons.forEach((v) => (v.disabled = true)); this._c.loadMoreButtons.forEach((v) => (v.disabled = true));
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this._c.loadMoreButtons.forEach((v) => (v.disabled = false)); this._c.loadMoreButtons.forEach((v) => (v.disabled = false));
@@ -456,14 +456,14 @@ export abstract class PaginatedList implements PageEventBindable {
this._c.itemsPerPage, this._c.itemsPerPage,
this._page, this._page,
this._c.appendNewItems, this._c.appendNewItems,
(resp: paginatedDTO) => { (resp: PaginatedDTO) => {
// Check before setting this.lastPage so we have a chance to cancel the timeout. // Check before setting this.lastPage so we have a chance to cancel the timeout.
if (resp.last_page) { if (resp.last_page) {
clearTimeout(timeout); clearTimeout(timeout);
this._c.loadAllButtons.forEach((v) => removeLoader(v)); this._c.loadAllButtons.forEach((v) => removeLoader(v));
} }
}, },
(resp: paginatedDTO) => { (resp: PaginatedDTO) => {
if (this._search.inSearch || loadAll) { if (this._search.inSearch || loadAll) {
if (this.lastPage) { if (this.lastPage) {
loadAll = false; loadAll = false;
@@ -481,8 +481,8 @@ export abstract class PaginatedList implements PageEventBindable {
); );
}; };
public abstract loadAll: (callback?: (resp?: paginatedDTO) => void) => void; public abstract loadAll: (callback?: (resp?: PaginatedDTO) => void) => void;
protected _loadAll = (callback?: (resp?: paginatedDTO) => void) => { protected _loadAll = (callback?: (resp?: PaginatedDTO) => void) => {
this._c.loadAllButtons.forEach((v) => { this._c.loadAllButtons.forEach((v) => {
addLoader(v, true); addLoader(v, true);
}); });

View File

@@ -1,26 +1,19 @@
export interface Page {
name: string;
title: string;
url: string;
show: () => boolean;
hide: () => boolean;
shouldSkip: () => boolean;
index?: number;
}
export interface PageConfig { export interface PageConfig {
hideOthersOnPageShow: boolean; hideOthersOnPageShow: boolean;
defaultName: string; defaultName: string;
defaultTitle: string; defaultTitle: string;
} }
export class PageManager { export class PageManager implements Pages {
pages: Map<string, Page>; pages: Map<string, Page>;
pageList: string[]; pageList: string[];
hideOthers: boolean; hideOthers: boolean;
defaultName: string = ""; defaultName: string = "";
defaultTitle: string = ""; defaultTitle: string = "";
private _listeners: Map<string, { params: string[]; func: (qp: URLSearchParams) => void }> = new Map();
private _previousParams = new URLSearchParams();
private _overridePushState = () => { private _overridePushState = () => {
const pushState = window.history.pushState; const pushState = window.history.pushState;
window.history.pushState = function (data: any, __: string, _: string | URL) { window.history.pushState = function (data: any, __: string, _: string | URL) {
@@ -32,6 +25,8 @@ export class PageManager {
}; };
private _onpopstate = (event: PopStateEvent) => { private _onpopstate = (event: PopStateEvent) => {
const prevParams = this._previousParams;
this._previousParams = new URLSearchParams(window.location.search);
let name = event.state; let name = event.state;
if (name == null) { if (name == null) {
// Attempt to use hash from URL, if it isn't there, try the last part of the URL. // Attempt to use hash from URL, if it isn't there, try the last part of the URL.
@@ -48,6 +43,14 @@ export class PageManager {
if (!success) { if (!success) {
return; return;
} }
if (this._listeners.has(name)) {
for (let qp of this._listeners.get(name).params) {
if (prevParams.get(qp) != this._previousParams.get(qp)) {
this._listeners.get(name).func(this._previousParams);
break;
}
}
}
if (!this.hideOthers) { if (!this.hideOthers) {
return; return;
} }
@@ -115,4 +118,17 @@ export class PageManager {
} }
this.loadPage(p); this.loadPage(p);
} }
// FIXME: Make PageManager global.
// registerParamListener allows registering a listener which will be called when one or many of the given query param names are changed. It will only be called once per navigation.
registerParamListener(pageName: string, func: (qp: URLSearchParams) => void, ...qps: string[]) {
const p: { params: string[]; func: (qp: URLSearchParams) => void } = this._listeners.get(pageName) || {
params: [],
func: null,
};
if (func) p.func = func;
p.params.push(...qps);
this._listeners.set(pageName, p);
}
} }

View File

@@ -45,8 +45,8 @@ export interface SearchConfiguration {
search?: HTMLInputElement; search?: HTMLInputElement;
queries: { [field: string]: QueryType }; queries: { [field: string]: QueryType };
setVisibility: (items: string[], visible: boolean, appendedItems: boolean) => void; setVisibility: (items: string[], visible: boolean, appendedItems: boolean) => void;
onSearchCallback: (newItems: boolean, loadAll: boolean, callback?: (resp: paginatedDTO) => void) => void; onSearchCallback: (newItems: boolean, loadAll: boolean, callback?: (resp: PaginatedDTO) => void) => void;
searchServer: (params: PaginatedReqDTO, newSearch: boolean) => void; searchServer: (params: PaginatedReqDTO, newSearch: boolean, then?: () => void) => void;
clearServerSearch: () => void; clearServerSearch: () => void;
loadMore?: () => void; loadMore?: () => void;
} }
@@ -556,7 +556,7 @@ export class Search implements Navigatable {
newItems: boolean = false, newItems: boolean = false,
appendedItems: boolean = false, appendedItems: boolean = false,
loadAll: boolean = false, loadAll: boolean = false,
callback?: (resp: paginatedDTO) => void, callback?: (resp: PaginatedDTO) => void,
) => { ) => {
const query = this._c.search.value; const query = this._c.search.value;
if (!query) { if (!query) {
@@ -694,15 +694,15 @@ export class Search implements Navigatable {
this._c.filterList.appendChild(filterListContainer); this._c.filterList.appendChild(filterListContainer);
}; };
onServerSearch = () => { onServerSearch = (then?: () => void) => {
const newServerSearch = !this.inServerSearch; const newServerSearch = !this.inServerSearch;
this.inServerSearch = true; this.inServerSearch = true;
this.setQueryParam(); // this.setQueryParam();
this.searchServer(newServerSearch); this.searchServer(newServerSearch, then);
}; };
searchServer = (newServerSearch: boolean) => { searchServer = (newServerSearch: boolean, then?: () => void) => {
this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch); this._c.searchServer(this.serverSearchParams(this._searchTerms, this._queries), newServerSearch, then);
}; };
serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => { serverSearchParams = (searchTerms: string[], queries: Query[]): PaginatedReqDTO => {
@@ -721,13 +721,25 @@ export class Search implements Navigatable {
return req; return req;
}; };
// setQueryParam sets the ?search query param to the current searchbox content. // setQueryParam sets the ?search query param to the current searchbox content,
setQueryParam = () => { // or value if given. If everything is set up correctly, this should trigger a search when it is
// set to a new value.
setQueryParam = (value?: string) => {
let triggerManually = false;
if (value === undefined || value == null) value = this._c.search.value;
const url = new URL(window.location.href); const url = new URL(window.location.href);
// FIXME: do better and make someone else clear this // FIXME: do better and make someone else clear this
url.searchParams.delete("user"); if (value.trim()) {
url.searchParams.set("search", this._c.search.value); url.searchParams.delete("user");
url.searchParams.set("search", value);
} else {
// If the query param is already blank, no change will mean no call to navigate()
triggerManually = !url.searchParams.has("search");
url.searchParams.delete("search");
}
console.log("pushing", url.toString());
window.history.pushState(null, "", url.toString()); window.history.pushState(null, "", url.toString());
if (triggerManually) this.navigate();
}; };
setServerSearchButtonsDisabled = (disabled: boolean) => { setServerSearchButtonsDisabled = (disabled: boolean) => {
@@ -740,12 +752,15 @@ export class Search implements Navigatable {
return Boolean(searchContent); return Boolean(searchContent);
}; };
navigate = (url?: string) => { // navigate pulls the current "search" query param, puts it in the search box and searches it.
navigate = (url?: string, then?: () => void) => {
(window as any).s = this;
const urlParams = new URLSearchParams(url || window.location.search); const urlParams = new URLSearchParams(url || window.location.search);
const searchContent = urlParams.get("search"); const searchContent = urlParams.get("search") || "";
console.log("navigate!, setting search box to ", searchContent);
this._c.search.value = searchContent; this._c.search.value = searchContent;
this.onSearchBoxChange(); this.onSearchBoxChange();
this.onServerSearch(); this.onServerSearch(then);
}; };
constructor(c: SearchConfiguration) { constructor(c: SearchConfiguration) {
@@ -761,7 +776,7 @@ export class Search implements Navigatable {
}; };
this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => { this._c.search.addEventListener("keyup", (ev: KeyboardEvent) => {
if (ev.key == "Enter") { if (ev.key == "Enter") {
this.onServerSearch(); this.setQueryParam();
} }
}); });
@@ -771,9 +786,8 @@ export class Search implements Navigatable {
) as Array<HTMLSpanElement>; ) as Array<HTMLSpanElement>;
for (let b of clearSearchButtons) { for (let b of clearSearchButtons) {
b.addEventListener("click", () => { b.addEventListener("click", () => {
this._c.search.value = "";
this.inServerSearch = false; this.inServerSearch = false;
this.onSearchBoxChange(); this.setQueryParam("");
}); });
} }
} }
@@ -783,7 +797,7 @@ export class Search implements Navigatable {
: []; : [];
for (let b of this._serverSearchButtons) { for (let b of this._serverSearchButtons) {
b.addEventListener("click", () => { b.addEventListener("click", () => {
this.onServerSearch(); this.setQueryParam();
}); });
} }
} }

View File

@@ -1,4 +1,4 @@
import { PageManager, Page } from "../modules/pages.js"; import { PageManager } from "../modules/pages.js";
export function isPageEventBindable(object: any): object is PageEventBindable { export function isPageEventBindable(object: any): object is PageEventBindable {
return "bindPageEvents" in object; return "bindPageEvents" in object;
@@ -8,15 +8,7 @@ export function isNavigatable(object: any): object is Navigatable {
return "isURL" in object && "navigate" in object; return "isURL" in object && "navigate" in object;
} }
export interface Tab { export class TabManager implements TabManager {
page: Page;
tabEl: HTMLDivElement;
buttonEl: HTMLSpanElement;
preFunc?: () => void;
postFunc?: () => void;
}
export class Tabs implements Tabs {
private _current: string = ""; private _current: string = "";
private _baseOffset = -1; private _baseOffset = -1;
tabs: Map<string, Tab>; tabs: Map<string, Tab>;

View File

@@ -150,12 +150,6 @@ declare interface NotificationBox {
customSuccess: (type: string, message: string) => void; customSuccess: (type: string, message: string) => void;
} }
declare interface Tabs {
current: string;
addTab: (tabID: string, url: string, preFunc?: () => void, postFunc?: () => void, unloadFunc?: () => void) => void;
switch: (tabID: string, noRun?: boolean, keepURL?: boolean) => void;
}
declare interface Modals { declare interface Modals {
about: Modal; about: Modal;
login: Modal; login: Modal;
@@ -188,18 +182,58 @@ declare interface Modals {
backups?: Modal; backups?: Modal;
} }
interface paginatedDTO { declare interface Page {
name: string;
title: string;
url: string;
show: () => boolean;
hide: () => boolean;
shouldSkip: () => boolean;
index?: number;
}
declare interface Tab {
page: Page;
tabEl: HTMLDivElement;
buttonEl: HTMLSpanElement;
preFunc?: () => void;
postFunc?: () => void;
}
declare interface Tabs {
tabs: Map<string, Tab>;
pages: Pages;
addTab(tabID: string, url: string, preFunc: () => void, postFunc: () => void, unloadFunc: () => void): void;
current: string;
switch(tabID: string, noRun?: boolean): void;
}
declare interface Pages {
pages: Map<string, Page>;
pageList: string[];
hideOthers: boolean;
defaultName: string;
defaultTitle: string;
setPage(p: Page): void;
load(name?: string): void;
loadPage(p: Page): void;
prev(name?: string): void;
next(name?: string): void;
registerParamListener(pageName: string, func: (qp: URLSearchParams) => void, ...qps: string[]): void;
}
declare interface PaginatedDTO {
last_page: boolean; last_page: boolean;
} }
interface PaginatedReqDTO { declare interface PaginatedReqDTO {
limit: number; limit: number;
page: number; page: number;
sortByField: string; sortByField: string;
ascending: boolean; ascending: boolean;
} }
interface DateAttempt { declare interface DateAttempt {
year?: number; year?: number;
month?: number; month?: number;
day?: number; day?: number;
@@ -208,7 +242,7 @@ interface DateAttempt {
offsetMinutesFromUTC?: number; offsetMinutesFromUTC?: number;
} }
interface ParsedDate { declare interface ParsedDate {
attempt: DateAttempt; attempt: DateAttempt;
date: Date; date: Date;
text: string; text: string;