export interface HashUrlData { path: (string | undefined)[]; query: { [key in QueryKeys]?: string | string[] | undefined; }; } export const hashJsonEncode = (data: any) => data ? encodeURIComponent(JSON.stringify(data)) : ""; export const hashJsonDecode = (str: string) => str ? JSON.parse(decodeURIComponent(str)) : undefined; export const hashUrlEncode = (data: HashUrlData): string => { // console.log(data); const d = typeof data === "string" ? hashUrlDecode(data) : data as HashUrlData; const dpath = d.path ?? []; const dquery = d.query ?? {}; const path = dpath.map(p => encodeURIComponent(p ?? "")).join("/"); // const params = new URLSearchParams(); const query = Object.entries(dquery) .reduce( (p, c) => { if (typeof c[1] === "string") { p.append(c[0], c[1]); } else if (c[1] && typeof c[1].length === "number") { (c[1] as Array).forEach(v => p.append(c[0], v)); } return p; }, new URLSearchParams()) .toString(); const str = `${path}${query ? "?" : ""}${query}`; // console.log(str); return str; }; export const hashUrlDecode = (str: string): HashUrlData => { // console.log(str); const [pathStr, queryStr] = str.split("?"); // const pathStr = str.substring(0, str.indexOf("?")); // const queryStr = str.substring(str.indexOf("?") + 1); const path = pathStr.split("/").map(p => decodeURIComponent(p)); const query = {} as any; for (const e of new URLSearchParams(queryStr).entries()) { const [k, v] = e; if (k in query) { if (typeof query[k] === "string") { query[k] = [query[k], v]; } else { query[k].push(v); } } else { query[k] = v; } } const data = { path, query }; // console.log(data); return data; }; export class Hash { private _cb?: (data: T) => void; private _emitWritten: boolean; private _writtenDataJson?: string; private _encode = (data: T) => String(data); private _decode = (str: string) => str as any as T; constructor(emitWritten = true) { this._emitWritten = emitWritten; } onChange(cb: (data: T) => void): this { this._cb = cb; return this; } coders(encode: (data: T) => string, decode: (data: string) => T): this { this._encode = encode; this._decode = decode; return this; } listen(): this { this._cb && this._cb(this.read()); if ("onhashchange" in window) { onhashchange = () => { const data = this.read(); // console.log("onhashchange", location.hash, data); if (!this._emitWritten) { const written = this._writtenDataJson === JSON.stringify(data); this._writtenDataJson = undefined; if (!written) { this._cb && this._cb(data); } } else { this._cb && this._cb(data); } }; } else { alert(`Browser "window.onhashchange" not implemented`); // let prevHash = location.hash; // if (this._iId) { // clearInterval(this._iId); // } // this._iId = setInterval(() => { // if (location.hash !== prevHash) { // prevHash = location.hash; // const written = this._dataJson === location.hash; // this._dataJson = undefined; // if (this._emitWritten || !written) { // this._cb && this._cb(data); // } // } // }, 500); } return this; } read(): T { return this._decode(location.hash.slice(1)); } write(data: T): this { location.hash = this._encode(data); if (!this._emitWritten) { this._writtenDataJson = JSON.stringify(data); this._cb && this._cb(data); } return this; } }