Merge "Save selection state across navigations."

This commit is contained in:
Aaron Boodman 2014-12-24 18:39:14 +00:00 committed by Gerrit Code Review
commit 773b4465b9
4 changed files with 40 additions and 27 deletions

View File

@ -379,7 +379,7 @@ cam.BlobItemContainerReact = React.createClass({
// NOTE: This method causes the URL bar to throb for a split second (at least on Chrome), so it should not be called constantly.
updateHistory_: function() {
this.props.history.replaceState({scroll:this.state.scroll});
this.props.history.replaceState(cam.object.extend(this.props.history.state, {scroll:this.state.scroll}));
},
fillVisibleAreaWithResults_: function() {

View File

@ -101,9 +101,11 @@ cam.IndexPage = React.createClass({
this.baseURL_ = newURL.resolve(new goog.Uri(this.props.config.uiRoot));
this.navigator_ = new cam.Navigator(this.props.eventTarget, this.props.location, this.props.history);
this.navigator_.onNavigate = this.handleNavigate_;
this.navigator_.onWillNavigate = this.handleWillNavigate_;
this.navigator_.onDidNavigate = this.handleDidNavigate_;
this.handleNavigate_(newURL);
this.handleWillNavigate_(newURL);
this.handleDidNavigate_();
},
componentDidMount: function() {
@ -165,6 +167,10 @@ cam.IndexPage = React.createClass({
},
setSelection_: function(selection) {
this.props.history.replaceState(cam.object.extend(this.props.history.state, {
selection: selection,
}), '', this.props.location.href);
this.setState({selection: selection});
this.setState({sidebarVisible: !goog.object.isEmpty(selection)});
},
@ -339,7 +345,7 @@ cam.IndexPage = React.createClass({
}
},
handleNavigate_: function(newURL) {
handleWillNavigate_: function(newURL) {
if (!goog.string.startsWith(newURL.toString(), this.baseURL_.toString())) {
return false;
}
@ -349,10 +355,14 @@ cam.IndexPage = React.createClass({
this.updateChildSearchSession_(targetBlobref, newURL);
this.pruneSearchSessionCache_();
this.setState({currentURL: newURL});
this.setSelection_({});
return true;
},
handleDidNavigate_: function() {
var s = this.props.history.state && this.props.history.state.selection;
this.setSelection_(s || {});
},
updateTargetSearchSession_: function(targetBlobref) {
if (targetBlobref) {
this.targetSearchSession_ = this.getSearchSession_(targetBlobref, {blobRefPrefix: targetBlobref});
@ -630,7 +640,8 @@ cam.IndexPage = React.createClass({
}
var blobref = goog.object.getAnyKey(this.state.selection);
if (this.childSearchSession_.getMeta(blobref).camliType != 'permanode') {
var m = this.childSearchSession_.getMeta(blobref);
if (!m || m.camliType != 'permanode') {
return null;
}

View File

@ -29,7 +29,11 @@ cam.Navigator = function(win, location, history) {
this.location_ = location;
this.history_ = history;
this.handlers_ = [];
this.updateState_('replace', location.href);
// This is needed so that in handlePopState_, we can differentiate navigating back to this frame from the initial load.
// We can't just initialize to {} because there can already be interesting state (e.g., in the case of the user pressing the refresh button).
history.replaceState(cam.object.extend(history.state), '', location.href);
this.win_.addEventListener('click', this.handleClick_.bind(this));
this.win_.addEventListener('popstate', this.handlePopState_.bind(this));
};
@ -55,10 +59,16 @@ cam.Navigator.shouldHandleClick = function(e) {
};
// Client should set this to handle navigation.
// If this method returns true, then Navigator considers the navigation handled locally, and will add an entry to history using pushState(). If this method returns false, Navigator lets the navigation fall through to the browser.
// @param goog.Uri newURL The URL to navigate to. At this point location.href has already been updated - this is just the parsed representation.
//
// This is called before the navigation has actually taken place: location.href will refer to the old URL, not the new one. Also, history.state will refer to previous state.
//
// If client returns true, then Navigator considers the navigation handled locally, and will add an entry to history using pushState(). If this method returns false, Navigator lets the navigation fall through to the browser.
// @param goog.Uri newURL The URL to navigate to.
// @return boolean Whether the navigation was handled locally.
cam.Navigator.prototype.onNavigate = function(newURL) {};
cam.Navigator.prototype.onWillNavigate = function(newURL) {};
// Called after a local (pushState) navigation has been performed. At this point, location.href and history.state have been updated.
cam.Navigator.prototype.onDidNavigate = function() {};
// Programmatically initiate a navigation to a URL. Useful for triggering navigations from things other than hyperlinks.
// @param goog.Uri url The URL to navigate to.
@ -92,6 +102,9 @@ cam.Navigator.prototype.handleClick_ = function(e) {
// Handles navigation via popstate.
cam.Navigator.prototype.handlePopState_ = function(e) {
// WebKit and older Chrome versions will fire a spurious initial popstate event after load.
// We can differentiate this event from ones corresponding to frames we generated ourselves with pushState() or replaceState() because our own frames always have a non-empty state.
// See: http://stackoverflow.com/questions/6421769/popstate-on-pages-load-in-chrome
if (!e.state) {
return;
}
@ -101,24 +114,13 @@ cam.Navigator.prototype.handlePopState_ = function(e) {
};
cam.Navigator.prototype.dispatchImpl_ = function(url, addState) {
if (this.onNavigate(url)) {
if (this.onWillNavigate(url)) {
if (addState) {
this.updateState_('push', url.toString());
// Pass an empty object rather than null or undefined so that we can filter out spurious initial popstate events in handlePopState_.
this.history_.pushState({}, '', url.toString());
}
this.onDidNavigate();
return true;
}
return false;
};
// @param {string} type 'push' or 'replace', the type of state modification to do.
// @param {string} url The URL to update the history state to.
cam.Navigator.prototype.updateState_ = function(type, url) {
var f = (type == 'push' && this.history_.pushState) || (type == 'replace' && this.history_.replaceState) || null;
if (!f) {
throw new Error('Unexpected type: ' + type);
}
// The empty object is needed to differentiate between the initial load and subsequent navigations because browsers.
// It is passed back to us in e.state in handlePopState_. See: http://stackoverflow.com/questions/6421769/popstate-on-pages-load-in-chrome
f.call(this.history_, {}, '', url);
};

View File

@ -60,7 +60,7 @@ describe('cam.Navigator', function() {
mockHistory = new MockHistory();
handler = new Handler();
navigator = new cam.Navigator(mockWindow, mockLocation, mockHistory);
navigator.onNavigate = handler.handle;
navigator.onWillNavigate = handler.handle;
});
it ('#constructor - seed initial state', function() {
@ -69,7 +69,7 @@ describe('cam.Navigator', function() {
it('#navigate - no handler', function() {
// We should do network navigation.
navigator.onNavigate = function(){};
navigator.onWillNavigate = function(){};
navigator.navigate(url);
assert.equal(mockLocation.href, url.toString());
assert.equal(mockHistory.states.length, 1);