diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 4f0b348b51..374026eab1 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -2,13 +2,6 @@ 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), ''); -} - /** * A history implementation for DOM environments that support the * HTML5 history API (pushState, replaceState, and the popstate event). @@ -42,16 +35,24 @@ class BrowserHistory extends DOMHistory { } setup() { - if (this.location == null) - this._updateLocation(); + if (this.location != null) + return; + + var path = getWindowPath(); + var key = null; + if (this.isSupported && window.history.state) + key = window.history.state.key; + + super.setup(path, { key }); } handlePopState(event) { if (event.state === undefined) return; // Ignore extraneous popstate events in WebKit. - this._updateLocation(NavigationTypes.POP); - this._notifyChange(); + var path = getWindowPath(); + var key = event.state && event.state.key; + this.handlePop(path, { key }); } addChangeListener(listener) { @@ -79,31 +80,24 @@ class BrowserHistory extends DOMHistory { } // 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); } } diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index 0f068601bc..8dcdfa64e1 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -18,6 +18,36 @@ 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; + } + + pushState(state, path) { + var location = this.location; + if (location && location.state && location.state.key) { + var key = location.state.key; + var curState = this.readState(key); + var scroll = this.getScrollPosition(); + this.saveState(key, {...curState, ...scroll}); + } + + super.pushState(state, path); + } + } export default DOMHistory; diff --git a/modules/HashHistory.js b/modules/HashHistory.js index 85387022d9..afb790f864 100644 --- a/modules/HashHistory.js +++ b/modules/HashHistory.js @@ -1,6 +1,5 @@ import warning from 'warning'; import DOMHistory from './DOMHistory'; -import NavigationTypes from './NavigationTypes'; import { getHashPath, replaceHashPath } from './DOMUtils'; import { isAbsolutePath } from './URLUtils'; @@ -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 @@ -79,17 +50,15 @@ class HashHistory extends DOMHistory { this.queryKey = this.queryKey ? DefaultQueryKey : null; } - _updateLocation(navigationType) { - var path = getHashPath(); - var state = this.queryKey ? readState(path, this.queryKey) : null; - this.location = this.createLocation(path, state, navigationType); - } - setup() { - if (this.location == null) { - ensureSlash(); - this._updateLocation(); - } + if (this.location != null) + return; + + ensureSlash(); + + var path = getHashPath(); + var key = getQueryStringValueFromPath(path, this.queryKey); + super.setup(path, { key }); } handleHashChange() { @@ -99,8 +68,9 @@ 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 }); } } @@ -128,40 +98,38 @@ class HashHistory extends DOMHistory { } } - pushState(state, path) { - warning( - this.queryKey || state == null, - 'HashHistory needs a queryKey in order to persist state' - ); - + push(path, key) { + var actualPath = path; if (this.queryKey) - updateCurrentState(this.queryKey, this.getScrollPosition()); + actualPath = addQueryStringValueToPath(path, this.queryKey, key); - state = this._createState(state); - if (this.queryKey) - path = saveState(path, this.queryKey, state); - - 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) { diff --git a/modules/History.js b/modules/History.js index 4beabe6dbd..027e019396 100644 --- a/modules/History.js +++ b/modules/History.js @@ -1,12 +1,10 @@ import invariant from 'invariant'; +import warning from 'warning'; +import NavigationTypes from './NavigationTypes'; import { getPathname, getQueryString, parseQueryString } from './URLUtils'; import Location from './Location'; -var RequiredHistorySubclassMethods = [ 'pushState', 'replaceState', 'go' ]; - -function createRandomKey() { - return Math.random().toString(36).substr(2); -} +var RequiredHistorySubclassMethods = [ 'push', 'replace', 'go' ]; /** * A history interface that normalizes the differences across @@ -30,7 +28,6 @@ class History { this.parseQueryString = options.parseQueryString || parseQueryString; this.changeListeners = []; - this.location = null; } _notifyChange() { @@ -48,6 +45,81 @@ class History { }); } + setup(path, entry = {}) { + if (this.location) + return; + + if (!entry.key) + entry = this.replace(path, this.createRandomKey()); + + var state = null; + if (typeof this.readState === 'function') + state = this.readState(entry.key); + + this._update(path, state, entry, NavigationTypes.POP, false); + } + + handlePop(path, entry = {}) { + var state = null; + if (entry.key && typeof this.readState === 'function') + state = this.readState(entry.key); + + this._update(path, state, entry, NavigationTypes.POP); + } + + createRandomKey() { + return Math.random().toString(36).substr(2); + } + + _saveNewState(state) { + var key = this.createRandomKey(); + + if (state != null) { + invariant( + typeof this.saveState === 'function', + '%s needs a saveState method in order to store state', + this.constructor.name + ); + + this.saveState(key, state); + } + + return key; + } + + pushState(state, path) { + var key = this._saveNewState(state); + + var entry = null; + if (this.path === path) { + entry = this.replace(path, key) || {}; + } else { + entry = this.push(path, key) || {}; + } + + warning( + entry.key || state == null, + '%s does not support storing state', + this.constructor.name + ); + + this._update(path, state, entry, NavigationTypes.PUSH); + } + + replaceState(state, path) { + var key = this._saveNewState(state); + + var entry = this.replace(path, key) || {}; + + warning( + entry.key || state == null, + '%s does not support storing state', + this.constructor.name + ); + + this._update(path, state, entry, NavigationTypes.REPLACE); + } + back() { this.go(-1); } @@ -56,20 +128,19 @@ class History { this.go(1); } - _createState(state) { - state = state || {}; - - if (!state.key) - state.key = createRandomKey(); + _update(path, state, entry, navigationType, notify=true) { + this.path = path; + this.location = this._createLocation(path, state, entry, navigationType); - return state; + if (notify) + this._notifyChange(); } - createLocation(path, state, navigationType) { + _createLocation(path, state, entry, navigationType) { var pathname = getPathname(path); var queryString = getQueryString(path); var query = queryString ? this.parseQueryString(queryString) : null; - return new Location(pathname, query, state, navigationType); + return new Location(pathname, query, {...state, ...entry}, navigationType); } } diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index d22f70d311..8d3abadbb4 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -2,16 +2,6 @@ import invariant from 'invariant'; import NavigationTypes from './NavigationTypes'; import History from './History'; -function createEntry(object) { - if (typeof object === 'string') - return { path: object }; - - if (typeof object === 'object' && object) - return object; - - throw new Error('Unable to create history entry from ' + object); -} - /** * A concrete History class that doesn't require a DOM. Ideal * for testing because it allows you to specify route history @@ -30,7 +20,7 @@ class MemoryHistory extends History { throw new Error('MemoryHistory needs an array of entries'); } - entries = entries.map(createEntry); + entries = entries.map(this._createEntry.bind(this)); if (current == null) { current = entries.length - 1; @@ -42,36 +32,59 @@ class MemoryHistory extends History { ); } - this.entries = entries; this.current = current; + this.entries = entries; + this.storage = entries + .filter(entry => entry.state) + .reduce((all, entry) => { + all[entry.key] = entry.state; + return all; + }, {}); + } - var currentEntry = entries[current]; + setup() { + if (this.location) + return; - this.location = this.createLocation( - currentEntry.path, - currentEntry.state - ); + var entry = this.entries[this.current]; + var path = entry.path; + var key = entry.key; + + super.setup(path, { key, current: this.current }); } - // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate - pushState(state, path) { - state = this._createState(state); + _createEntry(object) { + var key = this.createRandomKey(); + if (typeof object === 'string') + return { path: object, key }; + + if (typeof object === 'object' && object) + return {...object, key}; + throw new Error('Unable to create history entry from ' + object); + } + + // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate + push(path, key) { this.current += 1; - this.entries = this.entries.slice(0, this.current).concat([{ state, path }]); - this.location = this.createLocation(path, state, NavigationTypes.PUSH); + this.entries = this.entries.slice(0, this.current).concat([{ key, path }]); - this._notifyChange(); + return { key, current: this.current }; } // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate - replaceState(state, path) { - state = this._createState(state); + replace(path, key) { + this.entries[this.current] = { key, path }; - this.entries[this.current] = { state, path }; - this.location = this.createLocation(path, state, NavigationTypes.REPLACE); + return { key, current: this.current }; + } + + readState(key) { + return this.storage[key]; + } - this._notifyChange(); + saveState(key, state){ + this.storage[key] = state; } go(n) { @@ -87,13 +100,7 @@ class MemoryHistory extends History { this.current += n; var currentEntry = this.entries[this.current]; - this.location = this.createLocation( - currentEntry.path, - currentEntry.state, - NavigationTypes.POP - ); - - this._notifyChange(); + this.handlePop(currentEntry.path, { key: currentEntry.key, current: this.current }); } canGo(n) { diff --git a/modules/__tests__/scrollManagement-test.js b/modules/__tests__/scrollManagement-test.js index 012ee5c9c6..e22e8a17eb 100644 --- a/modules/__tests__/scrollManagement-test.js +++ b/modules/__tests__/scrollManagement-test.js @@ -60,11 +60,15 @@ describe('Scroll management', function () { ]; function execNextStep() { - try { - steps.shift().apply(this, arguments); - } catch (error) { - done(error); + if (steps.length < 1){ + done(); + return; } + + // Give the DOM a little time to reflect the hashchange. + setTimeout(() => { + steps.shift().call(this); + }, 10); } var history = new HashHistory({ queryKey: true });