Home | History | Annotate | Download | only in iron-selector
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="iron-selection.html">
     13 
     14 <script>
     15 
     16   /** @polymerBehavior */
     17   Polymer.IronSelectableBehavior = {
     18 
     19       /**
     20        * Fired when iron-selector is activated (selected or deselected).
     21        * It is fired before the selected items are changed.
     22        * Cancel the event to abort selection.
     23        *
     24        * @event iron-activate
     25        */
     26 
     27       /**
     28        * Fired when an item is selected
     29        *
     30        * @event iron-select
     31        */
     32 
     33       /**
     34        * Fired when an item is deselected
     35        *
     36        * @event iron-deselect
     37        */
     38 
     39       /**
     40        * Fired when the list of selectable items changes (e.g., items are
     41        * added or removed). The detail of the event is a mutation record that
     42        * describes what changed.
     43        *
     44        * @event iron-items-changed
     45        */
     46 
     47     properties: {
     48 
     49       /**
     50        * If you want to use an attribute value or property of an element for
     51        * `selected` instead of the index, set this to the name of the attribute
     52        * or property. Hyphenated values are converted to camel case when used to
     53        * look up the property of a selectable element. Camel cased values are
     54        * *not* converted to hyphenated values for attribute lookup. It's
     55        * recommended that you provide the hyphenated form of the name so that
     56        * selection works in both cases. (Use `attr-or-property-name` instead of
     57        * `attrOrPropertyName`.)
     58        */
     59       attrForSelected: {
     60         type: String,
     61         value: null
     62       },
     63 
     64       /**
     65        * Gets or sets the selected element. The default is to use the index of the item.
     66        * @type {string|number}
     67        */
     68       selected: {
     69         type: String,
     70         notify: true
     71       },
     72 
     73       /**
     74        * Returns the currently selected item.
     75        *
     76        * @type {?Object}
     77        */
     78       selectedItem: {
     79         type: Object,
     80         readOnly: true,
     81         notify: true
     82       },
     83 
     84       /**
     85        * The event that fires from items when they are selected. Selectable
     86        * will listen for this event from items and update the selection state.
     87        * Set to empty string to listen to no events.
     88        */
     89       activateEvent: {
     90         type: String,
     91         value: 'tap',
     92         observer: '_activateEventChanged'
     93       },
     94 
     95       /**
     96        * This is a CSS selector string.  If this is set, only items that match the CSS selector
     97        * are selectable.
     98        */
     99       selectable: String,
    100 
    101       /**
    102        * The class to set on elements when selected.
    103        */
    104       selectedClass: {
    105         type: String,
    106         value: 'iron-selected'
    107       },
    108 
    109       /**
    110        * The attribute to set on elements when selected.
    111        */
    112       selectedAttribute: {
    113         type: String,
    114         value: null
    115       },
    116 
    117       /**
    118        * Default fallback if the selection based on selected with `attrForSelected`
    119        * is not found.
    120        */
    121       fallbackSelection: {
    122         type: String,
    123         value: null
    124       },
    125 
    126       /**
    127        * The list of items from which a selection can be made.
    128        */
    129       items: {
    130         type: Array,
    131         readOnly: true,
    132         notify: true,
    133         value: function() {
    134           return [];
    135         }
    136       },
    137 
    138       /**
    139        * The set of excluded elements where the key is the `localName`
    140        * of the element that will be ignored from the item list.
    141        *
    142        * @default {template: 1}
    143        */
    144       _excludedLocalNames: {
    145         type: Object,
    146         value: function() {
    147           return {
    148             'template': 1
    149           };
    150         }
    151       }
    152     },
    153 
    154     observers: [
    155       '_updateAttrForSelected(attrForSelected)',
    156       '_updateSelected(selected)',
    157       '_checkFallback(fallbackSelection)'
    158     ],
    159 
    160     created: function() {
    161       this._bindFilterItem = this._filterItem.bind(this);
    162       this._selection = new Polymer.IronSelection(this._applySelection.bind(this));
    163     },
    164 
    165     attached: function() {
    166       this._observer = this._observeItems(this);
    167       this._updateItems();
    168       if (!this._shouldUpdateSelection) {
    169         this._updateSelected();
    170       }
    171       this._addListener(this.activateEvent);
    172     },
    173 
    174     detached: function() {
    175       if (this._observer) {
    176         Polymer.dom(this).unobserveNodes(this._observer);
    177       }
    178       this._removeListener(this.activateEvent);
    179     },
    180 
    181     /**
    182      * Returns the index of the given item.
    183      *
    184      * @method indexOf
    185      * @param {Object} item
    186      * @returns Returns the index of the item
    187      */
    188     indexOf: function(item) {
    189       return this.items.indexOf(item);
    190     },
    191 
    192     /**
    193      * Selects the given value.
    194      *
    195      * @method select
    196      * @param {string|number} value the value to select.
    197      */
    198     select: function(value) {
    199       this.selected = value;
    200     },
    201 
    202     /**
    203      * Selects the previous item.
    204      *
    205      * @method selectPrevious
    206      */
    207     selectPrevious: function() {
    208       var length = this.items.length;
    209       var index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length;
    210       this.selected = this._indexToValue(index);
    211     },
    212 
    213     /**
    214      * Selects the next item.
    215      *
    216      * @method selectNext
    217      */
    218     selectNext: function() {
    219       var index = (Number(this._valueToIndex(this.selected)) + 1) % this.items.length;
    220       this.selected = this._indexToValue(index);
    221     },
    222 
    223     /**
    224      * Selects the item at the given index.
    225      *
    226      * @method selectIndex
    227      */
    228     selectIndex: function(index) {
    229       this.select(this._indexToValue(index));
    230     },
    231 
    232     /**
    233      * Force a synchronous update of the `items` property.
    234      *
    235      * NOTE: Consider listening for the `iron-items-changed` event to respond to
    236      * updates to the set of selectable items after updates to the DOM list and
    237      * selection state have been made.
    238      *
    239      * WARNING: If you are using this method, you should probably consider an
    240      * alternate approach. Synchronously querying for items is potentially
    241      * slow for many use cases. The `items` property will update asynchronously
    242      * on its own to reflect selectable items in the DOM.
    243      */
    244     forceSynchronousItemUpdate: function() {
    245       this._updateItems();
    246     },
    247 
    248     get _shouldUpdateSelection() {
    249       return this.selected != null;
    250     },
    251 
    252     _checkFallback: function() {
    253       if (this._shouldUpdateSelection) {
    254         this._updateSelected();
    255       }
    256     },
    257 
    258     _addListener: function(eventName) {
    259       this.listen(this, eventName, '_activateHandler');
    260     },
    261 
    262     _removeListener: function(eventName) {
    263       this.unlisten(this, eventName, '_activateHandler');
    264     },
    265 
    266     _activateEventChanged: function(eventName, old) {
    267       this._removeListener(old);
    268       this._addListener(eventName);
    269     },
    270 
    271     _updateItems: function() {
    272       var nodes = Polymer.dom(this).queryDistributedElements(this.selectable || '*');
    273       nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
    274       this._setItems(nodes);
    275     },
    276 
    277     _updateAttrForSelected: function() {
    278       if (this._shouldUpdateSelection) {
    279         this.selected = this._indexToValue(this.indexOf(this.selectedItem));
    280       }
    281     },
    282 
    283     _updateSelected: function() {
    284       this._selectSelected(this.selected);
    285     },
    286 
    287     _selectSelected: function(selected) {
    288       this._selection.select(this._valueToItem(this.selected));
    289       // Check for items, since this array is populated only when attached
    290       // Since Number(0) is falsy, explicitly check for undefined
    291       if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
    292         this.selected = this.fallbackSelection;
    293       }
    294     },
    295 
    296     _filterItem: function(node) {
    297       return !this._excludedLocalNames[node.localName];
    298     },
    299 
    300     _valueToItem: function(value) {
    301       return (value == null) ? null : this.items[this._valueToIndex(value)];
    302     },
    303 
    304     _valueToIndex: function(value) {
    305       if (this.attrForSelected) {
    306         for (var i = 0, item; item = this.items[i]; i++) {
    307           if (this._valueForItem(item) == value) {
    308             return i;
    309           }
    310         }
    311       } else {
    312         return Number(value);
    313       }
    314     },
    315 
    316     _indexToValue: function(index) {
    317       if (this.attrForSelected) {
    318         var item = this.items[index];
    319         if (item) {
    320           return this._valueForItem(item);
    321         }
    322       } else {
    323         return index;
    324       }
    325     },
    326 
    327     _valueForItem: function(item) {
    328       var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
    329       return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected);
    330     },
    331 
    332     _applySelection: function(item, isSelected) {
    333       if (this.selectedClass) {
    334         this.toggleClass(this.selectedClass, isSelected, item);
    335       }
    336       if (this.selectedAttribute) {
    337         this.toggleAttribute(this.selectedAttribute, isSelected, item);
    338       }
    339       this._selectionChange();
    340       this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
    341     },
    342 
    343     _selectionChange: function() {
    344       this._setSelectedItem(this._selection.get());
    345     },
    346 
    347     // observe items change under the given node.
    348     _observeItems: function(node) {
    349       return Polymer.dom(node).observeNodes(function(mutation) {
    350         this._updateItems();
    351 
    352         if (this._shouldUpdateSelection) {
    353           this._updateSelected();
    354         }
    355 
    356         // Let other interested parties know about the change so that
    357         // we don't have to recreate mutation observers everywhere.
    358         this.fire('iron-items-changed', mutation, {
    359           bubbles: false,
    360           cancelable: false
    361         });
    362       });
    363     },
    364 
    365     _activateHandler: function(e) {
    366       var t = e.target;
    367       var items = this.items;
    368       while (t && t != this) {
    369         var i = items.indexOf(t);
    370         if (i >= 0) {
    371           var value = this._indexToValue(i);
    372           this._itemActivate(value, t);
    373           return;
    374         }
    375         t = t.parentNode;
    376       }
    377     },
    378 
    379     _itemActivate: function(value, item) {
    380       if (!this.fire('iron-activate',
    381           {selected: value, item: item}, {cancelable: true}).defaultPrevented) {
    382         this.select(value);
    383       }
    384     }
    385 
    386   };
    387 
    388 </script>
    389