Skip to content

Instantly share code, notes, and snippets.

@jamiebuilds
Last active January 21, 2022 07:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jamiebuilds/9c66b255845ba092e0bbddb30707a736 to your computer and use it in GitHub Desktop.
Save jamiebuilds/9c66b255845ba092e0bbddb30707a736 to your computer and use it in GitHub Desktop.
function t(){let t=new Set;return{listen:e=>(t.add(e),()=>{t.delete(e)}),emit(){t.forEach((t=>t()))}}}function e(){return Math.random().toString(36).substring(2,8)}function n(t,e,n){return Math.min(Math.max(t,e),n)}function r(t,e){return(t.startsWith(e)?"":e)+t}function a({pathname:t,search:e,hash:n}){return{pathname:r(t,"/"),search:r(e,"?"),hash:r(n,"#")}}function s({pathname:t,search:e,hash:n}){return t+("?"===e?"":e)+("#"===n?"":n)}function h(t){return s(a(t))}function u(t){let[e,n=""]=t.split("#"),[r,s=""]=e.split("?");return a({pathname:r,search:s,hash:n})}function i(t){return s(u(t.substring(1)))}function o(t){if("string"==typeof t)return t;{let{pathname:e="",search:n="",hash:r=""}=t;return h({pathname:e,search:n,hash:r})}}function l(t){return{location(){return e=t.path(),n=t.state(),r=t.key(),{...u(e),state:n,key:r};var e,n,r},push(e,n){let r=o(e);t.push(r,n)},replace(e,n){let r=o(e);t.push(r,n)},go(e){t.go(e)},back(){t.go(-1)},forward(){t.go(1)},listen:e=>t.listen(e)}}function p(n){let r=n.history,a=t();return{path:()=>h(n.location),state:()=>r.state.usr||null,key:()=>r.state.key||"default",push(t,n){r.pushState({usr:n,key:e()},"",t),a.emit()},replace(t,n){r.replaceState({usr:n,key:e()},"",t),a.emit()},go(t){n.history.go(t)},listen(t){let e=a.listen(t),r=()=>t();return n.addEventListener("popstate",r),()=>{e(),n.removeEventListener("popstate",r)}}}}export function browser(t){return l(p(t))}export function hash(t){let e=p(t);function n(){return i(t.location.hash)}return l({path:n,state:()=>e.state(),key:()=>e.key(),push(t,n){e.push("#"+t,n)},replace(t,n){e.replace("#"+t,n)},go(t){e.go(t)},listen(r){let a,s=()=>{let t=n();null!=a&&t!==a&&(r(),a=t)},h=e.listen(r);return t.addEventListener("hashchange",s),()=>{h(),t.removeEventListener("hashchange",s)}}})}let c={path:"/",state:null,key:e()};export function memory(r=[c],a=r.length-1){let s=t(),h=r,u=n(a,0,h.length-1);return l({path:()=>h[u].path,state:()=>h[u].state,key:()=>h[u].key,push(t,n){u++,h.splice(u,h.length,{path:t,state:n,key:e()}),s.emit()},replace(t,n){h[u]={path:t,state:n,key:e()},s.emit()},go(t){u=n(u+t,0,h.length-1),s.emit()},listen:t=>s.listen(t)})}
// Types
// -----
export interface Parts {
pathname: string;
search: string;
hash: string;
}
interface BaseHistory {
path(): string;
state(): any;
key(): string;
push(path: string, state?: any): void;
replace(path: string, state?: any): void;
go(delta: number): void;
listen(listener: Listener): Unlisten;
}
export type To = string | Partial<Parts>;
export interface Location {
pathname: string;
search: string;
hash: string;
state: any;
key: string;
}
export interface History {
location(): Location;
push(to: To, state?: any): void;
replace(to: To, state?: any): void;
go(delta: number): void;
back(): void;
forward(): void;
listen(listener: Listener): Unlisten;
}
export type Listener = () => void;
export type Unlisten = () => void;
// Utils
// -----
function createEventEmitter() {
let listeners = new Set<Listener>();
return {
listen(listener: Listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
emit() {
listeners.forEach((listener) => listener());
}
};
}
function createKey() {
return Math.random().toString(36).substring(2, 8);
}
function clamp(n: number, lowerBound: number, upperBound: number) {
return Math.min(Math.max(n, lowerBound), upperBound);
}
function ensureStartsWith(input: string, start: string) {
return (input.startsWith(start) ? "" : start) + input;
}
function normalizeParts({ pathname, search, hash }: Parts) {
return {
pathname: ensureStartsWith(pathname, "/"),
search: ensureStartsWith(search, "?"),
hash: ensureStartsWith(hash, "#")
};
}
function joinParts({ pathname, search, hash }: Parts) {
return pathname + (search === "?" ? "" : search) + (hash === "#" ? "" : hash);
}
function normalizeAndJoinParts(parts: Parts) {
return joinParts(normalizeParts(parts));
}
function toParts(path: string) {
let [beforeHash, hash = ""] = path.split("#");
let [pathname, search = ""] = beforeHash.split("?");
return normalizeParts({ pathname, search, hash });
}
function normalizePath(path: string) {
return joinParts(toParts(path));
}
function hashToPath(hash: string) {
return normalizePath(hash.substring(1));
}
function toPath(to: To) {
if (typeof to === "string") {
return to;
} else {
let { pathname = "", search = "", hash = "" } = to;
return normalizeAndJoinParts({ pathname, search, hash });
}
}
function toLocation(path: string, state: any, key: string): Location {
return { ...toParts(path), state, key };
}
// Enhance
// -------
function enhance(history: BaseHistory): History {
return {
location() {
return toLocation(history.path(), history.state(), history.key());
},
push(to, state) {
let path = toPath(to);
history.push(path, state);
},
replace(to, state) {
let path = toPath(to);
history.push(path, state);
},
go(delta) {
history.go(delta);
},
back() {
history.go(-1);
},
forward() {
history.go(1);
},
listen(listener) {
return history.listen(listener);
}
};
}
// Browser
// -------
function browserBase(window: Window): BaseHistory {
let history = window.history;
let listeners = createEventEmitter();
return {
path() {
return normalizeAndJoinParts(window.location);
},
state() {
return history.state.usr || null;
},
key() {
return history.state.key || "default";
},
push(path, state) {
history.pushState({ usr: state, key: createKey() }, "", path);
listeners.emit();
},
replace(path, state) {
history.replaceState({ usr: state, key: createKey() }, "", path);
listeners.emit();
},
go(delta) {
window.history.go(delta);
},
listen(listener) {
let unlisten = listeners.listen(listener);
let listenerWrapper = () => listener();
window.addEventListener("popstate", listenerWrapper);
return () => {
unlisten();
window.removeEventListener("popstate", listenerWrapper);
};
}
};
}
export function browser(window: Window): History {
return enhance(browserBase(window));
}
// Hash
// ----
export function hash(window: Window): History {
let base = browserBase(window);
function path() {
return hashToPath(window.location.hash);
}
return enhance({
path,
state() {
return base.state();
},
key() {
return base.key();
},
push(path, state) {
base.push("#" + path, state);
},
replace(path, state) {
base.replace("#" + path, state);
},
go(delta) {
base.go(delta);
},
listen(listener) {
let previousPath: string;
let listenerWrapper = () => {
let currentPath = path();
if (previousPath != null && currentPath !== previousPath) {
listener();
previousPath = currentPath;
}
};
let unlisten = base.listen(listener);
window.addEventListener("hashchange", listenerWrapper);
return () => {
unlisten();
window.removeEventListener("hashchange", listenerWrapper);
};
}
});
}
// Memory
// -------
export interface MemoryHistoryEntry {
path: string;
state: any;
key: string;
}
let INITIAL_ENTRY: MemoryHistoryEntry = {
path: "/",
state: null,
key: createKey()
};
export function memory(
initialEntries: MemoryHistoryEntry[] = [INITIAL_ENTRY],
initialIndex: number = initialEntries.length - 1
): History {
let listeners = createEventEmitter();
let entries = initialEntries;
let index = clamp(initialIndex, 0, entries.length - 1);
return enhance({
path() {
return entries[index].path;
},
state() {
return entries[index].state;
},
key() {
return entries[index].key;
},
push(path, state) {
index++;
entries.splice(index, entries.length, { path, state, key: createKey() });
listeners.emit();
},
replace(path, state) {
entries[index] = { path, state, key: createKey() };
listeners.emit();
},
go(delta) {
index = clamp(index + delta, 0, entries.length - 1);
listeners.emit();
},
listen(listener) {
return listeners.listen(listener);
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment