
- Use the prefix slot to show an optional search icon (used by search bar's gr-autocomplete) - Increase padding on search bar based on designs - In order for autocomplete dropdown to position correctly, allow a custom vertical offset parameter (that defaults to previous value) - Remove the 'Search' button itself, as material design searches do not include a button. Change-Id: Ia7405cb12b6d89d386397fa5a17ac4ca36948a33 (cherry picked from commit Ia7405cb12b6d89d386397fa5a17ac4ca36948a33)
386 lines
9.8 KiB
JavaScript
386 lines
9.8 KiB
JavaScript
// Copyright (C) 2016 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
(function() {
|
|
'use strict';
|
|
|
|
const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
|
|
|
|
Polymer({
|
|
is: 'gr-autocomplete',
|
|
|
|
/**
|
|
* Fired when a value is chosen.
|
|
*
|
|
* @event commit
|
|
*/
|
|
|
|
/**
|
|
* Fired when the user cancels.
|
|
*
|
|
* @event cancel
|
|
*/
|
|
|
|
/**
|
|
* Fired on keydown to allow for custom hooks into autocomplete textbox
|
|
* behavior.
|
|
*
|
|
* @event input-keydown
|
|
*/
|
|
|
|
properties: {
|
|
|
|
/**
|
|
* Query for requesting autocomplete suggestions. The function should
|
|
* accept the input as a string parameter and return a promise. The
|
|
* promise should yield an array of suggestion objects with "name" and
|
|
* "value" properties. The "name" property will be displayed in the
|
|
* suggestion entry. The "value" property will be emitted if that
|
|
* suggestion is selected.
|
|
*
|
|
* @type {function(string): Promise<?>}
|
|
*/
|
|
query: {
|
|
type: Function,
|
|
value() {
|
|
return function() {
|
|
return Promise.resolve([]);
|
|
};
|
|
},
|
|
},
|
|
|
|
/**
|
|
* The number of characters that must be typed before suggestions are
|
|
* made. If threshold is zero, default suggestions are enabled.
|
|
*/
|
|
threshold: {
|
|
type: Number,
|
|
value: 1,
|
|
},
|
|
|
|
allowNonSuggestedValues: Boolean,
|
|
borderless: Boolean,
|
|
disabled: Boolean,
|
|
showSearchIcon: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
// Vertical offset needed for a 1em font-size with no vertical padding.
|
|
// Inputs with additional padding will need to increase vertical offset.
|
|
verticalOffset: {
|
|
type: Number,
|
|
value: 20,
|
|
},
|
|
|
|
text: {
|
|
type: String,
|
|
value: '',
|
|
observer: '_updateSuggestions',
|
|
notify: true,
|
|
},
|
|
|
|
placeholder: String,
|
|
|
|
clearOnCommit: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/**
|
|
* When true, tab key autocompletes but does not fire the commit event.
|
|
* When false, tab key not caught, and focus is removed from the element.
|
|
* See Issue 4556, Issue 6645.
|
|
*/
|
|
tabComplete: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
value: String,
|
|
|
|
/**
|
|
* Multi mode appends autocompleted entries to the value.
|
|
* If false, autocompleted entries replace value.
|
|
*/
|
|
multi: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/**
|
|
* When true and uncommitted text is left in the autocomplete input after
|
|
* blurring, the text will appear red.
|
|
*/
|
|
warnUncommitted: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/** @type {?} */
|
|
_suggestions: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
|
|
_suggestionEls: {
|
|
type: Array,
|
|
value() { return []; },
|
|
},
|
|
|
|
_index: Number,
|
|
_disableSuggestions: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
_focused: {
|
|
type: Boolean,
|
|
value: false,
|
|
},
|
|
|
|
/** The DOM element of the selected suggestion. */
|
|
_selected: Object,
|
|
},
|
|
|
|
observers: [
|
|
'_maybeOpenDropdown(_suggestions, _focused)',
|
|
],
|
|
|
|
attached() {
|
|
this.listen(document.body, 'tap', '_handleBodyTap');
|
|
},
|
|
|
|
detached() {
|
|
this.unlisten(document.body, 'tap', '_handleBodyTap');
|
|
},
|
|
|
|
get focusStart() {
|
|
return this.$.input;
|
|
},
|
|
|
|
focus() {
|
|
this.$.input.focus();
|
|
},
|
|
|
|
selectAll() {
|
|
const nativeInputElement = this.$.input.inputElement;
|
|
nativeInputElement.setSelectionRange(0, this.$.input.value.length);
|
|
},
|
|
|
|
clear() {
|
|
this.text = '';
|
|
},
|
|
|
|
_handleItemSelect(e) {
|
|
// Let _handleKeydown deal with keyboard interaction.
|
|
if (e.detail.trigger !== 'tap') { return; }
|
|
this._selected = e.detail.selected;
|
|
this._commit();
|
|
},
|
|
|
|
get _inputElement() {
|
|
return this.$.input;
|
|
},
|
|
|
|
/**
|
|
* Set the text of the input without triggering the suggestion dropdown.
|
|
* @param {string} text The new text for the input.
|
|
*/
|
|
setText(text) {
|
|
this._disableSuggestions = true;
|
|
this.text = text;
|
|
this._disableSuggestions = false;
|
|
},
|
|
|
|
_onInputFocus() {
|
|
this._focused = true;
|
|
this._updateSuggestions();
|
|
this.$.input.classList.remove('warnUncommitted');
|
|
// Needed so that --paper-input-container-input updated style is applied.
|
|
this.updateStyles();
|
|
},
|
|
|
|
_onInputBlur() {
|
|
this.$.input.classList.toggle('warnUncommitted',
|
|
this.warnUncommitted && this.text.length && !this._focused);
|
|
// Needed so that --paper-input-container-input updated style is applied.
|
|
this.updateStyles();
|
|
},
|
|
|
|
_updateSuggestions() {
|
|
if (this._disableSuggestions) { return; }
|
|
if (this.text === undefined || this.text.length < this.threshold) {
|
|
this._suggestions = [];
|
|
this.value = '';
|
|
return;
|
|
}
|
|
const text = this.text;
|
|
|
|
this.query(text).then(suggestions => {
|
|
if (text !== this.text) {
|
|
// Late response.
|
|
return;
|
|
}
|
|
for (const suggestion of suggestions) {
|
|
suggestion.text = suggestion.name;
|
|
}
|
|
this._suggestions = suggestions;
|
|
Polymer.dom.flush();
|
|
if (this._index === -1) {
|
|
this.value = '';
|
|
}
|
|
});
|
|
},
|
|
|
|
_maybeOpenDropdown(suggestions, focused) {
|
|
if (suggestions.length > 0 && focused) {
|
|
return this.$.suggestions.open();
|
|
}
|
|
return this.$.suggestions.close();
|
|
},
|
|
|
|
_computeClass(borderless) {
|
|
return borderless ? 'borderless' : '';
|
|
},
|
|
|
|
/**
|
|
* _handleKeydown used for key handling in the this.$.input AND all child
|
|
* autocomplete options.
|
|
*/
|
|
_handleKeydown(e) {
|
|
this._focused = true;
|
|
switch (e.keyCode) {
|
|
case 38: // Up
|
|
e.preventDefault();
|
|
this.$.suggestions.cursorUp();
|
|
break;
|
|
case 40: // Down
|
|
e.preventDefault();
|
|
this.$.suggestions.cursorDown();
|
|
break;
|
|
case 27: // Escape
|
|
e.preventDefault();
|
|
this._cancel();
|
|
break;
|
|
case 9: // Tab
|
|
if (this._suggestions.length > 0 && this.tabComplete) {
|
|
e.preventDefault();
|
|
this._handleInputCommit(true);
|
|
this.focus();
|
|
} else {
|
|
this._focused = false;
|
|
}
|
|
break;
|
|
case 13: // Enter
|
|
e.preventDefault();
|
|
this._handleInputCommit();
|
|
break;
|
|
default:
|
|
// For any normal keypress, return focus to the input to allow for
|
|
// unbroken user input.
|
|
this.$.input.focus();
|
|
}
|
|
this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input});
|
|
},
|
|
|
|
_cancel() {
|
|
if (this._suggestions.length) {
|
|
this.set('_suggestions', []);
|
|
} else {
|
|
this.fire('cancel');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* @param {boolean=} opt_tabComplete
|
|
*/
|
|
_handleInputCommit(opt_tabComplete) {
|
|
// Nothing to do if the dropdown is not open.
|
|
if (!this.allowNonSuggestedValues
|
|
&& this.$.suggestions.isHidden) { return; }
|
|
|
|
this._selected = this.$.suggestions.getCursorTarget();
|
|
this._commit(opt_tabComplete);
|
|
},
|
|
|
|
_updateValue(suggestion, suggestions) {
|
|
if (!suggestion) { return; }
|
|
const completed = suggestions[suggestion.dataset.index].value;
|
|
if (this.multi) {
|
|
// Append the completed text to the end of the string.
|
|
// Allow spaces within quoted terms.
|
|
const tokens = this.text.match(TOKENIZE_REGEX);
|
|
tokens[tokens.length - 1] = completed;
|
|
this.value = tokens.join(' ');
|
|
} else {
|
|
this.value = completed;
|
|
}
|
|
},
|
|
|
|
_handleBodyTap(e) {
|
|
const eventPath = Polymer.dom(e).path;
|
|
for (let i = 0; i < eventPath.length; i++) {
|
|
if (eventPath[i] === this) {
|
|
return;
|
|
}
|
|
}
|
|
this._focused = false;
|
|
},
|
|
|
|
_handleSuggestionTap(e) {
|
|
e.stopPropagation();
|
|
this.$.cursor.setCursor(e.target);
|
|
this._commit();
|
|
},
|
|
|
|
/**
|
|
* Commits the suggestion, optionally firing the commit event.
|
|
*
|
|
* @param {boolean=} opt_silent Allows for silent committing of an
|
|
* autocomplete suggestion in order to handle cases like tab-to-complete
|
|
* without firing the commit event.
|
|
*/
|
|
_commit(opt_silent) {
|
|
// Allow values that are not in suggestion list iff suggestions are empty.
|
|
if (this._suggestions.length > 0) {
|
|
this._updateValue(this._selected, this._suggestions);
|
|
} else {
|
|
this.value = this.text || '';
|
|
}
|
|
|
|
const value = this.value;
|
|
|
|
// Value and text are mirrors of each other in multi mode.
|
|
if (this.multi) {
|
|
this.setText(this.value);
|
|
} else {
|
|
if (!this.clearOnCommit && this._selected) {
|
|
this.setText(this._suggestions[this._selected.dataset.index].name);
|
|
} else {
|
|
this.clear();
|
|
}
|
|
}
|
|
|
|
this._suggestions = [];
|
|
if (!opt_silent) {
|
|
this.fire('commit', {value});
|
|
}
|
|
|
|
this._textChangedSinceCommit = false;
|
|
},
|
|
|
|
_computeShowSearchIconClass(showSearchIcon) {
|
|
return showSearchIcon ? 'showSearchIcon' : '';
|
|
},
|
|
});
|
|
})();
|