export interface Page { name: string; title: string; url: string; show: () => boolean; hide: () => boolean; shouldSkip: () => boolean; index?: number; } export interface PageConfig { hideOthersOnPageShow: boolean; defaultName: string; defaultTitle: string; } export class PageManager { pages: Map; pageList: string[]; hideOthers: boolean; defaultName: string = ""; defaultTitle: string = ""; private _overridePushState = () => { const pushState = window.history.pushState; window.history.pushState = function (data: any, __: string, _: string | URL) { console.debug("Pushing state", arguments); pushState.apply(window.history, arguments); let ev = { state: data as string } as PopStateEvent; window.onpopstate(ev); }; }; private _onpopstate = (event: PopStateEvent) => { let name = event.state; if (name == null) { // Attempt to use hash from URL, if it isn't there, try the last part of the URL. if (window.location.hash && window.location.hash.charAt(0) == "#") { name = window.location.hash.substring(1); } else { name = window.location.pathname.split("/").filter(Boolean).at(-1); } } if (!this.pages.has(name)) { name = this.pageList[0]; } let success = this.pages.get(name).show(); if (!success) { return; } if (!this.hideOthers) { return; } for (let k of this.pageList) { if (name != k) { this.pages.get(k).hide(); } } }; constructor(c: PageConfig) { this.pages = new Map(); this.pageList = []; this.hideOthers = c.hideOthersOnPageShow; this.defaultName = c.defaultName; this.defaultTitle = c.defaultTitle; this._overridePushState(); window.onpopstate = this._onpopstate; } setPage(p: Page) { p.index = this.pageList.length; this.pages.set(p.name, p); this.pageList.push(p.name); } load(name: string = "") { name = decodeURI(name); if (!this.pages.has(name)) return window.history.pushState(name || this.defaultName, this.defaultTitle, ""); const p = this.pages.get(name); this.loadPage(p); } loadPage(p: Page) { let url = p.url; // Fix ordering of query params and hash if (url.includes("#")) { let split = url.split("#"); url = split[0] + window.location.search + "#" + split[1]; } else { url = url + window.location.search; } window.history.pushState(p.name || this.defaultName, p.title, url); } prev(name: string = "") { if (!this.pages.has(name)) return console.error(`previous page ${name} not found`); let p = this.pages.get(name); let shouldSkip = true; while (shouldSkip && p.index > 0) { p = this.pages.get(this.pageList[p.index - 1]); shouldSkip = p.shouldSkip(); } this.loadPage(p); } next(name: string = "") { if (!this.pages.has(name)) return console.error(`previous page ${name} not found`); let p = this.pages.get(name); let shouldSkip = true; while (shouldSkip && p.index < this.pageList.length) { p = this.pages.get(this.pageList[p.index + 1]); shouldSkip = p.shouldSkip(); } this.loadPage(p); } }