blob: a1dcac2e0a997d9848debc8173a230681efc330f [file] [log] [blame]
<!--
Copyright 2013 The Polymer Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<!--
/**
* @module Polymer Elements
*/
/**
* polymer-selector is used to manage a list of elements that can be selected.
* The attribute "selected" indicates which item element is being selected.
* The attribute "multi" indicates if multiple items can be selected at once.
* Tapping on the item element would fire "polymer-activate" event. Use
* "polymer-select" event to listen for selection changes.
*
* Example:
*
* <polymer-selector selected="0">
* <div>Item 1</div>
* <div>Item 2</div>
* <div>Item 3</div>
* </polymer-selector>
*
* polymer-selector is not styled. So one needs to use "polymer-selected" CSS
* class to style the selected element.
*
* <style>
* .item.polymer-selected {
* background: #eee;
* }
* </style>
* ...
* <polymer-selector>
* <div class="item">Item 1</div>
* <div class="item">Item 2</div>
* <div class="item">Item 3</div>
* </polymer-selector>
*
* @class polymer-selector
* @status stable
*/
/**
* Fired when an item's selection state is changed. This event is fired both
* when an item is selected or deselected. The `isSelected` detail property
* contains the selection state.
*
* @event polymer-select
* @param {Object} detail
* @param {boolean} detail.isSelected true for selection and false for deselection
* @param {Object} detail.item the item element
*/
/**
* Fired when an item element is tapped.
*
* @event polymer-activate
* @param {Object} detail
* @param {Object} detail.item the item element
*/
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../polymer-selection/polymer-selection.html">
<polymer-element name="polymer-selector"
attributes="selected multi valueattr selectedClass selectedProperty selectedAttribute selectedItem selectedModel selectedIndex notap target itemsSelector activateEvent">
<template>
<polymer-selection id="selection" multi="{{multi}}" on-polymer-select="{{selectionSelect}}"></polymer-selection>
<content id="items" select="*"></content>
</template>
<script>
Polymer('polymer-selector', {
/**
* Gets or sets the selected element. Default to use the index
* of the item element.
*
* If you want a specific attribute value of the element to be
* used instead of index, set "valueattr" to that attribute name.
*
* Example:
*
* <polymer-selector valueattr="label" selected="foo">
* <div label="foo"></div>
* <div label="bar"></div>
* <div label="zot"></div>
* </polymer-selector>
*
* In multi-selection this should be an array of values.
*
* Example:
*
* <polymer-selector id="selector" valueattr="label" multi>
* <div label="foo"></div>
* <div label="bar"></div>
* <div label="zot"></div>
* </polymer-selector>
*
* this.$.selector.selected = ['foo', 'zot'];
*
* @attribute selected
* @type Object
* @default null
*/
selected: null,
/**
* If true, multiple selections are allowed.
*
* @attribute multi
* @type boolean
* @default false
*/
multi: false,
/**
* Specifies the attribute to be used for "selected" attribute.
*
* @attribute valueattr
* @type string
* @default 'name'
*/
valueattr: 'name',
/**
* Specifies the CSS class to be used to add to the selected element.
*
* @attribute selectedClass
* @type string
* @default 'polymer-selected'
*/
selectedClass: 'polymer-selected',
/**
* Specifies the property to be used to set on the selected element
* to indicate its active state.
*
* @attribute selectedProperty
* @type string
* @default ''
*/
selectedProperty: '',
/**
* Specifies the property to be used to set on the selected element
* to indicate its active state.
*
* @attribute selectedProperty
* @type string
* @default 'active'
*/
selectedAttribute: 'active',
/**
* Returns the currently selected element. In multi-selection this returns
* an array of selected elements.
*
* @attribute selectedItem
* @type Object
* @default null
*/
selectedItem: null,
/**
* In single selection, this returns the model associated with the
* selected element.
*
* @attribute selectedModel
* @type Object
* @default null
*/
selectedModel: null,
/**
* In single selection, this returns the selected index.
*
* @attribute selectedIndex
* @type number
* @default -1
*/
selectedIndex: -1,
/**
* The target element that contains items. If this is not set
* polymer-selector is the container.
*
* @attribute target
* @type Object
* @default null
*/
target: null,
/**
* This can be used to query nodes from the target node to be used for
* selection items. Note this only works if the 'target' property is set.
*
* Example:
*
* <polymer-selector target="{{$.myForm}}" itemsSelector="input[type=radio]"></polymer-selector>
* <form id="myForm">
* <label><input type="radio" name="color" value="red"> Red</label> <br>
* <label><input type="radio" name="color" value="green"> Green</label> <br>
* <label><input type="radio" name="color" value="blue"> Blue</label> <br>
* <p>color = {{color}}</p>
* </form>
*
* @attribute itemSelector
* @type string
* @default ''
*/
itemsSelector: '',
/**
* The event that would be fired from the item element to indicate
* it is being selected.
*
* @attribute activateEvent
* @type string
* @default 'tap'
*/
activateEvent: 'tap',
notap: false,
ready: function() {
this.activateListener = this.activateHandler.bind(this);
this.observer = new MutationObserver(this.updateSelected.bind(this));
if (!this.target) {
this.target = this;
}
},
get items() {
var nodes = this.target !== this ? (this.itemsSelector ?
this.target.querySelectorAll(this.itemsSelector) :
this.target.children) : this.$.items.getDistributedNodes();
return Array.prototype.filter.call(nodes || [], function(n) {
return n && n.localName !== 'template';
});
},
targetChanged: function(old) {
if (old) {
this.removeListener(old);
this.observer.disconnect();
}
if (this.target) {
this.addListener(this.target);
this.observer.observe(this.target, {childList: true});
}
},
addListener: function(node) {
node.addEventListener(this.activateEvent, this.activateListener);
},
removeListener: function(node) {
node.removeEventListener(this.activateEvent, this.activateListener);
},
get selection() {
return this.$.selection.getSelection();
},
selectedChanged: function() {
this.updateSelected();
},
updateSelected: function() {
this.validateSelected();
if (this.multi) {
this.clearSelection();
this.selected && this.selected.forEach(function(s) {
this.valueToSelection(s);
}, this);
} else {
this.valueToSelection(this.selected);
}
},
validateSelected: function() {
// convert to an array for multi-selection
if (this.multi && !Array.isArray(this.selected) &&
this.selected !== null && this.selected !== undefined) {
this.selected = [this.selected];
}
},
clearSelection: function() {
if (this.multi) {
this.selection.slice().forEach(function(s) {
this.$.selection.setItemSelected(s, false);
}, this);
} else {
this.$.selection.setItemSelected(this.selection, false);
}
this.selectedItem = null;
this.$.selection.clear();
},
valueToSelection: function(value) {
var item = (value === null || value === undefined) ?
null : this.items[this.valueToIndex(value)];
this.$.selection.select(item);
},
updateSelectedItem: function() {
this.selectedItem = this.selection;
},
selectedItemChanged: function() {
if (this.selectedItem) {
var t = this.selectedItem.templateInstance;
this.selectedModel = t ? t.model : undefined;
} else {
this.selectedModel = null;
}
this.selectedIndex = this.selectedItem ?
parseInt(this.valueToIndex(this.selected)) : -1;
},
valueToIndex: function(value) {
// find an item with value == value and return it's index
for (var i=0, items=this.items, c; (c=items[i]); i++) {
if (this.valueForNode(c) == value) {
return i;
}
}
// if no item found, the value itself is probably the index
return value;
},
valueForNode: function(node) {
return node[this.valueattr] || node.getAttribute(this.valueattr);
},
// events fired from <polymer-selection> object
selectionSelect: function(e, detail) {
this.updateSelectedItem();
if (detail.item) {
this.applySelection(detail.item, detail.isSelected);
}
},
applySelection: function(item, isSelected) {
if (this.selectedClass) {
item.classList.toggle(this.selectedClass, isSelected);
}
if (this.selectedProperty) {
item[this.selectedProperty] = isSelected;
}
if (this.selectedAttribute && item.setAttribute) {
if (isSelected) {
item.setAttribute(this.selectedAttribute, '');
} else {
item.removeAttribute(this.selectedAttribute);
}
}
},
// event fired from host
activateHandler: function(e) {
if (!this.notap) {
var i = this.findDistributedTarget(e.target, this.items);
if (i >= 0) {
var item = this.items[i];
var s = this.valueForNode(item) || i;
if (this.multi) {
if (this.selected) {
this.addRemoveSelected(s);
} else {
this.selected = [s];
}
} else {
this.selected = s;
}
this.asyncFire('polymer-activate', {item: item});
}
}
},
addRemoveSelected: function(value) {
var i = this.selected.indexOf(value);
if (i >= 0) {
this.selected.splice(i, 1);
} else {
this.selected.push(value);
}
this.valueToSelection(value);
},
findDistributedTarget: function(target, nodes) {
// find first ancestor of target (including itself) that
// is in nodes, if any
while (target && target != this) {
var i = Array.prototype.indexOf.call(nodes, target);
if (i >= 0) {
return i;
}
target = target.parentNode;
}
}
});
</script>
</polymer-element>