five-bells-visualization
Version:
Tool to visualize Five Bells payments
547 lines (485 loc) • 17.3 kB
HTML
<!--
@license
Copyright (c) 2015 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
-->
<!--
**THIS ELEMENT IS EXPERIMENTAL. API AND NAME SUBJECT TO CHANGE.**
The `x-repeat` element is a custom `HTMLTemplateElement` type extension that
automatically stamps and binds one instance of template content to each object
in a user-provided array. `x-repeat` accepts an `items` property, and one
instance of the template is stamped for each item into the DOM at the location
of the `x-repeat` element. The `item` property will be set on each instance's
binding scope, thus templates should bind to sub-properties of `item`.
Example:
```html
<dom-module id="employee-list">
<template>
<div> Employee list: </div>
<template is="x-repeat" items="{{employees}}">
<div>First name: <span>{{item.first}}</span></div>
<div>Last name: <span>{{item.last}}</span></div>
</template>
</template>
<script>
Polymer({
is: 'employee-list',
ready: function() {
this.employees = [
{first: 'Bob', last: 'Smith'},
{first: 'Sally', last: 'Johnson'},
...
];
}
});
</script>
</dom-module>
```
Notifications for changes to items sub-properties will be forwarded to template
instances, which will update via the normal structured data notification system.
Mutations to the `items` array itself (`push`, `pop`, `splice`, `shift`,
`unshift`) are observed via `Array.observe` (where supported, or an
shim of this API on unsupported browsers), and template instances are kept in
sync with the data in the array.
A view-specific filter/sort may be applied to each `x-repeat` by supplying a
`filter` and/or `sort` property. This may be a string that names a function on
the host, or a function may be assigned to the property directly. The functions
should implemented following the standard `Array` filter/sort API.
In order to re-run the filter or sort functions based on changes to sub-fields
of `items`, the `observe` property may be set as a space-separated list of
`item` sub-fields that should cause a re-filter/sort when modified.
For example, for an `x-repeat` with a filter of the following:
```js
isEngineer: function(item) {
return item.type == 'engineer' || item.manager.type == 'engineer';
}
```
Then the `observe` property should be configured as follows:
```html
<template is="x-repeat" items="{{employees}}"
filter="isEngineer" observe="type manager.type">
```
-->
<link rel="import" href="templatizer.html">
<link rel="import" href="../array-observe.html">
<link rel="import" href="../collection.html">
<script>
Polymer({
is: 'x-repeat',
extends: 'template',
properties: {
/**
* An array containing items determining how many instances of the template
* to stamp and that that each template instance should bind to.
*/
items: {
type: Array
},
/**
* A function that should determine the sort order of the items. This
* property should either be provided as a string, indicating a method
* name on the element's host, or else be an actual function. The
* function should match the sort function passed to `Array.sort`.
* Using a sort function has no effect on the underlying `items` array.
*/
sort: {
type: Function,
observer: '_sortChanged'
},
/**
* A function that can be used to filter items out of the view. This
* property should either be provided as a string, indicating a method
* name on the element's host, or else be an actual function. The
* function should match the sort function passed to `Array.filter`.
* Using a filter function has no effect on the underlying `items` array.
*/
filter: {
type: Function,
observer: '_filterChanged'
},
/**
* When using a `filter` or `sort` function, the `observe` property
* should be set to a space-separated list of the names of item
* sub-fields that should trigger a re-sort or re-filter when changed.
* These should generally be fields of `item` that the sort or filter
* function depends on.
*/
observe: {
type: String,
observer: '_observeChanged'
},
/**
* When using a `filter` or `sort` function, the `delay` property
* determines a debounce time after a change to observed item
* properties that must pass before the filter or sort is re-run.
* This is useful in rate-limiting shuffing of the view when
* item changes may be frequent.
*/
delay: Number
},
behaviors: [
Polymer.Templatizer
],
observers: [
'_itemsChanged(items.*)'
],
created: function() {
this.boundCollectionObserver = this.render.bind(this);
},
ready: function() {
// Templatizing (generating the instance constructor) needs to wait
// until attached, since it may not have its template content handed
// back to it until then, following its host template stamping
if (!this.ctor) {
this.templatize(this);
}
},
_sortChanged: function() {
var dataHost = this._getRootDataHost();
this._sortFn = this.sort && (typeof this.sort == 'function' ?
this.sort : dataHost[this.sort].bind(this.host));
if (this.items) {
this.debounce('render', this.render);
}
},
_filterChanged: function() {
var dataHost = this._getRootDataHost();
this._filterFn = this.filter && (typeof this.filter == 'function' ?
this.filter : dataHost[this.filter].bind(this.host));
if (this.items) {
this.debounce('render', this.render);
}
},
_observeChanged: function() {
this._observePaths = this.observe &&
this.observe.replace('.*', '.').split(' ');
},
_itemsChanged: function(change) {
if (change.path == 'items') {
this._unobserveCollection();
if (change.value) {
this._observeCollection(change.value);
this.debounce('render', this.render);
}
} else {
this._forwardItemPath(change.path, change.value);
this._checkObservedPaths(change.path);
}
},
_checkObservedPaths: function(path) {
if (this._observePaths && path.indexOf('items.') === 0) {
path = path.substring(path.indexOf('.', 6) + 1);
var paths = this._observePaths;
for (var i=0; i<paths.length; i++) {
if (path.indexOf(paths[i]) === 0) {
this.debounce('render', this.render, this.delay);
return;
}
}
}
},
_observeCollection: function(items) {
this.collection = Array.isArray(items) ? Polymer.Collection.get(items) : items;
this.collection.observe(this.boundCollectionObserver);
},
_unobserveCollection: function() {
if (this.collection) {
this.collection.unobserve(this.boundCollectionObserver);
}
},
render: function(splices) {
this.flushDebouncer('render');
var c = this.collection;
if (splices) {
if (this._sortFn || splices[0].index == null) {
this._applySplicesViewSort(splices);
} else {
this._applySplicesArraySort(splices);
}
} else {
this._sortAndFilter();
}
var rowForKey = this._rowForKey = {};
var keys = this._orderedKeys;
// Assign items and keys
this.rows = this.rows || [];
for (var i=0; i<keys.length; i++) {
var key = keys[i];
var item = c.getItem(key);
var row = this.rows[i];
rowForKey[key] = i;
if (!row) {
this.rows.push(row = this._insertRow(i, null, item));
}
row.item = item;
row.key = key;
row.index = i;
}
// Remove extra
for (; i<this.rows.length; i++) {
this._detachRow(i);
}
this.rows.splice(keys.length, this.rows.length-keys.length);
},
_sortAndFilter: function() {
var c = this.collection;
this._orderedKeys = c.getKeys();
// Filter
if (this._filterFn) {
this._orderedKeys = this._orderedKeys.filter(function(a) {
return this._filterFn(c.getItem(a));
}, this);
}
// Sort
if (this._sortFn) {
this._orderedKeys.sort(function(a, b) {
return this._sortFn(c.getItem(a), c.getItem(b));
}.bind(this));
}
},
_keySort: function(a, b) {
return this.collection.getKey(a) - this.collection.getKey(b);
},
_applySplicesViewSort: function(splices) {
var c = this.collection;
var keys = this._orderedKeys;
var rows = this.rows;
var removedRows = [];
var addedKeys = [];
var pool = [];
var sortFn = this._sortFn || this._keySort.bind(this);
splices.forEach(function(s) {
// Collect all removed row idx's
for (var i=0; i<s.removed.length; i++) {
var idx = this._rowForKey[s.removed[i]];
if (idx != null) {
removedRows.push(idx);
}
}
// Collect all added keys
for (i=0; i<s.added.length; i++) {
addedKeys.push(s.added[i]);
}
}, this);
if (removedRows.length) {
// Sort removed rows idx's
removedRows.sort();
// Remove keys and pool rows (backwards, so we don't invalidate rowForKey)
for (i=removedRows.length-1; i>=0 ; i--) {
var idx = removedRows[i];
pool.push(this._detachRow(idx));
rows.splice(idx, 1);
keys.splice(idx, 1);
}
}
if (addedKeys.length) {
// Filter added keys
if (this._filterFn) {
addedKeys = addedKeys.filter(function(a) {
return this._filterFn(c.getItem(a));
}, this);
}
// Sort added keys
addedKeys.sort(function(a, b) {
return this.sortFn(c.getItem(a), c.getItem(b));
}, this);
// Insert new rows using sort (from pool or newly created)
var start = 0;
for (i=0; i<addedKeys.length; i++) {
start = this._insertRowIntoViewSort(start, addedKeys[i], pool);
}
}
},
_insertRowIntoViewSort: function(start, key, pool) {
var c = this.collection;
var item = c.getItem(key);
var end = this.rows.length - 1;
var idx = -1;
var sortFn = this._sortFn || this._keySort.bind(this);
// Binary search for insertion point
while (start <= end) {
var mid = (start + end) >> 1;
var midKey = this._orderedKeys[mid];
var cmp = sortFn(c.getItem(midKey), item);
if (cmp < 0) {
start = mid + 1;
} else if (cmp > 0) {
end = mid - 1;
} else {
idx = mid;
break;
}
}
if (idx < 0) {
idx = end + 1;
}
// Insert key & row at insertion point
this._orderedKeys.splice(idx, 0, key);
this.rows.splice(idx, 0, this._insertRow(idx, pool));
return idx;
},
_applySplicesArraySort: function(splices) {
var keys = this._orderedKeys;
var pool = [];
splices.forEach(function(s) {
// Remove & pool rows first, to ensure we can fully reuse removed rows
for (var i=0; i<s.removed.length; i++) {
pool.push(this._detachRow(s.index + i));
}
this.rows.splice(s.index, s.removed.length);
}, this);
var c = this.collection;
var filterDelta = 0;
splices.forEach(function(s) {
// Filter added keys
var addedKeys = s.added;
if (this._filterFn) {
addedKeys = addedKeys.filter(function(a) {
return this._filterFn(c.getItem(a));
}, this);
filterDelta += (s.added.length - addedKeys.length);
}
var idx = s.index - filterDelta;
// Apply splices to keys
var args = [idx, s.removed.length].concat(addedKeys);
keys.splice.apply(keys, args);
// Insert new rows (from pool or newly created)
var addedRows = [];
for (i=0; i<s.added.length; i++) {
addedRows.push(this._insertRow(idx + i, pool));
}
args = [s.index, 0].concat(addedRows);
this.rows.splice.apply(this.rows, args);
}, this);
},
_detachRow: function(idx) {
var row = this.rows[idx];
var parentNode = Polymer.dom(this).parentNode;
for (var i=0; i<row._children.length; i++) {
var el = row._children[i];
Polymer.dom(row.root).appendChild(el);
}
return row;
},
_insertRow: function(idx, pool, item) {
var row = (pool && pool.pop()) || this._generateRow(idx, item);
var beforeRow = this.rows[idx];
var beforeNode = beforeRow ? beforeRow._children[0] : this;
var parentNode = Polymer.dom(this).parentNode;
Polymer.dom(parentNode).insertBefore(row.root, beforeNode);
return row;
},
_generateRow: function(idx, item) {
var row = this.stamp({
index: idx,
key: this.collection.getKey(item),
item: item
});
// each row is a document fragment which is lost when we appendChild,
// so we have to track each child individually
var children = [];
for (var n = row.root.firstChild; n; n=n.nextSibling) {
children.push(n);
n._templateInstance = row;
}
// Since archetype overrides Base/HTMLElement, Safari complains
// when accessing `children`
row._children = children;
return row;
},
// Implements extension point from Templatizer mixin
_getStampedChildren: function() {
var children = [];
if (this.rows) {
for (var i=0; i<this.rows.length; i++) {
var c = this.rows[i]._children;
for (var j=0; j<c.length; j++)
children.push(c[j]);
}
}
return children;
},
// Implements extension point from Templatizer
// Called as a side effect of a template instance path change, responsible
// for notifying items.<key-for-row>.<path> change up to host
_forwardInstancePath: function(row, root, subPath, value) {
if (root == 'item') {
this.notifyPath('items.' + row.key + '.' + subPath, value);
}
},
// Implements extension point from Templatizer mixin
// Called as side-effect of a host property change, responsible for
// notifying parent.<prop> path change on each row
_forwardParentProp: function(prop, value) {
if (this.rows) {
this.rows.forEach(function(row) {
row.parent[prop] = value;
row.notifyPath('parent.' + prop, value, true);
}, this);
}
},
// Implements extension point from Templatizer
// Called as side-effect of a host path change, responsible for
// notifying parent.<path> path change on each row
_forwardParentPath: function(path, value) {
if (this.rows) {
this.rows.forEach(function(row) {
row.notifyPath('parent.' + path, value, true);
}, this);
}
},
// Called as a side effect of a host items.<key>.<path> path change,
// responsible for notifying item.<path> changes to row for key
_forwardItemPath: function(path, value) {
if (this._rowForKey) {
// 'items.'.length == 6
var dot = path.indexOf('.', 6);
var key = path.substring(6, dot < 0 ? path.length : dot);
var idx = this._rowForKey[key];
var row = this.rows[idx];
if (row) {
if (dot >= 0) {
path = 'item.' + path.substring(dot+1);
row.notifyPath(path, value, true);
} else {
row.item = value;
}
}
}
},
_instanceForElement: function(el) {
while (el && !el._templateInstance) {
el = el.parentNode;
}
return el && el._templateInstance;
},
/**
* Returns the item associated with a given element stamped by
* this `x-repeat`.
*/
itemForElement: function(el) {
var instance = this._instanceForElement(el);
return instance && instance.item;
},
/**
* Returns the `Polymer.Collection` key associated with a given
* element stamped by this `x-repeat`.
*/
keyForElement: function(el) {
var instance = this._instanceForElement(el);
return instance && instance.key;
},
/**
* Returns the index in `items` associated with a given element
* stamped by this `x-repeat`.
*/
indexForElement: function(el) {
var instance = this._instanceForElement(el);
return this.rows.indexOf(instance);
}
});
</script>