Skip to content

Commit 5e8d349

Browse files
committed
First commit, basic tests/docs
0 parents  commit 5e8d349

File tree

11 files changed

+3469
-0
lines changed

11 files changed

+3469
-0
lines changed

.github/workflows/test.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
6+
jobs:
7+
test:
8+
9+
runs-on: ubuntu-latest
10+
11+
strategy:
12+
matrix:
13+
node-version: [20.x]
14+
15+
steps:
16+
- uses: actions/checkout@v3
17+
- name: Use Node.js ${{ matrix.node-version }}
18+
uses: actions/setup-node@v3
19+
with:
20+
node-version: ${{ matrix.node-version }}
21+
cache: 'npm'
22+
- run: npm ci
23+
- run: npm test

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/*
2+
logs
3+
dist
4+
coverage
5+
*output*

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"useTabs": true,
3+
"semi": false,
4+
"singleQuote": true,
5+
"embeddedLanguageFormatting": "auto"
6+
}

.vscode/launch.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "test",
9+
"type": "node",
10+
"request": "launch",
11+
"args": [
12+
"--test",
13+
"${relativeFile}"
14+
],
15+
"runtimeArgs": [
16+
"--import",
17+
"tsx"
18+
],
19+
"cwd": "${workspaceRoot}",
20+
"protocol": "inspector",
21+
"internalConsoleOptions": "openOnSessionStart"
22+
}
23+
]
24+
}

.vscode/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"javascript.format.enable": false,
3+
"typescript.format.enable": false
4+
}

lib/index.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
export type Window = typeof window
2+
export type Location = typeof window.location
3+
export type History = typeof window.history
4+
5+
export type State = { path?: string }
6+
7+
export type OnChange = (state: State) => void
8+
9+
interface InternalInstance {
10+
back(): void
11+
go(path: string, options?: { replace: boolean }): void
12+
onChange: OnChange
13+
get(): State
14+
end(): void
15+
prefix: () => string
16+
child(options: { prefix: string; onChange?: OnChange }): InternalInstance
17+
}
18+
19+
interface Attrs {
20+
_window?: Window
21+
onChange?: (s: State) => any
22+
}
23+
24+
interface ChildAttrs {
25+
_window: Window
26+
prefix: string
27+
onChange: OnChange
28+
children: Set<InternalInstance>
29+
reportChanges(): void
30+
}
31+
32+
function normalizePath(str: string): string {
33+
if (str == '' || str == '/') {
34+
return '/'
35+
} else if (str.at(-1) == '/') {
36+
return str.slice(0, -1)
37+
} else {
38+
return str
39+
}
40+
}
41+
42+
function Superhistory({
43+
_window = globalThis.window,
44+
onChange = () => {},
45+
}: Attrs = {}): InternalInstance {
46+
const children = new Set<InternalInstance>()
47+
48+
const reportChanges = () => {
49+
onChange({
50+
path: _window.location.pathname,
51+
})
52+
for (let child of children) {
53+
child?.onChange?.(child.get())
54+
}
55+
}
56+
const onpopstate = () => {
57+
reportChanges()
58+
}
59+
60+
_window.addEventListener('popstate', onpopstate)
61+
62+
function go(path: string, options: { replace?: boolean } = {}) {
63+
path = normalizePath(path)
64+
_window.history[`${options.replace ? 'replace' : 'push'}State`](
65+
null,
66+
'',
67+
path,
68+
)
69+
onChange({
70+
path,
71+
})
72+
}
73+
74+
function back() {
75+
_window.history.back()
76+
}
77+
78+
function end() {
79+
_window.removeEventListener('popstate', onpopstate)
80+
}
81+
82+
function get() {
83+
return { path: normalizePath(_window.location.pathname) }
84+
}
85+
86+
function prefix() {
87+
return '/'
88+
}
89+
90+
function child({
91+
prefix = '',
92+
onChange,
93+
}: {
94+
prefix: string
95+
onChange: OnChange
96+
}) {
97+
const child = SuperhistoryChild({
98+
_window,
99+
prefix,
100+
onChange,
101+
children,
102+
reportChanges,
103+
})
104+
children.add(child)
105+
return child
106+
}
107+
108+
return { go, end, back, get, prefix, child, onChange }
109+
}
110+
111+
function SuperhistoryChild({
112+
_window,
113+
prefix: _prefix,
114+
onChange,
115+
children: parentChildren,
116+
reportChanges,
117+
}: ChildAttrs): InternalInstance {
118+
_prefix = normalizePath(_prefix)
119+
120+
const children = new Set<InternalInstance>()
121+
function back() {
122+
_window.history.back()
123+
}
124+
125+
function end() {
126+
// delete from the root list
127+
parentChildren.delete(self)
128+
129+
// loop through our list to let other child delete
130+
// from the root list
131+
// our list will gc on its own when it is de-referenced
132+
for( let child of children ) {
133+
child.end()
134+
}
135+
}
136+
137+
function go(path: string, options: { replace?: boolean } = {}) {
138+
path = normalizePath(path)
139+
_window.history[`${options.replace ? 'replace' : 'push'}State`](
140+
null,
141+
'',
142+
normalizePath(_prefix) + normalizePath(path),
143+
)
144+
reportChanges()
145+
}
146+
147+
function get() {
148+
const rootPath = _window.location.pathname
149+
if (!_prefix) {
150+
return { path: rootPath }
151+
}
152+
const [_, child] = rootPath.split(_prefix)
153+
return { path: child != null ? normalizePath(child) : child }
154+
}
155+
156+
function prefix() {
157+
return _prefix
158+
}
159+
160+
function child({
161+
prefix = '',
162+
onChange,
163+
}: {
164+
prefix: string
165+
onChange: OnChange
166+
}) {
167+
const child = SuperhistoryChild({
168+
_window,
169+
prefix: _prefix + prefix,
170+
onChange,
171+
children: parentChildren,
172+
reportChanges,
173+
})
174+
parentChildren.add(child)
175+
return child
176+
}
177+
178+
const self = { go, end, back, get, prefix, child, onChange }
179+
return self
180+
}
181+
182+
export default Superhistory

0 commit comments

Comments
 (0)