Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve query parameters when rebasing location path #67

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion address.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ define(function(require) {
, httpStatusCode: require('./http-status-code')
, interpolate: require('./interpolate')
, into: require('./into')
, location: require('./location')
, location: require('./location')()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The location module is effectively a singleton prior to these changes. I can understand how this makes testing more difficult, however it seems to me that by changing this to be an instantiated module with local state, this also breaks the API. Anything now depending on address.location can no longer rely on the state of this module to be the same as what's used by address internally. At least, that's how I read this – it's a bit difficult to follow the changes.

, middleware: require('./middleware')
, ok: require('./ok')
, redirect: require('./redirect')
Expand Down
301 changes: 152 additions & 149 deletions location.js
Original file line number Diff line number Diff line change
@@ -1,197 +1,200 @@
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 rebind = require('./rebind')
var dispatcher = require('d3-dispatch').dispatch('statechange')
var on = require('./on')

return rebind(api, dispatcher, 'on')
return function () {
var history = window.history
var location = window.location
var base = ''

function getState() {
return unbase(fullPath(location))
}
on.call(window, 'popstate.location', handleStateChange)
on.call(document, 'click.location', handleClick)

function setState(path) {
var actual = pushState(path)

if (actual) {
dispatcher.statechange(actual)
return actual
} else {
return false
if (isHashPath(location.hash)) {
// Redirect current hash fragment location to "real" path
history.replaceState(null, null, rebase(fullPath(location)) + location.search)
}
}

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 getState() {
return unbase(fullPath(location))
}
}

function deprecatedPushState(path) {
console.warn('deprecated : location.pushState, to be removed in v.4.0.0.')
return pushState(path)
}
function setState(path) {
var actual = pushState(path)

function pushState(path) {
return updateState(path, history.pushState.bind(history))
}
if (actual) {
dispatcher.statechange(actual)
return actual
} else {
return false
}
}

function replaceState(path) {
return updateState(path, history.replaceState.bind(history))
}
function trimPath(path) {
return '/' + trimSlashes(~path.indexOf('#/')? path.split('#/')[1] : path)
}

function openNewWindow(path, target) {
return window.open(rebase(path), target, '')
}
function updateState(path, method) {
path = unbase(trimPath(path))

function basePath(path) {
if (arguments.length === 0) return base
if (path === getState()) {
return false
} else {
method({ base: base, path: path }, null, rebase(path) + location.search)
return path
}
}

var cwd = unbase(fullPath(location))
function deprecatedPushState(path) {
console.warn('deprecated : location.pushState, to be removed in v.4.0.0.')
return pushState(path)
}

path = trimSlashes(path)
base = path? '/' + path : ''
function pushState(path) {
return updateState(path, history.pushState.bind(history))
}

history.replaceState(null, null, rebase(cwd))
}
function replaceState(path) {
return updateState(path, history.replaceState.bind(history))
}

function handleClick(event) {
var a
, target = event.target
, path
function openNewWindow(path, target) {
return window.open(rebase(path), target, '')
}

if (event.ctrlKey) return // Ignore ctrl+click
if (event.button !== 0) return // Ignore clicks by buttons other than primary (usually left button)
function basePath(path) {
if (arguments.length === 0) return base

a = findClosest.anchor(target)
var cwd = unbase(fullPath(location))

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
path = trimSlashes(path)
base = path? '/' + path : ''

history.replaceState(null, null, rebase(cwd))
}

var path
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(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.: `<a href="#"></a>`)
return
} else {
path = rebase(fullPath(a))
}
var path

if (path) {
event.preventDefault()
event.stopPropagation()
var actual = pushState(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.: `<a href="#"></a>`)
return
} else {
path = rebase(fullPath(a))
}

if (actual) {
dispatcher.statechange(actual)
if (path) {
event.preventDefault()
event.stopPropagation()
var actual = pushState(path)

if (actual) {
dispatcher.statechange(actual)
}
}
}
}

function handleStateChange(event) {
var path, base = (event.state && event.state.base) || ''
function handleStateChange(event) {
var path, base = (event.state && event.state.base) || ''

if (isHashPath(location.hash)) {
// "Redirect" current location to a proper path
path = location.hash.slice(1)
if (isHashPath(location.hash)) {
// "Redirect" current location to a proper path
path = location.hash.slice(1)

if (path) {
var state = { base: base, path: path }
history.replaceState(state, null, rebase(path))
if (path) {
var state = { base: base, path: path }
history.replaceState(state, null, rebase(path))
}
} else {
path = fullPath(location)
}
} else {
path = fullPath(location)

dispatcher.statechange(unbase(path))
}

dispatcher.statechange(unbase(path))
}
function isHashPath(hash) {
return (hash || '').slice(0, 2) === '#/'
}

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 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 {
var port

function origin(url) {
if (url.origin) {
return url.origin
} else {
var 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
}

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
}

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, '')
}
}
})
})
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"rimraf": "^2.5.2",
"sinon": "^1.14.1",
"squirejs": "^0.2.1",
"webpack": "^1.12.2"
"webpack": "^1.15.0"
},
"scripts": {
"test": "karma start --single-run",
Expand Down
2 changes: 1 addition & 1 deletion test/address.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
define(function(require) {
var sinon = require('sinon')
, zapp = require('z-app')
, location = require('location')
, location = require('location')()
, address
, web
, nap
Expand Down
Loading