-
-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[changed] Replace location objects with history
Please note: this commit is still a work in progress! All history objects subclass History and support 2 main methods: - pushState(state, path) - replaceState(state, path) This API more closely matches the HTML5 history API, with the notable omission of the title argument which is currently ignored in all major browsers. It provides the user with the ability to store state specific to the current invocation of the current URL without storing that data in the URL itself. However, history objects that do not use the HTML5 history API (HashHistory and RefreshHistory) store their state ID in the query string. This should help with #767 and #828. This work was inspired by work done by @taurose in #843 and @insin in #828.
- Loading branch information
Showing
24 changed files
with
579 additions
and
417 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
var PathUtils = require('./PathUtils'); | ||
|
||
var STATE_KEY_QUERY_PARAM = '_sk'; | ||
|
||
function getHashPath() { | ||
return decodeURI( | ||
// We can't use window.location.hash here because it's not | ||
// consistent across browsers - Firefox will pre-decode it! | ||
window.location.href.split('#')[1] || '' | ||
); | ||
} | ||
|
||
function getWindowPath() { | ||
return decodeURI( | ||
window.location.pathname + window.location.search | ||
); | ||
} | ||
|
||
function getState(path) { | ||
var stateID = getStateID(path); | ||
var serializedState = stateID && window.sessionStorage.getItem(stateID); | ||
return serializedState ? JSON.parse(serializedState) : null; | ||
} | ||
|
||
function getStateID(path) { | ||
var query = PathUtils.extractQuery(path); | ||
return query && query[STATE_KEY_QUERY_PARAM]; | ||
} | ||
|
||
function withStateID(path, stateID) { | ||
var query = Path.extractQuery(path) || {}; | ||
query[STATE_KEY_QUERY_PARAM] = stateID; | ||
return PathUtils.withQuery(PathUtils.withoutQuery(path), query); | ||
} | ||
|
||
function withoutStateID(path) { | ||
var query = PathUtils.extractQuery(path); | ||
|
||
if (STATE_KEY_QUERY_PARAM in query) { | ||
delete query[STATE_KEY_QUERY_PARAM]; | ||
return PathUtils.withQuery(PathUtils.withoutQuery(path), query); | ||
} | ||
|
||
return path; | ||
} | ||
|
||
function saveState(state) { | ||
var stateID = state.id; | ||
|
||
if (stateID == null) | ||
stateID = state.id = Math.random().toString(36).slice(2); | ||
|
||
window.sessionStorage.setItem( | ||
stateID, | ||
JSON.stringify(state) | ||
); | ||
|
||
return stateID; | ||
} | ||
|
||
function withState(path, state) { | ||
var stateID = state != null && saveState(state); | ||
return stateID ? withStateID(path, stateID) : withoutStateID(path); | ||
} | ||
|
||
module.exports = { | ||
getHashPath, | ||
getWindowPath, | ||
getState, | ||
getStateID, | ||
withStateID, | ||
withoutStateID, | ||
saveState, | ||
withState | ||
}; |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
class Location { | ||
|
||
constructor(path, state=null) { | ||
this.path = path; | ||
this.state = state; | ||
} | ||
|
||
} | ||
|
||
module.exports = Location; |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
var invariant = require('react/lib/invariant'); | ||
var History = require('./History'); | ||
|
||
class DOMHistory extends History { | ||
|
||
get length() { | ||
var state = this.getCurrentState(); | ||
return state && state.length || 1; | ||
} | ||
|
||
get current() { | ||
var state = this.getCurrentState(); | ||
return state && state.current || this.length - 1; | ||
} | ||
|
||
canGo(n) { | ||
if (n === 0) | ||
return true; | ||
|
||
var next = this.current + n; | ||
return next >= 0 && next < this.length; | ||
} | ||
|
||
go(n) { | ||
if (n === 0) | ||
return; | ||
|
||
invariant( | ||
this.canGo(n), | ||
'Cannot go(%s); there is not enough history', | ||
n | ||
); | ||
|
||
window.history.go(n); | ||
} | ||
|
||
} | ||
|
||
module.exports = DOMHistory; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
/* jshint -W058 */ | ||
var assign = require('react/lib/Object.assign'); | ||
var LocationActions = require('../LocationActions'); | ||
var { getWindowPath } = require('../DOMUtils'); | ||
var DOMHistory = require('./DOMHistory'); | ||
|
||
var isListening = false; | ||
|
||
function onPopState(event) { | ||
if (event.state === undefined) | ||
return; // Ignore extraneous popstate events in WebKit. | ||
|
||
HTML5History.notifyChange(LocationActions.POP); | ||
} | ||
|
||
/** | ||
* A history implementation for DOM environments that support the | ||
* HTML5 history API (pushState and replaceState). Provides the cleanest | ||
* URLs. This implementation should always be used if possible. | ||
*/ | ||
var HTML5History = assign(new DOMHistory, { | ||
|
||
addChangeListener(listener) { | ||
DOMHistory.prototype.addChangeListener.call(this, listener); | ||
|
||
if (!isListening) { | ||
if (window.addEventListener) { | ||
window.addEventListener('popstate', onPopState, false); | ||
} else { | ||
window.attachEvent('onpopstate', onPopState); | ||
} | ||
|
||
isListening = true; | ||
} | ||
}, | ||
|
||
removeChangeListener(listener) { | ||
DOMHistory.prototype.removeChangeListener.call(this, listener); | ||
|
||
if (this.changeListeners.length === 0) { | ||
if (window.addEventListener) { | ||
window.removeEventListener('popstate', onPopState, false); | ||
} else { | ||
window.removeEvent('onpopstate', onPopState); | ||
} | ||
|
||
isListening = false; | ||
} | ||
}, | ||
|
||
pushState(state, path) { | ||
window.history.pushState(state, '', path); | ||
this.notifyChange(LocationActions.PUSH); | ||
}, | ||
|
||
replaceState(state, path) { | ||
window.history.replaceState(state, '', path); | ||
this.notifyChange(LocationActions.REPLACE); | ||
}, | ||
|
||
getCurrentPath: getWindowPath, | ||
|
||
getCurrentState() { | ||
return window.history.state; | ||
} | ||
|
||
}); | ||
|
||
module.exports = HTML5History; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* jshint -W058 */ | ||
var assign = require('react/lib/Object.assign'); | ||
var LocationActions = require('../LocationActions'); | ||
var { getHashPath, withState, withoutStateID, getState } = require('../DOMUtils'); | ||
var DOMHistory = require('./DOMHistory'); | ||
|
||
var currentLocationAction; | ||
var isListening = false; | ||
|
||
function ensureSlash() { | ||
var path = HashHistory.getCurrentPath(); | ||
|
||
if (path.charAt(0) === '/') | ||
return true; | ||
|
||
HashHistory.replace('/' + path); | ||
|
||
return false; | ||
} | ||
|
||
function onHashChange() { | ||
if (ensureSlash()) { | ||
HashHistory.notifyChange( | ||
currentLocationAction || LocationActions.POP | ||
); | ||
|
||
currentLocationAction = null; | ||
} | ||
} | ||
|
||
/** | ||
* A history implementation for DOM environments that uses window.location.hash | ||
* to store the current path. This is a hack for older browsers that do not | ||
* support the HTML5 history API (IE <= 9). It is currently used as the | ||
* default in DOM environments because it offers the widest range of support. | ||
*/ | ||
var HashHistory = assign(new DOMHistory, { | ||
|
||
addChangeListener(listener) { | ||
DOMHistory.prototype.addChangeListener.call(this, listener); | ||
|
||
// Do this BEFORE listening for hashchange. | ||
ensureSlash(); | ||
|
||
if (!isListening) { | ||
if (window.addEventListener) { | ||
window.addEventListener('hashchange', onHashChange, false); | ||
} else { | ||
window.attachEvent('onhashchange', onHashChange); | ||
} | ||
|
||
isListening = true; | ||
} | ||
}, | ||
|
||
removeChangeListener(listener) { | ||
DOMHistory.prototype.removeChangeListener.call(this, listener); | ||
|
||
if (this.changeListeners.length === 0) { | ||
if (window.removeEventListener) { | ||
window.removeEventListener('hashchange', onHashChange, false); | ||
} else { | ||
window.removeEvent('onhashchange', onHashChange); | ||
} | ||
|
||
isListening = false; | ||
} | ||
}, | ||
|
||
pushState(state, path) { | ||
currentLocationAction = LocationActions.PUSH; | ||
window.location.hash = withState(path, state); | ||
}, | ||
|
||
replaceState(state, path) { | ||
currentLocationAction = LocationActions.REPLACE; | ||
window.location.replace( | ||
window.location.pathname + window.location.search + '#' + withState(path, state) | ||
); | ||
}, | ||
|
||
getCurrentPath() { | ||
return withoutStateID(getHashPath()); | ||
}, | ||
|
||
getCurrentState() { | ||
return getState(getHashPath()); | ||
} | ||
|
||
}); | ||
|
||
module.exports = HashHistory; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
var Location = require('../Location'); | ||
|
||
/** | ||
* An abstract base class for history objects. Requires subclasses | ||
* to implement the following properties/methods: | ||
* | ||
* - length | ||
* - pushState(state, path) | ||
* - replaceState(state, path) | ||
* - getCurrentPath() | ||
* - getCurrentState() | ||
* - go(n) | ||
*/ | ||
class History { | ||
|
||
addChangeListener(listener) { | ||
if (!this.changeListeners) | ||
this.changeListeners = []; | ||
|
||
this.changeListeners.push(listener); | ||
} | ||
|
||
removeChangeListener(listener) { | ||
if (!this.changeListeners) | ||
return; | ||
|
||
this.changeListeners = this.changeListeners.filter(function (li) { | ||
return li !== listener; | ||
}); | ||
} | ||
|
||
notifyChange(changeType) { | ||
if (!this.changeListeners) | ||
return; | ||
|
||
var location = this.getCurrentLocation(); | ||
|
||
for (var i = 0, len = this.changeListeners.length; i < len; ++i) | ||
this.changeListeners[i].call(this, location, changeType); | ||
} | ||
|
||
getCurrentLocation() { | ||
return new Location(this.getCurrentPath(), this.getCurrentState()); | ||
} | ||
|
||
canGo(n) { | ||
return n === 0; | ||
} | ||
|
||
canGoBack() { | ||
return this.canGo(-1); | ||
} | ||
|
||
canGoForward() { | ||
return this.canGo(1); | ||
} | ||
|
||
back() { | ||
this.go(-1); | ||
} | ||
|
||
forward() { | ||
this.go(1); | ||
} | ||
|
||
} | ||
|
||
module.exports = History; |
Oops, something went wrong.