blob: a19886e16cd5e67f78de6f944a7d5a3938e7c9de [file] [log] [blame]
<!--
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<!--
@group Polymer Core Elements
`<core-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 `core-activate` event. Use
`core-select` event to listen for selection changes.
Example:
<core-selector selected="0">
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>
</core-selector>
`<core-selector>` is not styled. Use the `core-selected` CSS class to style the selected element.
<style>
.item.core-selected {
background: #eee;
}
</style>
...
<core-selector>
<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
</core-selector>
@element core-selector
@status stable
@homepage github.io
-->
<!--
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 core-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 core-activate
@param {Object} detail
@param {Object} detail.item the item element
-->
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../core-selection/core-selection.html">
<polymer-element name="core-selector"
attributes="selected multi valueattr selectedClass selectedProperty selectedAttribute selectedItem selectedModel selectedIndex notap excludedLocalNames target itemsSelector activateEvent">
<template>
<core-selection id="selection" multi="{{multi}}" on-core-select="{{selectionSelect}}"></core-selection>
<content id="items" select="*"></content>
</template>
<script>
Polymer('core-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:
*
* <core-selector valueattr="label" selected="foo">
* <div label="foo"></div>
* <div label="bar"></div>
* <div label="zot"></div>
* </core-selector>
*
* In multi-selection this should be an array of values.
*
* Example:
*
* <core-selector id="selector" valueattr="label" multi>
* <div label="foo"></div>
* <div label="bar"></div>
* <div label="zot"></div>
* </core-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 'core-selected'
*/
selectedClass: 'core-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 attribute to set on the selected element to indicate
* its active state.
*
* @attribute selectedAttribute
* @type string
* @default 'active'
*/
selectedAttribute: 'active',
/**
* Returns the currently selected element. In multi-selection this returns
* an array of selected elements.
* Note that you should not use this to set the selection. Instead use
* `selected`.
*
* @attribute selectedItem
* @type Object
* @default null
*/
selectedItem: null,
/**
* In single selection, this returns the model associated with the
* selected element.
* Note that you should not use this to set the selection. Instead use
* `selected`.
*
* @attribute selectedModel
* @type Object
* @default null
*/
selectedModel: null,
/**
* In single selection, this returns the selected index.
* Note that you should not use this to set the selection. Instead use
* `selected`.
*
* @attribute selectedIndex
* @type number
* @default -1
*/
selectedIndex: -1,
/**
* Nodes with local name that are in the list will not be included
* in the selection items. In the following example, `items` returns four
* `core-item`'s and doesn't include `h3` and `hr`.
*
* <core-selector excludedLocalNames="h3 hr">
* <h3>Header</h3>
* <core-item>Item1</core-item>
* <core-item>Item2</core-item>
* <hr>
* <core-item>Item3</core-item>
* <core-item>Item4</core-item>
* </core-selector>
*
* @attribute excludedLocalNames
* @type string
* @default ''
*/
excludedLocalNames: '',
/**
* The target element that contains items. If this is not set
* core-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 `target` is set
* and is not `core-selector` itself.
*
* Example:
*
* <core-selector target="{{$.myForm}}" itemsSelector="input[type=radio]"></core-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 itemsSelector
* @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',
/**
* Set this to true to disallow changing the selection via the
* `activateEvent`.
*
* @attribute notap
* @type boolean
* @default false
*/
notap: false,
defaultExcludedLocalNames: 'template',
observe: {
'selected multi': 'selectedChanged'
},
ready: function() {
this.activateListener = this.activateHandler.bind(this);
this.itemFilter = this.filterItem.bind(this);
this.excludedLocalNamesChanged();
this.observer = new MutationObserver(this.updateSelected.bind(this));
if (!this.target) {
this.target = this;
}
},
/**
* Returns an array of all items.
*
* @property items
* @type array
*/
get items() {
if (!this.target) {
return [];
}
var nodes = this.target !== this ? (this.itemsSelector ?
this.target.querySelectorAll(this.itemsSelector) :
this.target.children) : this.$.items.getDistributedNodes();
return Array.prototype.filter.call(nodes, this.itemFilter);
},
filterItem: function(node) {
return !this._excludedNames[node.localName];
},
excludedLocalNamesChanged: function() {
this._excludedNames = {};
var s = this.defaultExcludedLocalNames;
if (this.excludedLocalNames) {
s += ' ' + this.excludedLocalNames;
}
s.split(/\s+/g).forEach(function(n) {
this._excludedNames[n] = 1;
}, this);
},
targetChanged: function(old) {
if (old) {
this.removeListener(old);
this.observer.disconnect();
this.clearSelection();
}
if (this.target) {
this.addListener(this.target);
this.observer.observe(this.target, {childList: true});
this.updateSelected();
}
},
addListener: function(node) {
Polymer.addEventListener(node, this.activateEvent, this.activateListener);
},
removeListener: function(node) {
Polymer.removeEventListener(node, this.activateEvent, this.activateListener);
},
/**
* Returns the selected item(s). If the `multi` property is true,
* this will return an array, otherwise it will return
* the selected item or undefined if there is no selection.
*/
get selection() {
return this.$.selection.getSelection();
},
selectedChanged: function() {
// TODO(ffu): Right now this is the only way to know that the `selected`
// is an array and was mutated, as opposed to newly assigned.
if (arguments.length === 1) {
this.processSplices(arguments[0]);
} else {
this.updateSelected();
}
},
updateSelected: function() {
this.validateSelected();
if (this.multi) {
this.clearSelection(this.selected);
this.selected && this.selected.forEach(function(s) {
this.setValueSelected(s, true);
}, 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 = [this.selected];
// use the first selected in the array for single-selection
} else if (!this.multi && Array.isArray(this.selected)) {
var s = this.selected[0];
this.clearSelection([s]);
this.selected = s;
}
},
processSplices: function(splices) {
for (var i = 0, splice; splice = splices[i]; i++) {
for (var j = 0; j < splice.removed.length; j++) {
this.setValueSelected(splice.removed[j], false);
}
for (var j = 0; j < splice.addedCount; j++) {
this.setValueSelected(this.selected[splice.index + j], true);
}
}
},
clearSelection: function(excludes) {
this.$.selection.selection.slice().forEach(function(item) {
var v = this.valueForNode(item) || this.items.indexOf(item);
if (!excludes || excludes.indexOf(v) < 0) {
this.$.selection.setItemSelected(item, false);
}
}, this);
},
valueToSelection: function(value) {
var item = this.valueToItem(value);
this.$.selection.select(item);
},
setValueSelected: function(value, isSelected) {
var item = this.valueToItem(value);
if (isSelected ^ this.$.selection.isSelected(item)) {
this.$.selection.setItemSelected(item, isSelected);
}
},
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;
},
valueToItem: function(value) {
return (value === null || value === undefined) ?
null : this.items[this.valueToIndex(value)];
},
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 <core-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('core-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);
}
},
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;
}
},
selectIndex: function(index) {
var item = this.items[index];
if (item) {
this.selected = this.valueForNode(item) || index;
return item;
}
},
/**
* Selects the previous item. This should be used in single selection only.
*
* @method selectPrevious
* @param {boolean} wrapped if true and it is already at the first item,
* wrap to the end
* @returns the previous item or undefined if there is none
*/
selectPrevious: function(wrapped) {
var i = wrapped && !this.selectedIndex ?
this.items.length - 1 : this.selectedIndex - 1;
return this.selectIndex(i);
},
/**
* Selects the next item. This should be used in single selection only.
*
* @method selectNext
* @param {boolean} wrapped if true and it is already at the last item,
* wrap to the front
* @returns the next item or undefined if there is none
*/
selectNext: function(wrapped) {
var i = wrapped && this.selectedIndex >= this.items.length - 1 ?
0 : this.selectedIndex + 1;
return this.selectIndex(i);
}
});
</script>
</polymer-element>