Skip to content

Commit

Permalink
First commit, basic tests/docs
Browse files Browse the repository at this point in the history
  • Loading branch information
JAForbes committed Feb 15, 2024
0 parents commit 5e8d349
Show file tree
Hide file tree
Showing 11 changed files with 3,469 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/test.yml
@@ -0,0 +1,23 @@
name: Test

on:
push:

jobs:
test:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
5 changes: 5 additions & 0 deletions .gitignore
@@ -0,0 +1,5 @@
node_modules/*
logs
dist
coverage
*output*
6 changes: 6 additions & 0 deletions .prettierrc
@@ -0,0 +1,6 @@
{
"useTabs": true,
"semi": false,
"singleQuote": true,
"embeddedLanguageFormatting": "auto"
}
24 changes: 24 additions & 0 deletions .vscode/launch.json
@@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "test",
"type": "node",
"request": "launch",
"args": [
"--test",
"${relativeFile}"
],
"runtimeArgs": [
"--import",
"tsx"
],
"cwd": "${workspaceRoot}",
"protocol": "inspector",
"internalConsoleOptions": "openOnSessionStart"
}
]
}
4 changes: 4 additions & 0 deletions .vscode/settings.json
@@ -0,0 +1,4 @@
{
"javascript.format.enable": false,
"typescript.format.enable": false
}
182 changes: 182 additions & 0 deletions lib/index.ts
@@ -0,0 +1,182 @@
export type Window = typeof window
export type Location = typeof window.location
export type History = typeof window.history

export type State = { path?: string }

export type OnChange = (state: State) => void

interface InternalInstance {
back(): void
go(path: string, options?: { replace: boolean }): void
onChange: OnChange
get(): State
end(): void
prefix: () => string
child(options: { prefix: string; onChange?: OnChange }): InternalInstance
}

interface Attrs {
_window?: Window
onChange?: (s: State) => any
}

interface ChildAttrs {
_window: Window
prefix: string
onChange: OnChange
children: Set<InternalInstance>
reportChanges(): void
}

function normalizePath(str: string): string {
if (str == '' || str == '/') {
return '/'
} else if (str.at(-1) == '/') {
return str.slice(0, -1)
} else {
return str
}
}

function Superhistory({
_window = globalThis.window,
onChange = () => {},
}: Attrs = {}): InternalInstance {
const children = new Set<InternalInstance>()

const reportChanges = () => {
onChange({
path: _window.location.pathname,
})
for (let child of children) {
child?.onChange?.(child.get())
}
}
const onpopstate = () => {
reportChanges()
}

_window.addEventListener('popstate', onpopstate)

function go(path: string, options: { replace?: boolean } = {}) {
path = normalizePath(path)
_window.history[`${options.replace ? 'replace' : 'push'}State`](
null,
'',
path,
)
onChange({
path,
})
}

function back() {
_window.history.back()
}

function end() {
_window.removeEventListener('popstate', onpopstate)
}

function get() {
return { path: normalizePath(_window.location.pathname) }
}

function prefix() {
return '/'
}

function child({
prefix = '',
onChange,
}: {
prefix: string
onChange: OnChange
}) {
const child = SuperhistoryChild({
_window,
prefix,
onChange,
children,
reportChanges,
})
children.add(child)
return child
}

return { go, end, back, get, prefix, child, onChange }
}

function SuperhistoryChild({
_window,
prefix: _prefix,
onChange,
children: parentChildren,
reportChanges,
}: ChildAttrs): InternalInstance {
_prefix = normalizePath(_prefix)

const children = new Set<InternalInstance>()
function back() {
_window.history.back()
}

function end() {
// delete from the root list
parentChildren.delete(self)

// loop through our list to let other child delete
// from the root list
// our list will gc on its own when it is de-referenced
for( let child of children ) {
child.end()
}
}

function go(path: string, options: { replace?: boolean } = {}) {
path = normalizePath(path)
_window.history[`${options.replace ? 'replace' : 'push'}State`](
null,
'',
normalizePath(_prefix) + normalizePath(path),
)
reportChanges()
}

function get() {
const rootPath = _window.location.pathname
if (!_prefix) {
return { path: rootPath }
}
const [_, child] = rootPath.split(_prefix)
return { path: child != null ? normalizePath(child) : child }
}

function prefix() {
return _prefix
}

function child({
prefix = '',
onChange,
}: {
prefix: string
onChange: OnChange
}) {
const child = SuperhistoryChild({
_window,
prefix: _prefix + prefix,
onChange,
children: parentChildren,
reportChanges,
})
parentChildren.add(child)
return child
}

const self = { go, end, back, get, prefix, child, onChange }
return self
}

export default Superhistory

0 comments on commit 5e8d349

Please sign in to comment.