Index: polymer_0.5.0/bower_components/core-list/core-list.html |
diff --git a/polymer_0.5.0/bower_components/core-list/core-list.html b/polymer_0.5.0/bower_components/core-list/core-list.html |
new file mode 100644 |
index 0000000000000000000000000000000000000000..8879092d495784434f2e77af79f37c95041f1f08 |
--- /dev/null |
+++ b/polymer_0.5.0/bower_components/core-list/core-list.html |
@@ -0,0 +1,1306 @@ |
+<!-- |
+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> |