url-state
Version:
Minimalist history API abstraction for building URL driven browser applications
169 lines (156 loc) • 4.81 kB
JavaScript
import qs from './qs.js'
var parser = document.createElement('A')
class UrlState extends EventTarget {
constructor (opts = {}) {
super()
// bound methods
this._onnavigation = this._onnavigation.bind(this)
this._onpopState = this._onpopState.bind(this)
// properties
this.virtual = opts.virtual === undefined ? window.parent !== window : opts.virtual
this._index = window.history.state || 0
this._queue = [{
href: window.location.href,
replace: true
}]
this._change()
}
push (href, replace) {
if (!href) {
this._queue.push({ type: 'forward' })
} else if (typeof href === 'object') {
href.type = 'query'
href.replace = replace !== undefined ? replace : href.replace
href.params = href.query ? href.query : href.params
this._queue.push(href)
} else {
this._queue.push({ href, replace })
}
this._change()
}
replace (href) {
this.push(href, true)
}
pop () {
this._queue.push({ type: 'back' })
this._change()
}
query (params, replace) {
this._queue.push({
type: 'query',
params,
replace
})
this._change()
}
_change () {
if (this._busy || this._queue.length === 0) return
var action = this._queue.shift()
if (action.type === 'back' || action.type === 'forward') {
if (this.virtual) throw new Error('back/forward not implemented for virtual')
const s = window.history.state
window.history[action.type]()
if (window.history.state !== s) return
action.href = this._lastHref
} else if (action.type === 'query') {
var params = this.params || {}
for (var key in action.params) {
var value = action.params[key]
if (value === null) {
delete params[key]
} else {
params[key] = action.params[key]
}
}
var search = qs.stringify(params) || ''
if (search) search = '?' + search
action.href = this.origin + (action.pathname || this.pathname) + search + (action.hash || this.hash)
}
this.back = false
this._busy = true
if (action.href !== this._lastHref) {
this._lastHref = action.href
this._parseHref(action.href)
if (!this.virtual) {
if (action.replace) {
window.history.replaceState(this._index, null, this.href)
} else {
window.history.pushState(++this._index, null, this.href)
}
}
this.dispatchEvent(new CustomEvent('change', { detail: this }))
}
this._busy = false
this._change()
}
_onnavigation (evt) {
if (evt.metaKey || evt.ctrlKey || evt.defaultPrevented) return
var href = null
var target = evt.target
if (evt.type === 'submit') {
if (!target.action || target.action === window.location.href) {
evt.preventDefault()
return
}
href = target.action
} else {
while (target) {
if (target.nodeName === 'A') {
if (target.target === '_blank' || target.hasAttribute('download')) return
href = target.href
break
}
target = target.parentElement
}
}
if (href === null) return
parser.href = href
var origin = parser.protocol + '//' + parser.host
if (origin !== window.location.origin) {
return
}
evt.preventDefault()
this._queue.push({ action: 'push', href })
this._change()
}
_onpopState (evt) {
this._parseHref(window.location)
this.back = evt.state < this._index
this._index += this.back ? -1 : 1
this._lastHref = this.href
this.dispatchEvent(new CustomEvent('change', { detail: this }))
this._busy = false
this._change()
}
_parseHref (href) {
parser.href = href
// https://url.spec.whatwg.org props:
this.href = parser.href
this.protocol = parser.protocol
this.hostname = parser.hostname
this.port = parser.port
this.pathname = parser.pathname
this.search = parser.search
this.hash = parser.hash
this.host = parser.host
// microsoft doesn't implement .origin:
this.origin = this.protocol + '//' + this.host
// non-standard but handy:
this.params = qs.parse(this.search.slice(1))
// remove trailing hash
if (this.href[this.href.length - 1] === '#') {
this.href = this.href.slice(0, -1)
}
// https://connect.microsoft.com/IE/feedbackdetail/view/1002846
if (this.pathname[0] !== '/') {
this.pathname = '/' + this.pathname
}
}
}
var urlState = new UrlState()
export default urlState
// link clicks and form submissions
window.addEventListener('click', urlState._onnavigation)
window.addEventListener('submit', urlState._onnavigation)
// back and forward buttons
window.addEventListener('popstate', urlState._onpopState)