+import consola from 'consola'
import BaseHistory from './base'
-import { HistoryLocation, NavigationCallback } from '../types/index'
+import {
+ HistoryLocation,
+ NavigationCallback,
+ HistoryState,
+ NavigationType,
+} from '../types/index'
+
+const cs = consola.withTag('html5')
+
+type PopStateListener = (this: Window, ev: PopStateEvent) => any
export default class HTML5History extends BaseHistory {
private history = window.history
- private _popStateListener:
- | null
- | ((this: Window, ev: PopStateEvent) => any) = null
+ private _popStateListeners: PopStateListener[] = []
+ private _listeners: NavigationCallback[] = []
+ private _teardowns: Array<() => void> = []
constructor() {
super()
}
+ // TODO: is this necessary
ensureLocation() {
- const to = window.location.pathname
- this.replace(to)
+ const to = buildFullPath()
+ cs.log('ensureLocation', to)
+ this.history.replaceState(
+ {
+ _back: null,
+ _current: to,
+ _forward: null,
+ },
+ '',
+ to
+ )
+ this.location = to
+ cs.warn('changed location to', this.location)
}
replace(to: HistoryLocation) {
if (to === this.location) return
- console.log('replace', this.location, to)
+ cs.info('replace', this.location, to)
this.history.replaceState(
{
- replacedState: this.history.state || {},
- from: this.location,
- to,
+ // TODO: this should be user's responsibility
+ // _replacedState: this.history.state || null,
+ _back: this.location,
+ _current: to,
+ _forward: null,
+ _replaced: true,
},
'',
to
)
this.location = to
+ cs.warn('changed location to', this.location)
}
- push(to: HistoryLocation) {
- // TODO: resolve url
+ push(to: HistoryLocation, data?: HistoryState) {
+ // replace current entry state to add the forward value
+ this.history.replaceState(
+ {
+ ...this.history.state,
+ _forward: to,
+ },
+ ''
+ )
// TODO: compare current location to prevent navigation
- if (to === this.location) return
+ // NEW NOTE: I think it shouldn't be history responsibility to check that
+ // if (to === this.location) return
const state = {
- from: this.location,
- to,
+ _back: this.location,
+ _current: to,
+ _forward: null,
+ ...data,
}
- console.log('push', this.location, to)
+ cs.info('push', this.location, '->', to, 'with state', state)
this.history.pushState(state, '', to)
this.location = to
+ cs.warn('changed location to', this.location)
}
listen(callback: NavigationCallback) {
- this._popStateListener = ({ state }) => {
+ // state is the same as history.state
+ const handler: PopStateListener = ({ state }) => {
+ cs.log(this)
+ cs.info('popstate fired', {
+ state,
+ location: this.location,
+ })
const from = this.location
// we have the state from the old entry, not the current one being removed
// TODO: correctly parse pathname
- this.location = state ? state.to : window.location.pathname
- callback(this.location, from)
+ this.location = state ? state._current : buildFullPath
+ cs.warn('changed location to', this.location)
+ callback(this.location, from, {
+ type:
+ from === state._forward
+ ? NavigationType.back
+ : NavigationType.forward,
+ })
}
- window.addEventListener('popstate', this._popStateListener)
- return () => {
- window.removeEventListener('popstate', this._popStateListener!)
+ // settup the listener and prepare teardown callbacks
+ this._popStateListeners.push(handler)
+ this._listeners.push(callback)
+ window.addEventListener('popstate', handler)
+
+ const teardown = () => {
+ this._popStateListeners.splice(
+ this._popStateListeners.indexOf(handler),
+ 1
+ )
+ this._listeners.splice(this._listeners.indexOf(callback), 1)
+ window.removeEventListener('popstate', handler)
}
+
+ this._teardowns.push(teardown)
+ return teardown
+ }
+
+ destroy() {
+ for (const teardown of this._teardowns) teardown()
+ this._teardowns = []
}
}
+
+const buildFullPath = () =>
+ window.location.pathname + window.location.search + window.location.hash
export type HistoryLocation = string
+// pushState clones the state passed and do not accept everything
+// it doesn't accept symbols, nor functions. It also ignores Symbols as keys
+type HistoryStateValue =
+ | string
+ | number
+ | boolean
+ | HistoryState
+ | HistoryStateArray
+export interface HistoryState {
+ [x: number]: HistoryStateValue
+ [x: string]: HistoryStateValue
+}
+interface HistoryStateArray extends Array<HistoryStateValue> {}
+// export type HistoryState = Record<string | number, string | number | boolean | undefined | null |
+
export const START: HistoryLocation = '/'
export const START_RECORD: RouteRecord = {
path: '/',
component: { render: h => h() },
}
+export enum NavigationType {
+ back,
+ forward,
+}
+
export interface NavigationCallback {
- (to: HistoryLocation, from: HistoryLocation): void
+ (
+ to: HistoryLocation,
+ from: HistoryLocation,
+ info: { type: NavigationType }
+ ): void
}
export type RemoveListener = () => void