Merge "Virtual scroll!"

This commit is contained in:
Aaron Boodman 2014-01-20 19:12:13 +00:00 committed by Gerrit Code Review
commit fc62c5c2b4
1 changed files with 69 additions and 22 deletions

View File

@ -63,29 +63,32 @@ cam.BlobItemContainerReact = React.createClass({
this.lastCheckedIndex_ = -1; this.lastCheckedIndex_ = -1;
this.scrollbarWidth_ = goog.style.getScrollbarWidth(); this.scrollbarWidth_ = goog.style.getScrollbarWidth();
this.layoutHeight_ = 0; this.layoutHeight_ = 0;
this.childProps_ = null;
this.lastSize_ = new goog.math.Size(this.props.style.width, this.props.style.height);
this.updateChildProps_();
}, },
componentDidMount: function() { componentDidMount: function() {
this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); this.eh_.listen(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_);
this.eh_.listen(this.getDOMNode(), 'scroll', this.handleScroll_); this.eh_.listen(this.getDOMNode(), 'scroll', this.handleScroll_);
if (this.props.history.state && this.props.history.state.scroll) { if (this.props.history.state && this.props.history.state.scroll) {
var el = this.getDOMNode(); this.getDOMNode().scrollTop = this.props.history.state.scroll;
var oldScroll = el.scrollTop;
el.scrollTop = this.props.history.state.scroll;
this.props.history.replaceState({scroll:0});
if (oldScroll != el.scrollTop) {
return;
}
} }
this.fillVisibleAreaWithResults_();
// If we weren't able to scroll to a remembered position, call handleScroll once anyway to scroll to zero. This will cause us to load our initial data.
this.handleScroll_();
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
if (nextProps.searchSession != this.props.searchSession) { if (nextProps.searchSession != this.props.searchSession) {
this.eh_.unlisten(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); this.eh_.unlisten(this.props.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_);
this.eh_.listen(nextProps.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_); this.eh_.listen(nextProps.searchSession, cam.SearchSession.SEARCH_SESSION_CHANGED, this.handleSearchSessionChanged_);
nextProps.searchSession.loadMoreResults();
}
var nextSize = new goog.math.Size(nextProps.style.width, nextProps.style.height);
if (nextProps.searchSession != this.props.searchSession || !goog.math.Size.equals(this.lastSize_, nextSize)) {
this.lastSize_ = nextSize;
this.updateChildProps_();
} }
}, },
@ -93,9 +96,41 @@ cam.BlobItemContainerReact = React.createClass({
this.eh_.dispose(); this.eh_.dispose();
}, },
getInitialState: function() {
return {
scroll:0,
};
},
render: function() { render: function() {
var results = this.props.searchSession.getCurrentResults();
var children = []; var children = [];
this.childProps_.forEach(function(props) {
if (this.isVisible_(props.position.y) || this.isVisible_(props.position.y + props.size.height)) {
children.push(cam.BlobItemReact(props));
}
}.bind(this));
children.push(React.DOM.div({
key: 'marker',
style: {
position: 'absolute',
top: this.layoutHeight_ - 1,
left: 0,
height: 1,
width: 1,
},
}));
// If we haven't filled the window with results, add some more.
this.fillVisibleAreaWithResults_();
return React.DOM.div({className:'cam-blobitemcontainer', style:this.props.style, onMouseDown:this.handleMouseDown_}, children);
},
updateChildProps_: function() {
this.childProps_ = [];
var results = this.props.searchSession.getCurrentResults();
var data = goog.array.map(results.blobs, function(blob) { var data = goog.array.map(results.blobs, function(blob) {
return new cam.BlobItemReactData(blob.blob, results.description.meta); return new cam.BlobItemReactData(blob.blob, results.description.meta);
}); });
@ -130,27 +165,23 @@ cam.BlobItemContainerReact = React.createClass({
rowWidth = nextWidth; rowWidth = nextWidth;
} }
currentTop += this.renderChildren_(data, children, rowStart, rowEnd, availWidth, rowWidth, currentTop) + this.BLOB_ITEM_MARGIN_; currentTop += this.updateChildPropsRow_(data, rowStart, rowEnd, availWidth, rowWidth, currentTop) + this.BLOB_ITEM_MARGIN_;
currentWidth = this.BLOB_ITEM_MARGIN_; currentWidth = this.BLOB_ITEM_MARGIN_;
rowStart = rowEnd + 1; rowStart = rowEnd + 1;
i = rowEnd; i = rowEnd;
} }
// If we haven't filled the window with results, add some more.
this.layoutHeight_ = currentTop; this.layoutHeight_ = currentTop;
return React.DOM.div({className:'cam-blobitemcontainer', style:this.props.style, onMouseDown:this.handleMouseDown_}, children);
}, },
renderChildren_: function(data, children, startIndex, endIndex, availWidth, usedWidth, top) { updateChildPropsRow_: function(data, startIndex, endIndex, availWidth, usedWidth, top) {
var currentLeft = 0; var currentLeft = 0;
var rowHeight = Number.POSITIVE_INFINITY; var rowHeight = Number.POSITIVE_INFINITY;
var numItems = endIndex - startIndex + 1; var numItems = endIndex - startIndex + 1;
var availThumbWidth = availWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1)); var availThumbWidth = availWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1));
var usedThumbWidth = usedWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1)); var usedThumbWidth = usedWidth - (this.BLOB_ITEM_MARGIN_ * (numItems + 1));
var rowProps = [];
for (var i = startIndex; i <= endIndex; i++) { for (var i = startIndex; i <= endIndex; i++) {
// We figure out the amount to adjust each item in this slightly non-intuitive way so that the adjustment is split up as fairly as possible. Figuring out a ratio up front and applying it to all items uniformly can end up with a large amount left over because of rounding. // We figure out the amount to adjust each item in this slightly non-intuitive way so that the adjustment is split up as fairly as possible. Figuring out a ratio up front and applying it to all items uniformly can end up with a large amount left over because of rounding.
@ -162,7 +193,7 @@ cam.BlobItemContainerReact = React.createClass({
var ratio = width / originalWidth; var ratio = width / originalWidth;
var height = Math.round(this.props.thumbnailSize * ratio); var height = Math.round(this.props.thumbnailSize * ratio);
rowProps.push({ this.childProps_.push({
key: item.blobref, key: item.blobref,
blobref: item.blobref, blobref: item.blobref,
checked: Boolean(this.props.selection[item.blobref]), checked: Boolean(this.props.selection[item.blobref]),
@ -179,15 +210,19 @@ cam.BlobItemContainerReact = React.createClass({
rowHeight = Math.min(rowHeight, height); rowHeight = Math.min(rowHeight, height);
} }
for (var i = 0; i < rowProps.length; i++) { for (var i = startIndex; i <= endIndex; i++) {
rowProps[i].size.height = rowHeight; this.childProps_[i].size.height = rowHeight;
children.push(cam.BlobItemReact(rowProps[i]));
} }
return rowHeight; return rowHeight;
}, },
isVisible_: function(y) {
return y >= this.state.scroll && y < (this.state.scroll + this.props.style.height);
},
handleSearchSessionChanged_: function() { handleSearchSessionChanged_: function() {
this.updateChildProps_();
this.forceUpdate(); this.forceUpdate();
}, },
@ -226,10 +261,22 @@ cam.BlobItemContainerReact = React.createClass({
}, },
handleScroll_: function() { handleScroll_: function() {
if (!this.isMounted()) {
return;
}
var scroll = this.getDOMNode().scrollTop; var scroll = this.getDOMNode().scrollTop;
this.props.history.replaceState({scroll:scroll}); this.props.history.replaceState({scroll:scroll});
this.setState({scroll:scroll});
this.fillVisibleAreaWithResults_();
},
if ((this.layoutHeight_ - scroll - this.props.style.height) > this.INFINITE_SCROLL_THRESHOLD_PX_) { fillVisibleAreaWithResults_: function() {
if (!this.isMounted()) {
return;
}
if ((this.layoutHeight_ - this.getDOMNode().scrollTop - this.props.style.height) > this.INFINITE_SCROLL_THRESHOLD_PX_) {
return; return;
} }