Index: third_party/polymer/components/core-list/core-list.html |
diff --git a/third_party/polymer/components/core-list/core-list.html b/third_party/polymer/components/core-list/core-list.html |
deleted file mode 100644 |
index 27f111387734beefd04e76270004d595d1244162..0000000000000000000000000000000000000000 |
--- a/third_party/polymer/components/core-list/core-list.html |
+++ /dev/null |
@@ -1,1306 +0,0 @@ |
-<!-- |
-Copyright (c) 2014 The Polymer Project Authors. All rights reserved. |
-This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
-The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
-The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
-Code distributed by Google as part of the polymer project is also |
-subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
---> |
- |
-<!-- |
-`core-list` displays a virtual, 'infinite' list. The template inside the |
-`core-list` element represents the DOM to create for each list item. The |
-`data` property specifies an array of list item data. |
- |
-For performance reasons, not every item in the list is rendered at once; instead |
-a small subset of actual template elements (enough to fill the viewport) are |
-rendered and reused as the user scrolls. As such, it is important that all |
-state of the list template be bound to the model driving it, since the view |
-may be reused with a new model at any time. Particularly, any state that |
-may change as the result of a user interaction with the list item must be |
-bound to the model to avoid view state inconsistency. |
- |
-### Template model |
- |
-List item templates should bind to template models of the following structure: |
- |
- { |
- index: 0, // data index for this item |
- selected: false, // selection state for this item |
- model: { // user data corresponding to data[index] |
- /* user item data */ |
- } |
- } |
- |
-For example, given the following data array: |
- |
- [ |
- {name: 'Bob', checked: true}, |
- {name: 'Tim', checked: false}, |
- ... |
- ] |
- |
-The following code would render the list (note the `name` and `checked` |
-properties are bound from the `model` object provided to the template |
-scope): |
- |
- <core-list data="{{data}}"> |
- <template> |
- <div class="row {{ {selected: selected} | tokenList }}"> |
- List row: {{index}}, User data from model: {{model.name}} |
- <input type="checkbox" checked="{{model.checked}}"> |
- </div> |
- </template> |
- </core-list> |
- |
-### Selection |
- |
-By default, the list supports selection via tapping. Styling selected items |
-should be done via binding to the `selected` property of each model (see examples |
-above. The data model for the selected item (for single-selection) or array of |
-models (for multi-selection) is published to the `selection` property. |
- |
-### Grouping **(experimental)** |
- |
-`core-list` supports showing dividers between groups of data by setting the |
-`groups` property to an array containing group information. An element with |
-a `divider` attribute set should be supplied a the top level of the template |
-next to the template item to provide the divider template. The template model |
-contains extra fields when `groups` is used, as follows: |
- |
- { |
- index: 0, // data index for this item |
- groupIndex: 0, // group index for this item |
- groupItemIndex: 0, // index within group for this item |
- selected: false, // selection state for this item |
- model: { // user data corresponding to data[index] |
- /* user item data */ |
- }, |
- groupModel: { // user group data corresponding to groups[index] |
- /* user group data */ |
- } |
- } |
- |
-Groups may be specified one of two ways (users should choose the data format |
-that closest matches their source data, to avoid the performance impact of |
-needing totransform data to fit the required structure): |
- |
-1. Flat data array - In this scenario, the `data` array is provided as |
-a flat list of models. Group lengths are determined by the `length` property |
-on each group object, with the `data` property providing user-specified group |
-data, typically for binding to dividers. For example: |
- |
- data = [ |
- { name: 'Adam' }, |
- { name: 'Alex' }, |
- { name: 'Bob' }, |
- { name: 'Chuck' }, |
- { name: 'Cathy' }, |
- ... |
- ]; |
- |
- groups = [ |
- { length: 2, data: { letter: 'A' } }, |
- { length: 1, data: { letter: 'B' } }, |
- { length: 2, data: { letter: 'C' } }, |
- ... |
- ]; |
- |
- <core-list data="{{data}}" groups="{{groups}}"> |
- <template> |
- <div divider class="divider">{{groupModel.letter}}</div> |
- <div class="item">{{model.name}}</div> |
- </template> |
- </core-list> |
- |
-2. Nested data array - In this scenario, the `data` array is a nested |
-array of arrays of models, where each array determines the length of the |
-group, and the `groups` models provide the user-specified data directly. |
-For example: |
- |
- data = [ |
- [ { name: 'Adam' }, { name: 'Alex' } ], |
- [ { name: 'Bob' } ], |
- [ { name: 'Chuck' }, { name: 'Cathy' } ], |
- ... |
- ]; |
- |
- groups = [ |
- { letter: 'A' }, |
- { letter: 'B' }, |
- { letter: 'C' }, |
- ... |
- ]; |
- |
- <core-list data="{{data}}" groups="{{groups}}"> |
- <template> |
- <div divider class="divider">{{groupModel.letter}}</div> |
- <div class="item">{{model.name}}</div> |
- </template> |
- </core-list> |
- |
-### Grid layout **(experimental)** |
- |
-`core-list` supports a grid layout in addition to linear layout by setting |
-the `grid` attribute. In this case, the list template item must have both fixed |
-width and height (e.g. via CSS), with the desired width of each grid item |
-specified by the `width` attribute. Based on this, the number of items |
-per row are determined automatically based on the size of the list viewport. |
- |
-### Non-native scrollers **(experimental)** |
- |
-By default, core-list assumes the `scrollTarget` (if set) is a native scrollable |
-element (e.g. `overflow:auto` or `overflow:y`) that fires the `scroll` event and |
-whose scroll position can be read/set via the `scrollTop` property. |
-`core-list` provides experimental support for setting `scrollTarget` |
-to a custom scroller element (e.g. a JS-based scroller) as long as it provides |
-the following abstract API: |
- |
- - `getScrollTop()` - returns the current scroll position |
- - `setScrollTop(y)` - sets the current scroll position |
- - Fires a `scroll` event indicating when the scroll position has changed |
- |
-@group Polymer Core Elements |
-@element core-list |
---> |
-<link rel="import" href="../polymer/polymer.html"> |
-<link rel="import" href="../core-selection/core-selection.html"> |
-<link rel="import" href="../core-resizable/core-resizable.html"> |
- |
-<polymer-element name="core-list" tabindex="-1"> |
-<template> |
- <core-selection id="selection" multi="{{multi}}" on-core-select="{{selectedHandler}}"></core-selection> |
- <link rel="stylesheet" href="core-list.css"> |
- <div id="viewport" class="core-list-viewport"><content></content></div> |
-</template> |
-<script> |
-(function() { |
- |
- var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/); |
- var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8; |
- |
- Polymer(Polymer.mixin({ |
- |
- publish: { |
- /** |
- * Fired when an item element is tapped. |
- * |
- * @event core-activate |
- * @param {Object} detail |
- * @param {Object} detail.item the item element |
- */ |
- |
- /** |
- * An array of source data for the list to display. Elements |
- * from this array will be set to the `model` peroperty on each |
- * template instance scope for binding. |
- * |
- * When `groups` is used, this array may either be flat, with |
- * the group lengths specified in the `groups` array; otherwise |
- * `data` may be specified as an array of arrays, such that the |
- * each array in `data` specifies a group. See examples above. |
- * |
- * @attribute data |
- * @type array |
- * @default null |
- */ |
- data: null, |
- |
- /** |
- * An array of data conveying information about groupings of items |
- * in the `data` array. Elements from this array will be set to the |
- * `groupModel` property of each template instance scope for binding. |
- * |
- * When `groups` is used, template children with the `divider` attribute |
- * will be shown above each group. Typically data from the `groupModel` |
- * would be bound to dividers. |
- * |
- * If `data` is specified as a flat array, the `groups` array must |
- * contain objects of the format `{ length: n, data: {...} }`, where |
- * `length` determines the number of items from the `data` array |
- * that should be grouped, and `data` specifies the user data that will |
- * be assigned to the `groupModel` property on the template instance |
- * scope. |
- * |
- * If `data` is specified as a nested array of arrays, group lengths |
- * are derived from these arrays, so each object in `groups` need only |
- * contain the user data to be assigned to `groupModel`. |
- * |
- * @attribute groups |
- * @type array |
- * @default null |
- */ |
- groups: null, |
- |
- /** |
- * |
- * An optional element on which to listen for scroll events. |
- * |
- * @attribute scrollTarget |
- * @type Element |
- * @default core-list |
- */ |
- scrollTarget: null, |
- |
- /** |
- * |
- * When true, tapping a row will select the item, placing its data model |
- * in the set of selected items retrievable via the `selection` property. |
- * |
- * Note that tapping focusable elements within the list item will not |
- * result in selection, since they are presumed to have their own action. |
- * |
- * @attribute selectionEnabled |
- * @type {boolean} |
- * @default true |
- */ |
- selectionEnabled: true, |
- |
- /** |
- * |
- * Set to true to support multiple selection. Note, existing selection |
- * state is maintained only when changing `multi` from `false` to `true`; |
- * it is cleared when changing from `true` to `false`. |
- * |
- * @attribute multi |
- * @type boolean |
- * @default false |
- */ |
- multi: false, |
- |
- /** |
- * |
- * Data record (or array of records, if `multi: true`) corresponding to |
- * the currently selected set of items. |
- * |
- * @attribute selection |
- * @type {any} |
- * @default null |
- */ |
- selection: null, |
- |
- /** |
- * |
- * When true, the list is rendered as a grid. Grid items must be fixed |
- * height and width, with the width of each item specified in the `width` |
- * property. |
- * |
- * @attribute grid |
- * @type boolean |
- * @default false |
- */ |
- grid: false, |
- |
- /** |
- * |
- * When `grid` is used, `width` determines the width of each grid item. |
- * This property has no meaning when not in `grid` mode. |
- * |
- * @attribute width |
- * @type number |
- * @default null |
- */ |
- width: null, |
- |
- /** |
- * The approximate height of a list item, in pixels. This is used only for determining |
- * the number of physical elements to render based on the viewport size |
- * of the list. Items themselves may vary in height between each other |
- * depending on their data model. There is typically no need to adjust |
- * this value unless the average size is much larger or smaller than the default. |
- * |
- * @attribute height |
- * @type number |
- * @default 200 |
- */ |
- height: 200, |
- |
- /** |
- * The amount of scrolling runway the list keeps rendered, as a factor of |
- * the list viewport size. There is typically no need to adjust this value |
- * other than for performance tuning. Larger value correspond to more |
- * physical elements being rendered. |
- * |
- * @attribute runwayFactor |
- * @type number |
- * @default 4 |
- */ |
- runwayFactor: 4 |
- |
- }, |
- |
- eventDelegates: { |
- tap: 'tapHandler', |
- 'core-resize': 'updateSize' |
- }, |
- |
- // Local cache of scrollTop |
- _scrollTop: 0, |
- |
- observe: { |
- 'isAttached data grid width template scrollTarget': 'initialize', |
- 'multi selectionEnabled': '_resetSelection' |
- }, |
- |
- ready: function() { |
- this._boundScrollHandler = this.scrollHandler.bind(this); |
- this._boundPositionItems = this._positionItems.bind(this); |
- this._oldMulti = this.multi; |
- this._oldSelectionEnabled = this.selectionEnabled; |
- this._virtualStart = 0; |
- this._virtualCount = 0; |
- this._physicalStart = 0; |
- this._physicalOffset = 0; |
- this._physicalSize = 0; |
- this._physicalSizes = []; |
- this._physicalAverage = 0; |
- this._itemSizes = []; |
- this._dividerSizes = []; |
- this._repositionedItems = []; |
- |
- this._aboveSize = 0; |
- |
- this._nestedGroups = false; |
- this._groupStart = 0; |
- this._groupStartIndex = 0; |
- }, |
- |
- attached: function() { |
- this.isAttached = true; |
- this.template = this.querySelector('template'); |
- if (!this.template.bindingDelegate) { |
- this.template.bindingDelegate = this.element.syntax; |
- } |
- this.resizableAttachedHandler(); |
- }, |
- |
- detached: function() { |
- this.isAttached = false; |
- if (this._target) { |
- this._target.removeEventListener('scroll', this._boundScrollHandler); |
- } |
- this.resizableDetachedHandler(); |
- }, |
- |
- /** |
- * To be called by the user when the list is manually resized |
- * or shown after being hidden. |
- * |
- * @method updateSize |
- */ |
- updateSize: function() { |
- if (!this._positionPending && !this._needItemInit) { |
- this._resetIndex(this._getFirstVisibleIndex() || 0); |
- this.initialize(); |
- } |
- }, |
- |
- _resetSelection: function() { |
- if (((this._oldMulti != this.multi) && !this.multi) || |
- ((this._oldSelectionEnabled != this.selectionEnabled) && |
- !this.selectionEnabled)) { |
- this._clearSelection(); |
- this.refresh(); |
- } else { |
- this.selection = this.$.selection.getSelection(); |
- } |
- this._oldMulti = this.multi; |
- this._oldSelectionEnabled = this.selectionEnabled; |
- }, |
- |
- // Adjust virtual start index based on changes to backing data |
- _adjustVirtualIndex: function(splices, group) { |
- if (this._targetSize === 0) { |
- return; |
- } |
- var totalDelta = 0; |
- for (var i=0; i<splices.length; i++) { |
- var s = splices[i]; |
- var idx = s.index; |
- var gidx, gitem; |
- if (group) { |
- gidx = this.data.indexOf(group); |
- idx += this.virtualIndexForGroup(gidx); |
- } |
- // We only need to care about changes happening above the current position |
- if (idx >= this._virtualStart) { |
- break; |
- } |
- var delta = Math.max(s.addedCount - s.removed.length, idx - this._virtualStart); |
- totalDelta += delta; |
- this._physicalStart += delta; |
- this._virtualStart += delta; |
- if (this._grouped) { |
- if (group) { |
- gitem = s.index; |
- } else { |
- var g = this.groupForVirtualIndex(s.index); |
- gidx = g.group; |
- gitem = g.groupIndex; |
- } |
- if (gidx == this._groupStart && gitem < this._groupStartIndex) { |
- this._groupStartIndex += delta; |
- } |
- } |
- } |
- // Adjust offset/scroll position based on total number of items changed |
- if (this._virtualStart < this._physicalCount) { |
- this._resetIndex(this._getFirstVisibleIndex() || 0); |
- } else { |
- totalDelta = Math.max((totalDelta / this._rowFactor) * this._physicalAverage, -this._physicalOffset); |
- this._physicalOffset += totalDelta; |
- this._scrollTop = this.setScrollTop(this._scrollTop + totalDelta); |
- } |
- }, |
- |
- _updateSelection: function(splices) { |
- for (var i=0; i<splices.length; i++) { |
- var s = splices[i]; |
- for (var j=0; j<s.removed.length; j++) { |
- var d = s.removed[j]; |
- this.$.selection.setItemSelected(d, false); |
- } |
- } |
- }, |
- |
- groupsChanged: function() { |
- if (!!this.groups != this._grouped) { |
- this.updateSize(); |
- } |
- }, |
- |
- initialize: function() { |
- if (!this.template || !this.isAttached) { |
- return; |
- } |
- |
- // TODO(kschaaf): Checking arguments.length currently the only way to |
- // know that the array was mutated as opposed to newly assigned; need |
- // a better API for Polymer observers |
- var splices; |
- if (arguments.length == 1) { |
- splices = arguments[0]; |
- if (!this._nestedGroups) { |
- this._adjustVirtualIndex(splices); |
- } |
- this._updateSelection(splices); |
- } else { |
- this._clearSelection(); |
- } |
- |
- // Initialize scroll target |
- var target = this.scrollTarget || this; |
- if (this._target !== target) { |
- this.initializeScrollTarget(target); |
- } |
- |
- // Initialize data |
- this.initializeData(splices, false); |
- }, |
- |
- initializeScrollTarget: function(target) { |
- // Listen for scroll events |
- if (this._target) { |
- this._target.removeEventListener('scroll', this._boundScrollHandler, false); |
- } |
- this._target = target; |
- target.addEventListener('scroll', this._boundScrollHandler, false); |
- // Support for non-native scrollers (must implement abstract API): |
- // getScrollTop, setScrollTop, sync |
- if ((target != this) && target.setScrollTop && target.getScrollTop) { |
- this.setScrollTop = function(val) { |
- target.setScrollTop(val); |
- return target.getScrollTop(); |
- }; |
- this.getScrollTop = target.getScrollTop.bind(target); |
- this.syncScroller = target.sync ? target.sync.bind(target) : function() {}; |
- // Adjusting scroll position on non-native scrollers is risky |
- this.adjustPositionAllowed = false; |
- } else { |
- this.setScrollTop = function(val) { |
- target.scrollTop = val; |
- return target.scrollTop; |
- }; |
- this.getScrollTop = function() { |
- return target.scrollTop; |
- }; |
- this.syncScroller = function() {}; |
- this.adjustPositionAllowed = true; |
- } |
- // Only use -webkit-overflow-touch from iOS8+, where scroll events are fired |
- if (IOS_TOUCH_SCROLLING) { |
- target.style.webkitOverflowScrolling = 'touch'; |
- // Adjusting scrollTop during iOS momentum scrolling is "no bueno" |
- this.adjustPositionAllowed = false; |
- } |
- // Force overflow as necessary |
- this._target.style.willChange = 'transform'; |
- if (getComputedStyle(this._target).position == 'static') { |
- this._target.style.position = 'relative'; |
- } |
- this.style.overflowY = (target == this) ? 'auto' : null; |
- }, |
- |
- updateGroupObservers: function(splices) { |
- // If we're going from grouped to non-grouped, remove all observers |
- if (!this._nestedGroups) { |
- if (this._groupObservers && this._groupObservers.length) { |
- splices = [{ |
- index: 0, |
- addedCount: 0, |
- removed: this._groupObservers |
- }]; |
- } else { |
- splices = null; |
- } |
- } |
- // Otherwise, create observers for all groups, unless this is a group splice |
- if (this._nestedGroups) { |
- splices = splices || [{ |
- index: 0, |
- addedCount: this.data.length, |
- removed: [] |
- }]; |
- } |
- if (splices) { |
- var observers = this._groupObservers || []; |
- // Apply the splices to the observer array |
- for (var i=0; i<splices.length; i++) { |
- var s = splices[i], j; |
- var args = [s.index, s.removed.length]; |
- if (s.removed.length) { |
- for (j=s.index; j<s.removed.length; j++) { |
- observers[j].close(); |
- } |
- } |
- if (s.addedCount) { |
- for (j=s.index; j<s.addedCount; j++) { |
- var o = new ArrayObserver(this.data[j]); |
- args.push(o); |
- o.open(this.getGroupDataHandler(this.data[j])); |
- } |
- } |
- observers.splice.apply(observers, args); |
- } |
- this._groupObservers = observers; |
- } |
- }, |
- |
- getGroupDataHandler: function(group) { |
- return function(splices) { |
- this.groupDataChanged(splices, group); |
- }.bind(this); |
- }, |
- |
- groupDataChanged: function(splices, group) { |
- this._adjustVirtualIndex(splices, group); |
- this._updateSelection(splices); |
- this.initializeData(null, true); |
- }, |
- |
- initializeData: function(splices, groupUpdate) { |
- var i; |
- |
- // Calculate row-factor for grid layout |
- if (this.grid) { |
- if (!this.width) { |
- throw 'Grid requires the `width` property to be set'; |
- } |
- this._rowFactor = Math.floor(this._target.offsetWidth / this.width) || 1; |
- var cs = getComputedStyle(this._target); |
- var padding = parseInt(cs.paddingLeft || 0) + parseInt(cs.paddingRight || 0); |
- this._rowMargin = (this._target.offsetWidth - (this._rowFactor * this.width) - padding) / 2; |
- } else { |
- this._rowFactor = 1; |
- this._rowMargin = 0; |
- } |
- |
- // Count virtual data size, depending on whether grouping is enabled |
- if (!this.data || !this.data.length) { |
- this._virtualCount = 0; |
- this._grouped = false; |
- this._nestedGroups = false; |
- } else if (this.groups) { |
- this._grouped = true; |
- this._nestedGroups = Array.isArray(this.data[0]); |
- if (this._nestedGroups) { |
- if (this.groups.length != this.data.length) { |
- throw 'When using nested grouped data, data.length and groups.length must agree!'; |
- } |
- this._virtualCount = 0; |
- for (i=0; i<this.groups.length; i++) { |
- this._virtualCount += this.data[i] && this.data[i].length; |
- } |
- } else { |
- this._virtualCount = this.data.length; |
- var len = 0; |
- for (i=0; i<this.groups.length; i++) { |
- len += this.groups[i].length; |
- } |
- if (len != this.data.length) { |
- throw 'When using groups data, the sum of group[n].length\'s and data.length must agree!'; |
- } |
- } |
- var g = this.groupForVirtualIndex(this._virtualStart); |
- this._groupStart = g.group; |
- this._groupStartIndex = g.groupIndex; |
- } else { |
- this._grouped = false; |
- this._nestedGroups = false; |
- this._virtualCount = this.data.length; |
- } |
- |
- // Update grouped array observers used when group data is nested |
- if (!groupUpdate) { |
- this.updateGroupObservers(splices); |
- } |
- |
- // Add physical items up to a max based on data length, viewport size, and extra item overhang |
- var currentCount = this._physicalCount || 0; |
- var height = this._target.offsetHeight; |
- if (!height && this._target.offsetParent) { |
- console.warn('core-list must either be sized or be inside an overflow:auto div that is sized'); |
- } |
- this._physicalCount = Math.min(Math.ceil(height / (this._physicalAverage || this.height)) * this.runwayFactor * this._rowFactor, this._virtualCount); |
- this._physicalCount = Math.max(currentCount, this._physicalCount); |
- this._physicalData = this._physicalData || new Array(this._physicalCount); |
- var needItemInit = false; |
- while (currentCount < this._physicalCount) { |
- var model = this.templateInstance ? Object.create(this.templateInstance.model) : {}; |
- this._physicalData[currentCount++] = model; |
- needItemInit = true; |
- } |
- this.template.model = this._physicalData; |
- this.template.setAttribute('repeat', ''); |
- this._dir = 0; |
- |
- // If we've added new items, wait until the template renders then |
- // initialize the new items before refreshing |
- if (!this._needItemInit) { |
- if (needItemInit) { |
- this._needItemInit = true; |
- this.resetMetrics(); |
- this.onMutation(this, this.initializeItems); |
- } else { |
- this.refresh(); |
- } |
- } |
- }, |
- |
- initializeItems: function() { |
- var currentCount = this._physicalItems && this._physicalItems.length || 0; |
- this._physicalItems = this._physicalItems || [new Array(this._physicalCount)]; |
- this._physicalDividers = this._physicalDividers || new Array(this._physicalCount); |
- for (var i = 0, item = this.template.nextElementSibling; |
- item && i < this._physicalCount; |
- item = item.nextElementSibling) { |
- if (item.getAttribute('divider') != null) { |
- this._physicalDividers[i] = item; |
- } else { |
- this._physicalItems[i++] = item; |
- } |
- } |
- this.refresh(); |
- this._needItemInit = false; |
- }, |
- |
- _updateItemData: function(force, physicalIndex, virtualIndex, groupIndex, groupItemIndex) { |
- var physicalItem = this._physicalItems[physicalIndex]; |
- var physicalDatum = this._physicalData[physicalIndex]; |
- var virtualDatum = this.dataForIndex(virtualIndex, groupIndex, groupItemIndex); |
- var needsReposition; |
- if (force || physicalDatum.model != virtualDatum) { |
- // Set model, index, and selected fields |
- physicalDatum.model = virtualDatum; |
- physicalDatum.index = virtualIndex; |
- physicalDatum.physicalIndex = physicalIndex; |
- physicalDatum.selected = this.selectionEnabled && virtualDatum ? |
- this._selectedData.get(virtualDatum) : null; |
- // Set group-related fields |
- if (this._grouped) { |
- var groupModel = this.groups[groupIndex]; |
- physicalDatum.groupModel = groupModel && (this._nestedGroups ? groupModel : groupModel.data); |
- physicalDatum.groupIndex = groupIndex; |
- physicalDatum.groupItemIndex = groupItemIndex; |
- physicalItem._isDivider = this.data.length && (groupItemIndex === 0); |
- physicalItem._isRowStart = (groupItemIndex % this._rowFactor) === 0; |
- } else { |
- physicalDatum.groupModel = null; |
- physicalDatum.groupIndex = null; |
- physicalDatum.groupItemIndex = null; |
- physicalItem._isDivider = false; |
- physicalItem._isRowStart = (virtualIndex % this._rowFactor) === 0; |
- } |
- // Hide physical items when not in use (no model assigned) |
- physicalItem.hidden = !virtualDatum; |
- var divider = this._physicalDividers[physicalIndex]; |
- if (divider && (divider.hidden == physicalItem._isDivider)) { |
- divider.hidden = !physicalItem._isDivider; |
- } |
- needsReposition = !force; |
- } else { |
- needsReposition = false; |
- } |
- return needsReposition || force; |
- }, |
- |
- scrollHandler: function() { |
- if (IOS_TOUCH_SCROLLING) { |
- // iOS sends multiple scroll events per rAF |
- // Align work to rAF to reduce overhead & artifacts |
- if (!this._raf) { |
- this._raf = requestAnimationFrame(function() { |
- this._raf = null; |
- this.refresh(); |
- }.bind(this)); |
- } |
- } else { |
- this.refresh(); |
- } |
- }, |
- |
- resetMetrics: function() { |
- this._physicalAverage = 0; |
- this._physicalAverageCount = 0; |
- }, |
- |
- updateMetrics: function(force) { |
- // Measure physical items & dividers |
- var totalSize = 0; |
- var count = 0; |
- for (var i=0; i<this._physicalCount; i++) { |
- var item = this._physicalItems[i]; |
- if (!item.hidden) { |
- var size = this._itemSizes[i] = item.offsetHeight; |
- if (item._isDivider) { |
- var divider = this._physicalDividers[i]; |
- if (divider) { |
- size += (this._dividerSizes[i] = divider.offsetHeight); |
- } |
- } |
- this._physicalSizes[i] = size; |
- if (item._isRowStart) { |
- totalSize += size; |
- count++; |
- } |
- } |
- } |
- this._physicalSize = totalSize; |
- |
- // Measure other DOM |
- this._viewportSize = this.$.viewport.offsetHeight; |
- this._targetSize = this._target.offsetHeight; |
- |
- // Measure content in scroller before virtualized items |
- if (this._target != this) { |
- this._aboveSize = this.offsetTop; |
- } else { |
- this._aboveSize = parseInt(getComputedStyle(this._target).paddingTop); |
- } |
- |
- // Calculate average height |
- if (count) { |
- totalSize = (this._physicalAverage * this._physicalAverageCount) + totalSize; |
- this._physicalAverageCount += count; |
- this._physicalAverage = Math.round(totalSize / this._physicalAverageCount); |
- } |
- }, |
- |
- getGroupLen: function(group) { |
- group = arguments.length ? group : this._groupStart; |
- if (this._nestedGroups) { |
- return this.data[group].length; |
- } else { |
- return this.groups[group].length; |
- } |
- }, |
- |
- changeStartIndex: function(inc) { |
- this._virtualStart += inc; |
- if (this._grouped) { |
- while (inc > 0) { |
- var groupMax = this.getGroupLen() - this._groupStartIndex - 1; |
- if (inc > groupMax) { |
- inc -= (groupMax + 1); |
- this._groupStart++; |
- this._groupStartIndex = 0; |
- } else { |
- this._groupStartIndex += inc; |
- inc = 0; |
- } |
- } |
- while (inc < 0) { |
- if (-inc > this._groupStartIndex) { |
- inc += this._groupStartIndex; |
- this._groupStart--; |
- this._groupStartIndex = this.getGroupLen(); |
- } else { |
- this._groupStartIndex += inc; |
- inc = this.getGroupLen(); |
- } |
- } |
- } |
- // In grid mode, virtualIndex must alway start on a row start! |
- if (this.grid) { |
- if (this._grouped) { |
- inc = this._groupStartIndex % this._rowFactor; |
- } else { |
- inc = this._virtualStart % this._rowFactor; |
- } |
- if (inc) { |
- this.changeStartIndex(-inc); |
- } |
- } |
- }, |
- |
- getRowCount: function(dir) { |
- if (!this.grid) { |
- return dir; |
- } else if (!this._grouped) { |
- return dir * this._rowFactor; |
- } else { |
- if (dir < 0) { |
- if (this._groupStartIndex > 0) { |
- return -Math.min(this._rowFactor, this._groupStartIndex); |
- } else { |
- var prevLen = this.getGroupLen(this._groupStart-1); |
- return -Math.min(this._rowFactor, prevLen % this._rowFactor || this._rowFactor); |
- } |
- } else { |
- return Math.min(this._rowFactor, this.getGroupLen() - this._groupStartIndex); |
- } |
- } |
- }, |
- |
- _virtualToPhysical: function(virtualIndex) { |
- var physicalIndex = (virtualIndex - this._physicalStart) % this._physicalCount; |
- return physicalIndex < 0 ? this._physicalCount + physicalIndex : physicalIndex; |
- }, |
- |
- groupForVirtualIndex: function(virtual) { |
- if (!this._grouped) { |
- return {}; |
- } else { |
- var group; |
- for (group=0; group<this.groups.length; group++) { |
- var groupLen = this.getGroupLen(group); |
- if (groupLen > virtual) { |
- break; |
- } else { |
- virtual -= groupLen; |
- } |
- } |
- return {group: group, groupIndex: virtual }; |
- } |
- }, |
- |
- virtualIndexForGroup: function(group, groupIndex) { |
- groupIndex = groupIndex ? Math.min(groupIndex, this.getGroupLen(group)) : 0; |
- group--; |
- while (group >= 0) { |
- groupIndex += this.getGroupLen(group--); |
- } |
- return groupIndex; |
- }, |
- |
- dataForIndex: function(virtual, group, groupIndex) { |
- if (this.data) { |
- if (this._nestedGroups) { |
- if (virtual < this._virtualCount) { |
- return this.data[group][groupIndex]; |
- } |
- } else { |
- return this.data[virtual]; |
- } |
- } |
- }, |
- |
- // Refresh the list at the current scroll position. |
- refresh: function() { |
- var i, deltaCount; |
- |
- // Determine scroll position & any scrollDelta that may have occurred |
- var lastScrollTop = this._scrollTop; |
- this._scrollTop = this.getScrollTop(); |
- var scrollDelta = this._scrollTop - lastScrollTop; |
- this._dir = scrollDelta < 0 ? -1 : scrollDelta > 0 ? 1 : 0; |
- |
- // Adjust virtual items and positioning offset if scroll occurred |
- if (Math.abs(scrollDelta) > Math.max(this._physicalSize, this._targetSize)) { |
- // Random access to point in list: guess new index based on average size |
- deltaCount = Math.round((scrollDelta / this._physicalAverage) * this._rowFactor); |
- deltaCount = Math.max(deltaCount, -this._virtualStart); |
- deltaCount = Math.min(deltaCount, this._virtualCount - this._virtualStart - 1); |
- this._physicalOffset += Math.max(scrollDelta, -this._physicalOffset); |
- this.changeStartIndex(deltaCount); |
- // console.log(this._scrollTop, 'Random access to ' + this._virtualStart, this._physicalOffset); |
- } else { |
- // Incremental movement: adjust index by flipping items |
- var base = this._aboveSize + this._physicalOffset; |
- var margin = 0.3 * Math.max((this._physicalSize - this._targetSize, this._physicalSize)); |
- this._upperBound = base + margin; |
- this._lowerBound = base + this._physicalSize - this._targetSize - margin; |
- var flipBound = this._dir > 0 ? this._upperBound : this._lowerBound; |
- if (((this._dir > 0 && this._scrollTop > flipBound) || |
- (this._dir < 0 && this._scrollTop < flipBound))) { |
- var flipSize = Math.abs(this._scrollTop - flipBound); |
- for (i=0; (i<this._physicalCount) && (flipSize > 0) && |
- ((this._dir < 0 && this._virtualStart > 0) || |
- (this._dir > 0 && this._virtualStart < this._virtualCount-this._physicalCount)); i++) { |
- var idx = this._virtualToPhysical(this._dir > 0 ? |
- this._virtualStart : |
- this._virtualStart + this._physicalCount -1); |
- var size = this._physicalSizes[idx]; |
- flipSize -= size; |
- var cnt = this.getRowCount(this._dir); |
- // console.log(this._scrollTop, 'flip ' + (this._dir > 0 ? 'down' : 'up'), cnt, this._virtualStart, this._physicalOffset); |
- if (this._dir > 0) { |
- // When scrolling down, offset is adjusted based on previous item's size |
- this._physicalOffset += size; |
- // console.log(' ->', this._virtualStart, size, this._physicalOffset); |
- } |
- this.changeStartIndex(cnt); |
- if (this._dir < 0) { |
- this._repositionedItems.push(this._virtualStart); |
- } |
- } |
- } |
- } |
- |
- // Assign data to items lazily if scrolling, otherwise force |
- if (this._updateItems(!scrollDelta)) { |
- // Position items after bindings resolve (method varies based on O.o impl) |
- if (Observer.hasObjectObserve) { |
- this.async(this._boundPositionItems); |
- } else { |
- Platform.flush(); |
- Platform.endOfMicrotask(this._boundPositionItems); |
- } |
- } |
- }, |
- |
- _updateItems: function(force) { |
- var i, virtualIndex, physicalIndex; |
- var needsReposition = false; |
- var groupIndex = this._groupStart; |
- var groupItemIndex = this._groupStartIndex; |
- for (i = 0; i < this._physicalCount; ++i) { |
- virtualIndex = this._virtualStart + i; |
- physicalIndex = this._virtualToPhysical(virtualIndex); |
- // Update physical item with new user data and list metadata |
- needsReposition = |
- this._updateItemData(force, physicalIndex, virtualIndex, groupIndex, groupItemIndex) || needsReposition; |
- // Increment |
- groupItemIndex++; |
- if (this.groups && groupIndex < this.groups.length - 1) { |
- if (groupItemIndex >= this.getGroupLen(groupIndex)) { |
- groupItemIndex = 0; |
- groupIndex++; |
- } |
- } |
- } |
- return needsReposition; |
- }, |
- |
- _positionItems: function() { |
- var i, virtualIndex, physicalIndex, physicalItem; |
- |
- // Measure |
- this.updateMetrics(); |
- |
- // Pre-positioning tasks |
- if (this._dir < 0) { |
- // When going up, remove offset after measuring size for |
- // new data for item being moved from bottom to top |
- while (this._repositionedItems.length) { |
- virtualIndex = this._repositionedItems.pop(); |
- physicalIndex = this._virtualToPhysical(virtualIndex); |
- this._physicalOffset -= this._physicalSizes[physicalIndex]; |
- // console.log(' <-', virtualIndex, this._physicalSizes[physicalIndex], this._physicalOffset); |
- } |
- // Adjust scroll position to home into top when going up |
- if (this._scrollTop + this._targetSize < this._viewportSize) { |
- this._updateScrollPosition(this._scrollTop); |
- } |
- } |
- |
- // Position items |
- var divider, upperBound, lowerBound; |
- var rowx = 0; |
- var x = this._rowMargin; |
- var y = this._physicalOffset; |
- var lastHeight = 0; |
- for (i = 0; i < this._physicalCount; ++i) { |
- // Calculate indices |
- virtualIndex = this._virtualStart + i; |
- physicalIndex = this._virtualToPhysical(virtualIndex); |
- physicalItem = this._physicalItems[physicalIndex]; |
- // Position divider |
- if (physicalItem._isDivider) { |
- if (rowx !== 0) { |
- y += lastHeight; |
- rowx = 0; |
- } |
- divider = this._physicalDividers[physicalIndex]; |
- x = this._rowMargin; |
- if (divider && (divider._translateX != x || divider._translateY != y)) { |
- divider.style.opacity = 1; |
- if (this.grid) { |
- divider.style.width = this.width * this._rowFactor + 'px'; |
- } |
- divider.style.transform = divider.style.webkitTransform = |
- 'translate3d(' + x + 'px,' + y + 'px,0)'; |
- divider._translateX = x; |
- divider._translateY = y; |
- } |
- y += this._dividerSizes[physicalIndex]; |
- } |
- // Position item |
- if (physicalItem._translateX != x || physicalItem._translateY != y) { |
- physicalItem.style.opacity = 1; |
- physicalItem.style.transform = physicalItem.style.webkitTransform = |
- 'translate3d(' + x + 'px,' + y + 'px,0)'; |
- physicalItem._translateX = x; |
- physicalItem._translateY = y; |
- } |
- // Increment offsets |
- lastHeight = this._itemSizes[physicalIndex]; |
- if (this.grid) { |
- rowx++; |
- if (rowx >= this._rowFactor) { |
- rowx = 0; |
- y += lastHeight; |
- } |
- x = this._rowMargin + rowx * this.width; |
- } else { |
- y += lastHeight; |
- } |
- } |
- |
- if (this._scrollTop >= 0) { |
- this._updateViewportHeight(); |
- } |
- }, |
- |
- _updateViewportHeight: function() { |
- var remaining = Math.max(this._virtualCount - this._virtualStart - this._physicalCount, 0); |
- remaining = Math.ceil(remaining / this._rowFactor); |
- var vs = this._physicalOffset + this._physicalSize + remaining * this._physicalAverage; |
- if (this._viewportSize != vs) { |
- // console.log(this._scrollTop, 'adjusting viewport height', vs - this._viewportSize, vs); |
- this._viewportSize = vs; |
- this.$.viewport.style.height = this._viewportSize + 'px'; |
- this.syncScroller(); |
- } |
- }, |
- |
- _updateScrollPosition: function(scrollTop) { |
- var deltaHeight = this._virtualStart === 0 ? this._physicalOffset : |
- Math.min(scrollTop + this._physicalOffset, 0); |
- if (deltaHeight) { |
- // console.log(scrollTop, 'adjusting scroll pos', this._virtualStart, -deltaHeight, scrollTop - deltaHeight); |
- if (this.adjustPositionAllowed) { |
- this._scrollTop = this.setScrollTop(scrollTop - deltaHeight); |
- } |
- this._physicalOffset -= deltaHeight; |
- } |
- }, |
- |
- // list selection |
- tapHandler: function(e) { |
- var n = e.target; |
- var p = e.path; |
- if (!this.selectionEnabled || (n === this)) { |
- return; |
- } |
- requestAnimationFrame(function() { |
- // Gambit: only select the item if the tap wasn't on a focusable child |
- // of the list (since anything with its own action should be focusable |
- // and not result in result in list selection). To check this, we |
- // asynchronously check that shadowRoot.activeElement is null, which |
- // means the tapped item wasn't focusable. On polyfill where |
- // activeElement doesn't follow the data-hinding part of the spec, we |
- // can check that document.activeElement is the list itself, which will |
- // catch focus in lieu of the tapped item being focusable, as we make |
- // the list focusable (tabindex="-1") for this purpose. Note we also |
- // allow the list items themselves to be focusable if desired, so those |
- // are excluded as well. |
- var active = window.ShadowDOMPolyfill ? |
- wrap(document.activeElement) : this.shadowRoot.activeElement; |
- if (active && (active != this) && (active.parentElement != this) && |
- (document.activeElement != document.body)) { |
- return; |
- } |
- // Unfortunately, Safari does not focus certain form controls via mouse, |
- // so we also blacklist input, button, & select |
- // (https://bugs.webkit.org/show_bug.cgi?id=118043) |
- if ((p[0].localName == 'input') || |
- (p[0].localName == 'button') || |
- (p[0].localName == 'select')) { |
- return; |
- } |
- |
- var model = n.templateInstance && n.templateInstance.model; |
- if (model) { |
- var data = this.dataForIndex(model.index, model.groupIndex, model.groupItemIndex); |
- var item = this._physicalItems[model.physicalIndex]; |
- if (!this.multi && data == this.selection) { |
- this.$.selection.select(null); |
- } else { |
- this.$.selection.select(data); |
- } |
- this.asyncFire('core-activate', {data: data, item: item}); |
- } |
- }.bind(this)); |
- }, |
- |
- selectedHandler: function(e, detail) { |
- this.selection = this.$.selection.getSelection(); |
- var id = this.indexesForData(detail.item); |
- // TODO(sorvell): we should be relying on selection to store the |
- // selected data but we want to optimize for lookup. |
- this._selectedData.set(detail.item, detail.isSelected); |
- if (id.physical >= 0 && id.virtual >= 0) { |
- this.refresh(); |
- } |
- }, |
- |
- /** |
- * Select the list item at the given index. |
- * |
- * @method selectItem |
- * @param {number} index |
- */ |
- selectItem: function(index) { |
- if (!this.selectionEnabled) { |
- return; |
- } |
- var data = this.data[index]; |
- if (data) { |
- this.$.selection.select(data); |
- } |
- }, |
- |
- /** |
- * Set the selected state of the list item at the given index. |
- * |
- * @method setItemSelected |
- * @param {number} index |
- * @param {boolean} isSelected |
- */ |
- setItemSelected: function(index, isSelected) { |
- var data = this.data[index]; |
- if (data) { |
- this.$.selection.setItemSelected(data, isSelected); |
- } |
- }, |
- |
- indexesForData: function(data) { |
- var virtual = -1; |
- var groupsLen = 0; |
- if (this._nestedGroups) { |
- for (var i=0; i<this.groups.length; i++) { |
- virtual = this.data[i].indexOf(data); |
- if (virtual < 0) { |
- groupsLen += this.data[i].length; |
- } else { |
- virtual += groupsLen; |
- break; |
- } |
- } |
- } else { |
- virtual = this.data.indexOf(data); |
- } |
- var physical = this.virtualToPhysicalIndex(virtual); |
- return { virtual: virtual, physical: physical }; |
- }, |
- |
- virtualToPhysicalIndex: function(index) { |
- for (var i=0, l=this._physicalData.length; i<l; i++) { |
- if (this._physicalData[i].index === index) { |
- return i; |
- } |
- } |
- return -1; |
- }, |
- |
- /** |
- * Clears the current selection state of the list. |
- * |
- * @method clearSelection |
- */ |
- clearSelection: function() { |
- this._clearSelection(); |
- this.refresh(); |
- }, |
- |
- _clearSelection: function() { |
- this._selectedData = new WeakMap(); |
- this.$.selection.clear(); |
- this.selection = this.$.selection.getSelection(); |
- }, |
- |
- _getFirstVisibleIndex: function() { |
- for (var i=0; i<this._physicalCount; i++) { |
- var virtualIndex = this._virtualStart + i; |
- var physicalIndex = this._virtualToPhysical(virtualIndex); |
- var item = this._physicalItems[physicalIndex]; |
- if (!item.hidden && item._translateY >= this._scrollTop - this._aboveSize) { |
- return virtualIndex; |
- } |
- } |
- }, |
- |
- _resetIndex: function(index) { |
- index = Math.min(index, this._virtualCount-1); |
- index = Math.max(index, 0); |
- this.changeStartIndex(index - this._virtualStart); |
- this._scrollTop = this.setScrollTop(this._aboveSize + (index / this._rowFactor) * this._physicalAverage); |
- this._physicalOffset = this._scrollTop - this._aboveSize; |
- this._dir = 0; |
- }, |
- |
- /** |
- * Scroll to an item. |
- * |
- * Note, when grouping is used, the index is based on the |
- * total flattened number of items. For scrolling to an item |
- * within a group, use the `scrollToGroupItem` API. |
- * |
- * @method scrollToItem |
- * @param {number} index |
- */ |
- scrollToItem: function(index) { |
- this.scrollToGroupItem(null, index); |
- }, |
- |
- /** |
- * Scroll to a group. |
- * |
- * @method scrollToGroup |
- * @param {number} group |
- */ |
- scrollToGroup: function(group) { |
- this.scrollToGroupItem(group, 0); |
- }, |
- |
- /** |
- * Scroll to an item within a group. |
- * |
- * @method scrollToGroupItem |
- * @param {number} group |
- * @param {number} index |
- */ |
- scrollToGroupItem: function(group, index) { |
- if (group != null) { |
- index = this.virtualIndexForGroup(group, index); |
- } |
- this._resetIndex(index); |
- this.refresh(); |
- } |
- |
- }, Polymer.CoreResizable)); |
- |
-})(); |
-</script> |
-</polymer-element> |