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

Update Histories #1376

Merged
merged 12 commits into from
Jul 16, 2015
88 changes: 25 additions & 63 deletions modules/BrowserHistory.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
import DOMHistory from './DOMHistory';
import { getWindowPath, supportsHistory } from './DOMUtils';
import NavigationTypes from './NavigationTypes';

function updateCurrentState(extraState) {
var state = window.history.state;

if (state)
window.history.replaceState(Object.assign(state, extraState), '');
}
import { addEventListener, removeEventListener, getWindowPath, supportsHistory } from './DOMUtils';

/**
* A history implementation for DOM environments that support the
Expand All @@ -27,83 +19,53 @@ class BrowserHistory extends DOMHistory {
this.isSupported = supportsHistory();
}

_updateLocation(navigationType) {
var state = null;
setup() {
if (this.location != null)
return;

if (this.isSupported) {
var historyState = window.history.state;
state = this._createState(historyState);
var path = getWindowPath();
var key = null;
if (this.isSupported && window.history.state)
key = window.history.state.key;

if (!historyState || !historyState.key)
window.history.replaceState(state, '');
}
super.setup(path, { key });

this.location = this.createLocation(getWindowPath(), state, navigationType);
addEventListener(window, 'popstate', this.handlePopState);
}

setup() {
if (this.location == null)
this._updateLocation();
teardown() {
removeEventListener(window, 'popstate', this.handlePopState);
super.teardown();
}

handlePopState(event) {
if (event.state === undefined)
return; // Ignore extraneous popstate events in WebKit.

this._updateLocation(NavigationTypes.POP);
this._notifyChange();
}

addChangeListener(listener) {
super.addChangeListener(listener);

if (this.changeListeners.length === 1) {
if (window.addEventListener) {
window.addEventListener('popstate', this.handlePopState, false);
} else {
window.attachEvent('onpopstate', this.handlePopState);
}
}
}

removeChangeListener(listener) {
super.removeChangeListener(listener);

if (this.changeListeners.length === 0) {
if (window.removeEventListener) {
window.removeEventListener('popstate', this.handlePopState, false);
} else {
window.detachEvent('onpopstate', this.handlePopState);
}
}
var path = getWindowPath();
var key = event.state && event.state.key;
this.handlePop(path, { key });
}

// http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate
pushState(state, path) {
push(path, key) {
if (this.isSupported) {
updateCurrentState(this.getScrollPosition());

state = this._createState(state);

var state = { key };
window.history.pushState(state, '', path);
this.location = this.createLocation(path, state, NavigationTypes.PUSH);
this._notifyChange();
} else {
window.location = path;
return state;
}

window.location = path;
}

// http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate
replaceState(state, path) {
replace(path, key) {
if (this.isSupported) {
state = this._createState(state);

var state = { key };
window.history.replaceState(state, '', path);
this.location = this.createLocation(path, state, NavigationTypes.REPLACE);
this._notifyChange();
} else {
window.location.replace(path);
return state;
}
window.location.replace(path);
}
}

Expand Down
52 changes: 51 additions & 1 deletion modules/DOMHistory.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import History from './History';
import { getWindowScrollPosition } from './DOMUtils';
import { addEventListener, removeEventListener, getWindowScrollPosition } from './DOMUtils';
import NavigationTypes from './NavigationTypes';

/**
* A history interface that assumes a DOM environment.
Expand All @@ -8,6 +9,17 @@ class DOMHistory extends History {
constructor(options={}) {
super(options);
this.getScrollPosition = options.getScrollPosition || getWindowScrollPosition;
this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
}

setup(path, entry) {
super.setup(path, entry);
addEventListener(window, 'beforeunload', this.handleBeforeUnload);
}

teardown() {
removeEventListener(window, 'beforeunload', this.handleBeforeUnload);
super.teardown();
}

go(n) {
Expand All @@ -17,6 +29,44 @@ class DOMHistory extends History {
window.history.go(n);
}

saveState(key, state) {
window.sessionStorage.setItem(key, JSON.stringify(state));
}

readState(key) {
var json = window.sessionStorage.getItem(key);

if (json) {
try {
return JSON.parse(json);
} catch (error) {
// Ignore invalid JSON in session storage.
}
}

return null;
}

beforeChange(location, done) {
super.beforeChange(location, () => {
if (location.navigationType === NavigationTypes.PUSH && this.canUpdateState()) {
var scrollPosition = this.getScrollPosition();
this.updateState(scrollPosition);
}
done();
});
}

handleBeforeUnload(event) {
var message = this.beforeChangeListener.call(this);

if (message != null) {
// cross browser, see https://developer.mozilla.org/en-US/docs/Web/Events/beforeunload
(event || window.event).returnValue = message;
return message;
}
}

}

export default DOMHistory;
16 changes: 16 additions & 0 deletions modules/DOMUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@ export var canUseDOM = !!(
typeof window !== 'undefined' && window.document && window.document.createElement
);

export function addEventListener(node, type, listener) {
if (node.addEventListener) {
node.addEventListener(type, listener, false);
} else {
node.attachEvent('on' + type, listener);
}
}

export function removeEventListener(node, type, listener) {
if (node.removeEventListener) {
node.removeEventListener(type, listener, false);
} else {
node.detachEvent('on' + type, listener);
}
}

export function getHashPath() {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
Expand Down
126 changes: 39 additions & 87 deletions modules/HashHistory.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import warning from 'warning';
import DOMHistory from './DOMHistory';
import NavigationTypes from './NavigationTypes';
import { getHashPath, replaceHashPath } from './DOMUtils';
import { addEventListener, removeEventListener, getHashPath, replaceHashPath } from './DOMUtils';
import { isAbsolutePath } from './URLUtils';

var DefaultQueryKey = '_qk';
Expand All @@ -26,34 +25,6 @@ function getQueryStringValueFromPath(path, key) {
return match && match[1];
}

function saveState(path, queryKey, state) {
window.sessionStorage.setItem(state.key, JSON.stringify(state));
return addQueryStringValueToPath(path, queryKey, state.key);
}

function readState(path, queryKey) {
var sessionKey = getQueryStringValueFromPath(path, queryKey);
var json = sessionKey && window.sessionStorage.getItem(sessionKey);

if (json) {
try {
return JSON.parse(json);
} catch (error) {
// Ignore invalid JSON in session storage.
}
}

return null;
}

function updateCurrentState(queryKey, extraState) {
var path = getHashPath();
var state = readState(path, queryKey);

if (state)
saveState(path, queryKey, Object.assign(state, extraState));
}

/**
* A history implementation for DOM environments that uses window.location.hash
* to store the current path. This is essentially a hack for older browsers that
Expand All @@ -79,17 +50,23 @@ class HashHistory extends DOMHistory {
this.queryKey = this.queryKey ? DefaultQueryKey : null;
}

_updateLocation(navigationType) {
setup() {
if (this.location != null)
return;

ensureSlash();

var path = getHashPath();
var state = this.queryKey ? readState(path, this.queryKey) : null;
this.location = this.createLocation(path, state, navigationType);
var key = getQueryStringValueFromPath(path, this.queryKey);

super.setup(path, { key });

addEventListener(window, 'hashchange', this.handleHashChange);
}

setup() {
if (this.location == null) {
ensureSlash();
this._updateLocation();
}
teardown() {
removeEventListener(window, 'hashchange', this.handleHashChange);
super.teardown();
}

handleHashChange() {
Expand All @@ -99,69 +76,44 @@ class HashHistory extends DOMHistory {
if (this._ignoreNextHashChange) {
this._ignoreNextHashChange = false;
} else {
this._updateLocation(NavigationTypes.POP);
this._notifyChange();
var path = getHashPath();
var key = getQueryStringValueFromPath(path, this.queryKey);
this.handlePop(path, { key });
}
}

addChangeListener(listener) {
super.addChangeListener(listener);

if (this.changeListeners.length === 1) {
if (window.addEventListener) {
window.addEventListener('hashchange', this.handleHashChange, false);
} else {
window.attachEvent('onhashchange', this.handleHashChange);
}
}
}

removeChangeListener(listener) {
super.removeChangeListener(listener);

if (this.changeListeners.length === 0) {
if (window.removeEventListener) {
window.removeEventListener('hashchange', this.handleHashChange, false);
} else {
window.detachEvent('onhashchange', this.handleHashChange);
}
}
}

pushState(state, path) {
warning(
this.queryKey || state == null,
'HashHistory needs a queryKey in order to persist state'
);

if (this.queryKey)
updateCurrentState(this.queryKey, this.getScrollPosition());

state = this._createState(state);

push(path, key) {
var actualPath = path;
if (this.queryKey)
path = saveState(path, this.queryKey, state);
actualPath = addQueryStringValueToPath(path, this.queryKey, key);

this._ignoreNextHashChange = true;
window.location.hash = path;

this.location = this.createLocation(path, state, NavigationTypes.PUSH);
if (actualPath === getHashPath()) {
warning(
false,
'HashHistory can not push the current path'
);
} else {
this._ignoreNextHashChange = true;
window.location.hash = actualPath;
}

this._notifyChange();
return { key: this.queryKey && key };
}

replaceState(state, path) {
state = this._createState(state);

replace(path, key) {
var actualPath = path;
if (this.queryKey)
path = saveState(path, this.queryKey, state);
actualPath = addQueryStringValueToPath(path, this.queryKey, key);


this._ignoreNextHashChange = true;
replaceHashPath(path);
if (actualPath !== getHashPath())
this._ignoreNextHashChange = true;

this.location = this.createLocation(path, state, NavigationTypes.REPLACE);
replaceHashPath(actualPath);

this._notifyChange();
return { key: this.queryKey && key };
}

makeHref(path) {
Expand Down
Loading