webdash-readme-preview
Version:
Preview your README.md straight from the dashboard
441 lines (405 loc) • 13.8 kB
HTML
<!--
Copyright (c) 2017 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
-->
<link rel="import" href="../../polymer-element.html">
<link rel="import" href="../utils/mixin.html">
<link rel="import" href="../utils/array-splice.html">
<script>
(function() {
'use strict';
/**
* Element mixin for recording dynamic associations between item paths in a
* master `items` array and a `selected` array such that path changes to the
* master array (at the host) element or elsewhere via data-binding) are
* correctly propagated to items in the selected array and vice-versa.
*
* 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.
*
* @polymer
* @mixinFunction
* @appliesMixin Polymer.ElementMixin
* @memberof Polymer
* @summary Element mixin for recording dynamic associations between item paths in a
* master `items` array and a `selected` array
*/
let ArraySelectorMixin = Polymer.dedupingMixin(superClass => {
/**
* @constructor
* @extends {superClass}
* @implements {Polymer_ElementMixin}
*/
let elementBase = Polymer.ElementMixin(superClass);
/**
* @polymer
* @mixinClass
* @implements {Polymer_ArraySelectorMixin}
* @unrestricted
*/
class ArraySelectorMixin extends elementBase {
static get properties() {
return {
/**
* 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.
* @type {?(Object|Array<!Object>)}
*/
selected: {
type: Object,
notify: true
},
/**
* When `multi` is false, this is the currently selected item, or `null`
* if no item is selected.
* @type {?Object}
*/
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
}
};
}
static get observers() {
return ['__updateSelection(multi, items.*)'];
}
constructor() {
super();
this.__lastItems = null;
this.__lastMulti = null;
this.__selectedMap = null;
}
__updateSelection(multi, itemsInfo) {
let path = itemsInfo.path;
if (path == 'items') {
// Case 1 - items array changed, so diff against previous array and
// deselect any removed items and adjust selected indices
let newItems = itemsInfo.base || [];
let lastItems = this.__lastItems;
let lastMulti = this.__lastMulti;
if (multi !== lastMulti) {
this.clearSelection();
}
if (lastItems) {
let splices = Polymer.ArraySplice.calculateSplices(newItems, lastItems);
this.__applySplices(splices);
}
this.__lastItems = newItems;
this.__lastMulti = multi;
} else if (itemsInfo.path == 'items.splices') {
// Case 2 - got specific splice information describing the array mutation:
// deselect any removed items and adjust selected indices
this.__applySplices(itemsInfo.value.indexSplices);
} else {
// Case 3 - an array element was changed, so deselect the previous
// item for that index if it was previously selected
let part = path.slice('items.'.length);
let idx = parseInt(part, 10);
if ((part.indexOf('.') < 0) && part == idx) {
this.__deselectChangedIdx(idx);
}
}
}
__applySplices(splices) {
let selected = this.__selectedMap;
// Adjust selected indices and mark removals
for (let i=0; i<splices.length; i++) {
let s = splices[i];
selected.forEach((idx, item) => {
if (idx < s.index) {
// no change
} else if (idx >= s.index + s.removed.length) {
// adjust index
selected.set(item, idx + s.addedCount - s.removed.length);
} else {
// remove index
selected.set(item, -1);
}
});
for (let j=0; j<s.addedCount; j++) {
let idx = s.index + j;
if (selected.has(this.items[idx])) {
selected.set(this.items[idx], idx);
}
}
}
// Update linked paths
this.__updateLinks();
// Remove selected items that were removed from the items array
let sidx = 0;
selected.forEach((idx, item) => {
if (idx < 0) {
if (this.multi) {
this.splice('selected', sidx, 1);
} else {
this.selected = this.selectedItem = null;
}
selected.delete(item);
} else {
sidx++;
}
});
}
__updateLinks() {
this.__dataLinkedPaths = {};
if (this.multi) {
let sidx = 0;
this.__selectedMap.forEach(idx => {
if (idx >= 0) {
this.linkPaths('items.' + idx, 'selected.' + sidx++);
}
});
} else {
this.__selectedMap.forEach(idx => {
this.linkPaths('selected', 'items.' + idx);
this.linkPaths('selectedItem', 'items.' + idx);
});
}
}
/**
* Clears the selection state.
* @return {void}
*/
clearSelection() {
// Unbind previous selection
this.__dataLinkedPaths = {};
// The selected map stores 3 pieces of information:
// key: items array object
// value: items array index
// order: selected array index
this.__selectedMap = new Map();
// Initialize selection
this.selected = this.multi ? [] : null;
this.selectedItem = null;
}
/**
* Returns whether the item is currently selected.
*
* @param {*} item Item from `items` array to test
* @return {boolean} Whether the item is selected
*/
isSelected(item) {
return this.__selectedMap.has(item);
}
/**
* Returns whether the item is currently selected.
*
* @param {number} idx Index from `items` array to test
* @return {boolean} Whether the item is selected
*/
isIndexSelected(idx) {
return this.isSelected(this.items[idx]);
}
__deselectChangedIdx(idx) {
let sidx = this.__selectedIndexForItemIndex(idx);
if (sidx >= 0) {
let i = 0;
this.__selectedMap.forEach((idx, item) => {
if (sidx == i++) {
this.deselect(item);
}
});
}
}
__selectedIndexForItemIndex(idx) {
let selected = this.__dataLinkedPaths['items.' + idx];
if (selected) {
return parseInt(selected.slice('selected.'.length), 10);
}
}
/**
* Deselects the given item if it is already selected.
*
* @param {*} item Item from `items` array to deselect
* @return {void}
*/
deselect(item) {
let idx = this.__selectedMap.get(item);
if (idx >= 0) {
this.__selectedMap.delete(item);
let sidx;
if (this.multi) {
sidx = this.__selectedIndexForItemIndex(idx);
}
this.__updateLinks();
if (this.multi) {
this.splice('selected', sidx, 1);
} else {
this.selected = this.selectedItem = null;
}
}
}
/**
* Deselects the given index if it is already selected.
*
* @param {number} idx Index from `items` array to deselect
* @return {void}
*/
deselectIndex(idx) {
this.deselect(this.items[idx]);
}
/**
* Selects the given item. When `toggle` is true, this will automatically
* deselect the item if already selected.
*
* @param {*} item Item from `items` array to select
* @return {void}
*/
select(item) {
this.selectIndex(this.items.indexOf(item));
}
/**
* Selects the given index. When `toggle` is true, this will automatically
* deselect the item if already selected.
*
* @param {number} idx Index from `items` array to select
* @return {void}
*/
selectIndex(idx) {
let item = this.items[idx];
if (!this.isSelected(item)) {
if (!this.multi) {
this.__selectedMap.clear();
}
this.__selectedMap.set(item, idx);
this.__updateLinks();
if (this.multi) {
this.push('selected', item);
} else {
this.selected = this.selectedItem = item;
}
} else if (this.toggle) {
this.deselectIndex(idx);
}
}
}
return ArraySelectorMixin;
});
// export mixin
Polymer.ArraySelectorMixin = ArraySelectorMixin;
/**
* @constructor
* @extends {Polymer.Element}
* @implements {Polymer_ArraySelectorMixin}
*/
let baseArraySelector = ArraySelectorMixin(Polymer.Element);
/**
* Element implementing the `Polymer.ArraySelector` mixin, which records
* dynamic associations between item paths in a master `items` array and a
* `selected` array such that path changes to the master array (at the host)
* element or elsewhere via data-binding) are correctly propagated to items
* in the selected array and vice-versa.
*
* 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.
*
* Example:
*
* ```html
* <dom-module id="employee-list">
*
* <template>
*
* <div> Employee list: </div>
* <dom-repeat id="employeeList" items="{{employees}}">
* <template>
* <div>First name: <span>{{item.first}}</span></div>
* <div>Last name: <span>{{item.last}}</span></div>
* <button on-click="toggleSelection">Select</button>
* </template>
* </dom-repeat>
*
* <array-selector id="selector" items="{{employees}}" selected="{{selected}}" multi toggle></array-selector>
*
* <div> Selected employees: </div>
* <dom-repeat items="{{selected}}">
* <template>
* <div>First name: <span>{{item.first}}</span></div>
* <div>Last name: <span>{{item.last}}</span></div>
* </template>
* </dom-repeat>
*
* </template>
*
* </dom-module>
* ```
*
* ```js
*class EmployeeList extends Polymer.Element {
* static get is() { return 'employee-list'; }
* static get properties() {
* return {
* employees: {
* value() {
* return [
* {first: 'Bob', last: 'Smith'},
* {first: 'Sally', last: 'Johnson'},
* ...
* ];
* }
* }
* };
* }
* toggleSelection(e) {
* let item = this.$.employeeList.itemForElement(e.target);
* this.$.selector.select(item);
* }
*}
* ```
*
* @polymer
* @customElement
* @extends {baseArraySelector}
* @appliesMixin Polymer.ArraySelectorMixin
* @memberof Polymer
* @summary Custom element that links paths between an input `items` array and
* an output `selected` item or array based on calls to its selection API.
*/
class ArraySelector extends baseArraySelector {
// Not needed to find template; can be removed once the analyzer
// can find the tag name from customElements.define call
static get is() { return 'array-selector'; }
}
customElements.define(ArraySelector.is, ArraySelector);
Polymer.ArraySelector = ArraySelector;
})();
</script>