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: {