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