• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject 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