diff --git a/features.json b/features.json index 62b0837ae71..5b7630e279a 100644 --- a/features.json +++ b/features.json @@ -8,6 +8,7 @@ "ember-testing-resume-test": null, "ember-factory-for": true, "ember-no-double-extend": null, - "ember-routing-router-service": null + "ember-routing-router-service": null, + "ember-unique-location-history-state": null } } diff --git a/packages/ember-routing/lib/location/history_location.js b/packages/ember-routing/lib/location/history_location.js index 2d477606ea3..9e632fc6134 100644 --- a/packages/ember-routing/lib/location/history_location.js +++ b/packages/ember-routing/lib/location/history_location.js @@ -1,6 +1,7 @@ import { get, - set + set, + isFeatureEnabled } from 'ember-metal'; import { Object as EmberObject } from 'ember-runtime'; @@ -13,6 +14,19 @@ import EmberLocation from './api'; let popstateFired = false; +let _uuid; + +if (isFeatureEnabled('ember-unique-location-history-state')) { + _uuid = function _uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r, v; + r = Math.random() * 16 | 0; + v = c === 'x' ? r : r & 3 | 8; + return v.toString(16); + }); + } +} + /** Ember.HistoryLocation implements the location API using the browser's history.pushState API. @@ -134,6 +148,10 @@ export default EmberObject.extend({ from getState may be null if an iframe has changed a window's history. + The object returned will contain a `path` for the given state as well + as a unique state `id`. The state index will allow the app to distinguish + between two states with similar paths but should be unique from one another. + @private @method getState @return state {Object} @@ -155,6 +173,9 @@ export default EmberObject.extend({ */ pushState(path) { let state = { path }; + if (isFeatureEnabled('ember-unique-location-history-state')) { + state.uuid = _uuid(); + } get(this, 'history').pushState(state, null, path); @@ -173,6 +194,10 @@ export default EmberObject.extend({ */ replaceState(path) { let state = { path }; + if (isFeatureEnabled('ember-unique-location-history-state')) { + state.uuid = _uuid(); + } + get(this, 'history').replaceState(state, null, path); this._historyState = state; diff --git a/packages/ember-routing/tests/location/history_location_test.js b/packages/ember-routing/tests/location/history_location_test.js index 09c356ceff5..034ba8efd23 100644 --- a/packages/ember-routing/tests/location/history_location_test.js +++ b/packages/ember-routing/tests/location/history_location_test.js @@ -1,4 +1,8 @@ -import { set, run } from 'ember-metal'; +import { + isFeatureEnabled, + set, + run +} from 'ember-metal'; import HistoryLocation from '../../location/history_location'; let FakeHistory, HistoryTestLocation, location; @@ -110,48 +114,96 @@ QUnit.test('base URL is removed when retrieving the current pathname', function( location.initState(); }); -QUnit.test('base URL is preserved when moving around', function() { - expect(1); +if (isFeatureEnabled('ember-unique-location-history-state')) { + QUnit.test('base URL is preserved when moving around', function() { + expect(2); - HistoryTestLocation.reopen({ - init() { - this._super(...arguments); + HistoryTestLocation.reopen({ + init() { + this._super(...arguments); - set(this, 'location', mockBrowserLocation('/base/foo/bar')); - set(this, 'baseURL', '/base/'); - } + set(this, 'location', mockBrowserLocation('/base/foo/bar')); + set(this, 'baseURL', '/base/'); + } + }); + + createLocation(); + location.initState(); + location.setURL('/one/two'); + + equal(location._historyState.path, '/base/one/two'); + ok(location._historyState.uuid); }); - createLocation(); - location.initState(); - location.setURL('/one/two'); + QUnit.test('setURL continues to set even with a null state (iframes may set this)', function() { + expect(2); - equal(location._historyState.path, '/base/one/two'); -}); + createLocation(); + location.initState(); -QUnit.test('setURL continues to set even with a null state (iframes may set this)', function() { - expect(1); + FakeHistory.pushState(null); + location.setURL('/three/four'); - createLocation(); - location.initState(); + equal(location._historyState.path, '/three/four'); + ok(location._historyState.uuid); + }); - FakeHistory.pushState(null); - location.setURL('/three/four'); + QUnit.test('replaceURL continues to set even with a null state (iframes may set this)', function() { + expect(2); - equal(location._historyState.path, '/three/four'); -}); + createLocation(); + location.initState(); -QUnit.test('replaceURL continues to set even with a null state (iframes may set this)', function() { - expect(1); + FakeHistory.pushState(null); + location.replaceURL('/three/four'); - createLocation(); - location.initState(); + equal(location._historyState.path, '/three/four'); + ok(location._historyState.uuid); + }); +} else { + QUnit.test('base URL is preserved when moving around', function() { + expect(1); - FakeHistory.pushState(null); - location.replaceURL('/three/four'); + HistoryTestLocation.reopen({ + init() { + this._super(...arguments); - equal(location._historyState.path, '/three/four'); -}); + set(this, 'location', mockBrowserLocation('/base/foo/bar')); + set(this, 'baseURL', '/base/'); + } + }); + + createLocation(); + location.initState(); + location.setURL('/one/two'); + + equal(location._historyState.path, '/base/one/two'); + }); + + QUnit.test('setURL continues to set even with a null state (iframes may set this)', function() { + expect(1); + + createLocation(); + location.initState(); + + FakeHistory.pushState(null); + location.setURL('/three/four'); + + equal(location._historyState.path, '/three/four'); + }); + + QUnit.test('replaceURL continues to set even with a null state (iframes may set this)', function() { + expect(1); + + createLocation(); + location.initState(); + + FakeHistory.pushState(null); + location.replaceURL('/three/four'); + + equal(location._historyState.path, '/three/four'); + }); +} QUnit.test('HistoryLocation.getURL() returns the current url, excluding both rootURL and baseURL', function() { expect(1);