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

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: review fix 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 100 matching lines...) Expand 10 before | Expand all | Expand 10 after
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 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 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 121 * can be expanded to show more detail. When the fixed height except the
122 * when the height of the lead item is changed with {@code set 122 * lead item is needed, use this value instead of fixedHeight = false, to
arv (Not doing code reviews) 2011/11/29 21:46:55 This sentence does not parse.
yoshiki 2011/11/30 12:01:40 Changed the sentence. And moved it to the comment
123 * leadItemHeight}, and presumed equal to {@code itemHeight_} otherwise. 123 * improve performance of caluculating heights of rows.
124 * It is explicitly set by client code when the height of the lead item is
125 * changed with {@code set leadItemHeight}, and presumed equal to {@code
126 * itemHeight_} otherwise.
127 * This value will be ignored when {@code fixedHeight_} is false.
124 * @type {number} 128 * @type {number}
125 * @private 129 * @private
126 */ 130 */
127 leadItemHeight_: 0, 131 leadItemHeight_: 0,
128 132
129 /** 133 /**
130 * Whether or not the list is autoexpanding. If true, the list resizes 134 * Whether or not the list is autoexpanding. If true, the list resizes
131 * its height to accomadate all children. 135 * its height to accomadate all children.
132 * @type {boolean} 136 * @type {boolean}
133 * @private 137 * @private
134 */ 138 */
135 autoExpands_: false, 139 autoExpands_: false,
136 140
137 /** 141 /**
142 * Whether or not the list view has a blank space below the last row.
143 * @type {boolean}
144 * @private
145 */
146 remainingSpace_: true,
147
148 /**
138 * Function used to create grid items. 149 * Function used to create grid items.
139 * @type {function(): !ListItem} 150 * @type {function(): !ListItem}
140 * @private 151 * @private
141 */ 152 */
142 itemConstructor_: cr.ui.ListItem, 153 itemConstructor_: cr.ui.ListItem,
143 154
144 /** 155 /**
145 * Function used to create grid items. 156 * Function used to create grid items.
146 * @type {function(): !ListItem} 157 * @type {function(): !ListItem}
147 */ 158 */
(...skipping 22 matching lines...) Expand all
170 this.boundHandleDataModelChange_ = 181 this.boundHandleDataModelChange_ =
171 this.handleDataModelChange_.bind(this); 182 this.handleDataModelChange_.bind(this);
172 } 183 }
173 184
174 if (this.dataModel_) { 185 if (this.dataModel_) {
175 this.dataModel_.removeEventListener( 186 this.dataModel_.removeEventListener(
176 'permuted', 187 'permuted',
177 this.boundHandleDataModelPermuted_); 188 this.boundHandleDataModelPermuted_);
178 this.dataModel_.removeEventListener('change', 189 this.dataModel_.removeEventListener('change',
179 this.boundHandleDataModelChange_); 190 this.boundHandleDataModelChange_);
191 this.dataModel_.removeEventListener('splice',
192 this.boundHandleDataModelChange_);
180 } 193 }
181 194
182 this.dataModel_ = dataModel; 195 this.dataModel_ = dataModel;
183 196
184 this.cachedItems_ = {}; 197 this.cachedItems_ = {};
198 this.cachedItemSizes_ = {};
185 this.selectionModel.clear(); 199 this.selectionModel.clear();
186 if (dataModel) 200 if (dataModel)
187 this.selectionModel.adjustLength(dataModel.length); 201 this.selectionModel.adjustLength(dataModel.length);
188 202
189 if (this.dataModel_) { 203 if (this.dataModel_) {
190 this.dataModel_.addEventListener( 204 this.dataModel_.addEventListener(
191 'permuted', 205 'permuted',
192 this.boundHandleDataModelPermuted_); 206 this.boundHandleDataModelPermuted_);
193 this.dataModel_.addEventListener('change', 207 this.dataModel_.addEventListener('change',
194 this.boundHandleDataModelChange_); 208 this.boundHandleDataModelChange_);
209 this.dataModel_.addEventListener('splice',
210 this.boundHandleDataModelChange_);
195 } 211 }
196 212
197 this.redraw(); 213 this.redraw();
198 } 214 }
199 }, 215 },
200 216
201 get dataModel() { 217 get dataModel() {
202 return this.dataModel_; 218 return this.dataModel_;
203 }, 219 },
204 220
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
242 return this.autoExpands_; 258 return this.autoExpands_;
243 }, 259 },
244 set autoExpands(autoExpands) { 260 set autoExpands(autoExpands) {
245 if (this.autoExpands_ == autoExpands) 261 if (this.autoExpands_ == autoExpands)
246 return; 262 return;
247 this.autoExpands_ = autoExpands; 263 this.autoExpands_ = autoExpands;
248 this.redraw(); 264 this.redraw();
249 }, 265 },
250 266
251 /** 267 /**
268 * Whether or not the rows on list have various heights.
269 * @type {boolean}
270 */
271 get fixedHeight() {
272 return this.fixedHeight_;
273 },
274 set fixedHeight(fixedHeight) {
275 if (this.fixedHeight_ == fixedHeight)
276 return;
277 this.fixedHeight_ = fixedHeight;
278 this.redraw();
279 },
280
281 /**
252 * Convenience alias for selectionModel.selectedItem 282 * Convenience alias for selectionModel.selectedItem
253 * @type {cr.ui.ListItem} 283 * @type {cr.ui.ListItem}
254 */ 284 */
255 get selectedItem() { 285 get selectedItem() {
256 var dataModel = this.dataModel; 286 var dataModel = this.dataModel;
257 if (dataModel) { 287 if (dataModel) {
258 var index = this.selectionModel.selectedIndex; 288 var index = this.selectionModel.selectedIndex;
259 if (index != -1) 289 if (index != -1)
260 return dataModel.item(index); 290 return dataModel.item(index);
261 } 291 }
262 return null; 292 return null;
263 }, 293 },
264 set selectedItem(selectedItem) { 294 set selectedItem(selectedItem) {
265 var dataModel = this.dataModel; 295 var dataModel = this.dataModel;
266 if (dataModel) { 296 if (dataModel) {
267 var index = this.dataModel.indexOf(selectedItem); 297 var index = this.dataModel.indexOf(selectedItem);
268 this.selectionModel.selectedIndex = index; 298 this.selectionModel.selectedIndex = index;
269 } 299 }
270 }, 300 },
271 301
272 /** 302 /**
273 * The height of the lead item. 303 * The height of the lead item.
274 * If set to 0, resets to the same height as other items. 304 * If set to 0, resets to the same height as other items.
275 * @type {number} 305 * @type {number}
276 */ 306 */
277 get leadItemHeight() { 307 get leadItemHeight() {
278 return this.leadItemHeight_ || this.getItemHeight_(); 308 return this.leadItemHeight_ || this.getDefaultItemHeight_();
279 }, 309 },
280 set leadItemHeight(height) { 310 set leadItemHeight(height) {
281 if (height) { 311 if (height) {
282 var size = this.getItemSize_(); 312 var size = this.getDefaultItemSize_();
283 this.leadItemHeight_ = Math.max(0, height + size.marginVertical); 313 this.leadItemHeight_ = Math.max(0, height + size.marginVertical);
284 } else { 314 } else {
285 this.leadItemHeight_ = 0; 315 this.leadItemHeight_ = 0;
286 } 316 }
287 }, 317 },
288 318
289 /** 319 /**
290 * Convenience alias for selectionModel.selectedItems 320 * Convenience alias for selectionModel.selectedItems
291 * @type {!Array<cr.ui.ListItem>} 321 * @type {!Array<cr.ui.ListItem>}
292 */ 322 */
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after
356 this.addEventListener('blur', this.handleElementBlur_, true); 386 this.addEventListener('blur', this.handleElementBlur_, true);
357 this.addEventListener('scroll', this.handleScroll.bind(this)); 387 this.addEventListener('scroll', this.handleScroll.bind(this));
358 this.setAttribute('role', 'listbox'); 388 this.setAttribute('role', 'listbox');
359 389
360 // Make list focusable 390 // Make list focusable
361 if (!this.hasAttribute('tabindex')) 391 if (!this.hasAttribute('tabindex'))
362 this.tabIndex = 0; 392 this.tabIndex = 0;
363 }, 393 },
364 394
365 /** 395 /**
366 * @return {number} The height of an item, measuring it if necessary. 396 * @return {number} The height of default item, measuring it if necessary.
367 * @private 397 * @private
368 */ 398 */
369 getItemHeight_: function() { 399 getDefaultItemHeight_: function() {
370 return this.getItemSize_().height; 400 return this.getDefaultItemSize_().height;
371 }, 401 },
372 402
373 /** 403 /**
374 * @return {number} The width of an item, measuring it if necessary. 404 * @param {number} index The index of the item.
405 * @return {number} The height of the item.
406 */
407 getItemHeightByIndex_: function(index) {
408 if (this.cachedItemSizes_[index])
409 return this.cachedItemSizes_[index].height;
410
411 var item = this.getListItemByIndex(index);
412 return item ? this.getItemSize_(item).height :
413 this.getDefaultItemHeight_();
414 },
415
416 /**
417 * @return {number} The width of default item, measuring it if necessary.
375 * @private 418 * @private
376 */ 419 */
377 getItemWidth_: function() { 420 getDefaultItemWidth_: function() {
378 return this.getItemSize_().width; 421 return this.getDefaultItemSize_().width;
379 }, 422 },
380 423
381 /** 424 /**
382 * @return {{height: number, width: number}} The height and width 425 * @return {{height: number, width: number}} The height and width
383 * of an item, measuring it if necessary. 426 * of default item, measuring it if necessary.
384 * @private 427 * @private
385 */ 428 */
386 getItemSize_: function() { 429 getDefaultItemSize_: function() {
387 if (!this.measured_ || !this.measured_.height) { 430 if (!this.measured_ || !this.measured_.height) {
388 this.measured_ = measureItem(this); 431 this.measured_ = measureItem(this);
389 } 432 }
390 return this.measured_; 433 return this.measured_;
391 }, 434 },
392 435
393 /** 436 /**
437 * @return {{height: number, width: number}} The height and width
438 * of an item, measuring it if necessary.
439 * @private
440 */
441 getItemSize_: function(item) {
442 if (this.cachedItemSizes_[item.listIndex])
443 return this.cachedItemSizes_[item.listIndex];
444
445 var size = measureItem(this, item);
446 if (!isNaN(size.height) && !isNaN(size.weight))
447 this.cachedItemSizes_[item.listIndex] = size;
448
449 return size;
450 },
451
452 /**
394 * Callback for the double click event. 453 * Callback for the double click event.
395 * @param {Event} e The mouse event object. 454 * @param {Event} e The mouse event object.
396 * @private 455 * @private
397 */ 456 */
398 handleDoubleClick_: function(e) { 457 handleDoubleClick_: function(e) {
399 if (this.disabled) 458 if (this.disabled)
400 return; 459 return;
401 460
402 var target = this.getListItemAncestor(e.target); 461 var target = this.getListItemAncestor(e.target);
403 if (target) 462 if (target)
(...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after
595 sm.adjustLength(e.newLength); 654 sm.adjustLength(e.newLength);
596 sm.adjustToReordering(e.permutation); 655 sm.adjustToReordering(e.permutation);
597 656
598 this.endBatchUpdates(); 657 this.endBatchUpdates();
599 658
600 if (sm.leadIndex != -1) 659 if (sm.leadIndex != -1)
601 this.scrollIndexIntoView(sm.leadIndex); 660 this.scrollIndexIntoView(sm.leadIndex);
602 }, 661 },
603 662
604 handleDataModelChange_: function(e) { 663 handleDataModelChange_: function(e) {
605 if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) { 664 if (e.index >= this.firstIndex_ &&
665 (e.index < this.lastIndex_ || this.remainingSpace_)) {
606 if (this.cachedItems_[e.index]) 666 if (this.cachedItems_[e.index])
607 delete this.cachedItems_[e.index]; 667 delete this.cachedItems_[e.index];
608 this.redraw(); 668 this.redraw();
609 } 669 }
610 }, 670 },
611 671
612 /** 672 /**
613 * @param {number} index The index of the item. 673 * @param {number} index The index of the item.
614 * @return {number} The top position of the item inside the list, not taking 674 * @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 */ 675 */
617 getItemTop: function(index) { 676 getItemTop: function(index) {
618 return index * this.getItemHeight_(); 677 if (this.fixedHeight_) {
678 var itemHeight = this.getDefaultItemHeight_();
679 var top = index * itemHeight;
680 if (this.selectionModel.leadIndex > -1 &&
arv (Not doing code reviews) 2011/11/29 21:46:55 Can we treat fixedHeight_ as not having a custom l
yoshiki 2011/11/30 12:01:40 Done.
681 this.selectionModel.leadIndex < index) {
682 top += this.leadItemHeight - itemHeight;
683 }
684 return top;
685 } else {
686 var top = 0;
687 for (var i = 0; i < index; i++) {
688 top += this.getItemHeightByIndex_(i);
689 }
690 return top;
691 }
619 }, 692 },
620 693
621 /** 694 /**
622 * @param {number} index The index of the item. 695 * @param {number} index The index of the item.
623 * @return {number} The row of the item. May vary in the case 696 * @return {number} The row of the item. May vary in the case
624 * of multiple columns. 697 * of multiple columns.
625 */ 698 */
626 getItemRow: function(index) { 699 getItemRow: function(index) {
627 return index; 700 return index;
628 }, 701 },
629 702
630 /** 703 /**
631 * @param {number} row The row. 704 * @param {number} row The row.
632 * @return {number} The index of the first item in the row. 705 * @return {number} The index of the first item in the row.
633 */ 706 */
634 getFirstItemInRow: function(row) { 707 getFirstItemInRow: function(row) {
635 return row; 708 return row;
636 }, 709 },
637 710
638 /** 711 /**
639 * Ensures that a given index is inside the viewport. 712 * Ensures that a given index is inside the viewport.
640 * @param {number} index The index of the item to scroll into view. 713 * @param {number} index The index of the item to scroll into view.
641 * @return {boolean} Whether any scrolling was needed. 714 * @return {boolean} Whether any scrolling was needed.
642 */ 715 */
643 scrollIndexIntoView: function(index) { 716 scrollIndexIntoView: function(index) {
644 var dataModel = this.dataModel; 717 var dataModel = this.dataModel;
645 if (!dataModel || index < 0 || index >= dataModel.length) 718 if (!dataModel || index < 0 || index >= dataModel.length)
646 return false; 719 return false;
647 720
648 var itemHeight = this.getItemHeight_(); 721 var itemHeight = this.getItemHeightByIndex_(index);
649 var scrollTop = this.scrollTop; 722 var scrollTop = this.scrollTop;
650 var top = this.getItemTop(index); 723 var top = this.getItemTop(index);
651 var leadIndex = this.selectionModel.leadIndex; 724 var clientHeight = this.clientHeight;
652 725
653 // Adjust for the lead item if it is above the given index. 726 var self = this;
654 if (leadIndex > -1 && leadIndex < index) 727 // Function to adjust the tops of viewport and row.
655 top += this.leadItemHeight - itemHeight; 728 function scrollToAdjustTop() {
656 else if (leadIndex == index) 729 self.scrollTop = top;
657 itemHeight = this.leadItemHeight; 730 return true;
731 };
732 // Function to adjust the bottoms of viewport and row.
733 function scrollToAdjustBottom() {
734 var cs = getComputedStyle(self);
735 var paddingY = parseInt(cs.paddingTop, 10) +
736 parseInt(cs.paddingBottom, 10);
658 737
659 if (top < scrollTop) { 738 if (top + itemHeight > scrollTop + clientHeight - paddingY) {
660 this.scrollTop = top; 739 self.scrollTop = top + itemHeight - clientHeight + paddingY;
661 return true; 740 return true;
741 }
742 return false;
743 };
744
745 // Check if the entire of given indexed row can be shown in the viewport.
746 if (itemHeight <= clientHeight) {
747 if (top < scrollTop)
748 return scrollToAdjustTop();
749 if (scrollTop + clientHeight < top + itemHeight)
750 return scrollToAdjustBottom();
662 } else { 751 } else {
663 var clientHeight = this.clientHeight; 752 if (scrollTop < top)
664 var cs = getComputedStyle(this); 753 return scrollToAdjustTop();
665 var paddingY = parseInt(cs.paddingTop, 10) + 754 if (top + itemHeight < scrollTop + clientHeight)
666 parseInt(cs.paddingBottom, 10); 755 return scrollToAdjustBottom();
667
668 if (top + itemHeight > scrollTop + clientHeight - paddingY) {
669 this.scrollTop = top + itemHeight - clientHeight + paddingY;
670 return true;
671 }
672 } 756 }
673
674 return false; 757 return false;
675 }, 758 },
676 759
677 /** 760 /**
678 * @return {!ClientRect} The rect to use for the context menu. 761 * @return {!ClientRect} The rect to use for the context menu.
679 */ 762 */
680 getRectForContextMenu: function() { 763 getRectForContextMenu: function() {
681 // TODO(arv): Add trait support so we can share more code between trees 764 // TODO(arv): Add trait support so we can share more code between trees
682 // and lists. 765 // and lists.
683 var index = this.selectionModel.selectedIndex; 766 var index = this.selectionModel.selectedIndex;
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
749 832
750 /** 833 /**
751 * Return the heights (in pixels) of the top of the given item index within 834 * 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 835 * the list, and the height of the given item itself, accounting for the
753 * possibility that the lead item may be a different height. 836 * possibility that the lead item may be a different height.
754 * @param {number} index The index to find the top height of. 837 * @param {number} index The index to find the top height of.
755 * @return {{top: number, height: number}} The heights for the given index. 838 * @return {{top: number, height: number}} The heights for the given index.
756 * @private 839 * @private
757 */ 840 */
758 getHeightsForIndex_: function(index) { 841 getHeightsForIndex_: function(index) {
759 var itemHeight = this.getItemHeight_(); 842 var itemHeight = this.getItemHeightByIndex_(index);
760 var top = this.getItemTop(index); 843 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}; 844 return {top: top, height: itemHeight};
768 }, 845 },
769 846
770 /** 847 /**
771 * Find the index of the list item containing the given y offset (measured 848 * 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, 849 * in pixels from the top) within the list. In the case of multiple columns,
773 * returns the first index in the row. 850 * returns the first index in the row.
774 * @param {number} offset The y offset in pixels to get the index of. 851 * @param {number} offset The y offset in pixels to get the index of.
775 * @return {number} The index of the list item. 852 * @return {number} The index of the list item. Returns the list size if
853 * given offset exceeds the height of list.
776 * @private 854 * @private
777 */ 855 */
778 getIndexForListOffset_: function(offset) { 856 getIndexForListOffset_: function(offset) {
779 var itemHeight = this.getItemHeight_(); 857 var itemHeight = this.getDefaultItemHeight_();
780 var leadIndex = this.selectionModel.leadIndex; 858 if (!itemHeight)
781 var leadItemHeight = this.leadItemHeight; 859 return this.dataModel.length;
782 if (leadIndex < 0 || leadItemHeight == itemHeight) { 860
783 // Simple case: no lead item or lead item height is not different. 861 if (this.fixedHeight_) {
862 var leadIndex = this.selectionModel.leadIndex;
863 var leadItemHeight = this.leadItemHeight;
864 if (leadIndex < 0 || leadItemHeight == itemHeight) {
865 // Simple case: no lead item or lead item height is not different.
866 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
867 }
868
869 var leadTop = this.getItemTop(leadIndex);
870
871 // If the given offset is above the lead item, it's also simple.
872 if (offset < leadTop)
873 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
874
875 // If the lead item contains the given offset, we just return its index.
876 if (offset < leadTop + leadItemHeight)
877 return this.getFirstItemInRow(this.getItemRow(leadIndex));
878
879 // The given offset must be below the lead item. Adjust and recalculate.
880 offset -= leadItemHeight - itemHeight;
784 return this.getFirstItemInRow(Math.floor(offset / itemHeight)); 881 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
882 } else {
883 // If offset exceeds the height of list.
884 var lastHeight = 0;
885 if (this.dataModel.length) {
886 var h = this.getHeightsForIndex_(this.dataModel.length - 1);
887 lastHeight = h.top + h.height;
888 }
889 if (lastHeight < offset)
890 return this.dataModel.length;
891
892 // Estimates index.
893 var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
894 this.dataModel.length - 1);
895 var isIncrementing = this.getItemTop(estimatedIndex) < offset;
896
897 // Searchs the correct index.
898 do {
899 var heights = this.getHeightsForIndex_(estimatedIndex);
900 var top = heights.top;
901 var height = heights.height;
902
903 if (top <= offset && offset <= (top + height))
904 break;
905
906 isIncrementing ? ++estimatedIndex: --estimatedIndex;
907 } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length)
908
909 return estimatedIndex;
785 } 910 }
786 var leadTop = this.getItemTop(leadIndex);
787 // If the given offset is above the lead item, it's also simple.
788 if (offset < leadTop)
789 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
790 // If the lead item contains the given offset, we just return its index.
791 if (offset < leadTop + leadItemHeight)
792 return this.getFirstItemInRow(this.getItemRow(leadIndex));
793 // The given offset must be below the lead item. Adjust and recalculate.
794 offset -= leadItemHeight - itemHeight;
795 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
796 }, 911 },
797 912
798 /** 913 /**
799 * Return the number of items that occupy the range of heights between the 914 * Return the number of items that occupy the range of heights between the
800 * top of the start item and the end offset. 915 * top of the start item and the end offset.
801 * @param {number} startIndex The index of the first visible item. 916 * @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. 917 * @param {number} endOffset The y offset in pixels of the end of the list.
803 * @return {number} The number of list items visible. 918 * @return {number} The number of list items visible.
804 * @private 919 * @private
805 */ 920 */
806 countItemsInRange_: function(startIndex, endOffset) { 921 countItemsInRange_: function(startIndex, endOffset) {
807 var endIndex = this.getIndexForListOffset_(endOffset); 922 var endIndex = this.getIndexForListOffset_(endOffset);
808 return endIndex - startIndex + 1; 923 return endIndex - startIndex + 1;
809 }, 924 },
810 925
811 /** 926 /**
812 * Calculates the number of items fitting in viewport given the index of 927 * 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. 928 * @param {number} scrollTop The scroll top position.
817 * @return {number} The number of items in view port. 929 * @param {number} clientHeight The height of viewport.
930 * @return {{first: number, length: number, last: number}} The index of
931 * first item in view port, The number of items, The item past the last.
818 */ 932 */
819 getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) { 933 getItemsInViewPort: function(scrollTop, clientHeight) {
820 // This is a bit tricky. We take the minimum of the available items to 934 if (this.autoExpands_) {
821 // show and the number we want to show, so as not to go off the end of the 935 return {
822 // list. For the number we want to show, we take the maximum of the number 936 first: 0,
823 // that would fit without a differently-sized lead item, and with one. We 937 length: this.dataModel.length,
824 // do this so that if the size of the lead item changes without a scroll 938 last: this.dataModel.length};
825 // event to trigger redrawing the list, we won't end up with empty space. 939 } else {
826 var clientHeight = this.clientHeight; 940 var firstIndex = this.getIndexForListOffset_(scrollTop);
827 return this.autoExpands_ ? this.dataModel.length : Math.min( 941 var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
828 this.dataModel.length - firstIndex, 942
829 Math.max( 943 return {
830 Math.ceil(clientHeight / itemHeight) + 1, 944 first: firstIndex,
831 this.countItemsInRange_(firstIndex, scrollTop + clientHeight))); 945 length: lastIndex - firstIndex + 1,
946 last: lastIndex + 1};
947 }
832 }, 948 },
833 949
834 /** 950 /**
835 * Adds items to the list and {@code newCachedItems}. 951 * Adds items to the list and {@code newCachedItems}.
836 * @param {number} firstIndex The index of first item, inclusively. 952 * @param {number} firstIndex The index of first item, inclusively.
837 * @param {number} lastIndex The index of last item, exclusively. 953 * @param {number} lastIndex The index of last item, exclusively.
838 * @param {Object.<string, ListItem>} cachedItems Old items cache. 954 * @param {Object.<string, ListItem>} cachedItems Old items cache.
839 * @param {Object.<string, ListItem>} newCachedItems New items cache. 955 * @param {Object.<string, ListItem>} newCachedItems New items cache.
840 */ 956 */
841 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) { 957 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
842 var listItem;
843 var dataModel = this.dataModel; 958 var dataModel = this.dataModel;
844 959
845 window.l = this; 960 window.l = this;
846 for (var y = firstIndex; y < lastIndex; y++) { 961 for (var y = firstIndex; y < lastIndex; y++) {
847 var dataItem = dataModel.item(y); 962 var dataItem = dataModel.item(y);
848 listItem = cachedItems[y] || this.createItem(dataItem); 963 var listItem = cachedItems[y] || this.createItem(dataItem);
849 listItem.listIndex = y; 964 listItem.listIndex = y;
850 this.appendChild(listItem); 965 this.appendChild(listItem);
851 newCachedItems[y] = listItem; 966 newCachedItems[y] = listItem;
852 } 967 }
968
969 // Mesurings must be placed after adding all the elements, to prevent
970 // performance reducing.
971 for (var y = firstIndex; y < lastIndex; y++) {
972 this.cachedItemSizes_[y] = measureItem(this, newCachedItems[y]);
973 }
853 }, 974 },
854 975
855 /** 976 /**
977 * Ensures that all the item sizes in the list have been already cached.
978 */
979 ensureAllItemSizesInCache: function() {
980 var measuringIndexes = [];
981 for (var y = 0; y < this.dataModel.length; y++) {
982 if (!this.cachedItemSizes_[y])
983 measuringIndexes.push(y);
984 }
985
986 var measuringItems = [];
987 // Adds temporary elements.
988 for (var y = 0; y < measuringIndexes.length; y++) {
989 var index = measuringIndexes[y];
990 var dataItem = this.dataModel.item(index);
991 var listItem = this.cachedItems_[index] || this.createItem(dataItem);
992 listItem.listIndex = index;
993 this.appendChild(listItem);
994 this.cachedItems_[index] = listItem;
995 measuringItems.push(listItem);
996 }
997
998 // All mesurings must be placed after adding all the elements, to prevent
999 // performance reducing.
1000 for (var y = 0; y < measuringIndexes.length; y++) {
1001 var index = measuringIndexes[y];
1002 this.cachedItemSizes_[index] = measureItem(this, measuringItems[y]);
1003 }
1004
1005 // Removes all the temprary elements.
1006 for (var y = 0; y < measuringIndexes.length; y++) {
1007 this.removeChild(measuringItems[y]);
1008 }
1009 },
1010
1011 /**
856 * Returns the height of after filler in the list. 1012 * Returns the height of after filler in the list.
857 * @param {number} lastIndex The index of item past the last in viewport. 1013 * @param {number} lastIndex The index of item past the last in viewport.
858 * @param {number} itemHeight The height of the item. 1014 * @param {number} itemHeight The height of the item.
859 * @return {number} The height of after filler. 1015 * @return {number} The height of after filler.
860 */ 1016 */
861 getAfterFillerHeight: function(lastIndex, itemHeight) { 1017 getAfterFillerHeight: function(lastIndex) {
862 return (this.dataModel.length - lastIndex) * itemHeight; 1018 if (this.fixedHeight_) {
1019 var itemHeight = this.getDefaultItemHeight_();
1020 var afterFillerHeight =
1021 (this.dataModel.length - lastIndex) * itemHeight;
1022
1023 var sm = this.selectionModel;
1024 var leadIndex = sm.leadIndex;
1025 if (leadIndex >= lastIndex)
1026 afterFillerHeight += this.leadItemHeight - itemHeight;
1027
1028 return afterFillerHeight;
1029 } else {
1030 var height = 0;
1031 for (var i = lastIndex; i < this.dataModel.length; i++)
1032 height += this.getItemHeightByIndex_(i);
1033 return height;
1034 }
863 }, 1035 },
864 1036
865 /** 1037 /**
866 * Redraws the viewport. 1038 * Redraws the viewport.
867 */ 1039 */
868 redraw: function() { 1040 redraw: function() {
869 if (this.batchCount_ != 0) 1041 if (this.batchCount_ != 0)
870 return; 1042 return;
871 1043
872 var dataModel = this.dataModel; 1044 var dataModel = this.dataModel;
873 if (!dataModel) { 1045 if (!dataModel) {
1046 this.cachedItems_ = {};
1047 this.firstIndex_ = 0;
1048 this.lastIndex_ = 0;
1049 this.remainingSpace_ = true;
874 this.textContent = ''; 1050 this.textContent = '';
875 return; 1051 return;
876 } 1052 }
877 1053
878 var scrollTop = this.scrollTop; 1054 // Store all the item sizes into the cache in advance, to prevent
879 var clientHeight = this.clientHeight; 1055 // interleave measuring with mutating dom.
880 1056 this.ensureAllItemSizesInCache();
881 var itemHeight = this.getItemHeight_();
882 1057
883 // We cache the list items since creating the DOM nodes is the most 1058 // We cache the list items since creating the DOM nodes is the most
884 // expensive part of redrawing. 1059 // expensive part of redrawing.
885 var cachedItems = this.cachedItems_ || {}; 1060 var cachedItems = this.cachedItems_ || {};
886 var newCachedItems = {}; 1061 var newCachedItems = {};
887 1062
888 var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top; 1063 var autoExpands = this.autoExpands_;
1064 var scrollTop = this.scrollTop;
1065 var clientHeight = this.clientHeight;
889 1066
890 var autoExpands = this.autoExpands_; 1067 var lastItemHeights = this.getHeightsForIndex_(dataModel.length - 1);
891 var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop); 1068 var desiredScrollHeight = lastItemHeights.top + lastItemHeights.height;
892 var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex,
893 scrollTop);
894 var lastIndex = firstIndex + itemsInViewPort;
895 1069
1070 var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
1071 // Draws the hidden rows just above/below the viewport to prevent
1072 // flashing in scroll.
1073 var firstIndex = Math.max(0, itemsInViewPort.first - 1);
1074 var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
1075
1076 var beforeFillerHeight =
1077 this.autoExpands ? 0 : this.getItemTop(firstIndex);
1078 var afterFillerHeight =
1079 this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
1080
1081 // Clear list and Adds elements on list.
896 this.textContent = ''; 1082 this.textContent = '';
897 1083
898 this.beforeFiller_.style.height = 1084 this.beforeFiller_.style.height = beforeFillerHeight + 'px';
899 this.getHeightsForIndex_(firstIndex).top + 'px';
900 this.appendChild(this.beforeFiller_); 1085 this.appendChild(this.beforeFiller_);
901 1086
1087 this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
1088
1089 this.afterFiller_.style.height = afterFillerHeight + 'px';
1090 this.appendChild(this.afterFiller_);
1091
902 var sm = this.selectionModel; 1092 var sm = this.selectionModel;
903 var leadIndex = sm.leadIndex; 1093 var leadIndex = sm.leadIndex;
904 1094
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 1095 // 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. 1096 // items, in case they force relayout in response to these events.
915 var listItem = null; 1097 var listItem = null;
916 if (newCachedItems[leadIndex]) 1098 if (leadIndex != -1 && newCachedItems[leadIndex])
917 newCachedItems[leadIndex].lead = true; 1099 newCachedItems[leadIndex].lead = true;
918 for (var y = firstIndex; y < lastIndex; y++) { 1100 for (var y = firstIndex; y < lastIndex; y++) {
919 if (sm.getIndexSelected(y)) 1101 if (sm.getIndexSelected(y))
920 newCachedItems[y].selected = true; 1102 newCachedItems[y].selected = true;
921 else if (y != leadIndex) 1103 else if (y != leadIndex)
922 listItem = newCachedItems[y]; 1104 listItem = newCachedItems[y];
923 } 1105 }
924 1106
925 this.scrollTop = scrollTop; 1107 this.scrollTop = scrollTop;
926 1108
927 this.firstIndex_ = firstIndex; 1109 this.firstIndex_ = firstIndex;
928 this.lastIndex_ = lastIndex; 1110 this.lastIndex_ = lastIndex;
929 1111
1112 this.remainingSpace_ = itemsInViewPort.last > dataModel.length;
930 this.cachedItems_ = newCachedItems; 1113 this.cachedItems_ = newCachedItems;
931 1114
932 // Measure again in case the item height has changed due to a page zoom. 1115 // Measure again in case the item height has changed due to a page zoom.
933 // 1116 //
934 // The measure above is only done the first time but this measure is done 1117 // 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 1118 // 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). 1119 // 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 1120 // By using a timeout the measuring will happen later when there is no
938 // need for a reflow. 1121 // need for a reflow.
939 if (listItem) { 1122 if (listItem && this.fixedHeight_) {
940 var list = this; 1123 var list = this;
941 window.setTimeout(function() { 1124 window.setTimeout(function() {
942 if (listItem.parentNode == list) { 1125 if (listItem.parentNode == list) {
943 list.measured_ = measureItem(list, listItem); 1126 list.measured_ = measureItem(list, listItem);
944 } 1127 }
945 }); 1128 });
946 } 1129 }
947 }, 1130 },
948 1131
949 /** 1132 /**
950 * Invalidates list by removing cached items. 1133 * Invalidates list by removing cached items.
951 */ 1134 */
952 invalidate: function() { 1135 invalidate: function() {
953 this.cachedItems_ = {}; 1136 this.cachedItems_ = {};
1137 this.cachedItemSized_ = {};
954 }, 1138 },
955 1139
956 /** 1140 /**
957 * Redraws a single item. 1141 * Redraws a single item.
958 * @param {number} index The row index to redraw. 1142 * @param {number} index The row index to redraw.
959 */ 1143 */
960 redrawItem: function(index) { 1144 redrawItem: function(index) {
961 if (index >= this.firstIndex_ && index < this.lastIndex_) { 1145 if (index >= this.firstIndex_ &&
1146 (index < this.lastIndex_ || this.remainingSpace_)) {
962 delete this.cachedItems_[index]; 1147 delete this.cachedItems_[index];
963 this.redraw(); 1148 this.redraw();
964 } 1149 }
965 }, 1150 },
966 1151
967 /** 1152 /**
968 * Called when a list item is activated, currently only by a double click 1153 * Called when a list item is activated, currently only by a double click
969 * event. 1154 * event.
970 * @param {number} index The index of the activated item. 1155 * @param {number} index The index of the activated item.
971 */ 1156 */
972 activateItemAtIndex: function(index) { 1157 activateItemAtIndex: function(index) {
973 }, 1158 },
974 }; 1159 };
975 1160
976 cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR); 1161 cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
977 1162
978 /** 1163 /**
979 * Whether the list or one of its descendents has focus. This is necessary 1164 * 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 1165 * 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 1166 * 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. 1167 * that point even though it doesn't actually have the page focus.
983 */ 1168 */
984 cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR); 1169 cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
985 1170
986 return { 1171 return {
987 List: List 1172 List: List
988 } 1173 }
989 }); 1174 });
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