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
3 // found in the LICENSE file.
5 // require: array_data_model.js
6 // require: list_selection_model.js
7 // require: list_selection_controller.js
8 // require: list_item.js
11 * @fileoverview This implements a list control.
14 cr.define('cr.ui', function() {
15 const ListSelectionModel = cr.ui.ListSelectionModel;
16 const ListSelectionController = cr.ui.ListSelectionController;
17 const ArrayDataModel = cr.ui.ArrayDataModel;
20 * Whether a mouse event is inside the element viewport. This will return
21 * false if the mouseevent was generated over a border or a scrollbar.
22 * @param {!HTMLElement} el The element to test the event with.
23 * @param {!Event} e The mouse event.
24 * @param {boolean} Whether the mouse event was inside the viewport.
26 function inViewport(el, e) {
27 var rect = el.getBoundingClientRect();
30 return x >= rect.left + el.clientLeft &&
31 x < rect.left + el.clientLeft + el.clientWidth &&
32 y >= rect.top + el.clientTop &&
33 y < rect.top + el.clientTop + el.clientHeight;
37 * Creates an item (dataModel.item(0)) and measures its height.
38 * @param {!List} list The list to create the item for.
39 * @param {ListItem=} opt_item The list item to use to do the measuring. If
40 * this is not provided an item will be created based on the first value
42 * @return {{height: number, marginVertical: number, width: number,
43 * marginHorizontal: number}} The height and width of the item, taking
44 * margins into account, and the height and width of the margins
47 function measureItem(list, opt_item) {
48 var dataModel = list.dataModel;
49 if (!dataModel || !dataModel.length)
51 var item = opt_item || list.createItem(dataModel.item(0));
53 list.appendChild(item);
55 var rect = item.getBoundingClientRect();
56 var cs = getComputedStyle(item);
57 var mt = parseFloat(cs.marginTop);
58 var mb = parseFloat(cs.marginBottom);
59 var ml = parseFloat(cs.marginLeft);
60 var mr = parseFloat(cs.marginRight);
66 // Handle margin collapsing.
67 if (mt < 0 && mb < 0) {
68 mv = Math.min(mt, mb);
69 } else if (mt >= 0 && mb >= 0) {
70 mv = Math.max(mt, mb);
76 if (ml < 0 && mr < 0) {
77 mh = Math.min(ml, mr);
78 } else if (ml >= 0 && mr >= 0) {
79 mh = Math.max(ml, mr);
86 list.removeChild(item);
88 height: Math.max(0, h), marginVertical: mv,
89 width: Math.max(0, w), marginHorizontal: mh};
92 function getComputedStyle(el) {
93 return el.ownerDocument.defaultView.getComputedStyle(el);
97 * Creates a new list element.
98 * @param {Object=} opt_propertyBag Optional properties.
100 * @extends {HTMLUListElement}
102 var List = cr.ui.define('list');
105 __proto__: HTMLUListElement.prototype,
108 * Measured size of list items. This is lazily calculated the first time it
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
112 * @type {{height: number, marginVertical: number, width: number,
113 * marginHorizontal: number}}
116 measured_: undefined,
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.
130 * Whether or not the list is autoexpanding. If true, the list resizes
131 * its height to accomadate all children.
138 * Function used to create grid items.
139 * @type {function(): !ListItem}
142 itemConstructor_: cr.ui.ListItem,
145 * Function used to create grid items.
146 * @type {function(): !ListItem}
148 get itemConstructor() {
149 return this.itemConstructor_;
151 set itemConstructor(func) {
152 if (func != this.itemConstructor_) {
153 this.itemConstructor_ = func;
154 this.cachedItems_ = {};
162 * The data model driving the list.
163 * @type {ArrayDataModel}
165 set dataModel(dataModel) {
166 if (this.dataModel_ != dataModel) {
167 if (!this.boundHandleDataModelPermuted_) {
168 this.boundHandleDataModelPermuted_ =
169 this.handleDataModelPermuted_.bind(this);
170 this.boundHandleDataModelChange_ =
171 this.handleDataModelChange_.bind(this);
174 if (this.dataModel_) {
175 this.dataModel_.removeEventListener(
177 this.boundHandleDataModelPermuted_);
178 this.dataModel_.removeEventListener('change',
179 this.boundHandleDataModelChange_);
182 this.dataModel_ = dataModel;
184 this.cachedItems_ = {};
185 this.selectionModel.clear();
187 this.selectionModel.adjustLength(dataModel.length);
189 if (this.dataModel_) {
190 this.dataModel_.addEventListener(
192 this.boundHandleDataModelPermuted_);
193 this.dataModel_.addEventListener('change',
194 this.boundHandleDataModelChange_);
202 return this.dataModel_;
206 * The selection model to use.
207 * @type {cr.ui.ListSelectionModel}
209 get selectionModel() {
210 return this.selectionModel_;
212 set selectionModel(sm) {
213 var oldSm = this.selectionModel_;
217 if (!this.boundHandleOnChange_) {
218 this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
219 this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this);
223 oldSm.removeEventListener('change', this.boundHandleOnChange_);
224 oldSm.removeEventListener('leadIndexChange',
225 this.boundHandleLeadChange_);
228 this.selectionModel_ = sm;
229 this.selectionController_ = this.createSelectionController(sm);
232 sm.addEventListener('change', this.boundHandleOnChange_);
233 sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
238 * Whether or not the list auto-expands.
242 return this.autoExpands_;
244 set autoExpands(autoExpands) {
245 if (this.autoExpands_ == autoExpands)
247 this.autoExpands_ = autoExpands;
252 * Convenience alias for selectionModel.selectedItem
253 * @type {cr.ui.ListItem}
256 var dataModel = this.dataModel;
258 var index = this.selectionModel.selectedIndex;
260 return dataModel.item(index);
264 set selectedItem(selectedItem) {
265 var dataModel = this.dataModel;
267 var index = this.dataModel.indexOf(selectedItem);
268 this.selectionModel.selectedIndex = index;
273 * The height of the lead item.
274 * If set to 0, resets to the same height as other items.
277 get leadItemHeight() {
278 return this.leadItemHeight_ || this.getItemHeight_();
280 set leadItemHeight(height) {
282 var size = this.getItemSize_();
283 this.leadItemHeight_ = Math.max(0, height + size.marginVertical);
285 this.leadItemHeight_ = 0;
290 * Convenience alias for selectionModel.selectedItems
291 * @type {!Array<cr.ui.ListItem>}
293 get selectedItems() {
294 var indexes = this.selectionModel.selectedIndexes;
295 var dataModel = this.dataModel;
297 return indexes.map(function(i) {
298 return dataModel.item(i);
305 * The HTML elements representing the items. This is just all the list item
306 * children but subclasses may override this to filter out certain elements.
307 * @type {HTMLCollection}
310 return Array.prototype.filter.call(this.children, function(child) {
311 return !child.classList.contains('spacer');
318 * When making a lot of updates to the list, the code could be wrapped in
319 * the startBatchUpdates and finishBatchUpdates to increase performance. Be
320 * sure that the code will not return without calling endBatchUpdates or the
321 * list will not be correctly updated.
323 startBatchUpdates: function() {
328 * See startBatchUpdates.
330 endBatchUpdates: function() {
332 if (this.batchCount_ == 0)
337 * Initializes the element.
339 decorate: function() {
341 this.beforeFiller_ = this.ownerDocument.createElement('div');
342 this.afterFiller_ = this.ownerDocument.createElement('div');
343 this.beforeFiller_.className = 'spacer';
344 this.afterFiller_.className = 'spacer';
345 this.appendChild(this.beforeFiller_);
346 this.appendChild(this.afterFiller_);
348 var length = this.dataModel ? this.dataModel.length : 0;
349 this.selectionModel = new ListSelectionModel(length);
351 this.addEventListener('dblclick', this.handleDoubleClick_);
352 this.addEventListener('mousedown', this.handleMouseDownUp_);
353 this.addEventListener('mouseup', this.handleMouseDownUp_);
354 this.addEventListener('keydown', this.handleKeyDown);
355 this.addEventListener('focus', this.handleElementFocus_, true);
356 this.addEventListener('blur', this.handleElementBlur_, true);
357 this.addEventListener('scroll', this.redraw.bind(this));
358 this.setAttribute('role', 'listbox');
360 // Make list focusable
361 if (!this.hasAttribute('tabindex'))
366 * @return {number} The height of an item, measuring it if necessary.
369 getItemHeight_: function() {
370 return this.getItemSize_().height;
374 * @return {number} The width of an item, measuring it if necessary.
377 getItemWidth_: function() {
378 return this.getItemSize_().width;
382 * @return {{height: number, width: number}} The height and width
383 * of an item, measuring it if necessary.
386 getItemSize_: function() {
387 if (!this.measured_ || !this.measured_.height) {
388 this.measured_ = measureItem(this);
390 return this.measured_;
394 * Callback for the double click event.
395 * @param {Event} e The mouse event object.
398 handleDoubleClick_: function(e) {
402 var target = this.getListItemAncestor(e.target);
404 this.activateItemAtIndex(this.getIndexOfListItem(target));
408 * Callback for mousedown and mouseup events.
409 * @param {Event} e The mouse event object.
412 handleMouseDownUp_: function(e) {
416 var target = e.target;
418 // If the target was this element we need to make sure that the user did
419 // not click on a border or a scrollbar.
420 if (target == this && !inViewport(target, e))
423 target = this.getListItemAncestor(target);
425 var index = target ? this.getIndexOfListItem(target) : -1;
426 this.selectionController_.handleMouseDownUp(e, index);
430 * Called when an element in the list is focused. Marks the list as having
431 * a focused element, and dispatches an event if it didn't have focus.
432 * @param {Event} e The focus event.
435 handleElementFocus_: function(e) {
436 if (!this.hasElementFocus) {
437 this.hasElementFocus = true;
438 // Force styles based on hasElementFocus to take effect.
439 this.forceRepaint_();
444 * Called when an element in the list is blurred. If focus moves outside
445 * the list, marks the list as no longer having focus and dispatches an
447 * @param {Event} e The blur event.
450 handleElementBlur_: function(e) {
451 // When the blur event happens we do not know who is getting focus so we
452 // delay this a bit until we know if the new focus node is outside the
455 var doc = e.target.ownerDocument;
456 window.setTimeout(function() {
457 var activeElement = doc.activeElement;
458 if (!list.contains(activeElement)) {
459 list.hasElementFocus = false;
460 // Force styles based on hasElementFocus to take effect.
461 list.forceRepaint_();
467 * Forces a repaint of the list. Changing custom attributes, even if there
468 * are style rules depending on them, doesn't cause a repaint
469 * (<https://bugs.webkit.org/show_bug.cgi?id=12519>), so this can be called
470 * to force the list to repaint.
473 forceRepaint_: function(e) {
474 var dummyElement = document.createElement('div');
475 this.appendChild(dummyElement);
476 this.removeChild(dummyElement);
480 * Returns the list item element containing the given element, or null if
481 * it doesn't belong to any list item element.
482 * @param {HTMLElement} element The element.
483 * @return {ListItem} The list item containing |element|, or null.
485 getListItemAncestor: function(element) {
486 var container = element;
487 while (container && container.parentNode != this) {
488 container = container.parentNode;
494 * Handle a keydown event.
495 * @param {Event} e The keydown event.
496 * @return {boolean} Whether the key event was handled.
498 handleKeyDown: function(e) {
502 return this.selectionController_.handleKeyDown(e);
506 * Callback from the selection model. We dispatch {@code change} events
507 * when the selection changes.
508 * @param {!Event} e Event with change info.
511 handleOnChange_: function(ce) {
512 ce.changes.forEach(function(change) {
513 var listItem = this.getListItemByIndex(change.index);
515 listItem.selected = change.selected;
518 cr.dispatchSimpleEvent(this, 'change');
522 * Handles a change of the lead item from the selection model.
523 * @property {Event} pe The property change event.
526 handleLeadChange_: function(pe) {
528 if (pe.oldValue != -1) {
529 if ((element = this.getListItemByIndex(pe.oldValue)))
530 element.lead = false;
533 if (pe.newValue != -1) {
534 if ((element = this.getListItemByIndex(pe.newValue)))
536 this.scrollIndexIntoView(pe.newValue);
537 // If the lead item has a different height than other items, then we
538 // may run into a problem that requires a second attempt to scroll
539 // it into view. The first scroll attempt will trigger a redraw,
540 // which will clear out the list and repopulate it with new items.
541 // During the redraw, the list may shrink temporarily, which if the
542 // lead item is the last item, will move the scrollTop up since it
543 // cannot extend beyond the end of the list. (Sadly, being scrolled to
544 // the bottom of the list is not "sticky.") So, we set a timeout to
545 // rescroll the list after this all gets sorted out. This is perhaps
546 // not the most elegant solution, but no others seem obvious.
548 window.setTimeout(function() {
549 self.scrollIndexIntoView(pe.newValue);
555 * This handles data model 'permuted' event.
556 * this event is dispatched as a part of sort or splice.
558 * - adjust the cache.
559 * - adjust selection.
561 * - scroll the list to show selection.
562 * It is important that the cache adjustment happens before selection model
564 * @param {Event} e The 'permuted' event.
566 handleDataModelPermuted_: function(e) {
567 var newCachedItems = {};
568 for (var index in this.cachedItems_) {
569 if (e.permutation[index] != -1)
570 newCachedItems[e.permutation[index]] = this.cachedItems_[index];
572 delete this.cachedItems_[index];
574 this.cachedItems_ = newCachedItems;
576 this.startBatchUpdates();
578 var sm = this.selectionModel;
579 sm.adjustLength(e.newLength);
580 sm.adjustToReordering(e.permutation);
582 this.endBatchUpdates();
584 if (sm.leadIndex != -1)
585 this.scrollIndexIntoView(sm.leadIndex);
588 handleDataModelChange_: function(e) {
589 if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) {
590 if (this.cachedItems_[e.index])
591 delete this.cachedItems_[e.index];
597 * @param {number} index The index of the item.
598 * @return {number} The top position of the item inside the list, not taking
599 * into account lead item. May vary in the case of multiple columns.
601 getItemTop: function(index) {
602 return index * this.getItemHeight_();
606 * @param {number} index The index of the item.
607 * @return {number} The row of the item. May vary in the case
608 * of multiple columns.
610 getItemRow: function(index) {
615 * @param {number} row The row.
616 * @return {number} The index of the first item in the row.
618 getFirstItemInRow: function(row) {
623 * Ensures that a given index is inside the viewport.
624 * @param {number} index The index of the item to scroll into view.
625 * @return {boolean} Whether any scrolling was needed.
627 scrollIndexIntoView: function(index) {
628 var dataModel = this.dataModel;
629 if (!dataModel || index < 0 || index >= dataModel.length)
632 var itemHeight = this.getItemHeight_();
633 var scrollTop = this.scrollTop;
634 var top = this.getItemTop(index);
635 var leadIndex = this.selectionModel.leadIndex;
637 // Adjust for the lead item if it is above the given index.
638 if (leadIndex > -1 && leadIndex < index)
639 top += this.leadItemHeight - itemHeight;
640 else if (leadIndex == index)
641 itemHeight = this.leadItemHeight;
643 if (top < scrollTop) {
644 this.scrollTop = top;
647 var clientHeight = this.clientHeight;
648 var cs = getComputedStyle(this);
649 var paddingY = parseInt(cs.paddingTop, 10) +
650 parseInt(cs.paddingBottom, 10);
652 if (top + itemHeight > scrollTop + clientHeight - paddingY) {
653 this.scrollTop = top + itemHeight - clientHeight + paddingY;
662 * @return {!ClientRect} The rect to use for the context menu.
664 getRectForContextMenu: function() {
665 // TODO(arv): Add trait support so we can share more code between trees
667 var index = this.selectionModel.selectedIndex;
668 var el = this.getListItemByIndex(index);
670 return el.getBoundingClientRect();
671 return this.getBoundingClientRect();
675 * Takes a value from the data model and finds the associated list item.
676 * @param {*} value The value in the data model that we want to get the list
678 * @return {ListItem} The first found list item or null if not found.
680 getListItem: function(value) {
681 var dataModel = this.dataModel;
683 var index = dataModel.indexOf(value);
684 return this.getListItemByIndex(index);
690 * Find the list item element at the given index.
691 * @param {number} index The index of the list item to get.
692 * @return {ListItem} The found list item or null if not found.
694 getListItemByIndex: function(index) {
695 return this.cachedItems_[index] || null;
699 * Find the index of the given list item element.
700 * @param {ListItem} item The list item to get the index of.
701 * @return {number} The index of the list item, or -1 if not found.
703 getIndexOfListItem: function(item) {
704 var index = item.listIndex;
705 if (this.cachedItems_[index] == item) {
712 * Creates a new list item.
713 * @param {*} value The value to use for the item.
714 * @return {!ListItem} The newly created list item.
716 createItem: function(value) {
717 var item = new this.itemConstructor_(value);
719 if (typeof item.decorate == 'function')
725 * Creates the selection controller to use internally.
726 * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
727 * @return {!cr.ui.ListSelectionController} The newly created selection
730 createSelectionController: function(sm) {
731 return new ListSelectionController(sm);
735 * Return the heights (in pixels) of the top of the given item index within
736 * the list, and the height of the given item itself, accounting for the
737 * possibility that the lead item may be a different height.
738 * @param {number} index The index to find the top height of.
739 * @return {{top: number, height: number}} The heights for the given index.
742 getHeightsForIndex_: function(index) {
743 var itemHeight = this.getItemHeight_();
744 var top = this.getItemTop(index);
745 if (this.selectionModel.leadIndex > -1 &&
746 this.selectionModel.leadIndex < index) {
747 top += this.leadItemHeight - itemHeight;
748 } else if (this.selectionModel.leadIndex == index) {
749 itemHeight = this.leadItemHeight;
751 return {top: top, height: itemHeight};
755 * Find the index of the list item containing the given y offset (measured
756 * in pixels from the top) within the list. In the case of multiple columns,
757 * returns the first index in the row.
758 * @param {number} offset The y offset in pixels to get the index of.
759 * @return {number} The index of the list item.
762 getIndexForListOffset_: function(offset) {
763 var itemHeight = this.getItemHeight_();
764 var leadIndex = this.selectionModel.leadIndex;
765 var leadItemHeight = this.leadItemHeight;
766 if (leadIndex < 0 || leadItemHeight == itemHeight) {
767 // Simple case: no lead item or lead item height is not different.
768 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
770 var leadTop = this.getItemTop(leadIndex);
771 // If the given offset is above the lead item, it's also simple.
772 if (offset < leadTop)
773 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
774 // If the lead item contains the given offset, we just return its index.
775 if (offset < leadTop + leadItemHeight)
776 return this.getFirstItemInRow(this.getItemRow(leadIndex));
777 // The given offset must be below the lead item. Adjust and recalculate.
778 offset -= leadItemHeight - itemHeight;
779 return this.getFirstItemInRow(Math.floor(offset / itemHeight));
783 * Return the number of items that occupy the range of heights between the
784 * top of the start item and the end offset.
785 * @param {number} startIndex The index of the first visible item.
786 * @param {number} endOffset The y offset in pixels of the end of the list.
787 * @return {number} The number of list items visible.
790 countItemsInRange_: function(startIndex, endOffset) {
791 var endIndex = this.getIndexForListOffset_(endOffset);
792 return endIndex - startIndex + 1;
796 * Calculates the number of items fitting in viewport given the index of
797 * first item and heights.
798 * @param {number} itemHeight The height of the item.
799 * @param {number} firstIndex Index of the first item in viewport.
800 * @param {number} scrollTop The scroll top position.
801 * @return {number} The number of items in view port.
803 getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) {
804 // This is a bit tricky. We take the minimum of the available items to
805 // show and the number we want to show, so as not to go off the end of the
806 // list. For the number we want to show, we take the maximum of the number
807 // that would fit without a differently-sized lead item, and with one. We
808 // do this so that if the size of the lead item changes without a scroll
809 // event to trigger redrawing the list, we won't end up with empty space.
810 var clientHeight = this.clientHeight;
811 return this.autoExpands_ ? this.dataModel.length : Math.min(
812 this.dataModel.length - firstIndex,
814 Math.ceil(clientHeight / itemHeight) + 1,
815 this.countItemsInRange_(firstIndex, scrollTop + clientHeight)));
819 * Adds items to the list and {@code newCachedItems}.
820 * @param {number} firstIndex The index of first item, inclusively.
821 * @param {number} lastIndex The index of last item, exclusively.
822 * @param {Object.<string, ListItem>} cachedItems Old items cache.
823 * @param {Object.<string, ListItem>} newCachedItems New items cache.
825 addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
827 var dataModel = this.dataModel;
830 for (var y = firstIndex; y < lastIndex; y++) {
831 var dataItem = dataModel.item(y);
832 listItem = cachedItems[y] || this.createItem(dataItem);
833 listItem.listIndex = y;
834 this.appendChild(listItem);
835 newCachedItems[y] = listItem;
840 * Returns the height of after filler in the list.
841 * @param {number} lastIndex The index of item past the last in viewport.
842 * @param {number} itemHeight The height of the item.
843 * @return {number} The height of after filler.
845 getAfterFillerHeight: function(lastIndex, itemHeight) {
846 return (this.dataModel.length - lastIndex) * itemHeight;
850 * Redraws the viewport.
853 if (this.batchCount_ != 0)
856 var dataModel = this.dataModel;
858 this.textContent = '';
862 var scrollTop = this.scrollTop;
863 var clientHeight = this.clientHeight;
865 var itemHeight = this.getItemHeight_();
867 // We cache the list items since creating the DOM nodes is the most
868 // expensive part of redrawing.
869 var cachedItems = this.cachedItems_ || {};
870 var newCachedItems = {};
872 var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top;
874 var autoExpands = this.autoExpands_;
875 var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop);
876 var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex,
878 var lastIndex = firstIndex + itemsInViewPort;
880 this.textContent = '';
882 this.beforeFiller_.style.height =
883 this.getHeightsForIndex_(firstIndex).top + 'px';
884 this.appendChild(this.beforeFiller_);
886 var sm = this.selectionModel;
887 var leadIndex = sm.leadIndex;
889 this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
891 var afterFillerHeight = this.getAfterFillerHeight(lastIndex, itemHeight);
892 if (leadIndex >= lastIndex)
893 afterFillerHeight += this.leadItemHeight - itemHeight;
894 this.afterFiller_.style.height = afterFillerHeight + 'px';
895 this.appendChild(this.afterFiller_);
897 // We don't set the lead or selected properties until after adding all
898 // items, in case they force relayout in response to these events.
900 if (newCachedItems[leadIndex])
901 newCachedItems[leadIndex].lead = true;
902 for (var y = firstIndex; y < lastIndex; y++) {
903 if (sm.getIndexSelected(y))
904 newCachedItems[y].selected = true;
905 else if (y != leadIndex)
906 listItem = newCachedItems[y];
909 this.firstIndex_ = firstIndex;
910 this.lastIndex_ = lastIndex;
912 this.cachedItems_ = newCachedItems;
914 // Measure again in case the item height has changed due to a page zoom.
916 // The measure above is only done the first time but this measure is done
917 // after every redraw. It is done in a timeout so it will not trigger
918 // a reflow (which made the redraw speed 3 times slower on my system).
919 // By using a timeout the measuring will happen later when there is no
920 // need for a reflow.
923 window.setTimeout(function() {
924 if (listItem.parentNode == list) {
925 list.measured_ = measureItem(list, listItem);
932 * Invalidates list by removing cached items.
934 invalidate: function() {
935 this.cachedItems_ = {};
939 * Redraws a single item.
940 * @param {number} index The row index to redraw.
942 redrawItem: function(index) {
943 if (index >= this.firstIndex_ && index < this.lastIndex_) {
944 delete this.cachedItems_[index];
950 * Called when a list item is activated, currently only by a double click
952 * @param {number} index The index of the activated item.
954 activateItemAtIndex: function(index) {
958 cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
961 * Whether the list or one of its descendents has focus. This is necessary
962 * because list items can contain controls that can be focused, and for some
963 * purposes (e.g., styling), the list can still be conceptually focused at
964 * that point even though it doesn't actually have the page focus.
966 cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);