OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 }); |
OLD | NEW |