Skip to content

Commit 212b9d5

Browse files
committed
[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.
1 parent cfff382 commit 212b9d5

24 files changed

+579
-417
lines changed

modules/DOMUtils.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
var PathUtils = require('./PathUtils');
2+
3+
var STATE_KEY_QUERY_PARAM = '_sk';
4+
5+
function getHashPath() {
6+
return decodeURI(
7+
// We can't use window.location.hash here because it's not
8+
// consistent across browsers - Firefox will pre-decode it!
9+
window.location.href.split('#')[1] || ''
10+
);
11+
}
12+
13+
function getWindowPath() {
14+
return decodeURI(
15+
window.location.pathname + window.location.search
16+
);
17+
}
18+
19+
function getState(path) {
20+
var stateID = getStateID(path);
21+
var serializedState = stateID && window.sessionStorage.getItem(stateID);
22+
return serializedState ? JSON.parse(serializedState) : null;
23+
}
24+
25+
function getStateID(path) {
26+
var query = PathUtils.extractQuery(path);
27+
return query && query[STATE_KEY_QUERY_PARAM];
28+
}
29+
30+
function withStateID(path, stateID) {
31+
var query = Path.extractQuery(path) || {};
32+
query[STATE_KEY_QUERY_PARAM] = stateID;
33+
return PathUtils.withQuery(PathUtils.withoutQuery(path), query);
34+
}
35+
36+
function withoutStateID(path) {
37+
var query = PathUtils.extractQuery(path);
38+
39+
if (STATE_KEY_QUERY_PARAM in query) {
40+
delete query[STATE_KEY_QUERY_PARAM];
41+
return PathUtils.withQuery(PathUtils.withoutQuery(path), query);
42+
}
43+
44+
return path;
45+
}
46+
47+
function saveState(state) {
48+
var stateID = state.id;
49+
50+
if (stateID == null)
51+
stateID = state.id = Math.random().toString(36).slice(2);
52+
53+
window.sessionStorage.setItem(
54+
stateID,
55+
JSON.stringify(state)
56+
);
57+
58+
return stateID;
59+
}
60+
61+
function withState(path, state) {
62+
var stateID = state != null && saveState(state);
63+
return stateID ? withStateID(path, stateID) : withoutStateID(path);
64+
}
65+
66+
module.exports = {
67+
getHashPath,
68+
getWindowPath,
69+
getState,
70+
getStateID,
71+
withStateID,
72+
withoutStateID,
73+
saveState,
74+
withState
75+
};

modules/History.js

Lines changed: 0 additions & 31 deletions
This file was deleted.

modules/Location.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class Location {
2+
3+
constructor(path, state=null) {
4+
this.path = path;
5+
this.state = state;
6+
}
7+
8+
}
9+
10+
module.exports = Location;
File renamed without changes.

modules/history/DOMHistory.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
var invariant = require('react/lib/invariant');
2+
var History = require('./History');
3+
4+
class DOMHistory extends History {
5+
6+
get length() {
7+
var state = this.getCurrentState();
8+
return state && state.length || 1;
9+
}
10+
11+
get current() {
12+
var state = this.getCurrentState();
13+
return state && state.current || this.length - 1;
14+
}
15+
16+
canGo(n) {
17+
if (n === 0)
18+
return true;
19+
20+
var next = this.current + n;
21+
return next >= 0 && next < this.length;
22+
}
23+
24+
go(n) {
25+
if (n === 0)
26+
return;
27+
28+
invariant(
29+
this.canGo(n),
30+
'Cannot go(%s); there is not enough history',
31+
n
32+
);
33+
34+
window.history.go(n);
35+
}
36+
37+
}
38+
39+
module.exports = DOMHistory;

modules/history/HTML5History.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* jshint -W058 */
2+
var assign = require('react/lib/Object.assign');
3+
var LocationActions = require('../LocationActions');
4+
var { getWindowPath } = require('../DOMUtils');
5+
var DOMHistory = require('./DOMHistory');
6+
7+
var isListening = false;
8+
9+
function onPopState(event) {
10+
if (event.state === undefined)
11+
return; // Ignore extraneous popstate events in WebKit.
12+
13+
HTML5History.notifyChange(LocationActions.POP);
14+
}
15+
16+
/**
17+
* A history implementation for DOM environments that support the
18+
* HTML5 history API (pushState and replaceState). Provides the cleanest
19+
* URLs. This implementation should always be used if possible.
20+
*/
21+
var HTML5History = assign(new DOMHistory, {
22+
23+
addChangeListener(listener) {
24+
DOMHistory.prototype.addChangeListener.call(this, listener);
25+
26+
if (!isListening) {
27+
if (window.addEventListener) {
28+
window.addEventListener('popstate', onPopState, false);
29+
} else {
30+
window.attachEvent('onpopstate', onPopState);
31+
}
32+
33+
isListening = true;
34+
}
35+
},
36+
37+
removeChangeListener(listener) {
38+
DOMHistory.prototype.removeChangeListener.call(this, listener);
39+
40+
if (this.changeListeners.length === 0) {
41+
if (window.addEventListener) {
42+
window.removeEventListener('popstate', onPopState, false);
43+
} else {
44+
window.removeEvent('onpopstate', onPopState);
45+
}
46+
47+
isListening = false;
48+
}
49+
},
50+
51+
pushState(state, path) {
52+
window.history.pushState(state, '', path);
53+
this.notifyChange(LocationActions.PUSH);
54+
},
55+
56+
replaceState(state, path) {
57+
window.history.replaceState(state, '', path);
58+
this.notifyChange(LocationActions.REPLACE);
59+
},
60+
61+
getCurrentPath: getWindowPath,
62+
63+
getCurrentState() {
64+
return window.history.state;
65+
}
66+
67+
});
68+
69+
module.exports = HTML5History;

modules/history/HashHistory.js

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* jshint -W058 */
2+
var assign = require('react/lib/Object.assign');
3+
var LocationActions = require('../LocationActions');
4+
var { getHashPath, withState, withoutStateID, getState } = require('../DOMUtils');
5+
var DOMHistory = require('./DOMHistory');
6+
7+
var currentLocationAction;
8+
var isListening = false;
9+
10+
function ensureSlash() {
11+
var path = HashHistory.getCurrentPath();
12+
13+
if (path.charAt(0) === '/')
14+
return true;
15+
16+
HashHistory.replace('/' + path);
17+
18+
return false;
19+
}
20+
21+
function onHashChange() {
22+
if (ensureSlash()) {
23+
HashHistory.notifyChange(
24+
currentLocationAction || LocationActions.POP
25+
);
26+
27+
currentLocationAction = null;
28+
}
29+
}
30+
31+
/**
32+
* A history implementation for DOM environments that uses window.location.hash
33+
* to store the current path. This is a hack for older browsers that do not
34+
* support the HTML5 history API (IE <= 9). It is currently used as the
35+
* default in DOM environments because it offers the widest range of support.
36+
*/
37+
var HashHistory = assign(new DOMHistory, {
38+
39+
addChangeListener(listener) {
40+
DOMHistory.prototype.addChangeListener.call(this, listener);
41+
42+
// Do this BEFORE listening for hashchange.
43+
ensureSlash();
44+
45+
if (!isListening) {
46+
if (window.addEventListener) {
47+
window.addEventListener('hashchange', onHashChange, false);
48+
} else {
49+
window.attachEvent('onhashchange', onHashChange);
50+
}
51+
52+
isListening = true;
53+
}
54+
},
55+
56+
removeChangeListener(listener) {
57+
DOMHistory.prototype.removeChangeListener.call(this, listener);
58+
59+
if (this.changeListeners.length === 0) {
60+
if (window.removeEventListener) {
61+
window.removeEventListener('hashchange', onHashChange, false);
62+
} else {
63+
window.removeEvent('onhashchange', onHashChange);
64+
}
65+
66+
isListening = false;
67+
}
68+
},
69+
70+
pushState(state, path) {
71+
currentLocationAction = LocationActions.PUSH;
72+
window.location.hash = withState(path, state);
73+
},
74+
75+
replaceState(state, path) {
76+
currentLocationAction = LocationActions.REPLACE;
77+
window.location.replace(
78+
window.location.pathname + window.location.search + '#' + withState(path, state)
79+
);
80+
},
81+
82+
getCurrentPath() {
83+
return withoutStateID(getHashPath());
84+
},
85+
86+
getCurrentState() {
87+
return getState(getHashPath());
88+
}
89+
90+
});
91+
92+
module.exports = HashHistory;

modules/history/History.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
var Location = require('../Location');
2+
3+
/**
4+
* An abstract base class for history objects. Requires subclasses
5+
* to implement the following properties/methods:
6+
*
7+
* - length
8+
* - pushState(state, path)
9+
* - replaceState(state, path)
10+
* - getCurrentPath()
11+
* - getCurrentState()
12+
* - go(n)
13+
*/
14+
class History {
15+
16+
addChangeListener(listener) {
17+
if (!this.changeListeners)
18+
this.changeListeners = [];
19+
20+
this.changeListeners.push(listener);
21+
}
22+
23+
removeChangeListener(listener) {
24+
if (!this.changeListeners)
25+
return;
26+
27+
this.changeListeners = this.changeListeners.filter(function (li) {
28+
return li !== listener;
29+
});
30+
}
31+
32+
notifyChange(changeType) {
33+
if (!this.changeListeners)
34+
return;
35+
36+
var location = this.getCurrentLocation();
37+
38+
for (var i = 0, len = this.changeListeners.length; i < len; ++i)
39+
this.changeListeners[i].call(this, location, changeType);
40+
}
41+
42+
getCurrentLocation() {
43+
return new Location(this.getCurrentPath(), this.getCurrentState());
44+
}
45+
46+
canGo(n) {
47+
return n === 0;
48+
}
49+
50+
canGoBack() {
51+
return this.canGo(-1);
52+
}
53+
54+
canGoForward() {
55+
return this.canGo(1);
56+
}
57+
58+
back() {
59+
this.go(-1);
60+
}
61+
62+
forward() {
63+
this.go(1);
64+
}
65+
66+
}
67+
68+
module.exports = History;

0 commit comments

Comments
 (0)