blob: 64690336dfd44033c58ba45cb1140103025802b5 [file] [log] [blame]
<!--
`paper-autocomplete` extends `paper-input`, adding autocomplete functionality.
The current value is used to prefix-match against the labels of this element's
child nodes (either `core-item` or `paper-item`). Those that match are shown,
while those that fail to match are hidden. The suggested items can be selected
using keyboard and mouse input.
Example:
<paper-autocomplete label="Country" on-change="{{changeHandler}}">
<core-item label="United States></core-item>
<core-item label="Canada"></core-item>
</paper-autocomplete>
All attributes and events of paper-input are inherited by paper-autocomplete.
New Attributes:
maxItems
caseSensitive
New Events:
none yet
-->
<polymer-element name="paper-autocomplete" extends="paper-input" attributes="maxItems caseSensitive">
<template>
<link href="paper-autocomplete.css" rel="stylesheet">
<div id="frame">
<div id="top-input">
<shadow></shadow>
</div>
<div id="bottom-suggest">
<core-selector id="suggest-box" class="suggest-box-style hidden"
on-core-activate="{{selectAction}}">
<content id="items" select="core-item,paper-item"></content>
</core-selector>
</div>
</div>
</template>
<script>
Polymer({
/**
* Maximum number of items to show in the suggestion box.
* A null value indicates that there is no limit.
*
* @attribute: maxItems
* @type: integer | null
*/
maxItems: null,
/**
* Flag that indicates the case-sensitivity of prefix-matching.
*
* @attribute: caseSensitive
* @type: boolean
*/
caseSensitive: false,
/**
* Whether this element is currently being edited or not. Becomes true
* when the internal paper-input gains focus or receives input.
*
* @type: boolean
*/
editing: false,
/**
* A special flag indicating that the suggestion box is going to close.
*/
closing: false,
/**
* List of indexes of children that can be shown. The children are
* filtered by the current value.
*
* @type: Array<integer>
*/
showing: [],
/**
* Tracks the highlight position in the showing array. The corresponding
* child node is highlighted with the 'highlighted' class.
*
* @type: int | null
*/
highlightIndex: null,
/**
* Called when this node is added to the DOM. Sets up event listeners
* through the shadow DOM to its internal paper-input. Additionally,
* watches for mutations in its light DOM children.
*/
ready: function() {
this.super();
// Add listener to the suggestBox to cancel the suggestion box from
// closing if the user starts to click on it.
var suggestBox = this.$['suggest-box'];
suggestBox.addEventListener(
'mousedown',
this.cancelClosing.bind(this)
);
// Listen to inputs to determine editing status.
this.addEventListener('input', this.inputAction.bind(this));
this.addEventListener('keydown', this.keydownAction.bind(this));
// Capture changes to children
this.watchMutation();
},
/**
* Whenever the light DOM children change, then rerender this element.
*/
watchMutation: function() {
var change = function() {
this.filterSuggestions();
// Note: onMutation unbinds after each use, so we need to rebind.
this.onMutation(this, change.bind(this));
}
this.onMutation(this, change.bind(this));
},
/**
* Wraps the subclass's focus action to set the editing flag to true.
*
* @Override
*/
inputFocusAction: function(e) {
this.super(e);
this.editing = true;
this.filterSuggestions();
},
/**
* Wraps the subclass's blur action to potentially set editing to false.
* Has a delay before the suggestion box is closed in case the user starts
* to click on the suggestion box.
*
* @Override
*/
inputBlurAction: function(e) {
this.super(e);
this.closing = true;
this.async(function() {
if (this.closing) {
this.showSuggestions(false);
}
}, null, 100);
},
/**
* If the mouse is pressed down on the suggestion box, set the closing
* flag to false.
*/
cancelClosing: function(e) {
this.async(function() {
if (this.closing) {
this.closing = false;
}
}, null, 0);
},
/**
* On the hidden input's 'input' event, the fact that data changed means
* that the user is editing. Re-render the suggestions.
* Note: Fires this element's 'input' event.
*/
inputAction: function(e) {
this.editing = true;
this.filterSuggestions();
// Convert a standard Event into the Custom Event for consistency.
if (!(e instanceof CustomEvent)) {
e.stopPropagation();
this.fireInput();
}
},
/**
* Filters the potential list of suggestions drawn from child node labels.
* Use a prefixMatch to decide which child nodes to show/hide.
* Finally, show the suggestion box if the conditions are right.
*/
filterSuggestions: function() {
var showing = [];
var childNodes = this.$.items.getDistributedNodes();
for (var i = 0; i < childNodes.length; i++) {
var item = childNodes[i];
// Remove style if necessary.
if (this.highlightIndex !== i) {
item.classList.toggle('highlighted', false);
}
// Show nodes with a prefix match, as long
// as the maxItems limit has not been reached.
if ((this.maxItems === null || showing.length < this.maxItems) &&
this.prefixMatch(item.label)) {
showing.push(i);
item.style.display = '';
} else {
item.style.display = 'none';
}
}
this.showing = showing;
this.showSuggestions(this.shouldShowSuggestions());
},
/**
* Depending on the caseSensitive attribute, returns whether or not the
* given word has a prefix match with the current value of this element.
*/
prefixMatch: function(word) {
var curValue = this.inputValue;
if (!this.caseSensitive) {
return word.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
}
return word.indexOf(curValue) === 0;
},
/**
* Determine whether or not the suggestBox should be shown.
* Show if this node is being edited and there are valid suggestions.
*/
shouldShowSuggestions: function() {
return this.editing && this.showing.length > 0;
},
/**
* The suggestion box is shown/hidden. If hidden, then additional internal
* values are reset.
*/
showSuggestions: function(shouldShow) {
this.$['suggest-box'].classList.toggle('hidden', !shouldShow);
if (!shouldShow) {
this.highlightReset();
this.editing = false;
this.closing = false;
}
},
/**
* Process the 'core-activate' event when a suggestion was tapped.
*/
selectAction: function(e) {
this.selectItem(e, e.detail.item);
},
/**
* When an item in the suggestion list is clicked or 'enter' is pressed
* while it is being highlighted, then replace this node's value.
*
* This node is no longer being edited, so re-render will hide it.
* Note: Fires this element's 'input' and 'change' events.
* Special: Click loses the element's focus, but pressing enter does not.
*/
selectItem: function(e, item) {
this.highlightReset();
// Set the internal input value and then commit it.
this.inputValue = item.label;
this.commit();
this.fireInput();
this.inputChangeAction(e);
this.showSuggestions(false);
},
/**
* Fire the input event. (The invocation matches core-input's.)
*/
fireInput: function() {
this.fire('input', null, this);
},
/**
* Remove highlight off the current item and stop tracking it.
*/
highlightReset: function() {
this.highlightItem(false);
this.highlightIndex = null;
},
/**
* Add/remove the highlighted class off the currently highlighted item.
*/
highlightItem: function(shouldHighlight) {
if (this.highlightIndex !== null) {
var selected = this.showing[this.highlightIndex];
this.$.items.getDistributedNodes()[selected].classList.toggle(
'highlighted',
shouldHighlight
);
}
},
/**
* Pressing up and down change the currently highlighted item.
* Pressing enter will select that item.
* Pressing escape will close the suggestBox.
* Pressing any other key will reset highlight.
*/
keydownAction: function(e) {
if (this.showing.length === 0) {
return;
}
var KEY_UP = 38;
var KEY_DOWN = 40;
var KEY_ENTER = 13;
var KEY_ESC = 27;
switch(e.keyCode) {
case KEY_UP: // Highlight the item above the current one.
e.preventDefault();
// unhighlight, change selection, highlight
this.highlightItem(false);
if (this.highlightIndex === null || this.highlightIndex === 0) {
this.highlightIndex = this.showing.length - 1;
} else {
this.highlightIndex--;
}
this.highlightItem(true);
break;
case KEY_DOWN: // Highlight the item below the current one.
e.preventDefault();
// unhighlight, change selection, highlight
this.highlightItem(false);
if (this.highlightIndex === null ||
this.highlightIndex === this.showing.length - 1) {
this.highlightIndex = 0;
} else {
this.highlightIndex++;
}
this.highlightItem(true);
break;
case KEY_ENTER: // Activate the highlighted node, if any.
if (this.highlightIndex !== null) {
var highlightIndex = this.showing[this.highlightIndex];
var item = this.$.items.getDistributedNodes()[highlightIndex];
this.selectItem(e, item);
}
break;
case KEY_ESC: // Hide the suggestions and reset the current highlight.
this.showSuggestions(false);
break;
default: // Reset the current highlight.
this.highlightReset();
}
}
});
</script>
</polymer-element>