OLD | NEW |
| (Empty) |
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.g
ithub.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/CONTRI
BUTORS.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/PATEN
TS.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 deselectio
n | |
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 selected
Attribute selectedItem selectedModel selectedIndex notap excludedLocalNames targ
et itemsSelector activateEvent"> | |
70 | |
71 <template> | |
72 <core-selection id="selection" multi="{{multi}}" on-core-select="{{selection
Select}}"></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 * Note that you should not use this to set the selection. Instead use | |
164 * `selected`. | |
165 * | |
166 * @attribute selectedItem | |
167 * @type Object | |
168 * @default null | |
169 */ | |
170 selectedItem: null, | |
171 | |
172 /** | |
173 * In single selection, this returns the model associated with the | |
174 * selected element. | |
175 * Note that you should not use this to set the selection. Instead use | |
176 * `selected`. | |
177 * | |
178 * @attribute selectedModel | |
179 * @type Object | |
180 * @default null | |
181 */ | |
182 selectedModel: null, | |
183 | |
184 /** | |
185 * In single selection, this returns the selected index. | |
186 * Note that you should not use this to set the selection. Instead use | |
187 * `selected`. | |
188 * | |
189 * @attribute selectedIndex | |
190 * @type number | |
191 * @default -1 | |
192 */ | |
193 selectedIndex: -1, | |
194 | |
195 /** | |
196 * Nodes with local name that are in the list will not be included | |
197 * in the selection items. In the following example, `items` returns four | |
198 * `core-item`'s and doesn't include `h3` and `hr`. | |
199 * | |
200 * <core-selector excludedLocalNames="h3 hr"> | |
201 * <h3>Header</h3> | |
202 * <core-item>Item1</core-item> | |
203 * <core-item>Item2</core-item> | |
204 * <hr> | |
205 * <core-item>Item3</core-item> | |
206 * <core-item>Item4</core-item> | |
207 * </core-selector> | |
208 * | |
209 * @attribute excludedLocalNames | |
210 * @type string | |
211 * @default '' | |
212 */ | |
213 excludedLocalNames: '', | |
214 | |
215 /** | |
216 * The target element that contains items. If this is not set | |
217 * core-selector is the container. | |
218 * | |
219 * @attribute target | |
220 * @type Object | |
221 * @default null | |
222 */ | |
223 target: null, | |
224 | |
225 /** | |
226 * This can be used to query nodes from the target node to be used for | |
227 * selection items. Note this only works if `target` is set | |
228 * and is not `core-selector` itself. | |
229 * | |
230 * Example: | |
231 * | |
232 * <core-selector target="{{$.myForm}}" itemsSelector="input[type=radi
o]"></core-selector> | |
233 * <form id="myForm"> | |
234 * <label><input type="radio" name="color" value="red"> Red</label>
<br> | |
235 * <label><input type="radio" name="color" value="green"> Green</lab
el> <br> | |
236 * <label><input type="radio" name="color" value="blue"> Blue</label
> <br> | |
237 * <p>color = {{color}}</p> | |
238 * </form> | |
239 * | |
240 * @attribute itemsSelector | |
241 * @type string | |
242 * @default '' | |
243 */ | |
244 itemsSelector: '', | |
245 | |
246 /** | |
247 * The event that would be fired from the item element to indicate | |
248 * it is being selected. | |
249 * | |
250 * @attribute activateEvent | |
251 * @type string | |
252 * @default 'tap' | |
253 */ | |
254 activateEvent: 'tap', | |
255 | |
256 /** | |
257 * Set this to true to disallow changing the selection via the | |
258 * `activateEvent`. | |
259 * | |
260 * @attribute notap | |
261 * @type boolean | |
262 * @default false | |
263 */ | |
264 notap: false, | |
265 | |
266 defaultExcludedLocalNames: 'template', | |
267 | |
268 ready: function() { | |
269 this.activateListener = this.activateHandler.bind(this); | |
270 this.itemFilter = this.filterItem.bind(this); | |
271 this.excludedLocalNamesChanged(); | |
272 this.observer = new MutationObserver(this.updateSelected.bind(this)); | |
273 if (!this.target) { | |
274 this.target = this; | |
275 } | |
276 }, | |
277 | |
278 /** | |
279 * Returns an array of all items. | |
280 * | |
281 * @property items | |
282 */ | |
283 get items() { | |
284 if (!this.target) { | |
285 return []; | |
286 } | |
287 var nodes = this.target !== this ? (this.itemsSelector ? | |
288 this.target.querySelectorAll(this.itemsSelector) : | |
289 this.target.children) : this.$.items.getDistributedNodes(); | |
290 return Array.prototype.filter.call(nodes, this.itemFilter); | |
291 }, | |
292 | |
293 filterItem: function(node) { | |
294 return !this._excludedNames[node.localName]; | |
295 }, | |
296 | |
297 excludedLocalNamesChanged: function() { | |
298 this._excludedNames = {}; | |
299 var s = this.defaultExcludedLocalNames; | |
300 if (this.excludedLocalNames) { | |
301 s += ' ' + this.excludedLocalNames; | |
302 } | |
303 s.split(/\s+/g).forEach(function(n) { | |
304 this._excludedNames[n] = 1; | |
305 }, this); | |
306 }, | |
307 | |
308 targetChanged: function(old) { | |
309 if (old) { | |
310 this.removeListener(old); | |
311 this.observer.disconnect(); | |
312 this.clearSelection(); | |
313 } | |
314 if (this.target) { | |
315 this.addListener(this.target); | |
316 this.observer.observe(this.target, {childList: true}); | |
317 this.updateSelected(); | |
318 } | |
319 }, | |
320 | |
321 addListener: function(node) { | |
322 Polymer.addEventListener(node, this.activateEvent, this.activateListener
); | |
323 }, | |
324 | |
325 removeListener: function(node) { | |
326 Polymer.removeEventListener(node, this.activateEvent, this.activateListe
ner); | |
327 }, | |
328 | |
329 /** | |
330 * Returns the selected item(s). If the `multi` property is true, | |
331 * this will return an array, otherwise it will return | |
332 * the selected item or undefined if there is no selection. | |
333 */ | |
334 get selection() { | |
335 return this.$.selection.getSelection(); | |
336 }, | |
337 | |
338 selectedChanged: function() { | |
339 this.updateSelected(); | |
340 }, | |
341 | |
342 updateSelected: function() { | |
343 this.validateSelected(); | |
344 if (this.multi) { | |
345 this.clearSelection(); | |
346 this.selected && this.selected.forEach(function(s) { | |
347 this.valueToSelection(s); | |
348 }, this); | |
349 } else { | |
350 this.valueToSelection(this.selected); | |
351 } | |
352 }, | |
353 | |
354 validateSelected: function() { | |
355 // convert to an array for multi-selection | |
356 if (this.multi && !Array.isArray(this.selected) && | |
357 this.selected !== null && this.selected !== undefined) { | |
358 this.selected = [this.selected]; | |
359 } | |
360 }, | |
361 | |
362 clearSelection: function() { | |
363 if (this.multi) { | |
364 this.selection.slice().forEach(function(s) { | |
365 this.$.selection.setItemSelected(s, false); | |
366 }, this); | |
367 } else { | |
368 this.$.selection.setItemSelected(this.selection, false); | |
369 } | |
370 this.selectedItem = null; | |
371 this.$.selection.clear(); | |
372 }, | |
373 | |
374 valueToSelection: function(value) { | |
375 var item = (value === null || value === undefined) ? | |
376 null : this.items[this.valueToIndex(value)]; | |
377 this.$.selection.select(item); | |
378 }, | |
379 | |
380 updateSelectedItem: function() { | |
381 this.selectedItem = this.selection; | |
382 }, | |
383 | |
384 selectedItemChanged: function() { | |
385 if (this.selectedItem) { | |
386 var t = this.selectedItem.templateInstance; | |
387 this.selectedModel = t ? t.model : undefined; | |
388 } else { | |
389 this.selectedModel = null; | |
390 } | |
391 this.selectedIndex = this.selectedItem ? | |
392 parseInt(this.valueToIndex(this.selected)) : -1; | |
393 }, | |
394 | |
395 valueToIndex: function(value) { | |
396 // find an item with value == value and return it's index | |
397 for (var i=0, items=this.items, c; (c=items[i]); i++) { | |
398 if (this.valueForNode(c) == value) { | |
399 return i; | |
400 } | |
401 } | |
402 // if no item found, the value itself is probably the index | |
403 return value; | |
404 }, | |
405 | |
406 valueForNode: function(node) { | |
407 return node[this.valueattr] || node.getAttribute(this.valueattr); | |
408 }, | |
409 | |
410 // events fired from <core-selection> object | |
411 selectionSelect: function(e, detail) { | |
412 this.updateSelectedItem(); | |
413 if (detail.item) { | |
414 this.applySelection(detail.item, detail.isSelected); | |
415 } | |
416 }, | |
417 | |
418 applySelection: function(item, isSelected) { | |
419 if (this.selectedClass) { | |
420 item.classList.toggle(this.selectedClass, isSelected); | |
421 } | |
422 if (this.selectedProperty) { | |
423 item[this.selectedProperty] = isSelected; | |
424 } | |
425 if (this.selectedAttribute && item.setAttribute) { | |
426 if (isSelected) { | |
427 item.setAttribute(this.selectedAttribute, ''); | |
428 } else { | |
429 item.removeAttribute(this.selectedAttribute); | |
430 } | |
431 } | |
432 }, | |
433 | |
434 // event fired from host | |
435 activateHandler: function(e) { | |
436 if (!this.notap) { | |
437 var i = this.findDistributedTarget(e.target, this.items); | |
438 if (i >= 0) { | |
439 var item = this.items[i]; | |
440 var s = this.valueForNode(item) || i; | |
441 if (this.multi) { | |
442 if (this.selected) { | |
443 this.addRemoveSelected(s); | |
444 } else { | |
445 this.selected = [s]; | |
446 } | |
447 } else { | |
448 this.selected = s; | |
449 } | |
450 this.asyncFire('core-activate', {item: item}); | |
451 } | |
452 } | |
453 }, | |
454 | |
455 addRemoveSelected: function(value) { | |
456 var i = this.selected.indexOf(value); | |
457 if (i >= 0) { | |
458 this.selected.splice(i, 1); | |
459 } else { | |
460 this.selected.push(value); | |
461 } | |
462 this.valueToSelection(value); | |
463 }, | |
464 | |
465 findDistributedTarget: function(target, nodes) { | |
466 // find first ancestor of target (including itself) that | |
467 // is in nodes, if any | |
468 while (target && target != this) { | |
469 var i = Array.prototype.indexOf.call(nodes, target); | |
470 if (i >= 0) { | |
471 return i; | |
472 } | |
473 target = target.parentNode; | |
474 } | |
475 }, | |
476 | |
477 selectIndex: function(index) { | |
478 var item = this.items[index]; | |
479 if (item) { | |
480 this.selected = this.valueForNode(item) || index; | |
481 return item; | |
482 } | |
483 }, | |
484 | |
485 /** | |
486 * Selects the previous item. This should be used in single selection onl
y. | |
487 * | |
488 * @method selectPrevious | |
489 * @param {boolean} wrap if true and it is already at the first item, wrap
to the end | |
490 * @returns the previous item or undefined if there is none | |
491 */ | |
492 selectPrevious: function(wrap) { | |
493 var i = wrap && !this.selectedIndex ? this.items.length - 1 : this.selec
tedIndex - 1; | |
494 return this.selectIndex(i); | |
495 }, | |
496 | |
497 /** | |
498 * Selects the next item. This should be used in single selection only. | |
499 * | |
500 * @method selectNext | |
501 * @param {boolean} wrap if true and it is already at the last item, wrap
to the front | |
502 * @returns the next item or undefined if there is none | |
503 */ | |
504 selectNext: function(wrap) { | |
505 var i = wrap && this.selectedIndex >= this.items.length - 1 ? 0 : this.s
electedIndex + 1; | |
506 return this.selectIndex(i); | |
507 } | |
508 | |
509 }); | |
510 </script> | |
511 </polymer-element> | |
OLD | NEW |