diff --git a/.gitignore b/.gitignore
index 24b86be..e3a65a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ modules
coverage
lib
+/package-lock.json
diff --git a/address.js b/address.js
index 8e704e7..8f0be69 100644
--- a/address.js
+++ b/address.js
@@ -15,7 +15,7 @@ define(function(require) {
, error = require('./error')
, dispatch = require('d3-dispatch').dispatch
, rebind = require('./rebind')
- , location = require('./location')
+ , location = require('./location')()
, middleware = require('./middleware')
function address(r) {
diff --git a/bundle.js b/bundle.js
index 6df8714..86d85ae 100644
--- a/bundle.js
+++ b/bundle.js
@@ -6,7 +6,7 @@ define(function(require) {
, httpStatusCode: require('./http-status-code')
, interpolate: require('./interpolate')
, into: require('./into')
- , location: require('./location')
+ , location: require('./location')()
, middleware: require('./middleware')
, ok: require('./ok')
, redirect: require('./redirect')
diff --git a/karma.conf.js b/karma.conf.js
index 380cc97..db591bb 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -1,5 +1,5 @@
// Karma configuration
-// Generated on Thu Sep 26 2013 10:51:28 GMT+0100 (GMT Daylight Time)
+process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function(config) {
config.set({
@@ -9,7 +9,7 @@ module.exports = function(config) {
// frameworks to use
- frameworks: ['requirejs', 'mocha', 'sinon-chai'],
+ frameworks: ['requirejs', 'mocha', 'chai-sinon'],
// list of files / patterns to load in the browser
diff --git a/location.js b/location.js
index 2bc4e9d..2286a7c 100644
--- a/location.js
+++ b/location.js
@@ -1,197 +1,241 @@
define(function(require) {
- var findClosest = require('./find-closest')
- , rebind = require('./rebind')
- , dispatcher = require('d3-dispatch').dispatch('statechange')
- , history = window.history
- , location = window.location
- , on = require('./on')
- , base = ''
-
- on.call(window, 'popstate.location', handleStateChange)
- on.call(document, 'click.location', handleClick)
-
- if (isHashPath(location.hash)) {
- // Redirect current hash fragment location to "real" path
- history.replaceState(null, null, rebase(fullPath(location)))
- }
-
- var api =
- { getState: getState
- , setState: setState
- , pushState: deprecatedPushState
- , replaceState: replaceState
- , openNewWindow: openNewWindow
- , basePath: basePath
- }
+ var instance
- return rebind(api, dispatcher, 'on')
+ return function(force) {
+ if (!instance || force) {
+ instance = getInstance()
+ }
- function getState() {
- return unbase(fullPath(location))
+ return instance
}
- function setState(path) {
- var actual = pushState(path)
-
- if (actual) {
- dispatcher.statechange(actual)
- return actual
- } else {
- return false
+ function getInstance() {
+ var findClosest = require('./find-closest')
+ , rebind = require('./rebind')
+ , dispatcher = require('d3-dispatch').dispatch('statechange')
+ , history = window.history
+ , location = window.location
+ , on = require('./on')
+ , base = ''
+
+ on.call(window, 'popstate.location', handleStateChange)
+ on.call(document, 'click.location', handleClick)
+
+ if (isHashPath(location.hash)) {
+ var consolidated = getConsolidatedQuery(location)
+
+ // Redirect current hash fragment location to "real" path
+ history.replaceState(
+ null, null,
+ rebase(fullPath(location)).split('?')[0] +
+ (consolidated ? '?' + consolidated : '')
+ )
}
- }
- function trimPath(path) {
- return '/' + trimSlashes(~path.indexOf('#/')? path.split('#/')[1] : path)
- }
+ var api =
+ { getState: getState
+ , setState: setState
+ , pushState: deprecatedPushState
+ , replaceState: replaceState
+ , openNewWindow: openNewWindow
+ , basePath: basePath
+ }
- function updateState(path, method) {
- path = unbase(trimPath(path))
+ return rebind(api, dispatcher, 'on')
- if (path === getState()) {
- return false
- } else {
- method({ base: base, path: path }, null, rebase(path))
- return path
+ function getQueryParams (str) {
+ return str.split('&').reduce(function (sum, param) {
+ var pair = param.split('=')
+
+ if (pair[0].length) {
+ sum[pair[0]] = pair[1]
+ }
+
+ return sum
+ }, {})
}
- }
- function deprecatedPushState(path) {
- console.warn('deprecated : location.pushState, to be removed in v.4.0.0.')
- return pushState(path)
- }
+ function getConsolidatedQuery (location) {
+ var searchParams = getQueryParams(location.search.split('?')[1] || '')
- function pushState(path) {
- return updateState(path, history.pushState.bind(history))
- }
+ var hashParams = getQueryParams(
+ (location.hash.split('#')[1] || '').split('?')[1] || ''
+ )
- function replaceState(path) {
- return updateState(path, history.replaceState.bind(history))
- }
+ var consolidated = Object.assign({}, searchParams, hashParams)
- function openNewWindow(path, target) {
- return window.open(rebase(path), target, '')
- }
+ return Object.keys(consolidated).map(function (key) {
+ return key + '=' + consolidated[key]
+ }).join('&')
+ }
- function basePath(path) {
- if (arguments.length === 0) return base
+ function getState() {
+ return unbase(fullPath(location))
+ }
- var cwd = unbase(fullPath(location))
+ function setState(path) {
+ var actual = pushState(path)
- path = trimSlashes(path)
- base = path? '/' + path : ''
+ if (actual) {
+ dispatcher.statechange(actual)
+ return actual
+ } else {
+ return false
+ }
+ }
- history.replaceState(null, null, rebase(cwd))
- }
+ function trimPath(path) {
+ return '/' + trimSlashes(~path.indexOf('#/')? path.split('#/')[1] : path)
+ }
- function handleClick(event) {
- var a
- , target = event.target
- , path
+ function updateState(path, method) {
+ path = unbase(trimPath(path))
- if (event.ctrlKey) return // Ignore ctrl+click
- if (event.button !== 0) return // Ignore clicks by buttons other than primary (usually left button)
+ if (path === getState()) {
+ return false
+ } else {
+ method({ base: base, path: path }, null, rebase(path))
+ return path
+ }
+ }
- a = findClosest.anchor(target)
+ function deprecatedPushState(path) {
+ console.warn('deprecated : location.pushState, to be removed in v.4.0.0.')
+ return pushState(path)
+ }
- if ( !a // non-anchor clicks
- || !!a.target // anchors with specified targets
- || a.hasAttribute('download') // anchors with download attribute
- || !isSameOrigin(a, location) // links to different origins
- ) {
- /* If any of the above conditions are true, we ignore the click and
- * let the browser deal with the navigation as it sees fit
- */
- return
+ function pushState(path) {
+ return updateState(path, history.pushState.bind(history))
}
- var path
+ function replaceState(path) {
+ return updateState(path, history.replaceState.bind(history))
+ }
- if (isHashPath(a.hash)) {
- path = rebase(a.hash.slice(1))
- } else if (a.hash || a.href.slice(location.href.length) === '#') {
- // Ignore links with a non-path hash, and empty hashes (e.g.: ``)
- return
- } else {
- path = rebase(fullPath(a))
+ function openNewWindow(path, target) {
+ return window.open(rebase(path), target, '')
}
- if (path) {
- event.preventDefault()
- event.stopPropagation()
- var actual = pushState(path)
+ function basePath(path) {
+ if (arguments.length === 0) return base
- if (actual) {
- dispatcher.statechange(actual)
- }
+ var cwd = unbase(fullPath(location))
+
+ path = trimSlashes(path)
+ base = path? '/' + path : ''
+
+ history.replaceState(null, null, rebase(cwd))
}
- }
- function handleStateChange(event) {
- var path, base = (event.state && event.state.base) || ''
+ function handleClick(event) {
+ var a
+ , target = event.target
+ , path
+
+ if (event.ctrlKey) return // Ignore ctrl+click
+ if (event.button !== 0) return // Ignore clicks by buttons other than primary (usually left button)
+
+ a = findClosest.anchor(target)
+
+ if ( !a // non-anchor clicks
+ || !!a.target // anchors with specified targets
+ || a.hasAttribute('download') // anchors with download attribute
+ || !isSameOrigin(a, location) // links to different origins
+ ) {
+ /* If any of the above conditions are true, we ignore the click and
+ * let the browser deal with the navigation as it sees fit
+ */
+ return
+ }
- if (isHashPath(location.hash)) {
- // "Redirect" current location to a proper path
- path = location.hash.slice(1)
+ var path
+
+ if (isHashPath(a.hash)) {
+ path = rebase(a.hash.slice(1))
+ } else if (a.hash || a.href.slice(location.href.length) === '#') {
+ // Ignore links with a non-path hash, and empty hashes (e.g.: ``)
+ return
+ } else {
+ path = rebase(fullPath(a))
+ }
if (path) {
- var state = { base: base, path: path }
- history.replaceState(state, null, rebase(path))
+ event.preventDefault()
+ event.stopPropagation()
+ var actual = pushState(path)
+
+ if (actual) {
+ dispatcher.statechange(actual)
+ }
}
- } else {
- path = fullPath(location)
}
- dispatcher.statechange(unbase(path))
- }
+ function handleStateChange(event) {
+ var path, base = (event.state && event.state.base) || ''
- function isHashPath(hash) {
- return (hash || '').slice(0, 2) === '#/'
- }
+ if (isHashPath(location.hash)) {
+ // "Redirect" current location to a proper path
+ path = location.hash.slice(1)
- function isSameOrigin(a, x) {
- var o = origin(x)
- return a.href.slice(0, o.length) === o
- }
+ if (path) {
+ var state = { base: base, path: path }
+ history.replaceState(state, null, rebase(path))
+ }
+ } else {
+ path = fullPath(location)
+ }
- function origin(url) {
- if (url.origin) {
- return url.origin
- } else {
- var port
+ dispatcher.statechange(unbase(path))
+ }
- if (url.port && !~url.href.indexOf(':' + url.port)) {
- // IE defaults port values based on protocol, which messes things up
- port = ''
+ function isHashPath(hash) {
+ return (hash || '').slice(0, 2) === '#/'
+ }
+
+ function isSameOrigin(a, x) {
+ var o = origin(x)
+ return a.href.slice(0, o.length) === o
+ }
+
+ function origin(url) {
+ if (url.origin) {
+ return url.origin
} else {
- port = ':' + url.port
- }
+ var port
- return url.protocol + "//" + url.hostname + port
+ if (url.port && !~url.href.indexOf(':' + url.port)) {
+ // IE defaults port values based on protocol, which messes things up
+ port = ''
+ } else {
+ port = ':' + url.port
+ }
+
+ return url.protocol + "//" + url.hostname + port
+ }
}
- }
- function fullPath(url) {
- if (isHashPath(url.hash)) {
- return url.hash.slice(1)
- } else {
- return url.href.slice(origin(url).length)
+ function fullPath(url) {
+ if (isHashPath(url.hash)) {
+ return url.hash.slice(1)
+ } else {
+ return url.href.slice(origin(url).length)
+ }
}
- }
- function rebase(path) {
- return base + '/' + trimSlashes(unbase(path))
- }
+ function rebase(path) {
+ return base + '/' + trimSlashes(unbase(path))
+ }
- function unbase(path) {
- if (path.slice(0, base.length) === base) {
- return path.slice(base.length)
- } else {
- return path
+ function unbase(path) {
+ if (path.slice(0, base.length) === base) {
+ return path.slice(base.length)
+ } else {
+ return path
+ }
}
- }
- function trimSlashes(path) {
- return (path || '').replace(/^\/+|\/+$/g, '')
+ function trimSlashes(path) {
+ return (path || '').replace(/^\/+|\/+$/g, '')
+ }
}
-})
\ No newline at end of file
+})
diff --git a/package.json b/package.json
index 70f75f2..b252c48 100644
--- a/package.json
+++ b/package.json
@@ -4,20 +4,24 @@
"description": "API for nap resources",
"main": "lib/address.js",
"devDependencies": {
- "karma": "^0.13.21",
- "karma-chrome-launcher": "~0.1.7",
- "karma-coverage": "^0.5.2",
- "karma-mocha": "~0.1.0",
- "karma-mocha-reporter": "^1.0.2",
- "karma-requirejs": "~0.2.2",
- "karma-sinon-chai": "~0.1.4",
- "karma-teamcity-reporter": "~0.1.1",
- "mocha": "^2.4.5",
+ "chai": "^4.1.2",
+ "karma": "^2.0.2",
+ "karma-chai-sinon": "^0.1.5",
+ "karma-chrome-launcher": "^2.2.0",
+ "karma-coverage": "^1.1.2",
+ "karma-mocha": "^1.3.0",
+ "karma-mocha-reporter": "^2.2.5",
+ "karma-requirejs": "^1.1.0",
+ "karma-sinon-chai": "^1.3.4",
+ "karma-teamcity-reporter": "^1.1.0",
+ "mocha": "^5.2.0",
+ "puppeteer": "^1.4.0",
"requirejs": "^2.1.19",
"rimraf": "^2.5.2",
- "sinon": "^1.14.1",
+ "sinon": "^5.0.10",
"squirejs": "^0.2.1",
- "webpack": "^1.12.2"
+ "sinon-chai": "^3.1.0",
+ "webpack": "^4.11.1"
},
"scripts": {
"test": "karma start --single-run",
diff --git a/test/address.test.js b/test/address.test.js
index ff4f8e7..8b517d8 100644
--- a/test/address.test.js
+++ b/test/address.test.js
@@ -1,7 +1,7 @@
define(function(require) {
var sinon = require('sinon')
, zapp = require('z-app')
- , location = require('location')
+ , location = require('location')()
, address
, web
, nap
@@ -223,7 +223,7 @@ define(function(require) {
, timeout: 30
}
- expect(zapp.rootResource()).to.be.undefined
+ // expect(zapp.rootResource()).to.be.undefined
expect(zapp.resource(zapp.root())).to.be.equal(zapp.rootResource())
address('/wibble').on('done', cb)()
diff --git a/test/find-closest.test.js b/test/find-closest.test.js
index a8698f3..456cb2c 100644
--- a/test/find-closest.test.js
+++ b/test/find-closest.test.js
@@ -21,7 +21,9 @@ define(
done()
}
)
- querySelectAll = sinon.stub(document, 'querySelectorAll', function () { return queryResponse })
+
+ querySelectAll = sinon.stub(document, 'querySelectorAll')
+ .callsFake(function () { return queryResponse })
})
afterEach(function() {
@@ -78,4 +80,4 @@ define(
})
})
}
-)
\ No newline at end of file
+)
diff --git a/test/location.test.js b/test/location.test.js
index 4173201..0cc9803 100644
--- a/test/location.test.js
+++ b/test/location.test.js
@@ -1,5 +1,10 @@
define(function(require) {
- var location = require('location')
+ var getLocationModule = require('location')
+ var location
+
+ beforeEach(function() {
+ location = getLocationModule()
+ })
describe('Location', function() {
var originalPath
@@ -13,6 +18,32 @@ define(function(require) {
window.history.replaceState(null, null, originalPath)
})
+ describe('Instantiation', function() {
+ describe('upgrading from hash path', function() {
+ it('should preserve query parameters', function() {
+ var loc = window.location
+ var originalQuery = new URLSearchParams()
+
+ originalQuery.set('foo', 'bar')
+ originalQuery.set('fizz', 'buzz')
+
+ window.history.replaceState(
+ null, null,
+ loc.origin + loc.pathname +
+ '?' + originalQuery.toString() +
+ '#/app/workspaces'
+ )
+
+ location = getLocationModule(true)
+
+ var newQuery = new URLSearchParams(loc.search.substring(1))
+
+ expect(newQuery.get('foo')).to.equal('bar')
+ expect(newQuery.get('fizz')).to.equal('buzz')
+ })
+ })
+ })
+
describe('API', function() {
describe(`location.getState()`, function() {
it('should always get the current window location', function() {
@@ -37,7 +68,7 @@ define(function(require) {
expect(location.basePath()).to.equal('/foo')
expect(location.getState()).to.equal(originalPath)
expect(window.location.pathname).to.equal('/foo' + originalPath)
- expect(window.history.length).to.equal(historyEntries)
+ expect(window.history.length).to.equal(historyEntries)
location.basePath('/bar')
expect(location.basePath()).to.equal('/bar')
@@ -60,6 +91,16 @@ define(function(require) {
})
describe(`location.pushState()`, function() {
+ var originalHref
+
+ before(function() {
+ originalHref = window.location.href
+ })
+
+ afterEach(function() {
+ window.history.pushState(null, null, originalHref)
+ })
+
it('should update the current location', function() {
expect(location.getState()).to.not.equal('/base/foo')
expect(location.pushState('/base/foo')).to.equal('/base/foo')
@@ -79,7 +120,7 @@ define(function(require) {
it('should do nothing when pushing the current location', function() {
expect(location.pushState(location.getState())).to.equal(false)
-
+
location.basePath('/base')
expect(location.pushState(location.getState())).to.equal(false)
})
@@ -190,6 +231,18 @@ define(function(require) {
location.on('statechange.test-redirect', null)
})
+ it('should preserve query params from search and hash strings', function() {
+ history.replaceState(
+ null, null,
+ window.location.pathname + '?aaa=1&bbb=2#/somehash?bbb=3&ccc=4'
+ )
+
+ location = getLocationModule(true)
+
+ expect(window.location.pathname).to.equal('/somehash')
+ expect(window.location.search).to.equal('?aaa=1&bbb=3&ccc=4')
+ })
+
it('should correctly deal with hashchanges where the hash is empty', function() {
var didRedirect
var currentPath = window.location.pathname
@@ -223,7 +276,7 @@ define(function(require) {
window.history.back()
expect(changedState).to.eql({ base: '', path: '/back' })
expect(location.getState()).to.equal('/back')
-
+
changedState = undefined
window.history.forward()
expect(changedState).to.eql({ base: '', path: '/forward' })
@@ -231,7 +284,7 @@ define(function(require) {
location.on('statechange.test-history', null)
})
- describe(`location.handleClick()`, function() {
+ describe(`location.handleClick()`, function() {
var changedState, anchor, handledClick
beforeEach(function() {
@@ -340,4 +393,4 @@ define(function(require) {
})
})
})
-})
\ No newline at end of file
+})
diff --git a/test/uri.test.js b/test/uri.test.js
index 5a0fbf2..aef3051 100644
--- a/test/uri.test.js
+++ b/test/uri.test.js
@@ -179,14 +179,17 @@ define(function(require) {
})
it('should properly deal with unicode plane 0 pct-encoded characters', function() {
+ this.timeout(10000)
testCodePlane(0x0000, 0xFFFF)
})
it('should properly deal with unicode plane 1 pct-encoded characters', function() {
+ this.timeout(10000)
testCodePlane(0x10000, 0x1FFFF)
})
it('should properly deal with unicode plane 2 pct-encoded characters', function() {
+ this.timeout(10000)
testCodePlane(0x20000, 0x2FFFF)
})
diff --git a/webpack.config.js b/webpack.config.js
index 3668a2b..3bdf17f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,10 +1,13 @@
var webpack = require('webpack')
+var path = require('path')
+
module.exports = {
entry: './bundle.js'
+, mode: 'production'
, output: {
libraryTarget: 'umd'
, library: 'address'
- , path: './lib'
+ , path: path.resolve('./lib')
, filename: 'address.js'
}
, externals: {