Skip to content
This repository has been archived by the owner on Mar 13, 2018. It is now read-only.

Commit

Permalink
core-list, a list element for displaying a lot of items.
Browse files Browse the repository at this point in the history
  • Loading branch information
sorvell committed Apr 21, 2014
0 parents commit dac95e7
Show file tree
Hide file tree
Showing 10 changed files with 521 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
core-list
============

A list for displaying a large number of items.
8 changes: 8 additions & 0 deletions bower.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "core-list",
"private": true,
"dependencies": {
"polymer": "Polymer/polymer#master",
"core-selection": "Polymer/core-selection#master"
}
}
20 changes: 20 additions & 0 deletions core-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
:host {
display: block;
overflow: auto;
/*-webkit-overflow-scrolling: touch;
-webkit-transform: translateZ(0);
transform: translateZ(0);*/
}

.core-list-viewport > * {
overflow: hidden;
}

.core-list-viewport.horizontal {
height: 100%;
white-space: nowrap;
}

.core-list-viewport.horizontal > * {
display: inline-block;
}
317 changes: 317 additions & 0 deletions core-list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
<!--
Copyright 2013 The Polymer Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!--
`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. The `height` property
represents the height of a list item.
By default, the list supports selection via tapping. Styling the selection
should be done via binding. The `selectedProperty` property is set on the
list view data for each selected item.
`core-list` manages a viewport of data based on the current scroll position.
For performance reasons, not every item in the list is rendered at once.
<core-list data="{{data}}" height="80">
<template repeat>
<div class="{{ {selected: selected} | tokenList }}">List row: {{index}}</div>
</template>
</core-list>
@group Polymer Core Elements
@element core-list
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../core-selection/core-selection.html">

<polymer-element name="core-list" attributes="height data multi" on-tap="{{tapHandler}}" on-scroll="{{scrollHandler}}">
<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() {

Polymer('core-list', {
/**
* 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.
*
* @attribute data
* @type array
* @default null
*/
data: null,

/**
*
* The height of a list item. `core-list` currently supports only fixed-height
* list items. This height must be specified via the height property.
*
* @attribute height
* @type number
* @default 80
*/
height: 80,

/**
*
* The number of extra items rendered above the minimum set required to
* fill the list's height.
*
* @attribute extraItems
* @type number
* @default 30
*/
extraItems: 30,

/**
*
* The property set on the list view data to represent selection state.
* This should set so that it does not conflict with other data properties.
* Note, selection data is not stored on the data in given in the data property.
*
* @attribute selectedProperty
* @type string
* @default 'selected'
*/
selectedProperty: 'selected',

/**
*
* Set to true to support multiple selection.
*
* @attribute multi
* @type boolean
* @default false
*/
multi: false,

observe: {
'data template': 'initialize'
},

attached: function() {
this.template = this.querySelector('template');
},

initialize: function() {
if (!this.data || !this.template) {
return;
}
this.initializeViewport();
this.initalizeData();
this.onMutation(this, this.initializeItems);
},

initializeViewport: function() {
this.$.viewport.style.height = this.height * this.data.length + 'px';
this._visibleCount = Math.ceil(this.offsetHeight / this.height);
this._physicalCount = this._visibleCount + this.extraItems;
this._physicalHeight = this.height * this._physicalCount;
},

initalizeData: function() {
var exampleDatum = this.data[0];
this._propertyNames = Object.getOwnPropertyNames(exampleDatum);
this.selectedData = [];
this._physicalData = new Array(this._physicalCount);
for (var i = 0; i < this._physicalCount; ++i) {
this._physicalData[i] = {};
this.updateItem(i, i);
}
this.template.model = this._physicalData;
},

initializeItems: function() {
this._physicalItems = new Array(this._physicalCount);
for (var i = 0, item = this.template.nextElementSibling;
item && i < this._physicalCount;
++i, item = item.nextElementSibling) {
this._physicalItems[i] = item;
item._transformValue = 0;
}
},

updateItem: function(virtualIndex, physicalIndex) {
var virtualDatum = this.data[virtualIndex];
var physicalDatum = this._physicalData[physicalIndex];
for (var i = 0; i < this._propertyNames.length; ++i) {
var propertyName = this._propertyNames[i];
physicalDatum[propertyName] = virtualDatum[propertyName];
physicalDatum._physicalIndex = physicalIndex;
physicalDatum._virtualIndex = virtualIndex;
}
if (this.selectedProperty) {
physicalDatum[this.selectedProperty] = this.selectedData[virtualIndex];
}
},

scrollHandler: function() {
this.refresh(false);
},

/**
* Refresh the list at the current scroll position.
*
* @method refresh
*/
refresh: function(force) {
var firstVisibleIndex = Math.floor(this.scrollTop / this.height);
var visibleMidpoint = firstVisibleIndex + this._visibleCount / 2;

var firstReifiedIndex = Math.max(0, Math.floor(visibleMidpoint -
this._physicalCount / 2));
firstReifiedIndex = Math.min(firstReifiedIndex, this.data.length -
this._physicalCount);

var firstPhysicalIndex = firstReifiedIndex % this._physicalCount;
var baseVirtualIndex = firstReifiedIndex - firstPhysicalIndex;

var baseTransformValue = Math.floor(this.height * baseVirtualIndex);
var nextTransformValue = Math.floor(baseTransformValue +
this._physicalHeight);

var baseTransformString = 'translate3d(0,' + baseTransformValue + 'px,0)';
var nextTransformString = 'translate3d(0,' + nextTransformValue + 'px,0)';

for (var i = 0; i < firstPhysicalIndex; ++i) {
var item = this._physicalItems[i];
if (force || item._transformValue != nextTransformValue) {
this.updateItem(baseVirtualIndex + this._physicalCount + i, i);
setTransform(item, nextTransformString, nextTransformValue);
}
}
for (var i = firstPhysicalIndex; i < this._physicalCount; ++i) {
var item = this._physicalItems[i];
if (force || item._transformValue != baseTransformValue) {
this.updateItem(baseVirtualIndex + i, i);
setTransform(item, baseTransformString, baseTransformValue);
}
}
},

// list selection
tapHandler: function(e) {
if (e.target === this) {
return;
}
var n = e.target;
var model = n.templateInstance && n.templateInstance.model;
if (model) {
var vi = model._virtualIndex, pi = model._physicalIndex;
var data = this.data[vi], item = this._physicalItems[pi];
this.$.selection.select(data);
this.asyncFire('core-activate', {data: data, item: item});
}
},

selectedHandler: function(e, detail) {
if (this.selectedProperty) {
var i$ = 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[i$.virtual] = detail.isSelected;
if (i$.physical >= 0) {
this.updateItem(i$.virtual, i$.physical);
}
}
},

/**
* Select the list item at the given index.
*
* @method selectItem
* @param {number} index
*/
selectItem: function(index) {
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 = 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]._virtualIndex === index) {
return i;
}
}
return -1;
},

get selection() {
return this.$.selection.getSelection();
},

selectedChanged: function() {
this.$.selection.select(this.selected);
},

clearSelection: function() {
if (this.multi) {
var s$ = this.selection;
for (var i=0, l=s$.length, s; (i<l) && (s=s$[i]); i++) {
this.$.selection.setItemSelected(s, false);
}
} else {
this.$.selection.setItemSelected(this.selection, false);
}
this.$.selection.clear();
},

scrollToItem: function(index) {
this.scrollTop = index * this.height;
}

});

// determine proper transform mechanizm
if (document.documentElement.style.transform !== undefined) {
function setTransform(element, string, value) {
element.style.transform = string;
element._transformValue = value;
}
} else {
function setTransform(element, string, value) {
element.style.webkitTransform = string;
element._transformValue = value;
}
}

})();
</script>
</polymer-element>
Loading

0 comments on commit dac95e7

Please sign in to comment.