From ba23ce6f2cc46c4aa9d523514e2e9a403f1b4dc7 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Fri, 19 Jun 2015 15:18:05 +0200 Subject: [PATCH 01/12] Rewrite internal behavior of Histories --- modules/BrowserHistory.js | 48 ++++++++---------- modules/DOMHistory.js | 30 ++++++++++++ modules/HashHistory.js | 96 ++++++++++++------------------------ modules/History.js | 100 +++++++++++++++++++++++++++++++------- modules/Location.js | 3 +- modules/MemoryHistory.js | 77 ++++++++++++++++------------- 6 files changed, 210 insertions(+), 144 deletions(-) diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 76d818ddb2..855811a16c 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 bb2cadeb31..0972186874 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -17,6 +17,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 5203ffead7..4e36286286 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..a86a380c6a 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,28 +45,97 @@ class History { }); } - back() { - this.go(-1); + 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.location = this._createLocation(path, state, entry); } - forward() { - this.go(1); + handlePop(path, entry = {}) { + var state = null; + if (entry.key && typeof this.readState === 'function') + state = this.readState(entry.key); + + this.location = this._createLocation(path, state, entry, NavigationTypes.POP); + this._notifyChange(); + } + + createRandomKey() { + return Math.random().toString(36).substr(2); } - _createState(state) { - state = state || {}; + _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 + ); - if (!state.key) - state.key = createRandomKey(); + this.saveState(key, state); + } + + return key; + } - return state; + pushState(state, path) { + var key = this._saveNewState(state); + + var entry = null; + if (this.location && this.location.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.location = this._createLocation(path, state, entry, NavigationTypes.PUSH); + this._notifyChange(); + } + + 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.location = this._createLocation(path, state, entry, NavigationTypes.REPLACE); + this._notifyChange(); + } + + back() { + this.go(-1); + } + + forward() { + this.go(1); } - 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(path, pathname, query, {...state, ...entry}, navigationType); } } diff --git a/modules/Location.js b/modules/Location.js index 0210b60c96..550730c0c7 100644 --- a/modules/Location.js +++ b/modules/Location.js @@ -12,7 +12,8 @@ class Location { return object instanceof Location; } - constructor(pathname='/', query=null, state=null, navigationType=NavigationTypes.POP) { + constructor(path='/', pathname=path, query=null, state=null, navigationType=NavigationTypes.POP) { + this.path = path; this.pathname = pathname; this.query = query; this.state = state; diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index d22f70d311..4c78e06945 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) { From 28a49f73af3397b85f6b6969896c6a9bd4383d3b Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Fri, 19 Jun 2015 21:52:16 +0200 Subject: [PATCH 02/12] Remove location.path --- modules/History.js | 23 ++++++++++++++--------- modules/Location.js | 3 +-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/modules/History.js b/modules/History.js index a86a380c6a..7feaaddc50 100644 --- a/modules/History.js +++ b/modules/History.js @@ -56,7 +56,7 @@ class History { if (typeof this.readState === 'function') state = this.readState(entry.key); - this.location = this._createLocation(path, state, entry); + this._handleChange(path, state, entry, NavigationTypes.POP, false); } handlePop(path, entry = {}) { @@ -64,8 +64,7 @@ class History { if (entry.key && typeof this.readState === 'function') state = this.readState(entry.key); - this.location = this._createLocation(path, state, entry, NavigationTypes.POP); - this._notifyChange(); + this._handleChange(path, state, entry, NavigationTypes.POP); } createRandomKey() { @@ -92,7 +91,7 @@ class History { var key = this._saveNewState(state); var entry = null; - if (this.location && this.location.path === path) { + if (this.path === path) { entry = this.replace(path, key) || {}; } else { entry = this.push(path, key) || {}; @@ -104,8 +103,7 @@ class History { this.constructor.name ); - this.location = this._createLocation(path, state, entry, NavigationTypes.PUSH); - this._notifyChange(); + this._handleChange(path, state, entry, NavigationTypes.PUSH); } replaceState(state, path) { @@ -119,8 +117,7 @@ class History { this.constructor.name ); - this.location = this._createLocation(path, state, entry, NavigationTypes.REPLACE); - this._notifyChange(); + this._handleChange(path, state, entry, NavigationTypes.REPLACE); } back() { @@ -131,11 +128,19 @@ class History { this.go(1); } + _handleChange(path, state, entry, navigationType, notify=true) { + this.path = path; + this.location = this._createLocation(path, state, entry, navigationType); + + if (notify) + this._notifyChange(); + } + _createLocation(path, state, entry, navigationType) { var pathname = getPathname(path); var queryString = getQueryString(path); var query = queryString ? this.parseQueryString(queryString) : null; - return new Location(path, pathname, query, {...state, ...entry}, navigationType); + return new Location(pathname, query, {...state, ...entry}, navigationType); } } diff --git a/modules/Location.js b/modules/Location.js index 550730c0c7..0210b60c96 100644 --- a/modules/Location.js +++ b/modules/Location.js @@ -12,8 +12,7 @@ class Location { return object instanceof Location; } - constructor(path='/', pathname=path, query=null, state=null, navigationType=NavigationTypes.POP) { - this.path = path; + constructor(pathname='/', query=null, state=null, navigationType=NavigationTypes.POP) { this.pathname = pathname; this.query = query; this.state = state; From 23c0aff679b38dbe0fbfcde8c8b1e46c4efd2c14 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sat, 20 Jun 2015 02:32:18 +0200 Subject: [PATCH 03/12] Style tweaks --- modules/BrowserHistory.js | 4 ++-- modules/History.js | 10 +++++----- modules/MemoryHistory.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 855811a16c..90004aea6f 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -43,7 +43,7 @@ class BrowserHistory extends DOMHistory { if (this.isSupported && window.history.state) key = window.history.state.key; - super.setup(path, {key}); + super.setup(path, { key }); } handlePopState(event) { @@ -52,7 +52,7 @@ class BrowserHistory extends DOMHistory { var path = getWindowPath(); var key = event.state && event.state.key; - this.handlePop(path, {key}); + this.handlePop(path, { key }); } addChangeListener(listener) { diff --git a/modules/History.js b/modules/History.js index 7feaaddc50..027e019396 100644 --- a/modules/History.js +++ b/modules/History.js @@ -56,7 +56,7 @@ class History { if (typeof this.readState === 'function') state = this.readState(entry.key); - this._handleChange(path, state, entry, NavigationTypes.POP, false); + this._update(path, state, entry, NavigationTypes.POP, false); } handlePop(path, entry = {}) { @@ -64,7 +64,7 @@ class History { if (entry.key && typeof this.readState === 'function') state = this.readState(entry.key); - this._handleChange(path, state, entry, NavigationTypes.POP); + this._update(path, state, entry, NavigationTypes.POP); } createRandomKey() { @@ -103,7 +103,7 @@ class History { this.constructor.name ); - this._handleChange(path, state, entry, NavigationTypes.PUSH); + this._update(path, state, entry, NavigationTypes.PUSH); } replaceState(state, path) { @@ -117,7 +117,7 @@ class History { this.constructor.name ); - this._handleChange(path, state, entry, NavigationTypes.REPLACE); + this._update(path, state, entry, NavigationTypes.REPLACE); } back() { @@ -128,7 +128,7 @@ class History { this.go(1); } - _handleChange(path, state, entry, navigationType, notify=true) { + _update(path, state, entry, navigationType, notify=true) { this.path = path; this.location = this._createLocation(path, state, entry, navigationType); diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index 4c78e06945..8d3abadbb4 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -50,7 +50,7 @@ class MemoryHistory extends History { var path = entry.path; var key = entry.key; - super.setup(path, {key, current: this.current}); + super.setup(path, { key, current: this.current }); } _createEntry(object) { @@ -69,14 +69,14 @@ class MemoryHistory extends History { this.current += 1; this.entries = this.entries.slice(0, this.current).concat([{ key, path }]); - return {key, current: this.current}; + return { key, current: this.current }; } // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-replacestate replace(path, key) { this.entries[this.current] = { key, path }; - return {key, current: this.current}; + return { key, current: this.current }; } readState(key) { @@ -100,7 +100,7 @@ class MemoryHistory extends History { this.current += n; var currentEntry = this.entries[this.current]; - this.handlePop(currentEntry.path, {key: currentEntry.key, current: this.current}); + this.handlePop(currentEntry.path, { key: currentEntry.key, current: this.current }); } canGo(n) { From af7eb555b699592c793b4fa46e7380bd16480781 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sun, 21 Jun 2015 03:36:20 +0200 Subject: [PATCH 04/12] [added] History.onBeforeChange --- modules/DOMHistory.js | 29 +++++++++++++++ modules/History.js | 78 +++++++++++++++++++++++++++++++++++----- modules/MemoryHistory.js | 8 +++-- 3 files changed, 103 insertions(+), 12 deletions(-) diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index 0972186874..ee4247495a 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -8,6 +8,7 @@ class DOMHistory extends History { constructor(options={}) { super(options); this.getScrollPosition = options.getScrollPosition || getWindowScrollPosition; + this.handleBeforeUnload = this.handleBeforeUnload.bind(this); } go(n) { @@ -35,6 +36,34 @@ class DOMHistory extends History { return null; } + onBeforeChange(listener) { + if (!this.beforeChangeListener && listener) { + if (window.addEventListener) { + window.addEventListener('beforeunload', this.handleBeforeUnload); + } else { + window.attachEvent('onbeforeunload', this.handleBeforeUnload); + } + } else if(this.beforeChangeListener && !listener) { + if (window.removeEventListener) { + window.removeEventListener('beforeunload', this.handleBeforeUnload); + } else { + window.detachEvent('onbeforeunload', this.handleBeforeUnload); + } + } + + super.onBeforeChange(listener); + } + + 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; + } + } + pushState(state, path) { var location = this.location; if (location && location.state && location.state.key) { diff --git a/modules/History.js b/modules/History.js index 027e019396..7da558773b 100644 --- a/modules/History.js +++ b/modules/History.js @@ -28,6 +28,9 @@ class History { this.parseQueryString = options.parseQueryString || parseQueryString; this.changeListeners = []; + + this.location = null; + this._pendingLocation = null; } _notifyChange() { @@ -45,6 +48,10 @@ class History { }); } + onBeforeChange(listener) { + this.beforeChangeListener = listener; + } + setup(path, entry = {}) { if (this.location) return; @@ -56,15 +63,30 @@ class History { if (typeof this.readState === 'function') state = this.readState(entry.key); - this._update(path, state, entry, NavigationTypes.POP, false); + var location = this._createLocation(path, state, entry, NavigationTypes.POP); + this._update(path, location, false); } - handlePop(path, entry = {}) { + handlePop(path, entry={}, applyEntry=null) { var state = null; if (entry.key && typeof this.readState === 'function') state = this.readState(entry.key); - this._update(path, state, entry, NavigationTypes.POP); + var location = this._createLocation(path, state, entry, NavigationTypes.POP); + + if (!this.beforeChangeListener) { + applyEntry && applyEntry(); + this._update(path, location); + } else { + this._pendingLocation = location; + this.beforeChangeListener.call(this, location, () => { + if (this._pendingLocation === location){ + this._pendingLocation = null; + applyEntry && applyEntry(); + this._update(path, location); + } + }); + } } createRandomKey() { @@ -88,9 +110,27 @@ class History { } pushState(state, path) { - var key = this._saveNewState(state); + if (!this.beforeChangeListener) { + this._doPushState(state, path); + } else { + var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH); + this._pendingLocation = pendingLocation; + + this.beforeChangeListener.call(this, pendingLocation, () => { + if (this._pendingLocation === pendingLocation) { + this._pendingLocation = null; + this._doPushState(state, path); + return true; + } + return false; + }); + } + } + _doPushState(state, path) { + var key = this._saveNewState(state); var entry = null; + if (this.path === path) { entry = this.replace(path, key) || {}; } else { @@ -103,12 +143,30 @@ class History { this.constructor.name ); - this._update(path, state, entry, NavigationTypes.PUSH); + var location = this._createLocation(path, state, entry, NavigationTypes.PUSH); + this._update(path, location); } replaceState(state, path) { - var key = this._saveNewState(state); + if (!this.beforeChangeListener) { + this._doReplaceState(state, path); + } else { + var pendingLocation = this._createLocation(path, state, null, NavigationTypes.REPLACE); + this._pendingLocation = pendingLocation; + + this.beforeChangeListener.call(this, pendingLocation, () => { + if (this._pendingLocation === pendingLocation) { + this._pendingLocation = null; + this._doReplaceState(state, path); + return true; + } + return false; + }); + } + } + _doReplaceState(state, path) { + var key = this._saveNewState(state); var entry = this.replace(path, key) || {}; warning( @@ -117,7 +175,8 @@ class History { this.constructor.name ); - this._update(path, state, entry, NavigationTypes.REPLACE); + var location = this._createLocation(path, state, entry, NavigationTypes.REPLACE); + this._update(path, location); } back() { @@ -128,9 +187,10 @@ class History { this.go(1); } - _update(path, state, entry, navigationType, notify=true) { + _update(path, location, notify=true) { this.path = path; - this.location = this._createLocation(path, state, entry, navigationType); + this.location = location; + this._pendingLocation = null; if (notify) this._notifyChange(); diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index 8d3abadbb4..3114212006 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -97,10 +97,12 @@ class MemoryHistory extends History { this.constructor.name, n ); - this.current += n; - var currentEntry = this.entries[this.current]; + var next = this.current + n; + var nextEntry = this.entries[next]; - this.handlePop(currentEntry.path, { key: currentEntry.key, current: this.current }); + this.handlePop(nextEntry.path, { key: nextEntry.key, current: this.current }, () => { + this.current = next; + }); } canGo(n) { From 928259e2112a0550d962409c335dfa7de243b4ce Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sun, 21 Jun 2015 05:11:25 +0200 Subject: [PATCH 05/12] Add missing return values --- modules/History.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/History.js b/modules/History.js index 7da558773b..5014a47cdb 100644 --- a/modules/History.js +++ b/modules/History.js @@ -84,7 +84,9 @@ class History { this._pendingLocation = null; applyEntry && applyEntry(); this._update(path, location); + return true; } + return false; }); } } From 3d1b090fcd0cd97c74404ad5354c5542fc04bdb9 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sun, 21 Jun 2015 23:04:15 +0200 Subject: [PATCH 06/12] Use History setup() and teardown() for event binding --- modules/BrowserHistory.js | 40 ++++++++++++++++----------------------- modules/DOMHistory.js | 38 +++++++++++++++++++------------------ modules/HashHistory.js | 40 ++++++++++++++++----------------------- modules/History.js | 14 ++++++++++++++ 4 files changed, 66 insertions(+), 66 deletions(-) diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 90004aea6f..6c6ef488fe 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -44,6 +44,22 @@ class BrowserHistory extends DOMHistory { key = window.history.state.key; super.setup(path, { key }); + + if (window.addEventListener) { + window.addEventListener('popstate', this.handlePopState, false); + } else { + window.attachEvent('onpopstate', this.handlePopState); + } + } + + teardown() { + if (window.removeEventListener) { + window.removeEventListener('popstate', this.handlePopState, false); + } else { + window.detachEvent('onpopstate', this.handlePopState); + } + + super.teardown(); } handlePopState(event) { @@ -55,30 +71,6 @@ class BrowserHistory extends DOMHistory { this.handlePop(path, { key }); } - 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); - } - } - } - // http://www.w3.org/TR/2011/WD-html5-20110113/history.html#dom-history-pushstate push(path, key) { if (this.isSupported) { diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index ee4247495a..9652c38ed0 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -11,6 +11,26 @@ class DOMHistory extends History { this.handleBeforeUnload = this.handleBeforeUnload.bind(this); } + setup(path, entry) { + super.setup(path, entry); + + if (window.addEventListener) { + window.addEventListener('beforeunload', this.handleBeforeUnload); + } else { + window.attachEvent('onbeforeunload', this.handleBeforeUnload); + } + } + + teardown() { + if (window.removeEventListener) { + window.removeEventListener('beforeunload', this.handleBeforeUnload); + } else { + window.detachEvent('onbeforeunload', this.handleBeforeUnload); + } + + super.teardown(); + } + go(n) { if (n === 0) return; @@ -36,24 +56,6 @@ class DOMHistory extends History { return null; } - onBeforeChange(listener) { - if (!this.beforeChangeListener && listener) { - if (window.addEventListener) { - window.addEventListener('beforeunload', this.handleBeforeUnload); - } else { - window.attachEvent('onbeforeunload', this.handleBeforeUnload); - } - } else if(this.beforeChangeListener && !listener) { - if (window.removeEventListener) { - window.removeEventListener('beforeunload', this.handleBeforeUnload); - } else { - window.detachEvent('onbeforeunload', this.handleBeforeUnload); - } - } - - super.onBeforeChange(listener); - } - handleBeforeUnload(event) { var message = this.beforeChangeListener.call(this); diff --git a/modules/HashHistory.js b/modules/HashHistory.js index 4e36286286..bf15d8023e 100644 --- a/modules/HashHistory.js +++ b/modules/HashHistory.js @@ -59,6 +59,22 @@ class HashHistory extends DOMHistory { var path = getHashPath(); var key = getQueryStringValueFromPath(path, this.queryKey); super.setup(path, { key }); + + if (window.addEventListener) { + window.addEventListener('hashchange', this.handleHashChange, false); + } else { + window.attachEvent('onhashchange', this.handleHashChange); + } + } + + teardown() { + if (window.removeEventListener) { + window.removeEventListener('hashchange', this.handleHashChange, false); + } else { + window.detachEvent('onhashchange', this.handleHashChange); + } + + super.teardown(); } handleHashChange() { @@ -74,30 +90,6 @@ class HashHistory extends DOMHistory { } } - 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); - } - } - } - push(path, key) { var actualPath = path; if (this.queryKey) diff --git a/modules/History.js b/modules/History.js index 5014a47cdb..1351b56219 100644 --- a/modules/History.js +++ b/modules/History.js @@ -49,6 +49,11 @@ class History { } onBeforeChange(listener) { + warning( + this.beforeChangeListener != null, + 'beforeChange listener of History should not be overwritten' + ); + this.beforeChangeListener = listener; } @@ -67,6 +72,15 @@ class History { this._update(path, location, false); } + teardown() { + this.changeListeners = []; + this.beforeChangeListener = null; + + this.path = null; + this.location = null; + this._pendingLocation = null; + } + handlePop(path, entry={}, applyEntry=null) { var state = null; if (entry.key && typeof this.readState === 'function') From 240cdf6c94c2f1d01aa5405c221ded12e9cbf495 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sun, 21 Jun 2015 23:11:16 +0200 Subject: [PATCH 07/12] Refactor Histories --- modules/BrowserHistory.js | 15 ---------- modules/History.js | 58 ++++++++++++++++----------------------- modules/MemoryHistory.js | 1 - 3 files changed, 23 insertions(+), 51 deletions(-) diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 6c6ef488fe..14ff0c0339 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -1,6 +1,5 @@ import DOMHistory from './DOMHistory'; import { getWindowPath, supportsHistory } from './DOMUtils'; -import NavigationTypes from './NavigationTypes'; /** * A history implementation for DOM environments that support the @@ -20,20 +19,6 @@ class BrowserHistory extends DOMHistory { this.isSupported = supportsHistory(); } - _updateLocation(navigationType) { - var state = null; - - if (this.isSupported) { - var historyState = window.history.state; - state = this._createState(historyState); - - if (!historyState || !historyState.key) - window.history.replaceState(state, ''); - } - - this.location = this.createLocation(getWindowPath(), state, navigationType); - } - setup() { if (this.location != null) return; diff --git a/modules/History.js b/modules/History.js index 1351b56219..84e6620c18 100644 --- a/modules/History.js +++ b/modules/History.js @@ -27,8 +27,11 @@ class History { }, this); this.parseQueryString = options.parseQueryString || parseQueryString; + this.changeListeners = []; + this.beforeChangeListener = null; + this.path = null; this.location = null; this._pendingLocation = null; } @@ -86,23 +89,13 @@ class History { if (entry.key && typeof this.readState === 'function') state = this.readState(entry.key); - var location = this._createLocation(path, state, entry, NavigationTypes.POP); + var pendingLocation = this._createLocation(path, state, entry, NavigationTypes.POP); - if (!this.beforeChangeListener) { + this._schedule(pendingLocation, () => { applyEntry && applyEntry(); + var location = this._createLocation(path, state, entry, NavigationTypes.POP); this._update(path, location); - } else { - this._pendingLocation = location; - this.beforeChangeListener.call(this, location, () => { - if (this._pendingLocation === location){ - this._pendingLocation = null; - applyEntry && applyEntry(); - this._update(path, location); - return true; - } - return false; - }); - } + }); } createRandomKey() { @@ -125,17 +118,16 @@ class History { return key; } - pushState(state, path) { + _schedule(location, done) { if (!this.beforeChangeListener) { - this._doPushState(state, path); + done(); } else { - var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH); - this._pendingLocation = pendingLocation; + this._pendingLocation = location; - this.beforeChangeListener.call(this, pendingLocation, () => { - if (this._pendingLocation === pendingLocation) { + this.beforeChangeListener.call(this, location, () => { + if (this._pendingLocation === location) { this._pendingLocation = null; - this._doPushState(state, path); + done(); return true; } return false; @@ -143,6 +135,13 @@ class History { } } + pushState(state, path) { + var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH); + this._schedule(pendingLocation, () => { + this._doPushState(state, path) + }); + } + _doPushState(state, path) { var key = this._saveNewState(state); var entry = null; @@ -164,21 +163,10 @@ class History { } replaceState(state, path) { - if (!this.beforeChangeListener) { + var pendingLocation = this._createLocation(path, state, null, NavigationTypes.REPLACE); + this._schedule(pendingLocation, () => { this._doReplaceState(state, path); - } else { - var pendingLocation = this._createLocation(path, state, null, NavigationTypes.REPLACE); - this._pendingLocation = pendingLocation; - - this.beforeChangeListener.call(this, pendingLocation, () => { - if (this._pendingLocation === pendingLocation) { - this._pendingLocation = null; - this._doReplaceState(state, path); - return true; - } - return false; - }); - } + }); } _doReplaceState(state, path) { diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index 3114212006..3fcbdbbc2c 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -1,5 +1,4 @@ import invariant from 'invariant'; -import NavigationTypes from './NavigationTypes'; import History from './History'; /** From 486bf2b60a0976345d4e37f4adab36cc039c2bef Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Mon, 22 Jun 2015 00:30:16 +0200 Subject: [PATCH 08/12] Add history.isPending(location) --- modules/History.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/History.js b/modules/History.js index 84e6620c18..e4657b3ad6 100644 --- a/modules/History.js +++ b/modules/History.js @@ -135,6 +135,10 @@ class History { } } + isPending(location) { + return this._pendingLocation === location; + } + pushState(state, path) { var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH); this._schedule(pendingLocation, () => { From a3bb27187fc7fed6bdb30d99a7e01270015c5e43 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Mon, 22 Jun 2015 00:46:52 +0200 Subject: [PATCH 09/12] Add history.updateState(extraState) --- modules/DOMHistory.js | 9 +++------ modules/History.js | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index 9652c38ed0..74375bf129 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -67,12 +67,9 @@ class DOMHistory extends History { } 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}); + if (this.canUpdateState()) { + var scrollPosition = this.getScrollPosition(); + this.updateState(scrollPosition); } super.pushState(state, path); diff --git a/modules/History.js b/modules/History.js index e4657b3ad6..7f0bf49a8d 100644 --- a/modules/History.js +++ b/modules/History.js @@ -118,6 +118,26 @@ class History { return key; } + canUpdateState() { + return typeof this.readState === 'function' + && typeof this.saveState === 'function' + && this.location + && this.location.state + && this.location.state.key; + } + + updateState(extraState) { + invariant( + this.canUpdateState(), + '%s is unable to update state right now', + this.constructor.name + ); + + var key = this.location.state.key; + var state = this.readState(key); + this.saveState(key, { ...state, ...extraState }); + } + _schedule(location, done) { if (!this.beforeChangeListener) { done(); From b66737f979ff79e586ad6c5609d72ff9330036c2 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Mon, 22 Jun 2015 01:43:10 +0200 Subject: [PATCH 10/12] Save scroll position at the right time --- modules/DOMHistory.js | 20 +++++++++++--------- modules/History.js | 11 +++++------ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index 74375bf129..983a4b2490 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -1,5 +1,6 @@ import History from './History'; import { getWindowScrollPosition } from './DOMUtils'; +import NavigationTypes from './NavigationTypes'; /** * A history interface that assumes a DOM environment. @@ -56,6 +57,16 @@ class DOMHistory extends History { 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); @@ -66,15 +77,6 @@ class DOMHistory extends History { } } - pushState(state, path) { - if (this.canUpdateState()) { - var scrollPosition = this.getScrollPosition(); - this.updateState(scrollPosition); - } - - super.pushState(state, path); - } - } export default DOMHistory; diff --git a/modules/History.js b/modules/History.js index 7f0bf49a8d..d08a4efdd2 100644 --- a/modules/History.js +++ b/modules/History.js @@ -91,10 +91,9 @@ class History { var pendingLocation = this._createLocation(path, state, entry, NavigationTypes.POP); - this._schedule(pendingLocation, () => { + this.beforeChange(pendingLocation, () => { applyEntry && applyEntry(); - var location = this._createLocation(path, state, entry, NavigationTypes.POP); - this._update(path, location); + this._update(path, pendingLocation); }); } @@ -138,7 +137,7 @@ class History { this.saveState(key, { ...state, ...extraState }); } - _schedule(location, done) { + beforeChange(location, done) { if (!this.beforeChangeListener) { done(); } else { @@ -161,7 +160,7 @@ class History { pushState(state, path) { var pendingLocation = this._createLocation(path, state, null, NavigationTypes.PUSH); - this._schedule(pendingLocation, () => { + this.beforeChange(pendingLocation, () => { this._doPushState(state, path) }); } @@ -188,7 +187,7 @@ class History { replaceState(state, path) { var pendingLocation = this._createLocation(path, state, null, NavigationTypes.REPLACE); - this._schedule(pendingLocation, () => { + this.beforeChange(pendingLocation, () => { this._doReplaceState(state, path); }); } From f7d3dbc202e472003af3216f9ad30ade4213faa9 Mon Sep 17 00:00:00 2001 From: Alexander Gundermann Date: Sat, 27 Jun 2015 02:43:26 +0200 Subject: [PATCH 11/12] Remove interface for postponing pop events --- modules/History.js | 3 +-- modules/MemoryHistory.js | 8 +++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/History.js b/modules/History.js index d08a4efdd2..6c3d09de55 100644 --- a/modules/History.js +++ b/modules/History.js @@ -84,7 +84,7 @@ class History { this._pendingLocation = null; } - handlePop(path, entry={}, applyEntry=null) { + handlePop(path, entry={}) { var state = null; if (entry.key && typeof this.readState === 'function') state = this.readState(entry.key); @@ -92,7 +92,6 @@ class History { var pendingLocation = this._createLocation(path, state, entry, NavigationTypes.POP); this.beforeChange(pendingLocation, () => { - applyEntry && applyEntry(); this._update(path, pendingLocation); }); } diff --git a/modules/MemoryHistory.js b/modules/MemoryHistory.js index 3fcbdbbc2c..e6d87373fb 100644 --- a/modules/MemoryHistory.js +++ b/modules/MemoryHistory.js @@ -96,12 +96,10 @@ class MemoryHistory extends History { this.constructor.name, n ); - var next = this.current + n; - var nextEntry = this.entries[next]; + this.current += n; + var entry = this.entries[this.current]; - this.handlePop(nextEntry.path, { key: nextEntry.key, current: this.current }, () => { - this.current = next; - }); + this.handlePop(entry.path, { key: entry.key, current: this.current }); } canGo(n) { From 836032130c4cffa7b6d97b892043f1d7ac2cd1f3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 29 Jun 2015 13:36:45 +0200 Subject: [PATCH 12/12] Add add/removeEventListener util methods --- modules/BrowserHistory.js | 15 +++------------ modules/DOMHistory.js | 16 +++------------- modules/DOMUtils.js | 16 ++++++++++++++++ modules/HashHistory.js | 16 ++++------------ 4 files changed, 26 insertions(+), 37 deletions(-) diff --git a/modules/BrowserHistory.js b/modules/BrowserHistory.js index 14ff0c0339..bdc05e7731 100644 --- a/modules/BrowserHistory.js +++ b/modules/BrowserHistory.js @@ -1,5 +1,5 @@ import DOMHistory from './DOMHistory'; -import { getWindowPath, supportsHistory } from './DOMUtils'; +import { addEventListener, removeEventListener, getWindowPath, supportsHistory } from './DOMUtils'; /** * A history implementation for DOM environments that support the @@ -30,20 +30,11 @@ class BrowserHistory extends DOMHistory { super.setup(path, { key }); - if (window.addEventListener) { - window.addEventListener('popstate', this.handlePopState, false); - } else { - window.attachEvent('onpopstate', this.handlePopState); - } + addEventListener(window, 'popstate', this.handlePopState); } teardown() { - if (window.removeEventListener) { - window.removeEventListener('popstate', this.handlePopState, false); - } else { - window.detachEvent('onpopstate', this.handlePopState); - } - + removeEventListener(window, 'popstate', this.handlePopState); super.teardown(); } diff --git a/modules/DOMHistory.js b/modules/DOMHistory.js index 983a4b2490..17c8851998 100644 --- a/modules/DOMHistory.js +++ b/modules/DOMHistory.js @@ -1,5 +1,5 @@ import History from './History'; -import { getWindowScrollPosition } from './DOMUtils'; +import { addEventListener, removeEventListener, getWindowScrollPosition } from './DOMUtils'; import NavigationTypes from './NavigationTypes'; /** @@ -14,21 +14,11 @@ class DOMHistory extends History { setup(path, entry) { super.setup(path, entry); - - if (window.addEventListener) { - window.addEventListener('beforeunload', this.handleBeforeUnload); - } else { - window.attachEvent('onbeforeunload', this.handleBeforeUnload); - } + addEventListener(window, 'beforeunload', this.handleBeforeUnload); } teardown() { - if (window.removeEventListener) { - window.removeEventListener('beforeunload', this.handleBeforeUnload); - } else { - window.detachEvent('onbeforeunload', this.handleBeforeUnload); - } - + removeEventListener(window, 'beforeunload', this.handleBeforeUnload); super.teardown(); } diff --git a/modules/DOMUtils.js b/modules/DOMUtils.js index 58f4963b5c..3952fa2712 100644 --- a/modules/DOMUtils.js +++ b/modules/DOMUtils.js @@ -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! diff --git a/modules/HashHistory.js b/modules/HashHistory.js index bf15d8023e..70fdfd00ed 100644 --- a/modules/HashHistory.js +++ b/modules/HashHistory.js @@ -1,6 +1,6 @@ import warning from 'warning'; import DOMHistory from './DOMHistory'; -import { getHashPath, replaceHashPath } from './DOMUtils'; +import { addEventListener, removeEventListener, getHashPath, replaceHashPath } from './DOMUtils'; import { isAbsolutePath } from './URLUtils'; var DefaultQueryKey = '_qk'; @@ -58,22 +58,14 @@ class HashHistory extends DOMHistory { var path = getHashPath(); var key = getQueryStringValueFromPath(path, this.queryKey); + super.setup(path, { key }); - if (window.addEventListener) { - window.addEventListener('hashchange', this.handleHashChange, false); - } else { - window.attachEvent('onhashchange', this.handleHashChange); - } + addEventListener(window, 'hashchange', this.handleHashChange); } teardown() { - if (window.removeEventListener) { - window.removeEventListener('hashchange', this.handleHashChange, false); - } else { - window.detachEvent('onhashchange', this.handleHashChange); - } - + removeEventListener(window, 'hashchange', this.handleHashChange); super.teardown(); }