2016-12-20 16:21:35 -08:00
|
|
|
<!--
|
|
|
|
|
@license
|
|
|
|
|
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
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
Keeping structured data in sync requires that Polymer understand the path
|
|
|
|
|
associations of data being bound. The `array-selector` element ensures path
|
|
|
|
|
linkage when selecting specific items from an array (either single or multiple).
|
|
|
|
|
The `items` property accepts an array of user data, and via the `select(item)`
|
|
|
|
|
and `deselect(item)` API, updates the `selected` property which may be bound to
|
|
|
|
|
other parts of the application, and any changes to sub-fields of `selected`
|
|
|
|
|
item(s) will be kept in sync with items in the `items` array. When `multi`
|
|
|
|
|
is false, `selected` is a property representing the last selected item. When
|
|
|
|
|
`multi` is true, `selected` is an array of multiply selected items.
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<dom-module id="employee-list">
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
|
|
|
|
<div> Employee list: </div>
|
|
|
|
|
<template is="dom-repeat" id="employeeList" items="{{employees}}">
|
|
|
|
|
<div>First name: <span>{{item.first}}</span></div>
|
|
|
|
|
<div>Last name: <span>{{item.last}}</span></div>
|
|
|
|
|
<button on-click="toggleSelection">Select</button>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<array-selector id="selector" items="{{employees}}" selected="{{selected}}" multi toggle></array-selector>
|
|
|
|
|
|
|
|
|
|
<div> Selected employees: </div>
|
|
|
|
|
<template is="dom-repeat" items="{{selected}}">
|
|
|
|
|
<div>First name: <span>{{item.first}}</span></div>
|
|
|
|
|
<div>Last name: <span>{{item.last}}</span></div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
Polymer({
|
|
|
|
|
is: 'employee-list',
|
|
|
|
|
ready() {
|
|
|
|
|
this.employees = [
|
|
|
|
|
{first: 'Bob', last: 'Smith'},
|
|
|
|
|
{first: 'Sally', last: 'Johnson'},
|
|
|
|
|
...
|
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
toggleSelection(e) {
|
|
|
|
|
var item = this.$.employeeList.itemForElement(e.target);
|
|
|
|
|
this.$.selector.select(item);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
</dom-module>
|
|
|
|
|
```
|
|
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
<link rel="import" href="../../polymer-element.html">
|
|
|
|
|
<link rel="import" href="../utils/utils.html">
|
|
|
|
|
<link rel="import" href="../utils/array-splice.html">
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
(function() {
|
|
|
|
|
|
2017-01-05 18:51:29 -08:00
|
|
|
// Limited polyfill of `Set` that accepts integer values backed by Object
|
|
|
|
|
// and relying on for/in iterating keys in insertion order ('.' prefix
|
|
|
|
|
// ensures insertion order on modern browsers, as integers are treated specially)
|
|
|
|
|
let IntegerSet = window.Set || class IntegerSet {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.clear();
|
|
|
|
|
}
|
|
|
|
|
add(value) {
|
|
|
|
|
this._set['.' + value] = true;
|
|
|
|
|
}
|
|
|
|
|
delete(value) {
|
|
|
|
|
if (this.has(value)) {
|
|
|
|
|
delete this._set['.' + value];
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
has(value) {
|
|
|
|
|
return Boolean(this._set['.' + value]);
|
|
|
|
|
}
|
|
|
|
|
forEach(cb) {
|
|
|
|
|
for (let k in this._set) {
|
|
|
|
|
cb(parseInt(k.slice(1)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
clear() {
|
|
|
|
|
this._set = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-20 16:21:35 -08:00
|
|
|
let ArraySelectorMixin = Polymer.Utils.dedupingMixin(superClass => {
|
|
|
|
|
|
|
|
|
|
return class extends superClass {
|
|
|
|
|
|
|
|
|
|
static get config() {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An array containing items from which selection will be made.
|
|
|
|
|
*/
|
|
|
|
|
items: {
|
|
|
|
|
type: Array,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `true`, multiple items may be selected at once (in this case,
|
|
|
|
|
* `selected` is an array of currently selected items). When `false`,
|
|
|
|
|
* only one item may be selected at a time.
|
|
|
|
|
*/
|
|
|
|
|
multi: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `multi` is true, this is an array that contains any selected.
|
|
|
|
|
* When `multi` is false, this is the currently selected item, or `null`
|
|
|
|
|
* if no item is selected.
|
|
|
|
|
*/
|
|
|
|
|
selected: {
|
|
|
|
|
type: Object,
|
|
|
|
|
notify: true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `multi` is false, this is the currently selected item, or `null`
|
|
|
|
|
* if no item is selected.
|
|
|
|
|
*/
|
|
|
|
|
selectedItem: {
|
|
|
|
|
type: Object,
|
|
|
|
|
notify: true
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* When `true`, calling `select` on an item that is already selected
|
|
|
|
|
* will deselect the item.
|
|
|
|
|
*/
|
|
|
|
|
toggle: {
|
|
|
|
|
type: Boolean,
|
|
|
|
|
value: false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
observers: ['_updateSelection(multi, items.*)'],
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-03 10:22:25 -08:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
this.__lastItems = null;
|
|
|
|
|
this.__lastMulti = null;
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-20 16:21:35 -08:00
|
|
|
_updateSelection(multi, itemsInfo) {
|
|
|
|
|
if (itemsInfo.path == 'items') {
|
2017-01-03 10:20:12 -08:00
|
|
|
let newItems = itemsInfo.base || [];
|
2016-12-20 16:21:35 -08:00
|
|
|
let lastItems = this.__lastItems;
|
2017-01-03 10:20:12 -08:00
|
|
|
let lastMulti = this.__lastMulti;
|
|
|
|
|
if (multi !== lastMulti) {
|
|
|
|
|
this.clearSelection();
|
|
|
|
|
}
|
2016-12-20 16:21:35 -08:00
|
|
|
if (lastItems) {
|
|
|
|
|
let splices = Polymer.ArraySplice.calculateSplices(newItems, lastItems);
|
2017-01-05 12:53:01 -08:00
|
|
|
this._applySplices(splices);
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-03 10:20:12 -08:00
|
|
|
this.__lastItems = newItems;
|
|
|
|
|
this.__lastMulti = multi;
|
2016-12-20 16:21:35 -08:00
|
|
|
} else if (itemsInfo.path == 'items.splices') {
|
|
|
|
|
this._applySplices(itemsInfo.value.indexSplices);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_applySplices(splices) {
|
|
|
|
|
for (let i=0; i<splices.length; i++) {
|
|
|
|
|
let s = splices[i];
|
2017-01-05 18:51:29 -08:00
|
|
|
let selected = new IntegerSet();
|
|
|
|
|
let sidx = 0;
|
|
|
|
|
let toRemove = [];
|
|
|
|
|
this._selectedSet.forEach(idx => {
|
|
|
|
|
if (idx < s.index) {
|
|
|
|
|
selected.add(idx);
|
|
|
|
|
} else if (idx >= s.index + s.removed.length) {
|
|
|
|
|
selected.add(idx + s.addedCount - s.removed.length);
|
2017-01-05 12:53:01 -08:00
|
|
|
} else {
|
2017-01-05 18:51:29 -08:00
|
|
|
toRemove.push(sidx);
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-05 18:51:29 -08:00
|
|
|
sidx++;
|
|
|
|
|
});
|
|
|
|
|
this._selectedSet = selected;
|
|
|
|
|
this._updateLinks();
|
|
|
|
|
for (let i=toRemove.length-1; i>=0; i--) {
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
this.splice('selected', toRemove[i], 1);
|
2017-01-05 12:53:01 -08:00
|
|
|
} else {
|
2017-01-05 18:51:29 -08:00
|
|
|
this.selected = this.selectedItem = null;
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-05 18:51:29 -08:00
|
|
|
}
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-05 18:51:29 -08:00
|
|
|
_updateLinks() {
|
|
|
|
|
this.__dataLinkedPaths = {};
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
let sidx = 0;
|
|
|
|
|
this._selectedSet.forEach(idx => {
|
|
|
|
|
this.linkPaths('items.' + idx, 'selected.' + sidx++);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
this._selectedSet.forEach(idx => {
|
|
|
|
|
this.linkPaths('selected', 'items.' + idx);
|
|
|
|
|
this.linkPaths('selectedItem', 'items.' + idx);
|
|
|
|
|
});
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clears the selection state.
|
|
|
|
|
*
|
|
|
|
|
* @method clearSelection
|
|
|
|
|
*/
|
|
|
|
|
clearSelection() {
|
|
|
|
|
// Unbind previous selection
|
2017-01-05 18:51:29 -08:00
|
|
|
this.__dataLinkedPaths = {};
|
|
|
|
|
this._selectedSet = new IntegerSet();
|
2016-12-20 16:21:35 -08:00
|
|
|
// Initialize selection
|
2017-01-05 18:51:29 -08:00
|
|
|
this.selected = this.multi ? [] : null
|
2016-12-20 16:21:35 -08:00
|
|
|
this.selectedItem = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns whether the item is currently selected.
|
|
|
|
|
*
|
|
|
|
|
* @method isSelected
|
|
|
|
|
* @param {*} item Item from `items` array to test
|
|
|
|
|
* @return {boolean} Whether the item is selected
|
|
|
|
|
*/
|
|
|
|
|
isSelected(item) {
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
return this.selected.indexOf(item) >= 0;
|
|
|
|
|
} else {
|
|
|
|
|
return this.selected == item;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-05 18:51:29 -08:00
|
|
|
isIndexSelected(idx) {
|
|
|
|
|
return this._selectedSet.has(idx);
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-20 16:21:35 -08:00
|
|
|
/**
|
|
|
|
|
* Deselects the given item if it is already selected.
|
|
|
|
|
*
|
|
|
|
|
* @method deselect
|
|
|
|
|
* @param {*} item Item from `items` array to deselect
|
|
|
|
|
*/
|
|
|
|
|
deselect(item) {
|
2017-01-05 18:51:29 -08:00
|
|
|
this.deselectIndex(this.items.indexOf(item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Deselects the given index if it is already selected.
|
|
|
|
|
*
|
|
|
|
|
* @method deselect
|
|
|
|
|
* @param {number} index Index from `items` array to deselect
|
|
|
|
|
*/
|
|
|
|
|
deselectIndex(idx) {
|
|
|
|
|
if (this._selectedSet.delete(idx)) {
|
|
|
|
|
let sidx;
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
sidx = parseInt(this.__dataLinkedPaths['items.' + idx].slice('selected.'.length));
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-05 18:51:29 -08:00
|
|
|
this._updateLinks();
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
this.splice('selected', sidx, 1);
|
|
|
|
|
} else {
|
|
|
|
|
this.selected = this.selectedItem = null;
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Selects the given item. When `toggle` is true, this will automatically
|
|
|
|
|
* deselect the item if already selected.
|
|
|
|
|
*
|
|
|
|
|
* @method select
|
|
|
|
|
* @param {*} item Item from `items` array to select
|
|
|
|
|
*/
|
|
|
|
|
select(item) {
|
2017-01-05 18:51:29 -08:00
|
|
|
this.selectIndex(this.items.indexOf(item));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Selects the given index. When `toggle` is true, this will automatically
|
|
|
|
|
* deselect the item if already selected.
|
|
|
|
|
*
|
|
|
|
|
* @method select
|
|
|
|
|
* @param {number} index Index from `items` array to select
|
|
|
|
|
*/
|
|
|
|
|
selectIndex(idx) {
|
|
|
|
|
if (!this._selectedSet.has(idx)) {
|
|
|
|
|
if (!this.multi) {
|
|
|
|
|
this._selectedSet.clear();
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-05 18:51:29 -08:00
|
|
|
this._selectedSet.add(idx);
|
|
|
|
|
this._updateLinks();
|
|
|
|
|
let item = this.items[idx];
|
|
|
|
|
if (this.multi) {
|
|
|
|
|
this.push('selected', item);
|
2016-12-20 16:21:35 -08:00
|
|
|
} else {
|
2017-01-05 18:51:29 -08:00
|
|
|
this.selected = this.selectedItem = item;
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
2017-01-05 18:51:29 -08:00
|
|
|
} else if (this.toggle) {
|
|
|
|
|
this.deselectIndex(idx);
|
2016-12-20 16:21:35 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// export mixin
|
|
|
|
|
Polymer.ArraySelectorMixin = ArraySelectorMixin;
|
|
|
|
|
|
|
|
|
|
// define element class & export
|
|
|
|
|
class ArraySelector extends ArraySelectorMixin(Polymer.Element) { }
|
|
|
|
|
customElements.define('array-selector', ArraySelector);
|
|
|
|
|
Polymer.ArraySelector = ArraySelector;
|
|
|
|
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
</script>
|