Last active
January 21, 2022 07:54
-
-
Save jamiebuilds/9c66b255845ba092e0bbddb30707a736 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)})} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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