<!-- @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 --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="iron-selection.html"> <script> /** @polymerBehavior */ Polymer.IronSelectableBehavior = { /** * Fired when iron-selector is activated (selected or deselected). * It is fired before the selected items are changed. * Cancel the event to abort selection. * * @event iron-activate */ /** * Fired when an item is selected * * @event iron-select */ /** * Fired when an item is deselected * * @event iron-deselect */ /** * Fired when the list of selectable items changes (e.g., items are * added or removed). The detail of the event is a mutation record that * describes what changed. * * @event iron-items-changed */ properties: { /** * If you want to use an attribute value or property of an element for * `selected` instead of the index, set this to the name of the attribute * or property. Hyphenated values are converted to camel case when used to * look up the property of a selectable element. Camel cased values are * *not* converted to hyphenated values for attribute lookup. It's * recommended that you provide the hyphenated form of the name so that * selection works in both cases. (Use `attr-or-property-name` instead of * `attrOrPropertyName`.) */ attrForSelected: { type: String, value: null }, /** * Gets or sets the selected element. The default is to use the index of the item. * @type {string|number} */ selected: { type: String, notify: true }, /** * Returns the currently selected item. * * @type {?Object} */ selectedItem: { type: Object, readOnly: true, notify: true }, /** * The event that fires from items when they are selected. Selectable * will listen for this event from items and update the selection state. * Set to empty string to listen to no events. */ activateEvent: { type: String, value: 'tap', observer: '_activateEventChanged' }, /** * This is a CSS selector string. If this is set, only items that match the CSS selector * are selectable. */ selectable: String, /** * The class to set on elements when selected. */ selectedClass: { type: String, value: 'iron-selected' }, /** * The attribute to set on elements when selected. */ selectedAttribute: { type: String, value: null }, /** * Default fallback if the selection based on selected with `attrForSelected` * is not found. */ fallbackSelection: { type: String, value: null }, /** * The list of items from which a selection can be made. */ items: { type: Array, readOnly: true, notify: true, value: function() { return []; } }, /** * The set of excluded elements where the key is the `localName` * of the element that will be ignored from the item list. * * @default {template: 1} */ _excludedLocalNames: { type: Object, value: function() { return { 'template': 1 }; } } }, observers: [ '_updateAttrForSelected(attrForSelected)', '_updateSelected(selected)', '_checkFallback(fallbackSelection)' ], created: function() { this._bindFilterItem = this._filterItem.bind(this); this._selection = new Polymer.IronSelection(this._applySelection.bind(this)); }, attached: function() { this._observer = this._observeItems(this); this._updateItems(); if (!this._shouldUpdateSelection) { this._updateSelected(); } this._addListener(this.activateEvent); }, detached: function() { if (this._observer) { Polymer.dom(this).unobserveNodes(this._observer); } this._removeListener(this.activateEvent); }, /** * Returns the index of the given item. * * @method indexOf * @param {Object} item * @returns Returns the index of the item */ indexOf: function(item) { return this.items.indexOf(item); }, /** * Selects the given value. * * @method select * @param {string|number} value the value to select. */ select: function(value) { this.selected = value; }, /** * Selects the previous item. * * @method selectPrevious */ selectPrevious: function() { var length = this.items.length; var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length; this.selected = this._indexToValue(index); }, /** * Selects the next item. * * @method selectNext */ selectNext: function() { var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.length; this.selected = this._indexToValue(index); }, /** * Selects the item at the given index. * * @method selectIndex */ selectIndex: function(index) { this.select(this._indexToValue(index)); }, /** * Force a synchronous update of the `items` property. * * NOTE: Consider listening for the `iron-items-changed` event to respond to * updates to the set of selectable items after updates to the DOM list and * selection state have been made. * * WARNING: If you are using this method, you should probably consider an * alternate approach. Synchronously querying for items is potentially * slow for many use cases. The `items` property will update asynchronously * on its own to reflect selectable items in the DOM. */ forceSynchronousItemUpdate: function() { this._updateItems(); }, get _shouldUpdateSelection() { return this.selected != null; }, _checkFallback: function() { if (this._shouldUpdateSelection) { this._updateSelected(); } }, _addListener: function(eventName) { this.listen(this, eventName, '_activateHandler'); }, _removeListener: function(eventName) { this.unlisten(this, eventName, '_activateHandler'); }, _activateEventChanged: function(eventName, old) { this._removeListener(old); this._addListener(eventName); }, _updateItems: function() { var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*'); nodes = Array.prototype.filter.call(nodes, this._bindFilterItem); this._setItems(nodes); }, _updateAttrForSelected: function() { if (this._shouldUpdateSelection) { this.selected = this._indexToValue(this.indexOf(this.selectedItem)); } }, _updateSelected: function() { this._selectSelected(this.selected); }, _selectSelected: function(selected) { this._selection.select(this._valueToItem(this.selected)); // Check for items, since this array is populated only when attached // Since Number(0) is falsy, explicitly check for undefined if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) { this.selected = this.fallbackSelection; } }, _filterItem: function(node) { return !this._excludedLocalNames[node.localName]; }, _valueToItem: function(value) { return (value == null) ? null : this.items[this._valueToIndex(value)]; }, _valueToIndex: function(value) { if (this.attrForSelected) { for (var i = 0, item; item = this.items[i]; i++) { if (this._valueForItem(item) == value) { return i; } } } else { return Number(value); } }, _indexToValue: function(index) { if (this.attrForSelected) { var item = this.items[index]; if (item) { return this._valueForItem(item); } } else { return index; } }, _valueForItem: function(item) { var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)]; return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected); }, _applySelection: function(item, isSelected) { if (this.selectedClass) { this.toggleClass(this.selectedClass, isSelected, item); } if (this.selectedAttribute) { this.toggleAttribute(this.selectedAttribute, isSelected, item); } this._selectionChange(); this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item}); }, _selectionChange: function() { this._setSelectedItem(this._selection.get()); }, // observe items change under the given node. _observeItems: function(node) { return Polymer.dom(node).observeNodes(function(mutation) { this._updateItems(); if (this._shouldUpdateSelection) { this._updateSelected(); } // Let other interested parties know about the change so that // we don't have to recreate mutation observers everywhere. this.fire('iron-items-changed', mutation, { bubbles: false, cancelable: false }); }); }, _activateHandler: function(e) { var t = e.target; var items = this.items; while (t && t != this) { var i = items.indexOf(t); if (i >= 0) { var value = this._indexToValue(i); this._itemActivate(value, t); return; } t = t.parentNode; } }, _itemActivate: function(value, item) { if (!this.fire('iron-activate', {selected: value, item: item}, {cancelable: true}).defaultPrevented) { this.select(value); } } }; </script>