Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(177)

Side by Side Diff: chrome/browser/resources/shared/js/cr/ui/list.js

Issue 8608007: cr/ui/list.js: Support rows with variable heights. (Closed) Base URL: http://git.chromium.org/chromium/src.git@master
Patch Set: Omit the custom lead item height Created 9 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 // require: array_data_model.js 5 // require: array_data_model.js
6 // require: list_selection_model.js 6 // require: list_selection_model.js
7 // require: list_selection_controller.js 7 // require: list_selection_controller.js
8 // require: list_item.js 8 // require: list_item.js
9 9
10 /** 10 /**
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after
109 * is needed. Note that lead item is allowed to have a different height, to 109 * is needed. Note that lead item is allowed to have a different height, to
110 * accommodate lists where a single item at a time can be expanded to show 110 * accommodate lists where a single item at a time can be expanded to show
111 * more detail. 111 * more detail.
112 * @type {{height: number, marginVertical: number, width: number, 112 * @type {{height: number, marginVertical: number, width: number,
113 * marginHorizontal: number}} 113 * marginHorizontal: number}}
114 * @private 114 * @private
115 */ 115 */
116 measured_: undefined, 116 measured_: undefined,
117 117
118 /** 118 /**
119 * The height of the lead item, which is allowed to have a different height
120 * than other list items to accommodate lists where a single item at a time
121 * can be expanded to show more detail. It is explicitly set by client code
122 * when the height of the lead item is changed with {@code set
123 * leadItemHeight}, and presumed equal to {@code itemHeight_} otherwise.
124 * @type {number}
125 * @private
126 */
127 leadItemHeight_: 0,
128
129 /**
130 * Whether or not the list is autoexpanding. If true, the list resizes 119 * Whether or not the list is autoexpanding. If true, the list resizes
131 * its height to accomadate all children. 120 * its height to accomadate all children.
132 * @type {boolean} 121 * @type {boolean}
133 * @private 122 * @private
134 */ 123 */
135 autoExpands_: false, 124 autoExpands_: false,
136 125
137 /** 126 /**
127 * Whether or not the list view has a blank space below the last row.
128 * @type {boolean}
129 * @private
130 */
131 remainingSpace_: true,
132
133 /**
138 * Function used to create grid items. 134 * Function used to create grid items.
139 * @type {function(): !ListItem} 135 * @type {function(): !ListItem}
140 * @private 136 * @private
141 */ 137 */
142 itemConstructor_: cr.ui.ListItem, 138 itemConstructor_: cr.ui.ListItem,
143 139
144 /** 140 /**
145 * Function used to create grid items. 141 * Function used to create grid items.
146 * @type {function(): !ListItem} 142 * @type {function(): !ListItem}
147 */ 143 */
(...skipping 22 matching lines...) Expand all
170 this.boundHandleDataModelChange_ = 166 this.boundHandleDataModelChange_ =
171 this.handleDataModelChange_.bind(this); 167 this.handleDataModelChange_.bind(this);
172 } 168 }
173 169
174 if (this.dataModel_) { 170 if (this.dataModel_) {
175 this.dataModel_.removeEventListener( 171 this.dataModel_.removeEventListener(
176 'permuted', 172 'permuted',
177 this.boundHandleDataModelPermuted_); 173 this.boundHandleDataModelPermuted_);
178 this.dataModel_.removeEventListener('change', 174 this.dataModel_.removeEventListener('change',
179 this.boundHandleDataModelChange_); 175 this.boundHandleDataModelChange_);
176 this.dataModel_.removeEventListener('splice',
177 this.boundHandleDataModelChange_);
180 } 178 }
181 179
182 this.dataModel_ = dataModel; 180 this.dataModel_ = dataModel;
183 181
184 this.cachedItems_ = {}; 182 this.cachedItems_ = {};
183 this.cachedItemSizes_ = {};
185 this.selectionModel.clear(); 184 this.selectionModel.clear();
186 if (dataModel) 185 if (dataModel)
187 this.selectionModel.adjustLength(dataModel.length); 186 this.selectionModel.adjustLength(dataModel.length);
188 187
189 if (this.dataModel_) { 188 if (this.dataModel_) {
190 this.dataModel_.addEventListener( 189 this.dataModel_.addEventListener(
191 'permuted', 190 'permuted',
192 this.boundHandleDataModelPermuted_); 191 this.boundHandleDataModelPermuted_);
193 this.dataModel_.addEventListener('change', 192 this.dataModel_.addEventListener('change',
194 this.boundHandleDataModelChange_); 193 this.boundHandleDataModelChange_);
194 this.dataModel_.addEventListener('splice',
195 this.boundHandleDataModelChange_);
195 } 196 }
196 197
197 this.redraw(); 198 this.redraw();
198 } 199 }
199 }, 200 },
200 201
201 get dataModel() { 202 get dataModel() {
202 return this.dataModel_; 203 return this.dataModel_;
203 }, 204 },
204 205
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
242 return this.autoExpands_; 243 return this.autoExpands_;
243 }, 244 },
244 set autoExpands(autoExpands) { 245 set autoExpands(autoExpands) {
245 if (this.autoExpands_ == autoExpands) 246 if (this.autoExpands_ == autoExpands)
246 return; 247 return;
247 this.autoExpands_ = autoExpands; 248 this.autoExpands_ = autoExpands;
248 this.redraw(); 249 this.redraw();
249 }, 250 },
250 251
251 /** 252 /**
253 * Whether or not the rows on list have various heights.
254 * @type {boolean}
255 */
256 get fixedHeight() {
257 return this.fixedHeight_;
258 },
259 set fixedHeight(fixedHeight) {
260 if (this.fixedHeight_ == fixedHeight)
261 return;
262 this.fixedHeight_ = fixedHeight;
263 this.redraw();
264 },
265
266 /**
252 * Convenience alias for selectionModel.selectedItem 267 * Convenience alias for selectionModel.selectedItem
253 * @type {cr.ui.ListItem} 268 * @type {cr.ui.ListItem}
254 */ 269 */
255 get selectedItem() { 270 get selectedItem() {
256 var dataModel = this.dataModel; 271 var dataModel = this.dataModel;
257 if (dataModel) { 272 if (dataModel) {
258 var index = this.selectionModel.selectedIndex; 273 var index = this.selectionModel.selectedIndex;
259 if (index != -1) 274 if (index != -1)
260 return dataModel.item(index); 275 return dataModel.item(index);
261 } 276 }
262 return null; 277 return null;
263 }, 278 },
264 set selectedItem(selectedItem) { 279 set selectedItem(selectedItem) {
265 var dataModel = this.dataModel; 280 var dataModel = this.dataModel;
266 if (dataModel) { 281 if (dataModel) {
267 var index = this.dataModel.indexOf(selectedItem); 282 var index = this.dataModel.indexOf(selectedItem);
268 this.selectionModel.selectedIndex = index; 283 this.selectionModel.selectedIndex = index;
269 } 284 }
270 }, 285 },
271 286
272 /** 287 /**
273 * The height of the lead item.
274 * If set to 0, resets to the same height as other items.
275 * @type {number}
276 */
277 get leadItemHeight() {
278 return this.leadItemHeight_ || this.getItemHeight_();
279 },
280 set leadItemHeight(height) {
281 if (height) {
282 var size = this.getItemSize_();
283 this.leadItemHeight_ = Math.max(0, height + size.marginVertical);
284 } else {
285 this.leadItemHeight_ = 0;
286 }
287 },
288
289 /**
290 * Convenience alias for selectionModel.selectedItems 288 * Convenience alias for selectionModel.selectedItems
291 * @type {!Array<cr.ui.ListItem>} 289 * @type {!Array<cr.ui.ListItem>}
292 */ 290 */
293 get selectedItems() { 291 get selectedItems() {
294 var indexes = this.selectionModel.selectedIndexes; 292 var indexes = this.selectionModel.selectedIndexes;
295 var dataModel = this.dataModel; 293 var dataModel = this.dataModel;
296 if (dataModel) { 294 if (dataModel) {
297 return indexes.map(function(i) { 295 return indexes.map(function(i) {
298 return dataModel.item(i); 296 return dataModel.item(i);
299 }); 297 });
(...skipping 56 matching lines...) Expand 10 before | Expand all | Expand 10 after
356 this.addEventListener('blur', this.handleElementBlur_, true); 354 this.addEventListener('blur', this.handleElementBlur_, true);
357 this.addEventListener('scroll', this.handleScroll.bind(this)); 355 this.addEventListener('scroll', this.handleScroll.bind(this));
358 this.setAttribute('role', 'listbox'); 356 this.setAttribute('role', 'listbox');
359 357
360 // Make list focusable 358 // Make list focusable
361 if (!this.hasAttribute('tabindex')) 359 if (!this.hasAttribute('tabindex'))
362 this.tabIndex = 0; 360 this.tabIndex = 0;
363 }, 361 },
364 362
365 /** 363 /**
366 * @return {number} The height of an item, measuring it if necessary. 364 * @return {number} The height of default item, measuring it if necessary.
367 * @private 365 * @private
368 */ 366 */
369 getItemHeight_: function() { 367 getDefaultItemHeight_: function() {
370 return this.getItemSize_().height; 368 return this.getDefaultItemSize_().height;
371 }, 369 },
372 370
373 /** 371 /**
374 * @return {number} The width of an item, measuring it if necessary. 372 * @param {number} index The index of the item.
373 * @return {number} The height of the item.
374 */
375 getItemHeightByIndex_: function(index) {
376 if (this.cachedItemSizes_[index])
377 return this.cachedItemSizes_[index].height;
378
379 var item = this.getListItemByIndex(index);
380 if (item)
381 return this.getItemSize_(item).height;
382
383 return this.getDefaultItemHeight_();
384 },
385
386 /**
387 * @return {number} The width of default item, measuring it if necessary.
375 * @private 388 * @private
376 */ 389 */
377 getItemWidth_: function() { 390 getDefaultItemWidth_: function() {
378 return this.getItemSize_().width; 391 return this.getDefaultItemSize_().width;
379 }, 392 },
380 393
381 /** 394 /**
382 * @return {{height: number, width: number}} The height and width 395 * @return {{height: number, width: number}} The height and width
383 * of an item, measuring it if necessary. 396 * of default item, measuring it if necessary.
384 * @private 397 * @private
385 */ 398 */
386 getItemSize_: function() { 399 getDefaultItemSize_: function() {
387 if (!this.measured_ || !this.measured_.height) { 400 if (!this.measured_ || !this.measured_.height) {
388 this.measured_ = measureItem(this); 401 this.measured_ = measureItem(this);
389 } 402 }
390 return this.measured_; 403 return this.measured_;
391 }, 404 },
392 405
393 /** 406 /**
407 * @return {{height: number, width: number}} The height and width
408 * of an item, measuring it if necessary.
409 * @private
410 */
411 getItemSize_: function(item) {
412 if (this.cachedItemSizes_[item.listIndex])
413 return this.cachedItemSizes_[item.listIndex];
414
415 var size = measureItem(this, item);
416 if (!isNaN(size.height) && !isNaN(size.weight))
417 this.cachedItemSizes_[item.listIndex] = size;
418
419 return size;
420 },
421
422 /**
394 * Callback for the double click event. 423 * Callback for the double click event.
395 * @param {Event} e The mouse event object. 424 * @param {Event} e The mouse event object.
396 * @private 425 * @private
397 */ 426 */
398 handleDoubleClick_: function(e) { 427 handleDoubleClick_: function(e) {
399 if (this.disabled) 428 if (this.disabled)
400 return; 429 return;
401 430
402 var target = this.getListItemAncestor(e.target); 431 var target = this.getListItemAncestor(e.target);
403 if (target) 432 if (target)
(...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after
595 sm.adjustLength(e.newLength); 624 sm.adjustLength(e.newLength);
596 sm.adjustToReordering(e.permutation); 625 sm.adjustToReordering(e.permutation);
597 626
598 this.endBatchUpdates(); 627 this.endBatchUpdates();
599 628
600 if (sm.leadIndex != -1) 629 if (sm.leadIndex != -1)
601 this.scrollIndexIntoView(sm.leadIndex); 630 this.scrollIndexIntoView(sm.leadIndex);
602 }, 631 },
603 632
604 handleDataModelChange_: function(e) { 633 handleDataModelChange_: function(e) {
605 if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) { 634 if (e.index >= this.firstIndex_ &&
635 (e.index < this.lastIndex_ || this.remainingSpace_)) {
606 if (this.cachedItems_[e.index]) 636 if (this.cachedItems_[e.index])
607 delete this.cachedItems_[e.index]; 637 delete this.cachedItems_[e.index];
608 this.redraw(); 638 this.redraw();
609 } 639 }
610 }, 640 },
611 641
612 /** 642 /**
613 * @param {number} index The index of the item. 643 * @param {number} index The index of the item.
614 * @return {number} The top position of the item inside the list, not taking 644 * @return {number} The top position of the item inside the list.
615 * into account lead item. May vary in the case of multiple columns.
616 */ 645 */
617 getItemTop: function(index) { 646 getItemTop: function(index) {
618 return index * this.getItemHeight_(); 647 if (this.fixedHeight_) {
648 var itemHeight = this.getDefaultItemHeight_();
649 return index * itemHeight;
650 } else {
651 var top = 0;
652 for (var i = 0; i < index; i++) {
653 top += this.getItemHeightByIndex_(i);
654 }
655 return top;
656 }
619 }, 657 },
620 658
621 /** 659 /**
622 * @param {number} index The index of the item. 660 * @param {number} index The index of the item.
623 * @return {number} The row of the item. May vary in the case 661 * @return {number} The row of the item. May vary in the case
624 * of multiple columns. 662 * of multiple columns.
625 */ 663 */
626 getItemRow: function(index) { 664 getItemRow: function(index) {
627 return index; 665 return index;
628 }, 666 },
629 667
630 /** 668 /**
631 * @param {number} row The row. 669 * @param {number} row The row.
632 * @return {number} The index of the first item in the row. 670 * @return {number} The index of the first item in the row.
633 */ 671 */
634 getFirstItemInRow: function(row) { 672 getFirstItemInRow: function(row) {
635 return row; 673 return row;
636 }, 674 },
637 675
638 /** 676 /**
639 * Ensures that a given index is inside the viewport. 677 * Ensures that a given index is inside the viewport.
640 * @param {number} index The index of the item to scroll into view. 678 * @param {number} index The index of the item to scroll into view.
641 * @return {boolean} Whether any scrolling was needed. 679 * @return {boolean} Whether any scrolling was needed.
642 */ 680 */
643 scrollIndexIntoView: function(index) { 681 scrollIndexIntoView: function(index) {
644 var dataModel = this.dataModel; 682 var dataModel = this.dataModel;
645 if (!dataModel || index < 0 || index >= dataModel.length) 683 if (!dataModel || index < 0 || index >= dataModel.length)
646 return false; 684 return false;
647 685
648 var itemHeight = this.getItemHeight_(); 686 var itemHeight = this.getItemHeightByIndex_(index);
649 var scrollTop = this.scrollTop; 687 var scrollTop = this.scrollTop;
650 var top = this.getItemTop(index); 688 var top = this.getItemTop(index);
651 var leadIndex = this.selectionModel.leadIndex; 689 var clientHeight = this.clientHeight;
652 690
653 // Adjust for the lead item if it is above the given index. 691 var self = this;
654 if (leadIndex > -1 && leadIndex < index) 692 // Function to adjust the tops of viewport and row.
655 top += this.leadItemHeight - itemHeight; 693 function scrollToAdjustTop() {
656 else if (leadIndex == index) 694 self.scrollTop = top;
657 itemHeight = this.leadItemHeight; 695 return true;
696 };
697 // Function to adjust the bottoms of viewport and row.
698 function scrollToAdjustBottom() {
699 var cs = getComputedStyle(self);
700 var paddingY = parseInt(cs.paddingTop, 10) +
701 parseInt(cs.paddingBottom, 10);
658 702
659 if (top < scrollTop) { 703 if (top + itemHeight > scrollTop + clientHeight - paddingY) {
660 this.scrollTop = top; 704 self.scrollTop = top + itemHeight - clientHeight + paddingY;
661 return true; 705 return true;
706 }
707 return false;
708 };
709
710 // Check if the entire of given indexed row can be shown in the viewport.
711 if (itemHeight <= clientHeight) {
712 if (top < scrollTop)
713 return scrollToAdjustTop();
714 if (scrollTop + clientHeight < top + itemHeight)
715 return scrollToAdjustBottom();
662 } else { 716 } else {
663 var clientHeight = this.clientHeight; 717 if (scrollTop < top)
664 var cs = getComputedStyle(this); 718 return scrollToAdjustTop();
665 var paddingY = parseInt(cs.paddingTop, 10) + 719 if (top + itemHeight < scrollTop + clientHeight)
666 parseInt(cs.paddingBottom, 10); 720 return scrollToAdjustBottom();
667
668 if (top + itemHeight > scrollTop + clientHeight - paddingY) {
669 this.scrollTop = top + itemHeight - clientHeight + paddingY;
670 return true;
671 }
672 } 721 }
673
674 return false; 722 return false;
675 }, 723 },
676 724
677 /** 725 /**
678 * @return {!ClientRect} The rect to use for the context menu. 726 * @return {!ClientRect} The rect to use for the context menu.
679 */ 727 */
680 getRectForContextMenu: function() { 728 getRectForContextMenu: function() {
681 // TODO(arv): Add trait support so we can share more code between trees 729 // TODO(arv): Add trait support so we can share more code between trees
682 // and lists. 730 // and lists.
683 var index = this.selectionModel.selectedIndex; 731 var index = this.selectionModel.selectedIndex;
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
749 797
750 /** 798 /**
751 * Return the heights (in pixels) of the top of the given item index within 799 * Return the heights (in pixels) of the top of the given item index within
752 * the list, and the height of the given item itself, accounting for the 800 * the list, and the height of the given item itself, accounting for the
753 * possibility that the lead item may be a different height. 801 * possibility that the lead item may be a different height.
754 * @param {number} index The index to find the top height of. 802 * @param {number} index The index to find the top height of.
755 * @return {{top: number, height: number}} The heights for the given index. 803 * @return {{top: number, height: number}} The heights for the given index.
756 * @private 804 * @private
757 */ 805 */
758 getHeightsForIndex_: function(index) { 806 getHeightsForIndex_: function(index) {
759 var itemHeight = this.getItemHeight_(); 807 var itemHeight = this.getItemHeightByIndex_(index);
760 var top = this.getItemTop(index); 808 var top = this.getItemTop(index);
761 if (this.selectionModel.leadIndex > -1 &&
762 this.selectionModel.leadIndex < index) {
763 top += this.leadItemHeight - itemHeight;
764 } else if (this.selectionModel.leadIndex == index) {
765 itemHeight = this.leadItemHeight;
766 }
767 return {top: top, height: itemHeight}; 809 return {top: top, height: itemHeight};
768 }, 810 },
769 811
770 /** 812 /**
771 * Find the index of the list item containing the given y offset (measured 813 * Find the index of the list item containing the given y offset (measured
772 * in pixels from the top) within the list. In the case of multiple columns, 814 * in pixels from the top) within the list. In the case of multiple columns,
773 * returns the first index in the row. 815 * returns the first index in the row.
774 * @param {number} offset The y offset in pixels to get the index of. 816 * @param {number} offset The y offset in pixels to get the index of.
775 * @return {number} The index of the list item. 817 * @return {number} The index of the list item. Returns the list size if
818 * given offset exceeds the height of list.
776 * @private 819 * @private
777 */ 820 */
778 getIndexForListOffset_: function(offset) { 821 getIndexForListOffset_: function(offset) {
779 var itemHeight = this.getItemHeight_(); 822 var itemHeight = this.getDefaultItemHeight_();
780 var leadIndex = this.selectionModel.leadIndex; 823 if (!itemHeight)
781 var leadItemHeight = this.leadItemHeight; 824 return this.dataModel.length;
782 if (leadIndex < 0 || leadItemHeight == itemHeight) { 825
783 // Simple case: no lead item or lead item height is not different. 826 if (this.fixedHeight_)
784 return this.getFirstItemInRow(Math.floor(offset / itemHeight)); 827 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
828
829 // If offset exceeds the height of list.
830 var lastHeight = 0;
831 if (this.dataModel.length) {
832 var h = this.getHeightsForIndex_(this.dataModel.length - 1);
833 lastHeight = h.top + h.height;
785 } 834 }
786 var leadTop = this.getItemTop(leadIndex); 835 if (lastHeight < offset)
787 // If the given offset is above the lead item, it's also simple. 836 return this.dataModel.length;
788 if (offset < leadTop) 837
789 return this.getFirstItemInRow(Math.floor(offset / itemHeight)); 838 // Estimates index.
790 // If the lead item contains the given offset, we just return its index. 839 var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
791 if (offset < leadTop + leadItemHeight) 840 this.dataModel.length - 1);
792 return this.getFirstItemInRow(this.getItemRow(leadIndex)); 841 var isIncrementing = this.getItemTop(estimatedIndex) < offset;
793 // The given offset must be below the lead item. Adjust and recalculate. 842
794 offset -= leadItemHeight - itemHeight; 843 // Searchs the correct index.
795 return this.getFirstItemInRow(Math.floor(offset / itemHeight)); 844 do {
845 var heights = this.getHeightsForIndex_(estimatedIndex);
846 var top = heights.top;
847 var height = heights.height;
848
849 if (top <= offset && offset <= (top + height))
850 break;
851
852 isIncrementing ? ++estimatedIndex: --estimatedIndex;
853 } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length)
854
855 return estimatedIndex;
796 }, 856 },
797 857
798 /** 858 /**
799 * Return the number of items that occupy the range of heights between the 859 * Return the number of items that occupy the range of heights between the
800 * top of the start item and the end offset. 860 * top of the start item and the end offset.
801 * @param {number} startIndex The index of the first visible item. 861 * @param {number} startIndex The index of the first visible item.
802 * @param {number} endOffset The y offset in pixels of the end of the list. 862 * @param {number} endOffset The y offset in pixels of the end of the list.
803 * @return {number} The number of list items visible. 863 * @return {number} The number of list items visible.
804 * @private 864 * @private
805 */ 865 */
806 countItemsInRange_: function(startIndex, endOffset) { 866 countItemsInRange_: function(startIndex, endOffset) {
807 var endIndex = this.getIndexForListOffset_(endOffset); 867 var endIndex = this.getIndexForListOffset_(endOffset);
808 return endIndex - startIndex + 1; 868 return endIndex - startIndex + 1;
809 }, 869 },
810 870
811 /** 871 /**
812 * Calculates the number of items fitting in viewport given the index of 872 * Calculates the number of items fitting in the given viewport.
813 * first item and heights.
814 * @param {number} itemHeight The height of the item.
815 * @param {number} firstIndex Index of the first item in viewport.
816 * @param {number} scrollTop The scroll top position. 873 * @param {number} scrollTop The scroll top position.
817 * @return {number} The number of items in view port. 874 * @param {number} clientHeight The height of viewport.
875 * @return {{first: number, length: number, last: number}} The index of
876 * first item in view port, The number of items, The item past the last.
818 */ 877 */
819 getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) { 878 getItemsInViewPort: function(scrollTop, clientHeight) {
820 // This is a bit tricky. We take the minimum of the available items to 879 if (this.autoExpands_) {
821 // show and the number we want to show, so as not to go off the end of the 880 return {
822 // list. For the number we want to show, we take the maximum of the number 881 first: 0,
823 // that would fit without a differently-sized lead item, and with one. We 882 length: this.dataModel.length,
824 // do this so that if the size of the lead item changes without a scroll 883 last: this.dataModel.length};
825 // event to trigger redrawing the list, we won't end up with empty space. 884 } else {
826 var clientHeight = this.clientHeight; 885 var firstIndex = this.getIndexForListOffset_(scrollTop);
827 return this.autoExpands_ ? this.dataModel.length : Math.min( 886 var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
828 this.dataModel.length - firstIndex, 887
829 Math.max( 888 return {
830 Math.ceil(clientHeight / itemHeight) + 1, 889 first: firstIndex,
831 this.countItemsInRange_(firstIndex, scrollTop + clientHeight))); 890 length: lastIndex - firstIndex + 1,
891 last: lastIndex + 1};
892 }
832 }, 893 },
833 894
834 /** 895 /**
835 * Adds items to the list and {@code newCachedItems}. 896 * Adds items to the list and {@code newCachedItems}.
836 * @param {number} firstIndex The index of first item, inclusively. 897 * @param {number} firstIndex The index of first item, inclusively.
837 * @param {number} lastIndex The index of last item, exclusively. 898 * @param {number} lastIndex The index of last item, exclusively.
838 * @param {Object.<string, ListItem>} cachedItems Old items cache. 899 * @param {Object.<string, ListItem>} cachedItems Old items cache.
839 * @param {Object.<string, ListItem>} newCachedItems New items cache. 900 * @param {Object.<string, ListItem>} newCachedItems New items cache.
840 */ 901 */
841 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) { 902 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
842 var listItem;
843 var dataModel = this.dataModel; 903 var dataModel = this.dataModel;
844 904
845 window.l = this; 905 window.l = this;
846 for (var y = firstIndex; y < lastIndex; y++) { 906 for (var y = firstIndex; y < lastIndex; y++) {
847 var dataItem = dataModel.item(y); 907 var dataItem = dataModel.item(y);
848 listItem = cachedItems[y] || this.createItem(dataItem); 908 var listItem = cachedItems[y] || this.createItem(dataItem);
849 listItem.listIndex = y; 909 listItem.listIndex = y;
850 this.appendChild(listItem); 910 this.appendChild(listItem);
851 newCachedItems[y] = listItem; 911 newCachedItems[y] = listItem;
852 } 912 }
913
914 // Mesurings must be placed after adding all the elements, to prevent
915 // performance reducing.
916 for (var y = firstIndex; y < lastIndex; y++) {
917 this.cachedItemSizes_[y] = measureItem(this, newCachedItems[y]);
918 }
853 }, 919 },
854 920
855 /** 921 /**
922 * Ensures that all the item sizes in the list have been already cached.
923 */
924 ensureAllItemSizesInCache: function() {
925 var measuringIndexes = [];
926 for (var y = 0; y < this.dataModel.length; y++) {
927 if (!this.cachedItemSizes_[y])
928 measuringIndexes.push(y);
929 }
930
931 var measuringItems = [];
932 // Adds temporary elements.
933 for (var y = 0; y < measuringIndexes.length; y++) {
934 var index = measuringIndexes[y];
935 var dataItem = this.dataModel.item(index);
936 var listItem = this.cachedItems_[index] || this.createItem(dataItem);
937 listItem.listIndex = index;
938 this.appendChild(listItem);
939 this.cachedItems_[index] = listItem;
940 measuringItems.push(listItem);
941 }
942
943 // All mesurings must be placed after adding all the elements, to prevent
944 // performance reducing.
945 for (var y = 0; y < measuringIndexes.length; y++) {
946 var index = measuringIndexes[y];
947 this.cachedItemSizes_[index] = measureItem(this, measuringItems[y]);
948 }
949
950 // Removes all the temprary elements.
951 for (var y = 0; y < measuringIndexes.length; y++) {
952 this.removeChild(measuringItems[y]);
953 }
954 },
955
956 /**
856 * Returns the height of after filler in the list. 957 * Returns the height of after filler in the list.
857 * @param {number} lastIndex The index of item past the last in viewport. 958 * @param {number} lastIndex The index of item past the last in viewport.
858 * @param {number} itemHeight The height of the item. 959 * @param {number} itemHeight The height of the item.
859 * @return {number} The height of after filler. 960 * @return {number} The height of after filler.
860 */ 961 */
861 getAfterFillerHeight: function(lastIndex, itemHeight) { 962 getAfterFillerHeight: function(lastIndex) {
862 return (this.dataModel.length - lastIndex) * itemHeight; 963 if (this.fixedHeight_) {
964 var itemHeight = this.getDefaultItemHeight_();
965 return (this.dataModel.length - lastIndex) * itemHeight;
966 }
967
968 var height = 0;
969 for (var i = lastIndex; i < this.dataModel.length; i++)
970 height += this.getItemHeightByIndex_(i);
971 return height;
863 }, 972 },
864 973
865 /** 974 /**
866 * Redraws the viewport. 975 * Redraws the viewport.
867 */ 976 */
868 redraw: function() { 977 redraw: function() {
869 if (this.batchCount_ != 0) 978 if (this.batchCount_ != 0)
870 return; 979 return;
871 980
872 var dataModel = this.dataModel; 981 var dataModel = this.dataModel;
873 if (!dataModel) { 982 if (!dataModel) {
983 this.cachedItems_ = {};
984 this.firstIndex_ = 0;
985 this.lastIndex_ = 0;
986 this.remainingSpace_ = true;
874 this.textContent = ''; 987 this.textContent = '';
875 return; 988 return;
876 } 989 }
877 990
878 var scrollTop = this.scrollTop; 991 // Store all the item sizes into the cache in advance, to prevent
879 var clientHeight = this.clientHeight; 992 // interleave measuring with mutating dom.
880 993 this.ensureAllItemSizesInCache();
881 var itemHeight = this.getItemHeight_();
882 994
883 // We cache the list items since creating the DOM nodes is the most 995 // We cache the list items since creating the DOM nodes is the most
884 // expensive part of redrawing. 996 // expensive part of redrawing.
885 var cachedItems = this.cachedItems_ || {}; 997 var cachedItems = this.cachedItems_ || {};
886 var newCachedItems = {}; 998 var newCachedItems = {};
887 999
888 var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top; 1000 var autoExpands = this.autoExpands_;
1001 var scrollTop = this.scrollTop;
1002 var clientHeight = this.clientHeight;
889 1003
890 var autoExpands = this.autoExpands_; 1004 var lastItemHeights = this.getHeightsForIndex_(dataModel.length - 1);
891 var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop); 1005 var desiredScrollHeight = lastItemHeights.top + lastItemHeights.height;
892 var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex,
893 scrollTop);
894 var lastIndex = firstIndex + itemsInViewPort;
895 1006
1007 var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
1008 // Draws the hidden rows just above/below the viewport to prevent
1009 // flashing in scroll.
1010 var firstIndex = Math.max(0, itemsInViewPort.first - 1);
1011 var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
1012
1013 var beforeFillerHeight =
1014 this.autoExpands ? 0 : this.getItemTop(firstIndex);
1015 var afterFillerHeight =
1016 this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
1017
1018 // Clear list and Adds elements on list.
896 this.textContent = ''; 1019 this.textContent = '';
897 1020
898 this.beforeFiller_.style.height = 1021 this.beforeFiller_.style.height = beforeFillerHeight + 'px';
899 this.getHeightsForIndex_(firstIndex).top + 'px';
900 this.appendChild(this.beforeFiller_); 1022 this.appendChild(this.beforeFiller_);
901 1023
1024 this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
1025
1026 this.afterFiller_.style.height = afterFillerHeight + 'px';
1027 this.appendChild(this.afterFiller_);
1028
902 var sm = this.selectionModel; 1029 var sm = this.selectionModel;
903 var leadIndex = sm.leadIndex; 1030 var leadIndex = sm.leadIndex;
904 1031
905 this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
906
907 var afterFillerHeight = this.getAfterFillerHeight(lastIndex, itemHeight);
908 if (leadIndex >= lastIndex)
909 afterFillerHeight += this.leadItemHeight - itemHeight;
910 this.afterFiller_.style.height = afterFillerHeight + 'px';
911 this.appendChild(this.afterFiller_);
912
913 // We don't set the lead or selected properties until after adding all 1032 // We don't set the lead or selected properties until after adding all
914 // items, in case they force relayout in response to these events. 1033 // items, in case they force relayout in response to these events.
915 var listItem = null; 1034 var listItem = null;
916 if (newCachedItems[leadIndex]) 1035 if (leadIndex != -1 && newCachedItems[leadIndex])
917 newCachedItems[leadIndex].lead = true; 1036 newCachedItems[leadIndex].lead = true;
918 for (var y = firstIndex; y < lastIndex; y++) { 1037 for (var y = firstIndex; y < lastIndex; y++) {
919 if (sm.getIndexSelected(y)) 1038 if (sm.getIndexSelected(y))
920 newCachedItems[y].selected = true; 1039 newCachedItems[y].selected = true;
921 else if (y != leadIndex) 1040 else if (y != leadIndex)
922 listItem = newCachedItems[y]; 1041 listItem = newCachedItems[y];
923 } 1042 }
924 1043
925 this.scrollTop = scrollTop; 1044 this.scrollTop = scrollTop;
926 1045
927 this.firstIndex_ = firstIndex; 1046 this.firstIndex_ = firstIndex;
928 this.lastIndex_ = lastIndex; 1047 this.lastIndex_ = lastIndex;
929 1048
1049 this.remainingSpace_ = itemsInViewPort.last > dataModel.length;
930 this.cachedItems_ = newCachedItems; 1050 this.cachedItems_ = newCachedItems;
931 1051
932 // Measure again in case the item height has changed due to a page zoom. 1052 // Measure again in case the item height has changed due to a page zoom.
933 // 1053 //
934 // The measure above is only done the first time but this measure is done 1054 // The measure above is only done the first time but this measure is done
935 // after every redraw. It is done in a timeout so it will not trigger 1055 // after every redraw. It is done in a timeout so it will not trigger
936 // a reflow (which made the redraw speed 3 times slower on my system). 1056 // a reflow (which made the redraw speed 3 times slower on my system).
937 // By using a timeout the measuring will happen later when there is no 1057 // By using a timeout the measuring will happen later when there is no
938 // need for a reflow. 1058 // need for a reflow.
939 if (listItem) { 1059 if (listItem && this.fixedHeight_) {
940 var list = this; 1060 var list = this;
941 window.setTimeout(function() { 1061 window.setTimeout(function() {
942 if (listItem.parentNode == list) { 1062 if (listItem.parentNode == list) {
943 list.measured_ = measureItem(list, listItem); 1063 list.measured_ = measureItem(list, listItem);
944 } 1064 }
945 }); 1065 });
946 } 1066 }
947 }, 1067 },
948 1068
949 /** 1069 /**
950 * Invalidates list by removing cached items. 1070 * Invalidates list by removing cached items.
951 */ 1071 */
952 invalidate: function() { 1072 invalidate: function() {
953 this.cachedItems_ = {}; 1073 this.cachedItems_ = {};
1074 this.cachedItemSized_ = {};
954 }, 1075 },
955 1076
956 /** 1077 /**
957 * Redraws a single item. 1078 * Redraws a single item.
958 * @param {number} index The row index to redraw. 1079 * @param {number} index The row index to redraw.
959 */ 1080 */
960 redrawItem: function(index) { 1081 redrawItem: function(index) {
961 if (index >= this.firstIndex_ && index < this.lastIndex_) { 1082 if (index >= this.firstIndex_ &&
1083 (index < this.lastIndex_ || this.remainingSpace_)) {
962 delete this.cachedItems_[index]; 1084 delete this.cachedItems_[index];
963 this.redraw(); 1085 this.redraw();
964 } 1086 }
965 }, 1087 },
966 1088
967 /** 1089 /**
968 * Called when a list item is activated, currently only by a double click 1090 * Called when a list item is activated, currently only by a double click
969 * event. 1091 * event.
970 * @param {number} index The index of the activated item. 1092 * @param {number} index The index of the activated item.
971 */ 1093 */
972 activateItemAtIndex: function(index) { 1094 activateItemAtIndex: function(index) {
973 }, 1095 },
974 }; 1096 };
975 1097
976 cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR); 1098 cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
977 1099
978 /** 1100 /**
979 * Whether the list or one of its descendents has focus. This is necessary 1101 * Whether the list or one of its descendents has focus. This is necessary
980 * because list items can contain controls that can be focused, and for some 1102 * because list items can contain controls that can be focused, and for some
981 * purposes (e.g., styling), the list can still be conceptually focused at 1103 * purposes (e.g., styling), the list can still be conceptually focused at
982 * that point even though it doesn't actually have the page focus. 1104 * that point even though it doesn't actually have the page focus.
983 */ 1105 */
984 cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR); 1106 cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
985 1107
986 return { 1108 return {
987 List: List 1109 List: List
988 } 1110 }
989 }); 1111 });
OLDNEW
« no previous file with comments | « chrome/browser/resources/shared/js/cr/ui/grid.js ('k') | chrome/browser/resources/shared/js/cr/ui/list_selection_model.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698