/**
@license
Copyright (c) 2015 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
*/
import '../polymer/polymer_bundled.min.js';
import {dom} from '../polymer/polymer_bundled.min.js';
import {dashToCamelCase} from '../polymer/polymer_bundled.min.js';
import {IronSelection} from './iron-selection.js';
/**
* @polymerBehavior
*/
export const IronSelectableBehavior = {
/**
* Fired when iron-selector is activated (selected or deselected).
* It is fired before the selected items are changed.
* Cancel the event to abort selection.
*
* @event iron-activate
*/
/**
* Fired when an item is selected
*
* @event iron-select
*/
/**
* Fired when an item is deselected
*
* @event iron-deselect
*/
/**
* Fired when the list of selectable items changes (e.g., items are
* added or removed). The detail of the event is a mutation record that
* describes what changed.
*
* @event iron-items-changed
*/
properties: {
/**
* If you want to use an attribute value or property of an element for
* `selected` instead of the index, set this to the name of the attribute
* or property. Hyphenated values are converted to camel case when used to
* look up the property of a selectable element. Camel cased values are
* *not* converted to hyphenated values for attribute lookup. It's
* recommended that you provide the hyphenated form of the name so that
* selection works in both cases. (Use `attr-or-property-name` instead of
* `attrOrPropertyName`.)
*/
attrForSelected: {type: String, value: null},
/**
* Gets or sets the selected element. The default is to use the index of the
* item.
* @type {string|number}
*/
selected: {type: String, notify: true},
/**
* Returns the currently selected item.
*
* @type {?Object}
*/
selectedItem: {type: Object, readOnly: true, notify: true},
/**
* The event that fires from items when they are selected. Selectable
* will listen for this event from items and update the selection state.
* Set to empty string to listen to no events.
*/
activateEvent:
{type: String, value: 'tap', observer: '_activateEventChanged'},
/**
* This is a CSS selector string. If this is set, only items that match the
* CSS selector are selectable.
*/
selectable: String,
/**
* The class to set on elements when selected.
*/
selectedClass: {type: String, value: 'iron-selected'},
/**
* The attribute to set on elements when selected.
*/
selectedAttribute: {type: String, value: null},
/**
* Default fallback if the selection based on selected with
* `attrForSelected` is not found.
*/
fallbackSelection: {type: String, value: null},
/**
* The list of items from which a selection can be made.
*/
items: {
type: Array,
readOnly: true,
notify: true,
value: function() {
return [];
}
},
/**
* The set of excluded elements where the key is the `localName`
* of the element that will be ignored from the item list.
*
* @default {template: 1}
*/
_excludedLocalNames: {
type: Object,
value: function() {
return {
'template': 1,
'dom-bind': 1,
'dom-if': 1,
'dom-repeat': 1,
};
}
}
},
observers: [
'_updateAttrForSelected(attrForSelected)',
'_updateSelected(selected)',
'_checkFallback(fallbackSelection)'
],
created: function() {
this._bindFilterItem = this._filterItem.bind(this);
this._selection = new IronSelection(this._applySelection.bind(this));
},
attached: function() {
this._observer = this._observeItems(this);
this._addListener(this.activateEvent);
},
detached: function() {
if (this._observer) {
dom(this).unobserveNodes(this._observer);
}
this._removeListener(this.activateEvent);
},
/**
* Returns the index of the given item.
*
* @method indexOf
* @param {Object} item
* @returns Returns the index of the item
*/
indexOf: function(item) {
return this.items ? this.items.indexOf(item) : -1;
},
/**
* Selects the given value.
*
* @method select
* @param {string|number} value the value to select.
*/
select: function(value) {
this.selected = value;
},
/**
* Selects the previous item.
*
* @method selectPrevious
*/
selectPrevious: function() {
var length = this.items.length;
var index = length - 1;
if (this.selected !== undefined) {
index = (Number(this._valueToIndex(this.selected)) - 1 + length) % length;
}
this.selected = this._indexToValue(index);
},
/**
* Selects the next item.
*
* @method selectNext
*/
selectNext: function() {
var index = 0;
if (this.selected !== undefined) {
index =
(Number(this._valueToIndex(this.selected)) + 1) % this.items.length;
}
this.selected = this._indexToValue(index);
},
/**
* Selects the item at the given index.
*
* @method selectIndex
*/
selectIndex: function(index) {
this.select(this._indexToValue(index));
},
/**
* Force a synchronous update of the `items` property.
*
* NOTE: Consider listening for the `iron-items-changed` event to respond to
* updates to the set of selectable items after updates to the DOM list and
* selection state have been made.
*
* WARNING: If you are using this method, you should probably consider an
* alternate approach. Synchronously querying for items is potentially
* slow for many use cases. The `items` property will update asynchronously
* on its own to reflect selectable items in the DOM.
*/
forceSynchronousItemUpdate: function() {
if (this._observer && typeof this._observer.flush === 'function') {
// NOTE(bicknellr): `dom.flush` above is no longer sufficient to trigger
// `observeNodes` callbacks. Polymer 2.x returns an object from
// `observeNodes` with a `flush` that synchronously gives the callback any
// pending MutationRecords (retrieved with `takeRecords`). Any case where
// ShadyDOM flushes were expected to synchronously trigger item updates
// will now require calling `forceSynchronousItemUpdate`.
this._observer.flush();
} else {
this._updateItems();
}
},
// UNUSED, FOR API COMPATIBILITY
get _shouldUpdateSelection() {
return this.selected != null;
},
_checkFallback: function() {
this._updateSelected();
},
_addListener: function(eventName) {
this.listen(this, eventName, '_activateHandler');
},
_removeListener: function(eventName) {
this.unlisten(this, eventName, '_activateHandler');
},
_activateEventChanged: function(eventName, old) {
this._removeListener(old);
this._addListener(eventName);
},
_updateItems: function() {
var nodes = dom(this).queryDistributedElements(this.selectable || '*');
nodes = Array.prototype.filter.call(nodes, this._bindFilterItem);
this._setItems(nodes);
},
_updateAttrForSelected: function() {
if (this.selectedItem) {
this.selected = this._valueForItem(this.selectedItem);
}
},
_updateSelected: function() {
this._selectSelected(this.selected);
},
_selectSelected: function(selected) {
if (!this.items) {
return;
}
var item = this._valueToItem(this.selected);
if (item) {
this._selection.select(item);
} else {
this._selection.clear();
}
// Check for items, since this array is populated only when attached
// Since Number(0) is falsy, explicitly check for undefined
if (this.fallbackSelection && this.items.length &&
(this._selection.get() === undefined)) {
this.selected = this.fallbackSelection;
}
},
_filterItem: function(node) {
return !this._excludedLocalNames[node.localName];
},
_valueToItem: function(value) {
return (value == null) ? null : this.items[this._valueToIndex(value)];
},
_valueToIndex: function(value) {
if (this.attrForSelected) {
for (var i = 0, item; item = this.items[i]; i++) {
if (this._valueForItem(item) == value) {
return i;
}
}
} else {
return Number(value);
}
},
_indexToValue: function(index) {
if (this.attrForSelected) {
var item = this.items[index];
if (item) {
return this._valueForItem(item);
}
} else {
return index;
}
},
_valueForItem: function(item) {
if (!item) {
return null;
}
if (!this.attrForSelected) {
var i = this.indexOf(item);
return i === -1 ? null : i;
}
var propValue = item[dashToCamelCase(this.attrForSelected)];
return propValue != undefined ? propValue :
item.getAttribute(this.attrForSelected);
},
_applySelection: function(item, isSelected) {
if (this.selectedClass) {
this.toggleClass(this.selectedClass, isSelected, item);
}
if (this.selectedAttribute) {
this.toggleAttribute(this.selectedAttribute, isSelected, item);
}
this._selectionChange();
this.fire('iron-' + (isSelected ? 'select' : 'deselect'), {item: item});
},
_selectionChange: function() {
this._setSelectedItem(this._selection.get());
},
// observe items change under the given node.
_observeItems: function(node) {
return dom(node).observeNodes(function(mutation) {
this._updateItems();
this._updateSelected();
// Let other interested parties know about the change so that
// we don't have to recreate mutation observers everywhere.
this.fire(
'iron-items-changed', mutation, {bubbles: false, cancelable: false});
});
},
_activateHandler: function(e) {
var t = e.target;
var items = this.items;
while (t && t != this) {
var i = items.indexOf(t);
if (i >= 0) {
var value = this._indexToValue(i);
this._itemActivate(value, t);
return;
}
t = t.parentNode;
}
},
_itemActivate: function(value, item) {
if (!this.fire('iron-activate', {selected: value, item: item}, {
cancelable: true
})
.defaultPrevented) {
this.select(value);
}
}
};