Home | History | Annotate | Download | only in core-selector
      1 <!--
      2 Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
      3 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      4 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      5 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      6 Code distributed by Google as part of the polymer project is also
      7 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      8 -->
      9 
     10 <!--
     11 @group Polymer Core Elements
     12 
     13 `<core-selector>` is used to manage a list of elements that can be selected.
     14 
     15 The attribute `selected` indicates which item element is being selected.
     16 The attribute `multi` indicates if multiple items can be selected at once.
     17 Tapping on the item element would fire `core-activate` event. Use
     18 `core-select` event to listen for selection changes.
     19 
     20 Example:
     21 
     22     <core-selector selected="0">
     23       <div>Item 1</div>
     24       <div>Item 2</div>
     25       <div>Item 3</div>
     26     </core-selector>
     27 
     28 `<core-selector>` is not styled. Use the `core-selected` CSS class to style the selected element.
     29 
     30     <style>
     31       .item.core-selected {
     32         background: #eee;
     33       }
     34     </style>
     35     ...
     36     <core-selector>
     37       <div class="item">Item 1</div>
     38       <div class="item">Item 2</div>
     39       <div class="item">Item 3</div>
     40     </core-selector>
     41 
     42 @element core-selector
     43 @status stable
     44 @homepage github.io
     45 -->
     46 
     47 <!--
     48 Fired when an item's selection state is changed. This event is fired both
     49 when an item is selected or deselected. The `isSelected` detail property
     50 contains the selection state.
     51 
     52 @event core-select
     53 @param {Object} detail
     54   @param {boolean} detail.isSelected true for selection and false for deselection
     55   @param {Object} detail.item the item element
     56 -->
     57 <!--
     58 Fired when an item element is tapped.
     59 
     60 @event core-activate
     61 @param {Object} detail
     62   @param {Object} detail.item the item element
     63 -->
     64 
     65 <link rel="import" href="../polymer/polymer.html">
     66 <link rel="import" href="../core-selection/core-selection.html">
     67 
     68 <polymer-element name="core-selector"
     69     attributes="selected multi valueattr selectedClass selectedProperty selectedAttribute selectedItem selectedModel selectedIndex notap target itemsSelector activateEvent">
     70 
     71   <template>
     72     <core-selection id="selection" multi="{{multi}}" on-core-select="{{selectionSelect}}"></core-selection>
     73     <content id="items" select="*"></content>
     74   </template>
     75 
     76   <script>
     77 
     78     Polymer('core-selector', {
     79 
     80       /**
     81        * Gets or sets the selected element.  Default to use the index
     82        * of the item element.
     83        *
     84        * If you want a specific attribute value of the element to be
     85        * used instead of index, set "valueattr" to that attribute name.
     86        *
     87        * Example:
     88        *
     89        *     <core-selector valueattr="label" selected="foo">
     90        *       <div label="foo"></div>
     91        *       <div label="bar"></div>
     92        *       <div label="zot"></div>
     93        *     </core-selector>
     94        *
     95        * In multi-selection this should be an array of values.
     96        *
     97        * Example:
     98        *
     99        *     <core-selector id="selector" valueattr="label" multi>
    100        *       <div label="foo"></div>
    101        *       <div label="bar"></div>
    102        *       <div label="zot"></div>
    103        *     </core-selector>
    104        *
    105        *     this.$.selector.selected = ['foo', 'zot'];
    106        *
    107        * @attribute selected
    108        * @type Object
    109        * @default null
    110        */
    111       selected: null,
    112 
    113       /**
    114        * If true, multiple selections are allowed.
    115        *
    116        * @attribute multi
    117        * @type boolean
    118        * @default false
    119        */
    120       multi: false,
    121 
    122       /**
    123        * Specifies the attribute to be used for "selected" attribute.
    124        *
    125        * @attribute valueattr
    126        * @type string
    127        * @default 'name'
    128        */
    129       valueattr: 'name',
    130 
    131       /**
    132        * Specifies the CSS class to be used to add to the selected element.
    133        * 
    134        * @attribute selectedClass
    135        * @type string
    136        * @default 'core-selected'
    137        */
    138       selectedClass: 'core-selected',
    139 
    140       /**
    141        * Specifies the property to be used to set on the selected element
    142        * to indicate its active state.
    143        *
    144        * @attribute selectedProperty
    145        * @type string
    146        * @default ''
    147        */
    148       selectedProperty: '',
    149 
    150       /**
    151        * Specifies the attribute to set on the selected element to indicate
    152        * its active state.
    153        *
    154        * @attribute selectedAttribute
    155        * @type string
    156        * @default 'active'
    157        */
    158       selectedAttribute: 'active',
    159 
    160       /**
    161        * Returns the currently selected element. In multi-selection this returns
    162        * an array of selected elements.
    163        * 
    164        * @attribute selectedItem
    165        * @type Object
    166        * @default null
    167        */
    168       selectedItem: null,
    169 
    170       /**
    171        * In single selection, this returns the model associated with the
    172        * selected element.
    173        * 
    174        * @attribute selectedModel
    175        * @type Object
    176        * @default null
    177        */
    178       selectedModel: null,
    179 
    180       /**
    181        * In single selection, this returns the selected index.
    182        *
    183        * @attribute selectedIndex
    184        * @type number
    185        * @default -1
    186        */
    187       selectedIndex: -1,
    188 
    189       /**
    190        * The target element that contains items.  If this is not set 
    191        * core-selector is the container.
    192        * 
    193        * @attribute target
    194        * @type Object
    195        * @default null
    196        */
    197       target: null,
    198 
    199       /**
    200        * This can be used to query nodes from the target node to be used for 
    201        * selection items.  Note this only works if the 'target' property is set.
    202        *
    203        * Example:
    204        *
    205        *     <core-selector target="{{$.myForm}}" itemsSelector="input[type=radio]"></core-selector>
    206        *     <form id="myForm">
    207        *       <label><input type="radio" name="color" value="red"> Red</label> <br>
    208        *       <label><input type="radio" name="color" value="green"> Green</label> <br>
    209        *       <label><input type="radio" name="color" value="blue"> Blue</label> <br>
    210        *       <p>color = {{color}}</p>
    211        *     </form>
    212        * 
    213        * @attribute itemsSelector
    214        * @type string
    215        * @default ''
    216        */
    217       itemsSelector: '',
    218 
    219       /**
    220        * The event that would be fired from the item element to indicate
    221        * it is being selected.
    222        *
    223        * @attribute activateEvent
    224        * @type string
    225        * @default 'tap'
    226        */
    227       activateEvent: 'tap',
    228 
    229       /**
    230        * Set this to true to disallow changing the selection via the
    231        * `activateEvent`.
    232        *
    233        * @attribute notap
    234        * @type boolean
    235        * @default false
    236        */
    237       notap: false,
    238 
    239       ready: function() {
    240         this.activateListener = this.activateHandler.bind(this);
    241         this.observer = new MutationObserver(this.updateSelected.bind(this));
    242         if (!this.target) {
    243           this.target = this;
    244         }
    245       },
    246 
    247       get items() {
    248         if (!this.target) {
    249           return [];
    250         }
    251         var nodes = this.target !== this ? (this.itemsSelector ? 
    252             this.target.querySelectorAll(this.itemsSelector) : 
    253                 this.target.children) : this.$.items.getDistributedNodes();
    254         return Array.prototype.filter.call(nodes || [], function(n) {
    255           return n && n.localName !== 'template';
    256         });
    257       },
    258 
    259       targetChanged: function(old) {
    260         if (old) {
    261           this.removeListener(old);
    262           this.observer.disconnect();
    263           this.clearSelection();
    264         }
    265         if (this.target) {
    266           this.addListener(this.target);
    267           this.observer.observe(this.target, {childList: true});
    268           this.updateSelected();
    269         }
    270       },
    271 
    272       addListener: function(node) {
    273         Polymer.addEventListener(node, this.activateEvent, this.activateListener);
    274       },
    275 
    276       removeListener: function(node) {
    277         Polymer.removeEventListener(node, this.activateEvent, this.activateListener);
    278       },
    279 
    280       get selection() {
    281         return this.$.selection.getSelection();
    282       },
    283 
    284       selectedChanged: function() {
    285         this.updateSelected();
    286       },
    287 
    288       updateSelected: function() {
    289         this.validateSelected();
    290         if (this.multi) {
    291           this.clearSelection();
    292           this.selected && this.selected.forEach(function(s) {
    293             this.valueToSelection(s);
    294           }, this);
    295         } else {
    296           this.valueToSelection(this.selected);
    297         }
    298       },
    299 
    300       validateSelected: function() {
    301         // convert to an array for multi-selection
    302         if (this.multi && !Array.isArray(this.selected) && 
    303             this.selected !== null && this.selected !== undefined) {
    304           this.selected = [this.selected];
    305         }
    306       },
    307 
    308       clearSelection: function() {
    309         if (this.multi) {
    310           this.selection.slice().forEach(function(s) {
    311             this.$.selection.setItemSelected(s, false);
    312           }, this);
    313         } else {
    314           this.$.selection.setItemSelected(this.selection, false);
    315         }
    316         this.selectedItem = null;
    317         this.$.selection.clear();
    318       },
    319 
    320       valueToSelection: function(value) {
    321         var item = (value === null || value === undefined) ? 
    322             null : this.items[this.valueToIndex(value)];
    323         this.$.selection.select(item);
    324       },
    325 
    326       updateSelectedItem: function() {
    327         this.selectedItem = this.selection;
    328       },
    329 
    330       selectedItemChanged: function() {
    331         if (this.selectedItem) {
    332           var t = this.selectedItem.templateInstance;
    333           this.selectedModel = t ? t.model : undefined;
    334         } else {
    335           this.selectedModel = null;
    336         }
    337         this.selectedIndex = this.selectedItem ? 
    338             parseInt(this.valueToIndex(this.selected)) : -1;
    339       },
    340 
    341       valueToIndex: function(value) {
    342         // find an item with value == value and return it's index
    343         for (var i=0, items=this.items, c; (c=items[i]); i++) {
    344           if (this.valueForNode(c) == value) {
    345             return i;
    346           }
    347         }
    348         // if no item found, the value itself is probably the index
    349         return value;
    350       },
    351 
    352       valueForNode: function(node) {
    353         return node[this.valueattr] || node.getAttribute(this.valueattr);
    354       },
    355 
    356       // events fired from <core-selection> object
    357       selectionSelect: function(e, detail) {
    358         this.updateSelectedItem();
    359         if (detail.item) {
    360           this.applySelection(detail.item, detail.isSelected);
    361         }
    362       },
    363 
    364       applySelection: function(item, isSelected) {
    365         if (this.selectedClass) {
    366           item.classList.toggle(this.selectedClass, isSelected);
    367         }
    368         if (this.selectedProperty) {
    369           item[this.selectedProperty] = isSelected;
    370         }
    371         if (this.selectedAttribute && item.setAttribute) {
    372           if (isSelected) {
    373             item.setAttribute(this.selectedAttribute, '');
    374           } else {
    375             item.removeAttribute(this.selectedAttribute);
    376           }
    377         }
    378       },
    379 
    380       // event fired from host
    381       activateHandler: function(e) {
    382         if (!this.notap) {
    383           var i = this.findDistributedTarget(e.target, this.items);
    384           if (i >= 0) {
    385             var item = this.items[i];
    386             var s = this.valueForNode(item) || i;
    387             if (this.multi) {
    388               if (this.selected) {
    389                 this.addRemoveSelected(s);
    390               } else {
    391                 this.selected = [s];
    392               }
    393             } else {
    394               this.selected = s;
    395             }
    396             this.asyncFire('core-activate', {item: item});
    397           }
    398         }
    399       },
    400 
    401       addRemoveSelected: function(value) {
    402         var i = this.selected.indexOf(value);
    403         if (i >= 0) {
    404           this.selected.splice(i, 1);
    405         } else {
    406           this.selected.push(value);
    407         }
    408         this.valueToSelection(value);
    409       },
    410 
    411       findDistributedTarget: function(target, nodes) {
    412         // find first ancestor of target (including itself) that
    413         // is in nodes, if any
    414         while (target && target != this) {
    415           var i = Array.prototype.indexOf.call(nodes, target);
    416           if (i >= 0) {
    417             return i;
    418           }
    419           target = target.parentNode;
    420         }
    421       }
    422     });
    423   </script>
    424 </polymer-element>
    425