import * as qs from 'query-string'; import { Doc } from '../../fields/Doc'; import { OmitKeys, Utils } from '../../Utils'; import { DocServer } from '../DocServer'; import { DashboardView } from '../views/DashboardView'; export namespace HistoryUtil { export interface DocInitializerList { [key: string]: string | number; } export interface DocUrl { type: 'doc'; docId: string; initializers?: { [docId: string]: DocInitializerList; }; safe?: boolean; readonly?: boolean; nro?: boolean; sharing?: boolean; } export type ParsedUrl = DocUrl; // const handlers: ((state: ParsedUrl | null) => void)[] = []; function onHistory(e: PopStateEvent) { if (window.location.pathname !== '/home') { const url = (e.state as ParsedUrl) || parseUrl(window.location); if (url) { switch (url.type) { case 'doc': onDocUrl(url); break; } } } // for (const handler of handlers) { // handler(e.state); // } } let _lastStatePush = 0; export function pushState(state: ParsedUrl) { if (Date.now() - _lastStatePush > 1000) { history.pushState(state, '', createUrl(state)); } _lastStatePush = Date.now(); } export function replaceState(state: ParsedUrl) { history.replaceState(state, '', createUrl(state)); } function copyState(state: ParsedUrl): ParsedUrl { return JSON.parse(JSON.stringify(state)); } export function getState(): ParsedUrl { const state = copyState(history.state); if (state) { state.initializers = state.initializers || {}; } return state ?? { initializers: {} }; } // export function addHandler(handler: (state: ParsedUrl | null) => void) { // handlers.push(handler); // } // export function removeHandler(handler: (state: ParsedUrl | null) => void) { // const index = handlers.indexOf(handler); // if (index !== -1) { // handlers.splice(index, 1); // } // } const parsers: { [type: string]: (pathname: string[], opts: qs.ParsedQuery) => ParsedUrl | undefined } = {}; const stringifiers: { [type: string]: (state: ParsedUrl) => string } = {}; type ParserValue = true | 'none' | 'json' | ((value: string) => any); type Parser = { [key: string]: ParserValue; }; function addParser(type: string, requiredFields: Parser, optionalFields: Parser, customParser?: (pathname: string[], opts: qs.ParsedQuery, current: ParsedUrl) => ParsedUrl | null | undefined) { function parse(parser: ParserValue, value: string | string[] | null | undefined) { if (value === undefined || value === null) { return value; } if (Array.isArray(value)) { } else if (parser === true || parser === 'json') { value = JSON.parse(value); } else if (parser === 'none') { } else { value = parser(value); } return value; } parsers[type] = (pathname, opts) => { const current: any = { type }; for (const required in requiredFields) { if (!(required in opts)) { return undefined; } const parser = requiredFields[required]; const value = parse(parser, opts[required]); if (value !== null && value !== undefined) { current[required] = value; } } for (const opt in optionalFields) { if (!(opt in opts)) { continue; } const parser = optionalFields[opt]; const value = parse(parser, opts[opt]); if (value !== undefined) { current[opt] = value; } } if (customParser) { const val = customParser(pathname, opts, current); if (val === null) { return undefined; } else if (val === undefined) { return current; } else { return val; } } return current; }; } function addStringifier(type: string, keys: string[], customStringifier?: (state: ParsedUrl, current: string) => string) { stringifiers[type] = state => { let path = Utils.prepend(`/${type}`); if (customStringifier) { path = customStringifier(state, path); } const queryObj = OmitKeys(state, keys).extract; const query: any = {}; Object.keys(queryObj).forEach(key => (query[key] = queryObj[key] === null ? null : JSON.stringify(queryObj[key]))); const queryString = qs.stringify(query); return path + (queryString ? `?${queryString}` : ''); }; } addParser('doc', {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => { if (pathname.length !== 2) return undefined; current.initializers = current.initializers || {}; const docId = pathname[1]; current.docId = docId; }); addStringifier('doc', ['initializers', 'readonly', 'nro'], (state, current) => { return `${current}/${state.docId}`; }); export function parseUrl(location: Location | URL): ParsedUrl | undefined { const pathname = location.pathname.substring(1); const search = location.search; const opts = search.length ? qs.parse(search, { sort: false }) : {}; const pathnameSplit = pathname.split('/'); const type = pathnameSplit[0]; if (type in parsers) { return parsers[type](pathnameSplit, opts); } return undefined; } export function createUrl(params: ParsedUrl): string { if (params.type in stringifiers) { return stringifiers[params.type](params); } return ''; } export async function initDoc(id: string, initializer: DocInitializerList) { const doc = await DocServer.GetRefField(id); if (!(doc instanceof Doc)) { return; } Doc.assign(doc, initializer); } async function onDocUrl(url: DocUrl) { const field = await DocServer.GetRefField(url.docId); const init = url.initializers; if (init) { await Promise.all(Object.keys(init).map(id => initDoc(id, init[id]))); } if (field instanceof Doc) { DashboardView.openDashboard(field, true); } } window.onpopstate = onHistory; }