| Index: chrome/browser/resources/shared/js/cr/ui/list.js
|
| ===================================================================
|
| --- chrome/browser/resources/shared/js/cr/ui/list.js (revision 177292)
|
| +++ chrome/browser/resources/shared/js/cr/ui/list.js (working copy)
|
| @@ -1,1277 +0,0 @@
|
| -// Copyright (c) 2012 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 */ var ListSelectionModel = cr.ui.ListSelectionModel;
|
| - /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
|
| - /** @const */ var 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;
|
| - }
|
| -
|
| - 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, marginTop: number, marginBottom:number,
|
| - * width: number, marginLeft: number, marginRight:number}}
|
| - * @private
|
| - */
|
| - measured_: undefined,
|
| -
|
| - /**
|
| - * Whether or not the list is autoexpanding. If true, the list resizes
|
| - * its height to accomadate all children.
|
| - * @type {boolean}
|
| - * @private
|
| - */
|
| - autoExpands_: false,
|
| -
|
| - /**
|
| - * Whether or not the rows on list have various heights. If true, all the
|
| - * rows have the same fixed height. Otherwise, each row resizes its height
|
| - * to accommodate all contents.
|
| - * @type {boolean}
|
| - * @private
|
| - */
|
| - fixedHeight_: true,
|
| -
|
| - /**
|
| - * Whether or not the list view has a blank space below the last row.
|
| - * @type {boolean}
|
| - * @private
|
| - */
|
| - remainingSpace_: true,
|
| -
|
| - /**
|
| - * 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.cachedItemHeights_ = {};
|
| - 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_;
|
| - },
|
| -
|
| -
|
| - /**
|
| - * Cached item for measuring the default item size by measureItem().
|
| - * @type {ListItem}
|
| - */
|
| - cachedMeasuredItem_: null,
|
| -
|
| - /**
|
| - * 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();
|
| - },
|
| -
|
| - /**
|
| - * Whether or not the rows on list have various heights.
|
| - * @type {boolean}
|
| - */
|
| - get fixedHeight() {
|
| - return this.fixedHeight_;
|
| - },
|
| - set fixedHeight(fixedHeight) {
|
| - if (this.fixedHeight_ == fixedHeight)
|
| - return;
|
| - this.fixedHeight_ = fixedHeight;
|
| - this.redraw();
|
| - },
|
| -
|
| - /**
|
| - * Convenience alias for selectionModel.selectedItem
|
| - * @type {*}
|
| - */
|
| - 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;
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Convenience alias for selectionModel.selectedItems
|
| - * @type {!Array<*>}
|
| - */
|
| - 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.
|
| - * @type {HTMLCollection}
|
| - */
|
| - get items() {
|
| - return Array.prototype.filter.call(this.children,
|
| - this.isItem, this);
|
| - },
|
| -
|
| - /**
|
| - * Returns true if the child is a list item. Subclasses may override this
|
| - * to filter out certain elements.
|
| - * @param {Node} child Child of the list.
|
| - * @return {boolean} True if a list item.
|
| - */
|
| - isItem: function(child) {
|
| - return child.nodeType == Node.ELEMENT_NODE &&
|
| - child != this.beforeFiller_ && child != this.afterFiller_;
|
| - },
|
| -
|
| - 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.textContent = '';
|
| - 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.handlePointerDownUp_);
|
| - this.addEventListener('mouseup', this.handlePointerDownUp_);
|
| - this.addEventListener('keydown', this.handleKeyDown);
|
| - this.addEventListener('focus', this.handleElementFocus_, true);
|
| - this.addEventListener('blur', this.handleElementBlur_, true);
|
| - this.addEventListener('scroll', this.handleScroll.bind(this));
|
| - this.setAttribute('role', 'list');
|
| -
|
| - // Make list focusable
|
| - if (!this.hasAttribute('tabindex'))
|
| - this.tabIndex = 0;
|
| -
|
| - // Try to get an unique id prefix from the id of this element or the
|
| - // nearest ancestor with an id.
|
| - var element = this;
|
| - while (element && !element.id)
|
| - element = element.parentElement;
|
| - if (element && element.id)
|
| - this.uniqueIdPrefix_ = element.id;
|
| - else
|
| - this.uniqueIdPrefix_ = 'list';
|
| -
|
| - // The next id suffix to use when giving each item an unique id.
|
| - this.nextUniqueIdSuffix_ = 0;
|
| - },
|
| -
|
| - /**
|
| - * @param {ListItem=} item The list item to measure.
|
| - * @return {number} The height of the given item. If the fixed height on CSS
|
| - * is set by 'px', uses that value as height. Otherwise, measures the size.
|
| - * @private
|
| - */
|
| - measureItemHeight_: function(item) {
|
| - return this.measureItem(item).height;
|
| - },
|
| -
|
| - /**
|
| - * @return {number} The height of default item, measuring it if necessary.
|
| - * @private
|
| - */
|
| - getDefaultItemHeight_: function() {
|
| - return this.getDefaultItemSize_().height;
|
| - },
|
| -
|
| - /**
|
| - * @param {number} index The index of the item.
|
| - * @return {number} The height of the item, measuring it if necessary.
|
| - */
|
| - getItemHeightByIndex_: function(index) {
|
| - // If |this.fixedHeight_| is true, all the rows have same default height.
|
| - if (this.fixedHeight_)
|
| - return this.getDefaultItemHeight_();
|
| -
|
| - if (this.cachedItemHeights_[index])
|
| - return this.cachedItemHeights_[index];
|
| -
|
| - var item = this.getListItemByIndex(index);
|
| - if (item) {
|
| - var h = this.measureItemHeight_(item);
|
| - this.cachedItemHeights_[index] = h;
|
| - return h;
|
| - }
|
| - return this.getDefaultItemHeight_();
|
| - },
|
| -
|
| - /**
|
| - * @return {{height: number, width: number}} The height and width
|
| - * of default item, measuring it if necessary.
|
| - * @private
|
| - */
|
| - getDefaultItemSize_: function() {
|
| - if (!this.measured_ || !this.measured_.height) {
|
| - this.measured_ = this.measureItem();
|
| - }
|
| - return this.measured_;
|
| - },
|
| -
|
| - /**
|
| - * Creates an item (dataModel.item(0)) and measures its height. The item is
|
| - * cached instead of creating a new one every time..
|
| - * @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, marginTop: number, marginBottom:number,
|
| - * width: number, marginLeft: number, marginRight:number}}
|
| - * The height and width of the item, taking
|
| - * margins into account, and the top, bottom, left and right margins
|
| - * themselves.
|
| - */
|
| - measureItem: function(opt_item) {
|
| - var dataModel = this.dataModel;
|
| - if (!dataModel || !dataModel.length)
|
| - return 0;
|
| - var item = opt_item || this.cachedMeasuredItem_ ||
|
| - this.createItem(dataModel.item(0));
|
| - if (!opt_item) {
|
| - this.cachedMeasuredItem_ = item;
|
| - this.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)
|
| - this.removeChild(item);
|
| - return {
|
| - height: Math.max(0, h),
|
| - marginTop: mt, marginBottom: mb,
|
| - width: Math.max(0, w),
|
| - marginLeft: ml, marginRight: mr};
|
| - },
|
| -
|
| - /**
|
| - * Callback for the double click event.
|
| - * @param {Event} e The mouse event object.
|
| - * @private
|
| - */
|
| - handleDoubleClick_: function(e) {
|
| - if (this.disabled)
|
| - return;
|
| -
|
| - var target = e.target;
|
| -
|
| - target = this.getListItemAncestor(target);
|
| - if (target)
|
| - this.activateItemAtIndex(this.getIndexOfListItem(target));
|
| - },
|
| -
|
| - /**
|
| - * Callback for mousedown and mouseup events.
|
| - * @param {Event} e The mouse event object.
|
| - * @private
|
| - */
|
| - handlePointerDownUp_: 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) {
|
| - if (inViewport(target, e))
|
| - this.selectionController_.handlePointerDownUp(e, -1);
|
| - return;
|
| - }
|
| -
|
| - target = this.getListItemAncestor(target);
|
| -
|
| - var index = this.getIndexOfListItem(target);
|
| - this.selectionController_.handlePointerDownUp(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;
|
| - },
|
| -
|
| - /**
|
| - * 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;
|
| - });
|
| - },
|
| -
|
| - /**
|
| - * 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);
|
| - },
|
| -
|
| - scrollTopBefore_: 0,
|
| -
|
| - /**
|
| - * Handle a scroll event.
|
| - * @param {Event} e The scroll event.
|
| - */
|
| - handleScroll: function(e) {
|
| - var scrollTop = this.scrollTop;
|
| - if (scrollTop != this.scrollTopBefore_) {
|
| - this.scrollTopBefore_ = scrollTop;
|
| - this.redraw();
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * 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;
|
| - if (change.selected) {
|
| - listItem.setAttribute('aria-posinset', change.index + 1);
|
| - listItem.setAttribute('aria-setsize', this.dataModel.length);
|
| - this.setAttribute('aria-activedescendant', listItem.id);
|
| - } else {
|
| - listItem.removeAttribute('aria-posinset');
|
| - listItem.removeAttribute('aria-setsize');
|
| - }
|
| - }
|
| - }, this);
|
| -
|
| - cr.dispatchSimpleEvent(this, 'change');
|
| - },
|
| -
|
| - /**
|
| - * Handles a change of the lead item from the selection model.
|
| - * @param {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;
|
| - if (pe.oldValue != pe.newValue) {
|
| - 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. (called in this.endBatchUpdates())
|
| - * 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) {
|
| - var newIndex = e.permutation[index];
|
| - newCachedItems[newIndex] = this.cachedItems_[index];
|
| - newCachedItems[newIndex].listIndex = newIndex;
|
| - }
|
| - }
|
| - this.cachedItems_ = newCachedItems;
|
| -
|
| - var newCachedItemHeights = {};
|
| - for (var index in this.cachedItemHeights_) {
|
| - if (e.permutation[index] != -1) {
|
| - newCachedItemHeights[e.permutation[index]] =
|
| - this.cachedItemHeights_[index];
|
| - }
|
| - }
|
| - this.cachedItemHeights_ = newCachedItemHeights;
|
| -
|
| - this.startBatchUpdates();
|
| -
|
| - var sm = this.selectionModel;
|
| - sm.adjustLength(e.newLength);
|
| - sm.adjustToReordering(e.permutation);
|
| -
|
| - this.endBatchUpdates();
|
| - },
|
| -
|
| - handleDataModelChange_: function(e) {
|
| - delete this.cachedItems_[e.index];
|
| - delete this.cachedItemHeights_[e.index];
|
| - this.cachedMeasuredItem_ = null;
|
| -
|
| - if (e.index >= this.firstIndex_ &&
|
| - (e.index < this.lastIndex_ || this.remainingSpace_)) {
|
| - this.redraw();
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * @param {number} index The index of the item.
|
| - * @return {number} The top position of the item inside the list.
|
| - */
|
| - getItemTop: function(index) {
|
| - if (this.fixedHeight_) {
|
| - var itemHeight = this.getDefaultItemHeight_();
|
| - return index * itemHeight;
|
| - } else {
|
| - this.ensureAllItemSizesInCache();
|
| - var top = 0;
|
| - for (var i = 0; i < index; i++) {
|
| - top += this.getItemHeightByIndex_(i);
|
| - }
|
| - return top;
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * @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.getItemHeightByIndex_(index);
|
| - var scrollTop = this.scrollTop;
|
| - var top = this.getItemTop(index);
|
| - var clientHeight = this.clientHeight;
|
| -
|
| - var self = this;
|
| - // Function to adjust the tops of viewport and row.
|
| - function scrollToAdjustTop() {
|
| - self.scrollTop = top;
|
| - return true;
|
| - };
|
| - // Function to adjust the bottoms of viewport and row.
|
| - function scrollToAdjustBottom() {
|
| - var cs = getComputedStyle(self);
|
| - var paddingY = parseInt(cs.paddingTop, 10) +
|
| - parseInt(cs.paddingBottom, 10);
|
| -
|
| - if (top + itemHeight > scrollTop + clientHeight - paddingY) {
|
| - self.scrollTop = top + itemHeight - clientHeight + paddingY;
|
| - return true;
|
| - }
|
| - return false;
|
| - };
|
| -
|
| - // Check if the entire of given indexed row can be shown in the viewport.
|
| - if (itemHeight <= clientHeight) {
|
| - if (top < scrollTop)
|
| - return scrollToAdjustTop();
|
| - if (scrollTop + clientHeight < top + itemHeight)
|
| - return scrollToAdjustBottom();
|
| - } else {
|
| - if (scrollTop < top)
|
| - return scrollToAdjustTop();
|
| - if (top + itemHeight < scrollTop + clientHeight)
|
| - return scrollToAdjustBottom();
|
| - }
|
| - 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;
|
| - item.id = this.uniqueIdPrefix_ + '-' + this.nextUniqueIdSuffix_++;
|
| - 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.getItemHeightByIndex_(index);
|
| - var top = this.getItemTop(index);
|
| - 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. Returns the list size if
|
| - * given offset exceeds the height of list.
|
| - * @private
|
| - */
|
| - getIndexForListOffset_: function(offset) {
|
| - var itemHeight = this.getDefaultItemHeight_();
|
| - if (!itemHeight)
|
| - return this.dataModel.length;
|
| -
|
| - if (this.fixedHeight_)
|
| - return this.getFirstItemInRow(Math.floor(offset / itemHeight));
|
| -
|
| - // If offset exceeds the height of list.
|
| - var lastHeight = 0;
|
| - if (this.dataModel.length) {
|
| - var h = this.getHeightsForIndex_(this.dataModel.length - 1);
|
| - lastHeight = h.top + h.height;
|
| - }
|
| - if (lastHeight < offset)
|
| - return this.dataModel.length;
|
| -
|
| - // Estimates index.
|
| - var estimatedIndex = Math.min(Math.floor(offset / itemHeight),
|
| - this.dataModel.length - 1);
|
| - var isIncrementing = this.getItemTop(estimatedIndex) < offset;
|
| -
|
| - // Searchs the correct index.
|
| - do {
|
| - var heights = this.getHeightsForIndex_(estimatedIndex);
|
| - var top = heights.top;
|
| - var height = heights.height;
|
| -
|
| - if (top <= offset && offset <= (top + height))
|
| - break;
|
| -
|
| - isIncrementing ? ++estimatedIndex : --estimatedIndex;
|
| - } while (0 < estimatedIndex && estimatedIndex < this.dataModel.length);
|
| -
|
| - return estimatedIndex;
|
| - },
|
| -
|
| - /**
|
| - * 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 the given viewport.
|
| - * @param {number} scrollTop The scroll top position.
|
| - * @param {number} clientHeight The height of viewport.
|
| - * @return {{first: number, length: number, last: number}} The index of
|
| - * first item in view port, The number of items, The item past the last.
|
| - */
|
| - getItemsInViewPort: function(scrollTop, clientHeight) {
|
| - if (this.autoExpands_) {
|
| - return {
|
| - first: 0,
|
| - length: this.dataModel.length,
|
| - last: this.dataModel.length};
|
| - } else {
|
| - var firstIndex = this.getIndexForListOffset_(scrollTop);
|
| - var lastIndex = this.getIndexForListOffset_(scrollTop + clientHeight);
|
| -
|
| - return {
|
| - first: firstIndex,
|
| - length: lastIndex - firstIndex + 1,
|
| - last: lastIndex + 1};
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Merges list items currently existing in the list with items in the range
|
| - * [firstIndex, lastIndex). Removes or adds items if needed.
|
| - * Doesn't delete {@code this.pinnedItem_} if it is present (instead hides
|
| - * it if it is out of the range). Adds items to {@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.
|
| - */
|
| - mergeItems: function(firstIndex, lastIndex, cachedItems, newCachedItems) {
|
| - var self = this;
|
| - var dataModel = this.dataModel;
|
| - var currentIndex = firstIndex;
|
| -
|
| - function insert() {
|
| - var dataItem = dataModel.item(currentIndex);
|
| - var newItem = cachedItems[currentIndex] || self.createItem(dataItem);
|
| - newItem.listIndex = currentIndex;
|
| - newCachedItems[currentIndex] = newItem;
|
| - self.insertBefore(newItem, item);
|
| - currentIndex++;
|
| - }
|
| -
|
| - function remove() {
|
| - var next = item.nextSibling;
|
| - if (item != self.pinnedItem_)
|
| - self.removeChild(item);
|
| - item = next;
|
| - }
|
| -
|
| - for (var item = this.beforeFiller_.nextSibling;
|
| - item != this.afterFiller_ && currentIndex < lastIndex;) {
|
| - if (!this.isItem(item)) {
|
| - item = item.nextSibling;
|
| - continue;
|
| - }
|
| -
|
| - var index = item.listIndex;
|
| - if (cachedItems[index] != item || index < currentIndex) {
|
| - remove();
|
| - } else if (index == currentIndex) {
|
| - newCachedItems[currentIndex] = item;
|
| - item = item.nextSibling;
|
| - currentIndex++;
|
| - } else { // index > currentIndex
|
| - insert();
|
| - }
|
| - }
|
| -
|
| - while (item != this.afterFiller_) {
|
| - if (this.isItem(item))
|
| - remove();
|
| - else
|
| - item = item.nextSibling;
|
| - }
|
| -
|
| - if (this.pinnedItem_) {
|
| - var index = this.pinnedItem_.listIndex;
|
| - this.pinnedItem_.hidden = index < firstIndex || index >= lastIndex;
|
| - newCachedItems[index] = this.pinnedItem_;
|
| - if (index >= lastIndex)
|
| - item = this.pinnedItem_; // Insert new items before this one.
|
| - }
|
| -
|
| - while (currentIndex < lastIndex)
|
| - insert();
|
| - },
|
| -
|
| - /**
|
| - * Ensures that all the item sizes in the list have been already cached.
|
| - */
|
| - ensureAllItemSizesInCache: function() {
|
| - var measuringIndexes = [];
|
| - var isElementAppended = [];
|
| - for (var y = 0; y < this.dataModel.length; y++) {
|
| - if (!this.cachedItemHeights_[y]) {
|
| - measuringIndexes.push(y);
|
| - isElementAppended.push(false);
|
| - }
|
| - }
|
| -
|
| - var measuringItems = [];
|
| - // Adds temporary elements.
|
| - for (var y = 0; y < measuringIndexes.length; y++) {
|
| - var index = measuringIndexes[y];
|
| - var dataItem = this.dataModel.item(index);
|
| - var listItem = this.cachedItems_[index] || this.createItem(dataItem);
|
| - listItem.listIndex = index;
|
| -
|
| - // If |listItems| is not on the list, apppends it to the list and sets
|
| - // the flag.
|
| - if (!listItem.parentNode) {
|
| - this.appendChild(listItem);
|
| - isElementAppended[y] = true;
|
| - }
|
| -
|
| - this.cachedItems_[index] = listItem;
|
| - measuringItems.push(listItem);
|
| - }
|
| -
|
| - // All mesurings must be placed after adding all the elements, to prevent
|
| - // performance reducing.
|
| - for (var y = 0; y < measuringIndexes.length; y++) {
|
| - var index = measuringIndexes[y];
|
| - this.cachedItemHeights_[index] =
|
| - this.measureItemHeight_(measuringItems[y]);
|
| - }
|
| -
|
| - // Removes all the temprary elements.
|
| - for (var y = 0; y < measuringIndexes.length; y++) {
|
| - // If the list item has been appended above, removes it.
|
| - if (isElementAppended[y])
|
| - this.removeChild(measuringItems[y]);
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Returns the height of after filler in the list.
|
| - * @param {number} lastIndex The index of item past the last in viewport.
|
| - * @return {number} The height of after filler.
|
| - */
|
| - getAfterFillerHeight: function(lastIndex) {
|
| - if (this.fixedHeight_) {
|
| - var itemHeight = this.getDefaultItemHeight_();
|
| - return (this.dataModel.length - lastIndex) * itemHeight;
|
| - }
|
| -
|
| - var height = 0;
|
| - for (var i = lastIndex; i < this.dataModel.length; i++)
|
| - height += this.getItemHeightByIndex_(i);
|
| - return height;
|
| - },
|
| -
|
| - /**
|
| - * Redraws the viewport.
|
| - */
|
| - redraw: function() {
|
| - if (this.batchCount_ != 0)
|
| - return;
|
| -
|
| - var dataModel = this.dataModel;
|
| - if (!dataModel || !this.autoExpands_ && this.clientHeight == 0) {
|
| - this.cachedItems_ = {};
|
| - this.firstIndex_ = 0;
|
| - this.lastIndex_ = 0;
|
| - this.remainingSpace_ = this.clientHeight != 0;
|
| - this.mergeItems(0, 0, {}, {});
|
| - return;
|
| - }
|
| -
|
| - // Save the previous positions before any manipulation of elements.
|
| - var scrollTop = this.scrollTop;
|
| - var clientHeight = this.clientHeight;
|
| -
|
| - // Store all the item sizes into the cache in advance, to prevent
|
| - // interleave measuring with mutating dom.
|
| - if (!this.fixedHeight_)
|
| - this.ensureAllItemSizesInCache();
|
| -
|
| - // We cache the list items since creating the DOM nodes is the most
|
| - // expensive part of redrawing.
|
| - var cachedItems = this.cachedItems_ || {};
|
| - var newCachedItems = {};
|
| -
|
| - var autoExpands = this.autoExpands_;
|
| -
|
| - var itemsInViewPort = this.getItemsInViewPort(scrollTop, clientHeight);
|
| - // Draws the hidden rows just above/below the viewport to prevent
|
| - // flashing in scroll.
|
| - var firstIndex = Math.max(0, Math.min(dataModel.length - 1,
|
| - itemsInViewPort.first - 1));
|
| - var lastIndex = Math.min(itemsInViewPort.last + 1, dataModel.length);
|
| -
|
| - var beforeFillerHeight =
|
| - this.autoExpands ? 0 : this.getItemTop(firstIndex);
|
| - var afterFillerHeight =
|
| - this.autoExpands ? 0 : this.getAfterFillerHeight(lastIndex);
|
| -
|
| - this.beforeFiller_.style.height = beforeFillerHeight + 'px';
|
| -
|
| - var sm = this.selectionModel;
|
| - var leadIndex = sm.leadIndex;
|
| -
|
| - if (this.pinnedItem_ &&
|
| - this.pinnedItem_ != cachedItems[leadIndex]) {
|
| - if (this.pinnedItem_.hidden)
|
| - this.removeChild(this.pinnedItem_);
|
| - this.pinnedItem_ = undefined;
|
| - }
|
| -
|
| - this.mergeItems(firstIndex, lastIndex, cachedItems, newCachedItems);
|
| -
|
| - if (!this.pinnedItem_ && newCachedItems[leadIndex] &&
|
| - newCachedItems[leadIndex].parentNode == this) {
|
| - this.pinnedItem_ = newCachedItems[leadIndex];
|
| - }
|
| -
|
| - this.afterFiller_.style.height = afterFillerHeight + 'px';
|
| -
|
| - // 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 (leadIndex != -1 && 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.remainingSpace_ = itemsInViewPort.last > dataModel.length;
|
| - this.cachedItems_ = newCachedItems;
|
| -
|
| - // Mesurings must be placed after adding all the elements, to prevent
|
| - // performance reducing.
|
| - if (!this.fixedHeight_) {
|
| - for (var y = firstIndex; y < lastIndex; y++)
|
| - this.cachedItemHeights_[y] =
|
| - this.measureItemHeight_(newCachedItems[y]);
|
| - }
|
| -
|
| - // 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 && this.fixedHeight_) {
|
| - var list = this;
|
| - window.setTimeout(function() {
|
| - if (listItem.parentNode == list) {
|
| - list.measured_ = list.measureItem(listItem);
|
| - }
|
| - });
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Restore the lead item that is present in the list but may be updated
|
| - * in the data model (supposed to be used inside a batch update). Usually
|
| - * such an item would be recreated in the redraw method. If reinsertion
|
| - * is undesirable (for instance to prevent losing focus) the item may be
|
| - * updated and restored. Assumed the listItem relates to the same data item
|
| - * as the lead item in the begin of the batch update.
|
| - *
|
| - * @param {ListItem} leadItem Already existing lead item.
|
| - */
|
| - restoreLeadItem: function(leadItem) {
|
| - delete this.cachedItems_[leadItem.listIndex];
|
| -
|
| - leadItem.listIndex = this.selectionModel.leadIndex;
|
| - this.pinnedItem_ = this.cachedItems_[leadItem.listIndex] = leadItem;
|
| - },
|
| -
|
| - /**
|
| - * Invalidates list by removing cached items.
|
| - */
|
| - invalidate: function() {
|
| - this.cachedItems_ = {};
|
| - this.cachedItemSized_ = {};
|
| - },
|
| -
|
| - /**
|
| - * Redraws a single item.
|
| - * @param {number} index The row index to redraw.
|
| - */
|
| - redrawItem: function(index) {
|
| - if (index >= this.firstIndex_ &&
|
| - (index < this.lastIndex_ || this.remainingSpace_)) {
|
| - 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) {
|
| - },
|
| -
|
| - /**
|
| - * Returns a ListItem for the leadIndex. If the item isn't present in the
|
| - * list creates it and inserts to the list (may be invisible if it's out of
|
| - * the visible range).
|
| - *
|
| - * Item returned from this method won't be removed until it remains a lead
|
| - * item or til the data model changes (unlike other items that could be
|
| - * removed when they go out of the visible range).
|
| - *
|
| - * @return {cr.ui.ListItem} The lead item for the list.
|
| - */
|
| - ensureLeadItemExists: function() {
|
| - var index = this.selectionModel.leadIndex;
|
| - if (index < 0)
|
| - return null;
|
| - var cachedItems = this.cachedItems_ || {};
|
| -
|
| - var item = cachedItems[index] ||
|
| - this.createItem(this.dataModel.item(index));
|
| - if (this.pinnedItem_ != item && this.pinnedItem_ &&
|
| - this.pinnedItem_.hidden) {
|
| - this.removeChild(this.pinnedItem_);
|
| - }
|
| - this.pinnedItem_ = item;
|
| - cachedItems[index] = item;
|
| - item.listIndex = index;
|
| - if (item.parentNode == this)
|
| - return item;
|
| -
|
| - if (this.batchCount_ != 0)
|
| - item.hidden = true;
|
| -
|
| - // Item will get to the right place in redraw. Choose place to insert
|
| - // reducing items reinsertion.
|
| - if (index <= this.firstIndex_)
|
| - this.insertBefore(item, this.beforeFiller_.nextSibling);
|
| - else
|
| - this.insertBefore(item, this.afterFiller_);
|
| - this.redraw();
|
| - return item;
|
| - },
|
| - };
|
| -
|
| - 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
|
| - };
|
| -});
|
|
|