| <!-- |
| |
| `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> |