0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 1 | const KEY = 'backLayer'; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 2 | const SETTLE_MS = 400; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 3 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 4 | export interface HistoryLike { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 5 | state: unknown; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 6 | pushState(state: unknown, unused: string, url?: string): void; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 7 | replaceState(state: unknown, unused: string, url?: string): void; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 8 | back(): void; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 9 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 10 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 11 | interface Layer { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 12 | id: string; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 13 | close: (id: string) => void; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 14 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 15 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 16 | function layerOf(state: unknown): string | undefined { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 17 | if (state && typeof state === 'object' && KEY in state) return String((state as Record<string, unknown>)[KEY]); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 18 | return undefined; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 19 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 20 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 21 | function plain(state: unknown): Record<string, unknown> { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 22 | const { [KEY]: _drop, ...rest } = (state && typeof state === 'object' ? state : {}) as Record<string, unknown>; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 23 | return rest; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 24 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 25 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 26 | export class BackStack { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 27 | private stack: Layer[] = []; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 28 | private queue: Array<() => void> = []; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 29 | private waiting = false; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 30 | private timer: ReturnType<typeof setTimeout> | null = null; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 31 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 32 | constructor(private readonly history: HistoryLike) {} |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 33 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 34 | /** A reload can land on a layer entry whose layer no longer exists. */ |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 35 | init(): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 36 | if (layerOf(this.history.state) !== undefined) this.history.replaceState(plain(this.history.state), ''); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 37 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 38 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 39 | get depth(): number { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 40 | return this.stack.length; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 41 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 42 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 43 | sync(id: string, open: boolean, close: (id: string) => void): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 44 | const index = this.stack.findIndex((l) => l.id === id); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 45 | if (open && index < 0) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 46 | this.stack.push({ id, close }); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 47 | this.run(() => this.history.pushState({ ...plain(this.history.state), [KEY]: id }, '')); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 48 | } else if (open) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 49 | this.stack[index].close = close; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 50 | } else if (index >= 0) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 51 | const top = index === this.stack.length - 1; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 52 | this.stack.splice(index, 1); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 53 | if (top) this.run(() => this.rewind()); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 54 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 55 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 56 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 57 | /** Records a route, replacing the layer entry on top when layers are being dismissed by the navigation. */ |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 58 | route(state: unknown, url: string): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 59 | this.run(() => { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 60 | if (this.stack.length > 0 && layerOf(this.history.state) !== undefined) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 61 | this.stack = []; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 62 | this.history.replaceState(state, '', url); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 63 | } else { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 64 | this.history.pushState(state, '', url); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 65 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 66 | }); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 67 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 68 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 69 | /** Returns true when the popstate belonged to the layer stack, false when it is a plain route change. */ |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 70 | pop(state: unknown): boolean { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 71 | if (this.waiting) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 72 | this.settle(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 73 | return true; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 74 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 75 | const id = layerOf(state); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 76 | if (id === undefined) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 77 | if (this.stack.length === 0) return false; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 78 | this.closeAbove(-1); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 79 | return true; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 80 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 81 | const index = this.stack.findIndex((l) => l.id === id); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 82 | if (index < 0) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 83 | this.history.back(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 84 | return true; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 85 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 86 | this.closeAbove(index); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 87 | return true; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 88 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 89 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 90 | private closeAbove(index: number): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 91 | const closing = this.stack.splice(index + 1); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 92 | for (const layer of closing.reverse()) layer.close(layer.id); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 93 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 94 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 95 | private rewind(): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 96 | if (layerOf(this.history.state) === undefined) return; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 97 | this.waiting = true; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 98 | this.timer = setTimeout(() => this.settle(), SETTLE_MS); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 99 | this.history.back(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 100 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 101 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 102 | private settle(): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 103 | this.waiting = false; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 104 | if (this.timer !== null) clearTimeout(this.timer); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 105 | this.timer = null; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 106 | const next = this.queue.shift(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 107 | if (next) this.run(next); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 108 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 109 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 110 | private run(op: () => void): void { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 111 | if (this.waiting) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 112 | this.queue.push(op); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 113 | return; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 114 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 115 | op(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 116 | if (!this.waiting) { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 117 | const next = this.queue.shift(); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 118 | if (next) this.run(next); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 119 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 120 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 121 | } |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 122 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 123 | export const backStack = new BackStack(typeof window === 'undefined' ? ({} as HistoryLike) : window.history); |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 124 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 125 | let instances = 0; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 126 | |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 127 | export function instanceLayerId(prefix: string): string { |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 128 | return `${prefix}-${++instances}`; |
0e50e0d137af | atagen | 2026-10-26 05:50 UTC | 129 | } |