2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
\r
3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
\r
4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
\r
5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
\r
6 Code distributed by Google as part of the polymer project is also
\r
7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
\r
11 `core-list` displays a virtual, 'infinite' list. The template inside the
\r
12 `core-list` element represents the dom to create for each list item. The
\r
13 `data` property specifies an array of list item data. The `height` property
\r
14 represents the height of a list item.
\r
16 By default, the list supports selection via tapping. Styling the selection
\r
17 should be done via binding. The `selectedProperty` property is set on the
\r
18 list view data for each selected item.
\r
20 `core-list` manages a viewport of data based on the current scroll position.
\r
21 For performance reasons, not every item in the list is rendered at once.
\r
23 <core-list data="{{data}}" height="80">
\r
25 <div class="{{ {selected: selected} | tokenList }}">List row: {{index}}</div>
\r
29 @group Polymer Core Elements
\r
32 <link rel="import" href="../polymer/polymer.html">
\r
33 <link rel="import" href="../core-selection/core-selection.html">
\r
35 <polymer-element name="core-list" on-tap="{{tapHandler}}">
\r
37 <core-selection id="selection" multi="{{multi}}" on-core-select="{{selectedHandler}}"></core-selection>
\r
38 <link rel="stylesheet" href="core-list.css">
\r
39 <div id="viewport" class="core-list-viewport"><content></content></div>
\r
44 Polymer('core-list', {
\r
48 * Fired when an item element is tapped.
\r
50 * @event core-activate
\r
51 * @param {Object} detail
\r
52 * @param {Object} detail.item the item element
\r
57 * An array of source data for the list to display.
\r
67 * An optional element on which to listen for scroll events.
\r
69 * @attribute scrollTarget
\r
71 * @default core-list
\r
77 * The height of a list item. `core-list` currently supports only fixed-height
\r
78 * list items. This height must be specified via the height property.
\r
88 * The number of extra items rendered above the minimum set required to
\r
89 * fill the list's height.
\r
91 * @attribute extraItems
\r
99 * The property set on the list view data to represent selection state.
\r
100 * This should set so that it does not conflict with other data properties.
\r
101 * Note, selection data is not stored on the data in given in the data property.
\r
103 * @attribute selectedProperty
\r
105 * @default 'selected'
\r
107 selectedProperty: 'selected',
\r
109 // TODO(sorvell): experimental
\r
112 * If true, data is sync'd from the list back to the list's data.
\r
122 * Set to true to support multiple selection.
\r
133 'data template scrollTarget': 'initialize'
\r
136 ready: function() {
\r
137 this.clearSelection();
\r
138 this._boundScrollHandler = this.scrollHandler.bind(this);
\r
141 attached: function() {
\r
142 this.template = this.querySelector('template');
\r
145 // TODO(sorvell): it'd be nice to dispense with 'data' and just use
\r
146 // template repeat's model. However, we need tighter integration
\r
147 // with TemplateBinding for this.
\r
148 initialize: function() {
\r
149 if (!this.data || !this.template) {
\r
152 var target = this.scrollTarget || this;
\r
153 if (this._target !== target) {
\r
154 if (this._target) {
\r
155 this._target.removeEventListener('scroll', this._boundScrollHandler, false);
\r
157 this._target = target;
\r
158 this._target.addEventListener('scroll', this._boundScrollHandler, false);
\r
161 this.initializeViewport();
\r
162 this.initalizeData();
\r
163 this.onMutation(this, this.initializeItems);
\r
166 // TODO(sorvell): need to handle resizing
\r
167 initializeViewport: function() {
\r
168 this.$.viewport.style.height = this.height * this.data.length + 'px';
\r
169 this._visibleCount = Math.ceil(this._target.offsetHeight / this.height);
\r
170 this._physicalCount = Math.min(this._visibleCount + this.extraItems,
\r
172 this._physicalHeight = this.height * this._physicalCount;
\r
175 // TODO(sorvell): selection currently cannot be maintained when
\r
176 // items are added or deleted.
\r
177 initalizeData: function() {
\r
178 var exampleDatum = this.data[0] || {};
\r
179 this._propertyNames = Object.getOwnPropertyNames(exampleDatum);
\r
180 this._physicalData = new Array(this._physicalCount);
\r
181 for (var i = 0; i < this._physicalCount; ++i) {
\r
182 this._physicalData[i] = {};
\r
183 this.updateItem(i, i);
\r
185 this.template.model = this._physicalData;
\r
186 this.template.setAttribute('repeat', '');
\r
189 initializeItems: function() {
\r
190 this._physicalItems = new Array(this._physicalCount);
\r
191 for (var i = 0, item = this.template.nextElementSibling;
\r
192 item && i < this._physicalCount;
\r
193 ++i, item = item.nextElementSibling) {
\r
194 this._physicalItems[i] = item;
\r
195 item._transformValue = 0;
\r
197 this.refresh(false);
\r
200 updateItem: function(virtualIndex, physicalIndex) {
\r
201 var virtualDatum = this.data[virtualIndex];
\r
202 var physicalDatum = this._physicalData[physicalIndex];
\r
203 this.pushItemData(virtualDatum, physicalDatum);
\r
204 physicalDatum._physicalIndex = physicalIndex;
\r
205 physicalDatum._virtualIndex = virtualIndex;
\r
206 if (this.selectedProperty) {
\r
207 physicalDatum[this.selectedProperty] = this._selectedData.get(virtualDatum);
\r
211 pushItemData: function(source, dest) {
\r
212 for (var i = 0; i < this._propertyNames.length; ++i) {
\r
213 var propertyName = this._propertyNames[i];
\r
214 dest[propertyName] = source[propertyName];
\r
218 // experimental: push physical data back to this.data.
\r
219 // this is optional when scrolling and needs to be called at other times.
\r
220 syncData: function() {
\r
221 if (this.firstPhysicalIndex === undefined ||
\r
222 this.baseVirtualIndex === undefined) {
\r
226 for (var i = 0; i < this.firstPhysicalIndex; ++i) {
\r
227 p = this._physicalData[i];
\r
228 v = this.data[this.baseVirtualIndex + this._physicalCount + i];
\r
229 this.pushItemData(p, v);
\r
231 for (var i = this.firstPhysicalIndex; i < this._physicalCount; ++i) {
\r
232 p = this._physicalData[i];
\r
233 v = this.data[this.baseVirtualIndex + i];
\r
234 this.pushItemData(p, v);
\r
238 scrollHandler: function(e, detail) {
\r
239 this._scrollTop = e.detail ? e.detail.target.scrollTop : e.target.scrollTop;
\r
240 this.refresh(false);
\r
244 * Refresh the list at the current scroll position.
\r
248 refresh: function(force) {
\r
249 var firstVisibleIndex = Math.floor(this._scrollTop / this.height);
\r
250 var visibleMidpoint = firstVisibleIndex + this._visibleCount / 2;
\r
252 var firstReifiedIndex = Math.max(0, Math.floor(visibleMidpoint -
\r
253 this._physicalCount / 2));
\r
254 firstReifiedIndex = Math.min(firstReifiedIndex, this.data.length -
\r
255 this._physicalCount);
\r
257 var firstPhysicalIndex = firstReifiedIndex % this._physicalCount;
\r
258 var baseVirtualIndex = firstReifiedIndex - firstPhysicalIndex;
\r
260 var baseTransformValue = Math.floor(this.height * baseVirtualIndex);
\r
261 var nextTransformValue = Math.floor(baseTransformValue +
\r
262 this._physicalHeight);
\r
264 var baseTransformString = 'translate3d(0,' + baseTransformValue + 'px,0)';
\r
265 var nextTransformString = 'translate3d(0,' + nextTransformValue + 'px,0)';
\r
266 // TODO(sorvell): experiemental for sync'ing back to virtual data.
\r
270 this.firstPhysicalIndex = firstPhysicalIndex;
\r
271 this.baseVirtualIndex = baseVirtualIndex;
\r
273 for (var i = 0; i < firstPhysicalIndex; ++i) {
\r
274 var item = this._physicalItems[i];
\r
275 if (force || item._transformValue != nextTransformValue) {
\r
276 this.updateItem(baseVirtualIndex + this._physicalCount + i, i);
\r
277 setTransform(item, nextTransformString, nextTransformValue);
\r
280 for (var i = firstPhysicalIndex; i < this._physicalCount; ++i) {
\r
281 var item = this._physicalItems[i];
\r
282 if (force || item._transformValue != baseTransformValue) {
\r
283 this.updateItem(baseVirtualIndex + i, i);
\r
284 setTransform(item, baseTransformString, baseTransformValue);
\r
290 tapHandler: function(e) {
\r
291 if (e.target === this) {
\r
298 var model = n.templateInstance && n.templateInstance.model;
\r
300 var vi = model._virtualIndex, pi = model._physicalIndex;
\r
301 var data = this.data[vi], item = this._physicalItems[pi];
\r
302 this.$.selection.select(data);
\r
303 this.asyncFire('core-activate', {data: data, item: item});
\r
307 selectedHandler: function(e, detail) {
\r
308 if (this.selectedProperty) {
\r
309 var i$ = this.indexesForData(detail.item);
\r
310 // TODO(sorvell): we should be relying on selection to store the
\r
311 // selected data but we want to optimize for lookup.
\r
312 this._selectedData.set(detail.item, detail.isSelected);
\r
313 if (i$.physical >= 0) {
\r
314 this.updateItem(i$.virtual, i$.physical);
\r
320 * Select the list item at the given index.
\r
322 * @method selectItem
\r
323 * @param {number} index
\r
325 selectItem: function(index) {
\r
326 var data = this.data[index];
\r
328 this.$.selection.select(data);
\r
333 * Set the selected state of the list item at the given index.
\r
335 * @method setItemSelected
\r
336 * @param {number} index
\r
337 * @param {boolean} isSelected
\r
339 setItemSelected: function(index, isSelected) {
\r
340 var data = this.data[index];
\r
342 this.$.selection.setItemSelected(data, isSelected);
\r
346 indexesForData: function(data) {
\r
347 var virtual = this.data.indexOf(data);
\r
348 var physical = this.virtualToPhysicalIndex(virtual);
\r
349 return { virtual: virtual, physical: physical };
\r
352 virtualToPhysicalIndex: function(index) {
\r
353 for (var i=0, l=this._physicalData.length; i<l; i++) {
\r
354 if (this._physicalData[i]._virtualIndex === index) {
\r
362 return this.$.selection.getSelection();
\r
365 selectedChanged: function() {
\r
366 this.$.selection.select(this.selected);
\r
369 clearSelection: function() {
\r
370 this._selectedData = new WeakMap();
\r
372 var s$ = this.selection;
\r
373 for (var i=0, l=s$.length, s; (i<l) && (s=s$[i]); i++) {
\r
374 this.$.selection.setItemSelected(s, false);
\r
377 this.$.selection.setItemSelected(this.selection, false);
\r
379 this.$.selection.clear();
\r
382 scrollToItem: function(index) {
\r
383 this.scrollTop = index * this.height;
\r
388 // determine proper transform mechanizm
\r
389 if (document.documentElement.style.transform !== undefined) {
\r
390 function setTransform(element, string, value) {
\r
391 element.style.transform = string;
\r
392 element._transformValue = value;
\r
395 function setTransform(element, string, value) {
\r
396 element.style.webkitTransform = string;
\r
397 element._transformValue = value;
\r