| Index: chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js
|
| diff --git a/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..291d5ff81284abd6ca37eb45410e12437b6bb14e
|
| --- /dev/null
|
| +++ b/chrome/common/extensions/docs/examples/extensions/plugin_settings/domui/js/cr/ui/list.js
|
| @@ -0,0 +1,971 @@
|
| +// Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +// require: array_data_model.js
|
| +// require: list_selection_model.js
|
| +// require: list_selection_controller.js
|
| +// require: list_item.js
|
| +
|
| +/**
|
| + * @fileoverview This implements a list control.
|
| + */
|
| +
|
| +cr.define('cr.ui', function() {
|
| + const ListSelectionModel = cr.ui.ListSelectionModel;
|
| + const ListSelectionController = cr.ui.ListSelectionController;
|
| + const ArrayDataModel = cr.ui.ArrayDataModel;
|
| +
|
| + /**
|
| + * Whether a mouse event is inside the element viewport. This will return
|
| + * false if the mouseevent was generated over a border or a scrollbar.
|
| + * @param {!HTMLElement} el The element to test the event with.
|
| + * @param {!Event} e The mouse event.
|
| + * @param {boolean} Whether the mouse event was inside the viewport.
|
| + */
|
| + function inViewport(el, e) {
|
| + var rect = el.getBoundingClientRect();
|
| + var x = e.clientX;
|
| + var y = e.clientY;
|
| + return x >= rect.left + el.clientLeft &&
|
| + x < rect.left + el.clientLeft + el.clientWidth &&
|
| + y >= rect.top + el.clientTop &&
|
| + y < rect.top + el.clientTop + el.clientHeight;
|
| + }
|
| +
|
| + /**
|
| + * Creates an item (dataModel.item(0)) and measures its height.
|
| + * @param {!List} list The list to create the item for.
|
| + * @param {ListItem=} opt_item The list item to use to do the measuring. If
|
| + * this is not provided an item will be created based on the first value
|
| + * in the model.
|
| + * @return {{height: number, marginVertical: number, width: number,
|
| + * marginHorizontal: number}} The height and width of the item, taking
|
| + * margins into account, and the height and width of the margins
|
| + * themselves.
|
| + */
|
| + function measureItem(list, opt_item) {
|
| + var dataModel = list.dataModel;
|
| + if (!dataModel || !dataModel.length)
|
| + return 0;
|
| + var item = opt_item || list.createItem(dataModel.item(0));
|
| + if (!opt_item)
|
| + list.appendChild(item);
|
| +
|
| + var rect = item.getBoundingClientRect();
|
| + var cs = getComputedStyle(item);
|
| + var mt = parseFloat(cs.marginTop);
|
| + var mb = parseFloat(cs.marginBottom);
|
| + var ml = parseFloat(cs.marginLeft);
|
| + var mr = parseFloat(cs.marginRight);
|
| + var h = rect.height;
|
| + var w = rect.width;
|
| + var mh = 0;
|
| + var mv = 0;
|
| +
|
| + // Handle margin collapsing.
|
| + if (mt < 0 && mb < 0) {
|
| + mv = Math.min(mt, mb);
|
| + } else if (mt >= 0 && mb >= 0) {
|
| + mv = Math.max(mt, mb);
|
| + } else {
|
| + mv = mt + mb;
|
| + }
|
| + h += mv;
|
| +
|
| + if (ml < 0 && mr < 0) {
|
| + mh = Math.min(ml, mr);
|
| + } else if (ml >= 0 && mr >= 0) {
|
| + mh = Math.max(ml, mr);
|
| + } else {
|
| + mh = ml + mr;
|
| + }
|
| + w += mh;
|
| +
|
| + if (!opt_item)
|
| + list.removeChild(item);
|
| + return {
|
| + height: Math.max(0, h), marginVertical: mv,
|
| + width: Math.max(0, w), marginHorizontal: mh};
|
| + }
|
| +
|
| + function getComputedStyle(el) {
|
| + return el.ownerDocument.defaultView.getComputedStyle(el);
|
| + }
|
| +
|
| + /**
|
| + * Creates a new list element.
|
| + * @param {Object=} opt_propertyBag Optional properties.
|
| + * @constructor
|
| + * @extends {HTMLUListElement}
|
| + */
|
| + var List = cr.ui.define('list');
|
| +
|
| + List.prototype = {
|
| + __proto__: HTMLUListElement.prototype,
|
| +
|
| + /**
|
| + * Measured size of list items. This is lazily calculated the first time it
|
| + * is needed. Note that lead item is allowed to have a different height, to
|
| + * accommodate lists where a single item at a time can be expanded to show
|
| + * more detail.
|
| + * @type {{height: number, marginVertical: number, width: number,
|
| + * marginHorizontal: number}}
|
| + * @private
|
| + */
|
| + measured_: undefined,
|
| +
|
| + /**
|
| + * The height of the lead item, which is allowed to have a different height
|
| + * than other list items to accommodate lists where a single item at a time
|
| + * can be expanded to show more detail. It is explicitly set by client code
|
| + * when the height of the lead item is changed with {@code set
|
| + * leadItemHeight}, and presumed equal to {@code itemHeight_} otherwise.
|
| + * @type {number}
|
| + * @private
|
| + */
|
| + leadItemHeight_: 0,
|
| +
|
| + /**
|
| + * Whether or not the list is autoexpanding. If true, the list resizes
|
| + * its height to accomadate all children.
|
| + * @type {boolean}
|
| + * @private
|
| + */
|
| + autoExpands_: false,
|
| +
|
| + /**
|
| + * Function used to create grid items.
|
| + * @type {function(): !ListItem}
|
| + * @private
|
| + */
|
| + itemConstructor_: cr.ui.ListItem,
|
| +
|
| + /**
|
| + * Function used to create grid items.
|
| + * @type {function(): !ListItem}
|
| + */
|
| + get itemConstructor() {
|
| + return this.itemConstructor_;
|
| + },
|
| + set itemConstructor(func) {
|
| + if (func != this.itemConstructor_) {
|
| + this.itemConstructor_ = func;
|
| + this.cachedItems_ = {};
|
| + this.redraw();
|
| + }
|
| + },
|
| +
|
| + dataModel_: null,
|
| +
|
| + /**
|
| + * The data model driving the list.
|
| + * @type {ArrayDataModel}
|
| + */
|
| + set dataModel(dataModel) {
|
| + if (this.dataModel_ != dataModel) {
|
| + if (!this.boundHandleDataModelPermuted_) {
|
| + this.boundHandleDataModelPermuted_ =
|
| + this.handleDataModelPermuted_.bind(this);
|
| + this.boundHandleDataModelChange_ =
|
| + this.handleDataModelChange_.bind(this);
|
| + }
|
| +
|
| + if (this.dataModel_) {
|
| + this.dataModel_.removeEventListener(
|
| + 'permuted',
|
| + this.boundHandleDataModelPermuted_);
|
| + this.dataModel_.removeEventListener('change',
|
| + this.boundHandleDataModelChange_);
|
| + }
|
| +
|
| + this.dataModel_ = dataModel;
|
| +
|
| + this.cachedItems_ = {};
|
| + this.selectionModel.clear();
|
| + if (dataModel)
|
| + this.selectionModel.adjustLength(dataModel.length);
|
| +
|
| + if (this.dataModel_) {
|
| + this.dataModel_.addEventListener(
|
| + 'permuted',
|
| + this.boundHandleDataModelPermuted_);
|
| + this.dataModel_.addEventListener('change',
|
| + this.boundHandleDataModelChange_);
|
| + }
|
| +
|
| + this.redraw();
|
| + }
|
| + },
|
| +
|
| + get dataModel() {
|
| + return this.dataModel_;
|
| + },
|
| +
|
| + /**
|
| + * The selection model to use.
|
| + * @type {cr.ui.ListSelectionModel}
|
| + */
|
| + get selectionModel() {
|
| + return this.selectionModel_;
|
| + },
|
| + set selectionModel(sm) {
|
| + var oldSm = this.selectionModel_;
|
| + if (oldSm == sm)
|
| + return;
|
| +
|
| + if (!this.boundHandleOnChange_) {
|
| + this.boundHandleOnChange_ = this.handleOnChange_.bind(this);
|
| + this.boundHandleLeadChange_ = this.handleLeadChange_.bind(this);
|
| + }
|
| +
|
| + if (oldSm) {
|
| + oldSm.removeEventListener('change', this.boundHandleOnChange_);
|
| + oldSm.removeEventListener('leadIndexChange',
|
| + this.boundHandleLeadChange_);
|
| + }
|
| +
|
| + this.selectionModel_ = sm;
|
| + this.selectionController_ = this.createSelectionController(sm);
|
| +
|
| + if (sm) {
|
| + sm.addEventListener('change', this.boundHandleOnChange_);
|
| + sm.addEventListener('leadIndexChange', this.boundHandleLeadChange_);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Whether or not the list auto-expands.
|
| + * @type {boolean}
|
| + */
|
| + get autoExpands() {
|
| + return this.autoExpands_;
|
| + },
|
| + set autoExpands(autoExpands) {
|
| + if (this.autoExpands_ == autoExpands)
|
| + return;
|
| + this.autoExpands_ = autoExpands;
|
| + this.redraw();
|
| + },
|
| +
|
| + /**
|
| + * Convenience alias for selectionModel.selectedItem
|
| + * @type {cr.ui.ListItem}
|
| + */
|
| + get selectedItem() {
|
| + var dataModel = this.dataModel;
|
| + if (dataModel) {
|
| + var index = this.selectionModel.selectedIndex;
|
| + if (index != -1)
|
| + return dataModel.item(index);
|
| + }
|
| + return null;
|
| + },
|
| + set selectedItem(selectedItem) {
|
| + var dataModel = this.dataModel;
|
| + if (dataModel) {
|
| + var index = this.dataModel.indexOf(selectedItem);
|
| + this.selectionModel.selectedIndex = index;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * The height of the lead item.
|
| + * If set to 0, resets to the same height as other items.
|
| + * @type {number}
|
| + */
|
| + get leadItemHeight() {
|
| + return this.leadItemHeight_ || this.getItemHeight_();
|
| + },
|
| + set leadItemHeight(height) {
|
| + if (height) {
|
| + var size = this.getItemSize_();
|
| + this.leadItemHeight_ = Math.max(0, height + size.marginVertical);
|
| + } else {
|
| + this.leadItemHeight_ = 0;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Convenience alias for selectionModel.selectedItems
|
| + * @type {!Array<cr.ui.ListItem>}
|
| + */
|
| + get selectedItems() {
|
| + var indexes = this.selectionModel.selectedIndexes;
|
| + var dataModel = this.dataModel;
|
| + if (dataModel) {
|
| + return indexes.map(function(i) {
|
| + return dataModel.item(i);
|
| + });
|
| + }
|
| + return [];
|
| + },
|
| +
|
| + /**
|
| + * The HTML elements representing the items. This is just all the list item
|
| + * children but subclasses may override this to filter out certain elements.
|
| + * @type {HTMLCollection}
|
| + */
|
| + get items() {
|
| + return Array.prototype.filter.call(this.children, function(child) {
|
| + return !child.classList.contains('spacer');
|
| + });
|
| + },
|
| +
|
| + batchCount_: 0,
|
| +
|
| + /**
|
| + * When making a lot of updates to the list, the code could be wrapped in
|
| + * the startBatchUpdates and finishBatchUpdates to increase performance. Be
|
| + * sure that the code will not return without calling endBatchUpdates or the
|
| + * list will not be correctly updated.
|
| + */
|
| + startBatchUpdates: function() {
|
| + this.batchCount_++;
|
| + },
|
| +
|
| + /**
|
| + * See startBatchUpdates.
|
| + */
|
| + endBatchUpdates: function() {
|
| + this.batchCount_--;
|
| + if (this.batchCount_ == 0)
|
| + this.redraw();
|
| + },
|
| +
|
| + /**
|
| + * Initializes the element.
|
| + */
|
| + decorate: function() {
|
| + // Add fillers.
|
| + this.beforeFiller_ = this.ownerDocument.createElement('div');
|
| + this.afterFiller_ = this.ownerDocument.createElement('div');
|
| + this.beforeFiller_.className = 'spacer';
|
| + this.afterFiller_.className = 'spacer';
|
| + this.appendChild(this.beforeFiller_);
|
| + this.appendChild(this.afterFiller_);
|
| +
|
| + var length = this.dataModel ? this.dataModel.length : 0;
|
| + this.selectionModel = new ListSelectionModel(length);
|
| +
|
| + this.addEventListener('dblclick', this.handleDoubleClick_);
|
| + this.addEventListener('mousedown', this.handleMouseDownUp_);
|
| + this.addEventListener('mouseup', this.handleMouseDownUp_);
|
| + this.addEventListener('keydown', this.handleKeyDown);
|
| + this.addEventListener('focus', this.handleElementFocus_, true);
|
| + this.addEventListener('blur', this.handleElementBlur_, true);
|
| + this.addEventListener('scroll', this.redraw.bind(this));
|
| + this.setAttribute('role', 'listbox');
|
| +
|
| + // Make list focusable
|
| + if (!this.hasAttribute('tabindex'))
|
| + this.tabIndex = 0;
|
| + },
|
| +
|
| + /**
|
| + * @return {number} The height of an item, measuring it if necessary.
|
| + * @private
|
| + */
|
| + getItemHeight_: function() {
|
| + return this.getItemSize_().height;
|
| + },
|
| +
|
| + /**
|
| + * @return {number} The width of an item, measuring it if necessary.
|
| + * @private
|
| + */
|
| + getItemWidth_: function() {
|
| + return this.getItemSize_().width;
|
| + },
|
| +
|
| + /**
|
| + * @return {{height: number, width: number}} The height and width
|
| + * of an item, measuring it if necessary.
|
| + * @private
|
| + */
|
| + getItemSize_: function() {
|
| + if (!this.measured_ || !this.measured_.height) {
|
| + this.measured_ = measureItem(this);
|
| + }
|
| + return this.measured_;
|
| + },
|
| +
|
| + /**
|
| + * Callback for the double click event.
|
| + * @param {Event} e The mouse event object.
|
| + * @private
|
| + */
|
| + handleDoubleClick_: function(e) {
|
| + if (this.disabled)
|
| + return;
|
| +
|
| + var target = this.getListItemAncestor(e.target);
|
| + if (target)
|
| + this.activateItemAtIndex(this.getIndexOfListItem(target));
|
| + },
|
| +
|
| + /**
|
| + * Callback for mousedown and mouseup events.
|
| + * @param {Event} e The mouse event object.
|
| + * @private
|
| + */
|
| + handleMouseDownUp_: function(e) {
|
| + if (this.disabled)
|
| + return;
|
| +
|
| + var target = e.target;
|
| +
|
| + // If the target was this element we need to make sure that the user did
|
| + // not click on a border or a scrollbar.
|
| + if (target == this && !inViewport(target, e))
|
| + return;
|
| +
|
| + target = this.getListItemAncestor(target);
|
| +
|
| + var index = target ? this.getIndexOfListItem(target) : -1;
|
| + this.selectionController_.handleMouseDownUp(e, index);
|
| + },
|
| +
|
| + /**
|
| + * Called when an element in the list is focused. Marks the list as having
|
| + * a focused element, and dispatches an event if it didn't have focus.
|
| + * @param {Event} e The focus event.
|
| + * @private
|
| + */
|
| + handleElementFocus_: function(e) {
|
| + if (!this.hasElementFocus) {
|
| + this.hasElementFocus = true;
|
| + // Force styles based on hasElementFocus to take effect.
|
| + this.forceRepaint_();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Called when an element in the list is blurred. If focus moves outside
|
| + * the list, marks the list as no longer having focus and dispatches an
|
| + * event.
|
| + * @param {Event} e The blur event.
|
| + * @private
|
| + */
|
| + handleElementBlur_: function(e) {
|
| + // When the blur event happens we do not know who is getting focus so we
|
| + // delay this a bit until we know if the new focus node is outside the
|
| + // list.
|
| + var list = this;
|
| + var doc = e.target.ownerDocument;
|
| + window.setTimeout(function() {
|
| + var activeElement = doc.activeElement;
|
| + if (!list.contains(activeElement)) {
|
| + list.hasElementFocus = false;
|
| + // Force styles based on hasElementFocus to take effect.
|
| + list.forceRepaint_();
|
| + }
|
| + });
|
| + },
|
| +
|
| + /**
|
| + * Forces a repaint of the list. Changing custom attributes, even if there
|
| + * are style rules depending on them, doesn't cause a repaint
|
| + * (<https://bugs.webkit.org/show_bug.cgi?id=12519>), so this can be called
|
| + * to force the list to repaint.
|
| + * @private
|
| + */
|
| + forceRepaint_: function(e) {
|
| + var dummyElement = document.createElement('div');
|
| + this.appendChild(dummyElement);
|
| + this.removeChild(dummyElement);
|
| + },
|
| +
|
| + /**
|
| + * Returns the list item element containing the given element, or null if
|
| + * it doesn't belong to any list item element.
|
| + * @param {HTMLElement} element The element.
|
| + * @return {ListItem} The list item containing |element|, or null.
|
| + */
|
| + getListItemAncestor: function(element) {
|
| + var container = element;
|
| + while (container && container.parentNode != this) {
|
| + container = container.parentNode;
|
| + }
|
| + return container;
|
| + },
|
| +
|
| + /**
|
| + * Handle a keydown event.
|
| + * @param {Event} e The keydown event.
|
| + * @return {boolean} Whether the key event was handled.
|
| + */
|
| + handleKeyDown: function(e) {
|
| + if (this.disabled)
|
| + return;
|
| +
|
| + return this.selectionController_.handleKeyDown(e);
|
| + },
|
| +
|
| + /**
|
| + * Callback from the selection model. We dispatch {@code change} events
|
| + * when the selection changes.
|
| + * @param {!cr.Event} e Event with change info.
|
| + * @private
|
| + */
|
| + handleOnChange_: function(ce) {
|
| + ce.changes.forEach(function(change) {
|
| + var listItem = this.getListItemByIndex(change.index);
|
| + if (listItem)
|
| + listItem.selected = change.selected;
|
| + }, this);
|
| +
|
| + cr.dispatchSimpleEvent(this, 'change');
|
| + },
|
| +
|
| + /**
|
| + * Handles a change of the lead item from the selection model.
|
| + * @property {Event} pe The property change event.
|
| + * @private
|
| + */
|
| + handleLeadChange_: function(pe) {
|
| + var element;
|
| + if (pe.oldValue != -1) {
|
| + if ((element = this.getListItemByIndex(pe.oldValue)))
|
| + element.lead = false;
|
| + }
|
| +
|
| + if (pe.newValue != -1) {
|
| + if ((element = this.getListItemByIndex(pe.newValue)))
|
| + element.lead = true;
|
| + this.scrollIndexIntoView(pe.newValue);
|
| + // If the lead item has a different height than other items, then we
|
| + // may run into a problem that requires a second attempt to scroll
|
| + // it into view. The first scroll attempt will trigger a redraw,
|
| + // which will clear out the list and repopulate it with new items.
|
| + // During the redraw, the list may shrink temporarily, which if the
|
| + // lead item is the last item, will move the scrollTop up since it
|
| + // cannot extend beyond the end of the list. (Sadly, being scrolled to
|
| + // the bottom of the list is not "sticky.") So, we set a timeout to
|
| + // rescroll the list after this all gets sorted out. This is perhaps
|
| + // not the most elegant solution, but no others seem obvious.
|
| + var self = this;
|
| + window.setTimeout(function() {
|
| + self.scrollIndexIntoView(pe.newValue);
|
| + });
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * This handles data model 'permuted' event.
|
| + * this event is dispatched as a part of sort or splice.
|
| + * We need to
|
| + * - adjust the cache.
|
| + * - adjust selection.
|
| + * - redraw.
|
| + * - scroll the list to show selection.
|
| + * It is important that the cache adjustment happens before selection model
|
| + * adjustments.
|
| + * @param {Event} e The 'permuted' event.
|
| + */
|
| + handleDataModelPermuted_: function(e) {
|
| + var newCachedItems = {};
|
| + for (var index in this.cachedItems_) {
|
| + if (e.permutation[index] != -1)
|
| + newCachedItems[e.permutation[index]] = this.cachedItems_[index];
|
| + else
|
| + delete this.cachedItems_[index];
|
| + }
|
| + this.cachedItems_ = newCachedItems;
|
| +
|
| + this.startBatchUpdates();
|
| +
|
| + var sm = this.selectionModel;
|
| + sm.adjustLength(e.newLength);
|
| + sm.adjustToReordering(e.permutation);
|
| +
|
| + this.endBatchUpdates();
|
| +
|
| + if (sm.leadIndex != -1)
|
| + this.scrollIndexIntoView(sm.leadIndex);
|
| + },
|
| +
|
| + handleDataModelChange_: function(e) {
|
| + if (e.index >= this.firstIndex_ && e.index < this.lastIndex_) {
|
| + if (this.cachedItems_[e.index])
|
| + delete this.cachedItems_[e.index];
|
| + this.redraw();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @param {number} index The index of the item.
|
| + * @return {number} The top position of the item inside the list, not taking
|
| + * into account lead item. May vary in the case of multiple columns.
|
| + */
|
| + getItemTop: function(index) {
|
| + return index * this.getItemHeight_();
|
| + },
|
| +
|
| + /**
|
| + * @param {number} index The index of the item.
|
| + * @return {number} The row of the item. May vary in the case
|
| + * of multiple columns.
|
| + */
|
| + getItemRow: function(index) {
|
| + return index;
|
| + },
|
| +
|
| + /**
|
| + * @param {number} row The row.
|
| + * @return {number} The index of the first item in the row.
|
| + */
|
| + getFirstItemInRow: function(row) {
|
| + return row;
|
| + },
|
| +
|
| + /**
|
| + * Ensures that a given index is inside the viewport.
|
| + * @param {number} index The index of the item to scroll into view.
|
| + * @return {boolean} Whether any scrolling was needed.
|
| + */
|
| + scrollIndexIntoView: function(index) {
|
| + var dataModel = this.dataModel;
|
| + if (!dataModel || index < 0 || index >= dataModel.length)
|
| + return false;
|
| +
|
| + var itemHeight = this.getItemHeight_();
|
| + var scrollTop = this.scrollTop;
|
| + var top = this.getItemTop(index);
|
| + var leadIndex = this.selectionModel.leadIndex;
|
| +
|
| + // Adjust for the lead item if it is above the given index.
|
| + if (leadIndex > -1 && leadIndex < index)
|
| + top += this.leadItemHeight - itemHeight;
|
| + else if (leadIndex == index)
|
| + itemHeight = this.leadItemHeight;
|
| +
|
| + if (top < scrollTop) {
|
| + this.scrollTop = top;
|
| + return true;
|
| + } else {
|
| + var clientHeight = this.clientHeight;
|
| + var cs = getComputedStyle(this);
|
| + var paddingY = parseInt(cs.paddingTop, 10) +
|
| + parseInt(cs.paddingBottom, 10);
|
| +
|
| + if (top + itemHeight > scrollTop + clientHeight - paddingY) {
|
| + this.scrollTop = top + itemHeight - clientHeight + paddingY;
|
| + return true;
|
| + }
|
| + }
|
| +
|
| + return false;
|
| + },
|
| +
|
| + /**
|
| + * @return {!ClientRect} The rect to use for the context menu.
|
| + */
|
| + getRectForContextMenu: function() {
|
| + // TODO(arv): Add trait support so we can share more code between trees
|
| + // and lists.
|
| + var index = this.selectionModel.selectedIndex;
|
| + var el = this.getListItemByIndex(index);
|
| + if (el)
|
| + return el.getBoundingClientRect();
|
| + return this.getBoundingClientRect();
|
| + },
|
| +
|
| + /**
|
| + * Takes a value from the data model and finds the associated list item.
|
| + * @param {*} value The value in the data model that we want to get the list
|
| + * item for.
|
| + * @return {ListItem} The first found list item or null if not found.
|
| + */
|
| + getListItem: function(value) {
|
| + var dataModel = this.dataModel;
|
| + if (dataModel) {
|
| + var index = dataModel.indexOf(value);
|
| + return this.getListItemByIndex(index);
|
| + }
|
| + return null;
|
| + },
|
| +
|
| + /**
|
| + * Find the list item element at the given index.
|
| + * @param {number} index The index of the list item to get.
|
| + * @return {ListItem} The found list item or null if not found.
|
| + */
|
| + getListItemByIndex: function(index) {
|
| + return this.cachedItems_[index] || null;
|
| + },
|
| +
|
| + /**
|
| + * Find the index of the given list item element.
|
| + * @param {ListItem} item The list item to get the index of.
|
| + * @return {number} The index of the list item, or -1 if not found.
|
| + */
|
| + getIndexOfListItem: function(item) {
|
| + var index = item.listIndex;
|
| + if (this.cachedItems_[index] == item) {
|
| + return index;
|
| + }
|
| + return -1;
|
| + },
|
| +
|
| + /**
|
| + * Creates a new list item.
|
| + * @param {*} value The value to use for the item.
|
| + * @return {!ListItem} The newly created list item.
|
| + */
|
| + createItem: function(value) {
|
| + var item = new this.itemConstructor_(value);
|
| + item.label = value;
|
| + if (typeof item.decorate == 'function')
|
| + item.decorate();
|
| + return item;
|
| + },
|
| +
|
| + /**
|
| + * Creates the selection controller to use internally.
|
| + * @param {cr.ui.ListSelectionModel} sm The underlying selection model.
|
| + * @return {!cr.ui.ListSelectionController} The newly created selection
|
| + * controller.
|
| + */
|
| + createSelectionController: function(sm) {
|
| + return new ListSelectionController(sm);
|
| + },
|
| +
|
| + /**
|
| + * Return the heights (in pixels) of the top of the given item index within
|
| + * the list, and the height of the given item itself, accounting for the
|
| + * possibility that the lead item may be a different height.
|
| + * @param {number} index The index to find the top height of.
|
| + * @return {{top: number, height: number}} The heights for the given index.
|
| + * @private
|
| + */
|
| + getHeightsForIndex_: function(index) {
|
| + var itemHeight = this.getItemHeight_();
|
| + var top = this.getItemTop(index);
|
| + if (this.selectionModel.leadIndex > -1 &&
|
| + this.selectionModel.leadIndex < index) {
|
| + top += this.leadItemHeight - itemHeight;
|
| + } else if (this.selectionModel.leadIndex == index) {
|
| + itemHeight = this.leadItemHeight;
|
| + }
|
| + return {top: top, height: itemHeight};
|
| + },
|
| +
|
| + /**
|
| + * Find the index of the list item containing the given y offset (measured
|
| + * in pixels from the top) within the list. In the case of multiple columns,
|
| + * returns the first index in the row.
|
| + * @param {number} offset The y offset in pixels to get the index of.
|
| + * @return {number} The index of the list item.
|
| + * @private
|
| + */
|
| + getIndexForListOffset_: function(offset) {
|
| + var itemHeight = this.getItemHeight_();
|
| + var leadIndex = this.selectionModel.leadIndex;
|
| + var leadItemHeight = this.leadItemHeight;
|
| + if (leadIndex < 0 || leadItemHeight == itemHeight) {
|
| + // Simple case: no lead item or lead item height is not different.
|
| + return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| + }
|
| + var leadTop = this.getItemTop(leadIndex);
|
| + // If the given offset is above the lead item, it's also simple.
|
| + if (offset < leadTop)
|
| + return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| + // If the lead item contains the given offset, we just return its index.
|
| + if (offset < leadTop + leadItemHeight)
|
| + return this.getFirstItemInRow(this.getItemRow(leadIndex));
|
| + // The given offset must be below the lead item. Adjust and recalculate.
|
| + offset -= leadItemHeight - itemHeight;
|
| + return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| + },
|
| +
|
| + /**
|
| + * Return the number of items that occupy the range of heights between the
|
| + * top of the start item and the end offset.
|
| + * @param {number} startIndex The index of the first visible item.
|
| + * @param {number} endOffset The y offset in pixels of the end of the list.
|
| + * @return {number} The number of list items visible.
|
| + * @private
|
| + */
|
| + countItemsInRange_: function(startIndex, endOffset) {
|
| + var endIndex = this.getIndexForListOffset_(endOffset);
|
| + return endIndex - startIndex + 1;
|
| + },
|
| +
|
| + /**
|
| + * Calculates the number of items fitting in viewport given the index of
|
| + * first item and heights.
|
| + * @param {number} itemHeight The height of the item.
|
| + * @param {number} firstIndex Index of the first item in viewport.
|
| + * @param {number} scrollTop The scroll top position.
|
| + * @return {number} The number of items in view port.
|
| + */
|
| + getItemsInViewPort: function(itemHeight, firstIndex, scrollTop) {
|
| + // This is a bit tricky. We take the minimum of the available items to
|
| + // show and the number we want to show, so as not to go off the end of the
|
| + // list. For the number we want to show, we take the maximum of the number
|
| + // that would fit without a differently-sized lead item, and with one. We
|
| + // do this so that if the size of the lead item changes without a scroll
|
| + // event to trigger redrawing the list, we won't end up with empty space.
|
| + var clientHeight = this.clientHeight;
|
| + return this.autoExpands_ ? this.dataModel.length : Math.min(
|
| + this.dataModel.length - firstIndex,
|
| + Math.max(
|
| + Math.ceil(clientHeight / itemHeight) + 1,
|
| + this.countItemsInRange_(firstIndex, scrollTop + clientHeight)));
|
| + },
|
| +
|
| + /**
|
| + * Adds items to the list and {@code newCachedItems}.
|
| + * @param {number} firstIndex The index of first item, inclusively.
|
| + * @param {number} lastIndex The index of last item, exclusively.
|
| + * @param {Object.<string, ListItem>} cachedItems Old items cache.
|
| + * @param {Object.<string, ListItem>} newCachedItems New items cache.
|
| + */
|
| + addItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
|
| + var listItem;
|
| + var dataModel = this.dataModel;
|
| +
|
| + window.l = this;
|
| + for (var y = firstIndex; y < lastIndex; y++) {
|
| + var dataItem = dataModel.item(y);
|
| + listItem = cachedItems[y] || this.createItem(dataItem);
|
| + listItem.listIndex = y;
|
| + this.appendChild(listItem);
|
| + newCachedItems[y] = listItem;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns the height of after filler in the list.
|
| + * @param {number} lastIndex The index of item past the last in viewport.
|
| + * @param {number} itemHeight The height of the item.
|
| + * @return {number} The height of after filler.
|
| + */
|
| + getAfterFillerHeight: function(lastIndex, itemHeight) {
|
| + return (this.dataModel.length - lastIndex) * itemHeight;
|
| + },
|
| +
|
| + /**
|
| + * Redraws the viewport.
|
| + */
|
| + redraw: function() {
|
| + if (this.batchCount_ != 0)
|
| + return;
|
| +
|
| + var dataModel = this.dataModel;
|
| + if (!dataModel) {
|
| + this.textContent = '';
|
| + return;
|
| + }
|
| +
|
| + var scrollTop = this.scrollTop;
|
| + var clientHeight = this.clientHeight;
|
| +
|
| + var itemHeight = this.getItemHeight_();
|
| +
|
| + // We cache the list items since creating the DOM nodes is the most
|
| + // expensive part of redrawing.
|
| + var cachedItems = this.cachedItems_ || {};
|
| + var newCachedItems = {};
|
| +
|
| + var desiredScrollHeight = this.getHeightsForIndex_(dataModel.length).top;
|
| +
|
| + var autoExpands = this.autoExpands_;
|
| + var firstIndex = autoExpands ? 0 : this.getIndexForListOffset_(scrollTop);
|
| + var itemsInViewPort = this.getItemsInViewPort(itemHeight, firstIndex,
|
| + scrollTop);
|
| + var lastIndex = firstIndex + itemsInViewPort;
|
| +
|
| + this.textContent = '';
|
| +
|
| + this.beforeFiller_.style.height =
|
| + this.getHeightsForIndex_(firstIndex).top + 'px';
|
| + this.appendChild(this.beforeFiller_);
|
| +
|
| + var sm = this.selectionModel;
|
| + var leadIndex = sm.leadIndex;
|
| +
|
| + this.addItems(firstIndex, lastIndex, cachedItems, newCachedItems);
|
| +
|
| + var afterFillerHeight = this.getAfterFillerHeight(lastIndex, itemHeight);
|
| + if (leadIndex >= lastIndex)
|
| + afterFillerHeight += this.leadItemHeight - itemHeight;
|
| + this.afterFiller_.style.height = afterFillerHeight + 'px';
|
| + this.appendChild(this.afterFiller_);
|
| +
|
| + // We don't set the lead or selected properties until after adding all
|
| + // items, in case they force relayout in response to these events.
|
| + var listItem = null;
|
| + if (newCachedItems[leadIndex])
|
| + newCachedItems[leadIndex].lead = true;
|
| + for (var y = firstIndex; y < lastIndex; y++) {
|
| + if (sm.getIndexSelected(y))
|
| + newCachedItems[y].selected = true;
|
| + else if (y != leadIndex)
|
| + listItem = newCachedItems[y];
|
| + }
|
| +
|
| + this.firstIndex_ = firstIndex;
|
| + this.lastIndex_ = lastIndex;
|
| +
|
| + this.cachedItems_ = newCachedItems;
|
| +
|
| + // Measure again in case the item height has changed due to a page zoom.
|
| + //
|
| + // The measure above is only done the first time but this measure is done
|
| + // after every redraw. It is done in a timeout so it will not trigger
|
| + // a reflow (which made the redraw speed 3 times slower on my system).
|
| + // By using a timeout the measuring will happen later when there is no
|
| + // need for a reflow.
|
| + if (listItem) {
|
| + var list = this;
|
| + window.setTimeout(function() {
|
| + if (listItem.parentNode == list) {
|
| + list.measured_ = measureItem(list, listItem);
|
| + }
|
| + });
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Invalidates list by removing cached items.
|
| + */
|
| + invalidate: function() {
|
| + this.cachedItems_ = {};
|
| + },
|
| +
|
| + /**
|
| + * Redraws a single item.
|
| + * @param {number} index The row index to redraw.
|
| + */
|
| + redrawItem: function(index) {
|
| + if (index >= this.firstIndex_ && index < this.lastIndex_) {
|
| + delete this.cachedItems_[index];
|
| + this.redraw();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Called when a list item is activated, currently only by a double click
|
| + * event.
|
| + * @param {number} index The index of the activated item.
|
| + */
|
| + activateItemAtIndex: function(index) {
|
| + },
|
| + };
|
| +
|
| + cr.defineProperty(List, 'disabled', cr.PropertyKind.BOOL_ATTR);
|
| +
|
| + /**
|
| + * Whether the list or one of its descendents has focus. This is necessary
|
| + * because list items can contain controls that can be focused, and for some
|
| + * purposes (e.g., styling), the list can still be conceptually focused at
|
| + * that point even though it doesn't actually have the page focus.
|
| + */
|
| + cr.defineProperty(List, 'hasElementFocus', cr.PropertyKind.BOOL_ATTR);
|
| +
|
| + return {
|
| + List: List
|
| + }
|
| +});
|
|
|