From 6ab3170c64a1e5fc97166150b300832e29333deb Mon Sep 17 00:00:00 2001 From: Brian Cardarella Date: Wed, 3 Aug 2016 08:06:23 -0400 Subject: [PATCH] Track unique history location states This PR will allow the HistoryLocation API to keep track of unique state IDs for each state that is pushed/replaced. This change should not break backwards compatibility and can allow for other libraries to use this information to set proper scroll position. The path alone does not provide enough information. For example, if you visit page A, scroll down, then click on a link to page B, then click on a link back to page A. Your actual browser history stack is [A, B, A]. Each of those nodes in the history should have their own unique scroll position. In order to record this position we need an ID that is unique for each node in the history. Moved from stateCounter to uuid Set under Feature Flag --- features.json | 3 +- .../lib/location/history_location.js | 27 ++++- .../tests/location/history_location_test.js | 112 +++++++++++++----- 3 files changed, 110 insertions(+), 32 deletions(-) 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);