1316 lines
47 KiB
HTML
1316 lines
47 KiB
HTML
<!--
|
|
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.
|
|
|
|
IMPORTANT: `core-list` must ether be explicitly sized, or delegate scrolling to
|
|
an explicitly sized parent. By "explicitly sized", we mean it either has an
|
|
explicit CSS `height` property set via a class or inline style, or else is sized
|
|
by other layout means (e.g. `flex` or `fit`). Alternatively, `core-list` can
|
|
delegate scrolling to a scrollable element that contains the list by setting the
|
|
`scrollTarget` property, and the same explicit sizing requiremets will apply
|
|
to that element.
|
|
|
|
### 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
|
|
@mixins Polymer.CoreResizable https://github.com/polymer/core-resizable
|
|
-->
|
|
<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._target.style.boxSizing = this._target.style.mozBoxSizing = 'border-box';
|
|
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';
|
|
}
|
|
var cs = getComputedStyle(this._target);
|
|
var padding = parseInt(cs.paddingLeft || 0) + parseInt(cs.paddingRight || 0);
|
|
this._rowFactor = Math.floor((this._target.offsetWidth - padding) / this.width) || 1;
|
|
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>
|