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