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 |
+ } |
+}); |