+4
-1
deno.json
+4
-1
deno.json
+115
lib/history-stack.ts
+115
lib/history-stack.ts
···
1
+
/**
2
+
* @module
3
+
* provides an implementation that maintains a best-effort snapshot of the
4
+
* browser's history stack.
5
+
*/
6
+
import type { BrowserHistory, Location } from './mod.ts';
7
+
8
+
/**
9
+
* represents a read-only view of a location stack synchronized with browser history.
10
+
*/
11
+
export interface HistoryStack {
12
+
/**
13
+
* current location entry.
14
+
*/
15
+
readonly current: Location;
16
+
/**
17
+
* index of the active entry.
18
+
*/
19
+
readonly active: number;
20
+
/**
21
+
* list of entries; null denotes a gap in entries.
22
+
*/
23
+
readonly entries: (Location | null)[];
24
+
/**
25
+
* whether a backward traversal is possible.
26
+
*/
27
+
readonly canGoBack: boolean;
28
+
/**
29
+
* whether a forward traversal is possible.
30
+
*/
31
+
readonly canGoForward: boolean;
32
+
}
33
+
34
+
/**
35
+
* constructs a derived stack view from a browser history instance.
36
+
* @param history source history to observe
37
+
* @param keepForwardEntries when true, preserves forward entries during backward traversal; when false, forward entries may be dropped
38
+
* @returns a live history stack view
39
+
*/
40
+
export const createHistoryStack = (history: BrowserHistory, keepForwardEntries = true): HistoryStack => {
41
+
const loc = history.location;
42
+
43
+
let active = loc.index;
44
+
let entries = arr(active + 1, (i) => (i === active ? loc : null));
45
+
46
+
history.listen(({ action, location }) => {
47
+
const index = location.index;
48
+
49
+
if (action === 'push') {
50
+
// new page pushed
51
+
52
+
entries = entries.toSpliced(active + 1, entries.length, location);
53
+
} else if (action === 'replace' || action === 'update') {
54
+
// current page replaced, or updated with new state
55
+
56
+
entries = entries.with(active, location);
57
+
} else if (action === 'traverse') {
58
+
// traversal happened
59
+
60
+
if (keepForwardEntries) {
61
+
if (index >= entries.length) {
62
+
const length = entries.length;
63
+
const delta = index - length;
64
+
65
+
const extras = arr(delta + 1, (i) => (i === delta ? location : null));
66
+
67
+
entries = entries.concat(extras);
68
+
} else if (entries[index] === null) {
69
+
entries = entries.with(index, location);
70
+
}
71
+
} else {
72
+
if (index < active) {
73
+
if (entries[index] !== null) {
74
+
entries = entries.slice(0, index + 1);
75
+
} else {
76
+
entries = entries.toSpliced(index, entries.length, location);
77
+
}
78
+
} else if (index >= entries.length) {
79
+
const length = entries.length;
80
+
const delta = index - length;
81
+
82
+
const extras = arr(delta + 1, (i) => (i === delta ? location : null));
83
+
84
+
entries = entries.concat(extras);
85
+
}
86
+
}
87
+
}
88
+
89
+
active = index;
90
+
});
91
+
92
+
return {
93
+
get current() {
94
+
// current entry is guaranteed to exist
95
+
return entries[active]!;
96
+
},
97
+
get active() {
98
+
return active;
99
+
},
100
+
get entries() {
101
+
return entries;
102
+
},
103
+
104
+
get canGoBack() {
105
+
return active !== 0;
106
+
},
107
+
get canGoForward() {
108
+
return active !== entries.length - 1;
109
+
},
110
+
};
111
+
};
112
+
113
+
const arr = <T>(length: number, map: (index: number) => T): T[] => {
114
+
return Array.from({ length }, (_, idx) => map(idx));
115
+
};