UNPKG

@metapages/hash-query

Version:

Get/set URL parameters in the hash string instead of the query string.

373 lines (320 loc) 9.8 kB
/** * Core logic for getting/setting hash params * Important note: the internal hash string does NOT have the leading # */ import stringify from 'fast-json-stable-stringify'; export type SetHashParamOpts = { modifyHistory?: boolean; }; export const blobToBase64String = (blob: Record<string, any>) => { return stringToBase64String(stringify(blob)); }; export const blobFromBase64String = (value: string | undefined) => { if (value && value.length > 0) { const blob = JSON.parse(stringFromBase64String(value)); return blob; } return undefined; }; export const stringToBase64String = (value: string) :string => { return btoa(encodeURIComponent(value)); }; export const stringFromBase64String = (value: string) :string => { https://github.com/metapages/metaframe-js/issues/11 while (value.endsWith("%3D")) { value = value.slice(0, -3); } return decodeURIComponent(atob(value)); }; // Get everything after # then after ? export const getUrlHashParams = ( url: string ): [string, Record<string, string>] => { const urlBlob = new URL(url); return getUrlHashParamsFromHashString(urlBlob.hash); }; export const getUrlHashParamsFromHashString = ( hash: string ): [string, Record<string, string>] => { let hashString = hash; while (hashString.startsWith("#")) { hashString = hashString.substring(1); } const queryIndex = hashString.indexOf("?"); if (queryIndex === -1) { return [hashString, {}]; } const preHashString = hashString.substring(0, queryIndex); hashString = hashString.substring(queryIndex + 1); const hashObject: Record<string, string> = {}; hashString .split("&") .filter((s) => s.length > 0) .map((s) => { const dividerIndex = s.indexOf("="); if (dividerIndex === -1) { return [s, ""]; } const key = s.substring(0, dividerIndex); const value = s.substring(dividerIndex + 1); return [key, value]; }) .forEach(([key, value]) => { hashObject[key] = value; }); Object.keys(hashObject).forEach((key) => { try { hashObject[key] = decodeURIComponent(hashObject[key]); } catch (ignored) { hashObject[key] = hashObject[key]; } }); return [preHashString, hashObject]; }; export const getHashParamValue = ( url: string, key: string ): string | undefined => { const [_, hashParams] = getUrlHashParams(url); return hashParams[key]; }; export const getHashParamFromWindow = (key: string): string | undefined => { return getHashParamsFromWindow()[1][key]; }; export const getHashParamsFromWindow = (): [string, Record<string, string>] => { return getUrlHashParams(window.location.href); }; export const setHashParamInWindow = ( key: string, value: string | undefined, opts?: SetHashParamOpts ) => { const hash = window.location.hash.startsWith("#") ? window.location.hash.substring(1) : window.location.hash; const newHash = setHashParamValueInHashString(hash, key, value); if (newHash === hash) { return; } if (opts?.modifyHistory) { // adds to browser history, so affects back button // fires "hashchange" event window.location.hash = newHash; } else { // The following will NOT work to trigger a 'hashchange' event: // Replace the state so the back button works correctly window.history.replaceState( null, document.title, `${window.location.pathname}${window.location.search}${ newHash.startsWith("#") ? "" : "#" }${newHash}` ); // Manually trigger a hashchange event: // I don't know how to add the previous and new url parameters window.dispatchEvent(new HashChangeEvent("hashchange")); } }; // returns hash string export const setHashParamValueInHashString = ( hash: string, key: string, value: string | undefined ) => { const [preHashParamString, hashObject] = getUrlHashParamsFromHashString(hash); let changed = false; if ( (hashObject.hasOwnProperty(key) && value === null) || value === undefined ) { delete hashObject[key]; changed = true; } else { if (hashObject[key] !== value) { hashObject[key] = value; changed = true; } } // don't do work if unneeded if (!changed) { return hash; } const keys = Object.keys(hashObject); keys.sort(); const hashStringNew = keys .map((key, i) => { return `${key}=${encodeURIComponent(hashObject[key])}`; }) .join("&"); // replace after the ? but keep before that return `${preHashParamString}?${hashStringNew}`; }; // returns URL string export const setHashParamValueInUrl = ( url: string, key: string, value: string | undefined ) => { const urlBlob = new URL(url); const newHash = setHashParamValueInHashString(urlBlob.hash, key, value); urlBlob.hash = newHash; return urlBlob.href; }; /* json */ export const setHashParamValueJsonInUrl = <T>( url: string, key: string, value: T | undefined ) :string => { const urlBlob = new URL(url); urlBlob.hash = setHashParamValueJsonInHashString(urlBlob.hash, key, value); return urlBlob.href; }; export const getHashParamValueJsonFromUrl = <T>( url: string, key: string ): T | undefined => { const valueString = getHashParamValue(url, key); if (valueString && valueString !== "") { const value = blobFromBase64String(valueString); return value; } return; }; export const setHashParamValueJsonInWindow =<T>( key: string, value: T | undefined, opts?: SetHashParamOpts ) :void => { const valueString = value ? blobToBase64String(value) : undefined; setHashParamInWindow(key, valueString, opts); }; export const getHashParamValueJsonFromWindow = <T>( key: string ): T | undefined => { return getHashParamValueJsonFromUrl(window.location.href, key); }; export const setHashParamValueJsonInHashString = <T>( hash: string, key: string, value: T | undefined ) => { const valueString = value ? blobToBase64String(value) : undefined; return setHashParamValueInHashString(hash, key, valueString); }; /* float */ export const setHashParamValueFloatInUrl = ( url: string, key: string, value: number | undefined ) :string => { return setHashParamValueInUrl(url, key, value ? value.toString() : undefined); }; export const getHashParamValueFloatFromUrl = ( url: string, key: string ): number | undefined => { const hashParamString = getHashParamValue(url, key); return hashParamString ? parseFloat(hashParamString) : undefined; }; export const setHashParamValueFloatInWindow = ( key: string, value: number | undefined, opts?: SetHashParamOpts ) :void => { setHashParamInWindow(key, value !== undefined && value !== null ? value.toString() : undefined, opts); }; export const getHashParamValueFloatFromWindow = (key: string): number | undefined => { return getHashParamValueFloatFromUrl(window.location.href, key); }; /* integer */ export const setHashParamValueIntInUrl = ( url: string, key: string, value: number | undefined ) :string => { return setHashParamValueInUrl(url, key, value !== undefined && value !== null ? value.toString() : undefined); }; export const getHashParamValueIntFromUrl = ( url: string, key: string ): number | undefined => { const hashParamString = getHashParamValue(url, key); return hashParamString ? parseInt(hashParamString) : undefined; }; export const setHashParamValueIntInWindow = ( key: string, value: number | undefined, opts?: SetHashParamOpts ) :void => { setHashParamValueFloatInWindow(key, value, opts); }; export const getHashParamValueIntFromWindow = (key: string): number | undefined => { return getHashParamValueIntFromUrl(window.location.href, key); }; /* boolean */ export const setHashParamValueBooleanInUrl = ( url: string, key: string, value: boolean | undefined ) :string => { return setHashParamValueInUrl(url, key, value ? "true" : undefined); }; export const getHashParamValueBooleanFromUrl = ( url: string, key: string ): boolean | undefined => { const hashParamString = getHashParamValue(url, key); return hashParamString === "true" ? true : false; }; export const setHashParamValueBooleanInWindow = ( key: string, value: boolean | undefined, opts?: SetHashParamOpts ) :void => { setHashParamInWindow(key, value ? "true" : undefined, opts); }; export const getHashParamValueBooleanFromWindow = (key: string): boolean | undefined => { return getHashParamValueBooleanFromUrl(window.location.href, key); }; /* HashValueBase64 */ export const setHashParamValueBase64EncodedInUrl = ( url: string, key: string, value: string | undefined ) :string => { return setHashParamValueInUrl(url, key, value === null || value === undefined ? undefined : stringToBase64String(value)); }; export const getHashParamValueBase64DecodedFromUrl = ( url: string, key: string ): string | undefined => { const valueString = getHashParamValue(url, key); return valueString && valueString !== "" ? stringFromBase64String(valueString) : undefined; }; export const setHashParamValueBase64EncodedInWindow = ( key: string, value: string | undefined, opts?: SetHashParamOpts ) :void => { const encodedValue = value === null || value === undefined ? undefined : stringToBase64String(value); setHashParamInWindow(key, encodedValue, opts); }; export const getHashParamValueBase64DecodedFromWindow = ( key: string ): string | undefined => { return getHashParamValueBase64DecodedFromUrl(window.location.href, key); }; export const deleteHashParamFromWindow = ( key: string, opts?: SetHashParamOpts ): void => { setHashParamInWindow(key, undefined, opts); }; export const deleteHashParamFromUrl = ( url: string, key: string ): string => { return setHashParamValueInUrl(url, key, undefined); };