| Index: chrome/browser/resources/shared/js/cr/ui/list.js
|
| diff --git a/chrome/browser/resources/shared/js/cr/ui/list.js b/chrome/browser/resources/shared/js/cr/ui/list.js
|
| index af323dcd9a0ad9d7020d54e9b6804458d1ecc09e..491b75168ddc8f2233113041fdc6003872876def 100644
|
| --- a/chrome/browser/resources/shared/js/cr/ui/list.js
|
| +++ b/chrome/browser/resources/shared/js/cr/ui/list.js
|
| @@ -116,23 +116,19 @@ cr.define('cr.ui', function() {
|
| measured_: undefined,
|
|
|
| /**
|
| - * The height of the lead item, which is allowed to have a different height
|
| - * than other list items to accommodate lists where a single item at a time
|
| - * can be expanded to show more detail. It is explicitly set by client code
|
| - * when the height of the lead item is changed with {@code set
|
| - * leadItemHeight}, and presumed equal to {@code itemHeight_} otherwise.
|
| - * @type {number}
|
| + * Whether or not the list is autoexpanding. If true, the list resizes
|
| + * its height to accomadate all children.
|
| + * @type {boolean}
|
| * @private
|
| */
|
| - leadItemHeight_: 0,
|
| + autoExpands_: false,
|
|
|
| /**
|
| - * Whether or not the list is autoexpanding. If true, the list resizes
|
| - * its height to accomadate all children.
|
| + * Whether or not the list view has a blank space below the last row.
|
| * @type {boolean}
|
| * @private
|
| */
|
| - autoExpands_: false,
|
| + remainingSpace_: true,
|
|
|
| /**
|
| * Function used to create grid items.
|
| @@ -177,11 +173,14 @@ cr.define('cr.ui', function() {
|
| this.boundHandleDataModelPermuted_);
|
| this.dataModel_.removeEventListener('change',
|
| this.boundHandleDataModelChange_);
|
| + this.dataModel_.removeEventListener('splice',
|
| + this.boundHandleDataModelChange_);
|
| }
|
|
|
| this.dataModel_ = dataModel;
|
|
|
| this.cachedItems_ = {};
|
| + this.cachedItemSizes_ = {};
|
| this.selectionModel.clear();
|
| if (dataModel)
|
| this.selectionModel.adjustLength(dataModel.length);
|
| @@ -192,6 +191,8 @@ cr.define('cr.ui', function() {
|
| this.boundHandleDataModelPermuted_);
|
| this.dataModel_.addEventListener('change',
|
| this.boundHandleDataModelChange_);
|
| + this.dataModel_.addEventListener('splice',
|
| + this.boundHandleDataModelChange_);
|
| }
|
|
|
| this.redraw();
|
| @@ -249,6 +250,20 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| + * Whether or not the rows on list have various heights.
|
| + * @type {boolean}
|
| + */
|
| + get fixedHeight() {
|
| + return this.fixedHeight_;
|
| + },
|
| + set fixedHeight(fixedHeight) {
|
| + if (this.fixedHeight_ == fixedHeight)
|
| + return;
|
| + this.fixedHeight_ = fixedHeight;
|
| + this.redraw();
|
| + },
|
| +
|
| + /**
|
| * Convenience alias for selectionModel.selectedItem
|
| * @type {cr.ui.ListItem}
|
| */
|
| @@ -270,23 +285,6 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| - * The height of the lead item.
|
| - * If set to 0, resets to the same height as other items.
|
| - * @type {number}
|
| - */
|
| - get leadItemHeight() {
|
| - return this.leadItemHeight_ || this.getItemHeight_();
|
| - },
|
| - set leadItemHeight(height) {
|
| - if (height) {
|
| - var size = this.getItemSize_();
|
| - this.leadItemHeight_ = Math.max(0, height + size.marginVertical);
|
| - } else {
|
| - this.leadItemHeight_ = 0;
|
| - }
|
| - },
|
| -
|
| - /**
|
| * Convenience alias for selectionModel.selectedItems
|
| * @type {!Array<cr.ui.ListItem>}
|
| */
|
| @@ -363,27 +361,42 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| - * @return {number} The height of an item, measuring it if necessary.
|
| + * @return {number} The height of default item, measuring it if necessary.
|
| * @private
|
| */
|
| - getItemHeight_: function() {
|
| - return this.getItemSize_().height;
|
| + getDefaultItemHeight_: function() {
|
| + return this.getDefaultItemSize_().height;
|
| + },
|
| +
|
| + /**
|
| + * @param {number} index The index of the item.
|
| + * @return {number} The height of the item.
|
| + */
|
| + getItemHeightByIndex_: function(index) {
|
| + if (this.cachedItemSizes_[index])
|
| + return this.cachedItemSizes_[index].height;
|
| +
|
| + var item = this.getListItemByIndex(index);
|
| + if (item)
|
| + return this.getItemSize_(item).height;
|
| +
|
| + return this.getDefaultItemHeight_();
|
| },
|
|
|
| /**
|
| - * @return {number} The width of an item, measuring it if necessary.
|
| + * @return {number} The width of default item, measuring it if necessary.
|
| * @private
|
| */
|
| - getItemWidth_: function() {
|
| - return this.getItemSize_().width;
|
| + getDefaultItemWidth_: function() {
|
| + return this.getDefaultItemSize_().width;
|
| },
|
|
|
| /**
|
| * @return {{height: number, width: number}} The height and width
|
| - * of an item, measuring it if necessary.
|
| + * of default item, measuring it if necessary.
|
| * @private
|
| */
|
| - getItemSize_: function() {
|
| + getDefaultItemSize_: function() {
|
| if (!this.measured_ || !this.measured_.height) {
|
| this.measured_ = measureItem(this);
|
| }
|
| @@ -391,6 +404,22 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| + * @return {{height: number, width: number}} The height and width
|
| + * of an item, measuring it if necessary.
|
| + * @private
|
| + */
|
| + getItemSize_: function(item) {
|
| + if (this.cachedItemSizes_[item.listIndex])
|
| + return this.cachedItemSizes_[item.listIndex];
|
| +
|
| + var size = measureItem(this, item);
|
| + if (!isNaN(size.height) && !isNaN(size.weight))
|
| + this.cachedItemSizes_[item.listIndex] = size;
|
| +
|
| + return size;
|
| + },
|
| +
|
| + /**
|
| * Callback for the double click event.
|
| * @param {Event} e The mouse event object.
|
| * @private
|
| @@ -602,7 +631,8 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| handleDataModelChange_: function(e) {
|
| - if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) {
|
| + if (e.index >= this.firstIndex_ &&
|
| + (e.index < this.lastIndex_ || this.remainingSpace_)) {
|
| if (this.cachedItems_[e.index])
|
| delete this.cachedItems_[e.index];
|
| this.redraw();
|
| @@ -611,11 +641,19 @@ cr.define('cr.ui', function() {
|
|
|
| /**
|
| * @param {number} index The index of the item.
|
| - * @return {number} The top position of the item inside the list, not taking
|
| - * into account lead item. May vary in the case of multiple columns.
|
| + * @return {number} The top position of the item inside the list.
|
| */
|
| getItemTop: function(index) {
|
| - return index * this.getItemHeight_();
|
| + if (this.fixedHeight_) {
|
| + var itemHeight = this.getDefaultItemHeight_();
|
| + return index * itemHeight;
|
| + } else {
|
| + var top = 0;
|
| + for (var i = 0; i < index; i++) {
|
| + top += this.getItemHeightByIndex_(i);
|
| + }
|
| + return top;
|
| + }
|
| },
|
|
|
| /**
|
| @@ -645,32 +683,42 @@ cr.define('cr.ui', function() {
|
| if (!dataModel || index < 0 || index >= dataModel.length)
|
| return false;
|
|
|
| - var itemHeight = this.getItemHeight_();
|
| + var itemHeight = this.getItemHeightByIndex_(index);
|
| var scrollTop = this.scrollTop;
|
| var top = this.getItemTop(index);
|
| - var leadIndex = this.selectionModel.leadIndex;
|
| -
|
| - // Adjust for the lead item if it is above the given index.
|
| - if (leadIndex > -1 && leadIndex < index)
|
| - top += this.leadItemHeight - itemHeight;
|
| - else if (leadIndex == index)
|
| - itemHeight = this.leadItemHeight;
|
| -
|
| - if (top < scrollTop) {
|
| - this.scrollTop = top;
|
| - return true;
|
| - } else {
|
| - var clientHeight = this.clientHeight;
|
| - var cs = getComputedStyle(this);
|
| - var paddingY = parseInt(cs.paddingTop, 10) +
|
| - parseInt(cs.paddingBottom, 10);
|
| + var clientHeight = this.clientHeight;
|
|
|
| - if (top + itemHeight > scrollTop + clientHeight - paddingY) {
|
| - this.scrollTop = top + itemHeight - clientHeight + paddingY;
|
| + var self = this;
|
| + // Function to adjust the tops of viewport and row.
|
| + function scrollToAdjustTop() {
|
| + self.scrollTop = top;
|
| return true;
|
| - }
|
| + };
|
| + // Function to adjust the bottoms of viewport and row.
|
| + function scrollToAdjustBottom() {
|
| + var cs = getComputedStyle(self);
|
| + var paddingY = parseInt(cs.paddingTop, 10) +
|
| + parseInt(cs.paddingBottom, 10);
|
| +
|
| + if (top + itemHeight > scrollTop + clientHeight - paddingY) {
|
| + self.scrollTop = top + itemHeight - clientHeight + paddingY;
|
| + return true;
|
| + }
|
| + return false;
|
| + };
|
| +
|
| + // Check if the entire of given indexed row can be shown in the viewport.
|
| + if (itemHeight <= clientHeight) {
|
| + if (top < scrollTop)
|
| + return scrollToAdjustTop();
|
| + if (scrollTop + clientHeight < top + itemHeight)
|
| + return scrollToAdjustBottom();
|
| + } else {
|
| + if (scrollTop < top)
|
| + return scrollToAdjustTop();
|
| + if (top + itemHeight < scrollTop + clientHeight)
|
| + return scrollToAdjustBottom();
|
| }
|
| -
|
| return false;
|
| },
|
|
|
| @@ -756,14 +804,8 @@ cr.define('cr.ui', function() {
|
| * @private
|
| */
|
| getHeightsForIndex_: function(index) {
|
| - var itemHeight = this.getItemHeight_();
|
| + var itemHeight = this.getItemHeightByIndex_(index);
|
| var top = this.getItemTop(index);
|
| - if (this.selectionModel.leadIndex > -1 &&
|
| - this.selectionModel.leadIndex < index) {
|
| - top += this.leadItemHeight - itemHeight;
|
| - } else if (this.selectionModel.leadIndex == index) {
|
| - itemHeight = this.leadItemHeight;
|
| - }
|
| return {top: top, height: itemHeight};
|
| },
|
|
|
| @@ -772,27 +814,45 @@ cr.define('cr.ui', function() {
|
| * in pixels from the top) within the list. In the case of multiple columns,
|
| * returns the first index in the row.
|
| * @param {number} offset The y offset in pixels to get the index of.
|
| - * @return {number} The index of the list item.
|
| + * @return {number} The index of the list item. Returns the list size if
|
| + * given offset exceeds the height of list.
|
| * @private
|
| */
|
| getIndexForListOffset_: function(offset) {
|
| - var itemHeight = this.getItemHeight_();
|
| - var leadIndex = this.selectionModel.leadIndex;
|
| - var leadItemHeight = this.leadItemHeight;
|
| - if (leadIndex < 0 || leadItemHeight == itemHeight) {
|
| - // Simple case: no lead item or lead item height is not different.
|
| + var itemHeight = this.getDefaultItemHeight_();
|
| + if (!itemHeight)
|
| + return this.dataModel.length;
|
| +
|
| + if (this.fixedHeight_)
|
| return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| +
|
| + // If offset exceeds the height of list.
|
| + var lastHeight = 0;
|
| + if (this.dataModel.length) {
|
| + var h = this.getHeightsForIndex_(this.dataModel.length - 1);
|
| + lastHeight = h.top + h.height;
|
| }
|
| - var leadTop = this.getItemTop(leadIndex);
|
| - // If the given offset is above the lead item, it's also simple.
|
| - if (offset < leadTop)
|
| - return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| - // If the lead item contains the given offset, we just return its index.
|
| - if (offset < leadTop + leadItemHeight)
|
| - return this.getFirstItemInRow(this.getItemRow(leadIndex));
|
| - // The given offset must be below the lead item. Adjust and recalculate.
|
| - offset -= leadItemHeight - itemHeight;
|
| - return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| + if (lastHeight < offset)
|
| + return this.dataModel.length;
|
| +
|
| + // Estimates index.
|
| + var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
|
| + this.dataModel.length - 1);
|
| + var isIncrementing = this.getItemTop(estimatedIndex) < offset;
|
| +
|
| + // Searchs the correct index.
|
| + do {
|
| + var heights = this.getHeightsForIndex_(estimatedIndex);
|
| + var top = heights.top;
|
| + var height = heights.height;
|
| +
|
| + if (top <= offset && offset <= (top + height))
|
| + break;
|
| +
|
| + isIncrementing ? ++estimatedIndex: --estimatedIndex;
|
| + } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length)
|
| +
|
| + return estimatedIndex;
|
| },
|
|
|
| /**
|
| @@ -809,26 +869,27 @@ cr.define('cr.ui', function() {
|
| },
|
|
|
| /**
|
| - * Calculates the number of items fitting in viewport given the index of
|
| - * first item and heights.
|
| - * @param {number} itemHeight The height of the item.
|
| - * @param {number} firstIndex Index of the first item in viewport.
|
| + * Calculates the number of items fitting in the given viewport.
|
| * @param {number} scrollTop The scroll top position.
|
| - * @return {number} The number of items in view port.
|
| - */
|
| - getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) {
|
| - // This is a bit tricky. We take the minimum of the available items to
|
| - // show and the number we want to show, so as not to go off the end of the
|
| - // list. For the number we want to show, we take the maximum of the number
|
| - // that would fit without a differently-sized lead item, and with one. We
|
| - // do this so that if the size of the lead item changes without a scroll
|
| - // event to trigger redrawing the list, we won't end up with empty space.
|
| - var clientHeight = this.clientHeight;
|
| - return this.autoExpands_ ? this.dataModel.length : Math.min(
|
| - this.dataModel.length - firstIndex,
|
| - Math.max(
|
| - Math.ceil(clientHeight / itemHeight) + 1,
|
| - this.countItemsInRange_(firstIndex, scrollTop + clientHeight)));
|
| + * @param {number} clientHeight The height of viewport.
|
| + * @return {{first: number, length: number, last: number}} The index of
|
| + * first item in view port, The number of items, The item past the last.
|
| + */
|
| + getItemsInViewPort: function(scrollTop, clientHeight) {
|
| + if (this.autoExpands_) {
|
| + return {
|
| + first: 0,
|
| + length: this.dataModel.length,
|
| + last: this.dataModel.length};
|
| + } else {
|
| + var firstIndex = this.getIndexForListOffset_(scrollTop);
|
| + var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
|
| +
|
| + return {
|
| + first: firstIndex,
|
| + length: lastIndex - firstIndex + 1,
|
| + last: lastIndex + 1};
|
| + }
|
| },
|
|
|
| /**
|
| @@ -839,17 +900,57 @@ cr.define('cr.ui', function() {
|
| * @param {Object.<string, ListItem>} newCachedItems New items cache.
|
| */
|
| addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
|
| - var listItem;
|
| var dataModel = this.dataModel;
|
|
|
| window.l = this;
|
| for (var y = firstIndex; y < lastIndex; y++) {
|
| var dataItem = dataModel.item(y);
|
| - listItem = cachedItems[y] || this.createItem(dataItem);
|
| + var listItem = cachedItems[y] || this.createItem(dataItem);
|
| listItem.listIndex = y;
|
| this.appendChild(listItem);
|
| newCachedItems[y] = listItem;
|
| }
|
| +
|
| + // Mesurings must be placed after adding all the elements, to prevent
|
| + // performance reducing.
|
| + for (var y = firstIndex; y < lastIndex; y++) {
|
| + this.cachedItemSizes_[y] = measureItem(this, newCachedItems[y]);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures that all the item sizes in the list have been already cached.
|
| + */
|
| + ensureAllItemSizesInCache: function() {
|
| + var measuringIndexes = [];
|
| + for (var y = 0; y < this.dataModel.length; y++) {
|
| + if (!this.cachedItemSizes_[y])
|
| + measuringIndexes.push(y);
|
| + }
|
| +
|
| + var measuringItems = [];
|
| + // Adds temporary elements.
|
| + for (var y = 0; y < measuringIndexes.length; y++) {
|
| + var index = measuringIndexes[y];
|
| + var dataItem = this.dataModel.item(index);
|
| + var listItem = this.cachedItems_[index] || this.createItem(dataItem);
|
| + listItem.listIndex = index;
|
| + this.appendChild(listItem);
|
| + this.cachedItems_[index] = listItem;
|
| + measuringItems.push(listItem);
|
| + }
|
| +
|
| + // All mesurings must be placed after adding all the elements, to prevent
|
| + // performance reducing.
|
| + for (var y = 0; y < measuringIndexes.length; y++) {
|
| + var index = measuringIndexes[y];
|
| + this.cachedItemSizes_[index] = measureItem(this, measuringItems[y]);
|
| + }
|
| +
|
| + // Removes all the temprary elements.
|
| + for (var y = 0; y < measuringIndexes.length; y++) {
|
| + this.removeChild(measuringItems[y]);
|
| + }
|
| },
|
|
|
| /**
|
| @@ -858,8 +959,16 @@ cr.define('cr.ui', function() {
|
| * @param {number} itemHeight The height of the item.
|
| * @return {number} The height of after filler.
|
| */
|
| - getAfterFillerHeight: function(lastIndex, itemHeight) {
|
| - return (this.dataModel.length - lastIndex) * itemHeight;
|
| + getAfterFillerHeight: function(lastIndex) {
|
| + if (this.fixedHeight_) {
|
| + var itemHeight = this.getDefaultItemHeight_();
|
| + return (this.dataModel.length - lastIndex) * itemHeight;
|
| + }
|
| +
|
| + var height = 0;
|
| + for (var i = lastIndex; i < this.dataModel.length; i++)
|
| + height += this.getItemHeightByIndex_(i);
|
| + return height;
|
| },
|
|
|
| /**
|
| @@ -871,49 +980,59 @@ cr.define('cr.ui', function() {
|
|
|
| var dataModel = this.dataModel;
|
| if (!dataModel) {
|
| + this.cachedItems_ = {};
|
| + this.firstIndex_ = 0;
|
| + this.lastIndex_ = 0;
|
| + this.remainingSpace_ = true;
|
| this.textContent = '';
|
| return;
|
| }
|
|
|
| - var scrollTop = this.scrollTop;
|
| - var clientHeight = this.clientHeight;
|
| -
|
| - var itemHeight = this.getItemHeight_();
|
| + // Store all the item sizes into the cache in advance, to prevent
|
| + // interleave measuring with mutating dom.
|
| + this.ensureAllItemSizesInCache();
|
|
|
| // We cache the list items since creating the DOM nodes is the most
|
| // expensive part of redrawing.
|
| var cachedItems = this.cachedItems_ || {};
|
| var newCachedItems = {};
|
|
|
| - var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top;
|
| -
|
| var autoExpands = this.autoExpands_;
|
| - var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop);
|
| - var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex,
|
| - scrollTop);
|
| - var lastIndex = firstIndex + itemsInViewPort;
|
| + var scrollTop = this.scrollTop;
|
| + var clientHeight = this.clientHeight;
|
| +
|
| + var lastItemHeights = this.getHeightsForIndex_(dataModel.length - 1);
|
| + var desiredScrollHeight = lastItemHeights.top + lastItemHeights.height;
|
| +
|
| + var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
|
| + // Draws the hidden rows just above/below the viewport to prevent
|
| + // flashing in scroll.
|
| + var firstIndex = Math.max(0, itemsInViewPort.first - 1);
|
| + var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
|
| +
|
| + var beforeFillerHeight =
|
| + this.autoExpands ? 0 : this.getItemTop(firstIndex);
|
| + var afterFillerHeight =
|
| + this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
|
|
|
| + // Clear list and Adds elements on list.
|
| this.textContent = '';
|
|
|
| - this.beforeFiller_.style.height =
|
| - this.getHeightsForIndex_(firstIndex).top + 'px';
|
| + this.beforeFiller_.style.height = beforeFillerHeight + 'px';
|
| this.appendChild(this.beforeFiller_);
|
|
|
| - var sm = this.selectionModel;
|
| - var leadIndex = sm.leadIndex;
|
| -
|
| this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
|
|
|
| - var afterFillerHeight = this.getAfterFillerHeight(lastIndex, itemHeight);
|
| - if (leadIndex >= lastIndex)
|
| - afterFillerHeight += this.leadItemHeight - itemHeight;
|
| this.afterFiller_.style.height = afterFillerHeight + 'px';
|
| this.appendChild(this.afterFiller_);
|
|
|
| + var sm = this.selectionModel;
|
| + var leadIndex = sm.leadIndex;
|
| +
|
| // We don't set the lead or selected properties until after adding all
|
| // items, in case they force relayout in response to these events.
|
| var listItem = null;
|
| - if (newCachedItems[leadIndex])
|
| + if (leadIndex != -1 && newCachedItems[leadIndex])
|
| newCachedItems[leadIndex].lead = true;
|
| for (var y = firstIndex; y < lastIndex; y++) {
|
| if (sm.getIndexSelected(y))
|
| @@ -927,6 +1046,7 @@ cr.define('cr.ui', function() {
|
| this.firstIndex_ = firstIndex;
|
| this.lastIndex_ = lastIndex;
|
|
|
| + this.remainingSpace_ = itemsInViewPort.last > dataModel.length;
|
| this.cachedItems_ = newCachedItems;
|
|
|
| // Measure again in case the item height has changed due to a page zoom.
|
| @@ -936,7 +1056,7 @@ cr.define('cr.ui', function() {
|
| // a reflow (which made the redraw speed 3 times slower on my system).
|
| // By using a timeout the measuring will happen later when there is no
|
| // need for a reflow.
|
| - if (listItem) {
|
| + if (listItem && this.fixedHeight_) {
|
| var list = this;
|
| window.setTimeout(function() {
|
| if (listItem.parentNode == list) {
|
| @@ -951,6 +1071,7 @@ cr.define('cr.ui', function() {
|
| */
|
| invalidate: function() {
|
| this.cachedItems_ = {};
|
| + this.cachedItemSized_ = {};
|
| },
|
|
|
| /**
|
| @@ -958,7 +1079,8 @@ cr.define('cr.ui', function() {
|
| * @param {number} index The row index to redraw.
|
| */
|
| redrawItem: function(index) {
|
| - if (index >= this.firstIndex_ && index < this.lastIndex_) {
|
| + if (index >= this.firstIndex_ &&
|
| + (index < this.lastIndex_ || this.remainingSpace_)) {
|
| delete this.cachedItems_[index];
|
| this.redraw();
|
| }
|
|
|