Skip to content

Commit

Permalink
Merge pull request #1376 from rackt/histories
Browse files Browse the repository at this point in the history
Update Histories
  • Loading branch information
mjackson committed Jul 16, 2015
2 parents 50c9a23 + 8360321 commit 9483867
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 201 deletions.
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

0 comments on commit 9483867

Please sign in to comment.