blob: b09b70ac39b95aa8bf71fdf903f3cec4ef77077b [file] [log] [blame]
<!--
This in an HTML Import-able file that contains the definition
of the following elements:
<autocomplete-input-sk>
To use this file import it:
<link href="/res/imp/autocomplete-input-sk.html" rel="import" />
Usage:
<autocomplete-input-sk></autocomplete-input-sk>
Properties:
autocomplete: Array<String> values for auto-completing.
heading: String, the heading to display
value: String Any typed-in value
display-options-on-focus: Boolean, show all matching values during focus
accept-custom-value: Boolean, can choose a value not from suggestions
Methods:
None.
Events:
change: Fires when the value stored in the autocomplete-input-sk changes.
-->
<link rel="import" href="/res/imp/bower_components/iron-icons/iron-icons.html">
<link rel="import" href="/res/imp/bower_components/paper-listbox/paper-listbox.html">
<link rel="import" href="/res/imp/bower_components/paper-input/paper-input.html">
<link rel="import" href="/res/imp/bower_components/paper-item/paper-item.html">
<link rel="import" href="/res/common/imp/url-params-sk.html">
<dom-module id="autocomplete-input-sk">
<template>
<style>
#suggestions_div {
border: 1px solid black;
position: absolute;
z-index: 1;
}
</style>
<!--
This element just wraps a paper-input. We trap the on-change events because
we only want to fire that event when the user has typed or accepted one of
the autocomplete suggestions. Every key press occurring when the paper-input
has focus results in a call to _keyup. We treat some key presses specially;
up and down arrow cause different autocomplete suggestions to be
highlighted, and the enter key accepts a suggestion when one is highlighted.
Otherwise, we recompute the suggestions and display them. When the user
accepts a suggestion by clicking it, hitting enter when it is highlighted,
or simply by typing it out and unfocusing the paper-input, we call _commit,
which fires the on-change event.
-->
<paper-input id="input" label="[[label]]"
on-change="_change"
on-keyup="_keyup"
on-focus="_onFocus"
on-blur="_keyup"
></paper-input>
<div id="suggestions_div" hidden$="{{_show_suggestions(_suggestions.*)}}">
<paper-listbox id="suggestions_box" on-iron-select="_suggestion_changed">
<template is="dom-repeat" items="[[_suggestions]]">
<paper-item class="suggestion" value="{{item}}">{{item}}</paper-item>
</template>
</paper-listbox>
</div>
</template>
<script>
(function(){
var DOWN_ARROW = 40;
var UP_ARROW = 38;
var ENTER = 13;
Polymer({
is:"autocomplete-input-sk",
properties: {
autocomplete: {
type: Array,
},
label: {
type: String,
},
value: {
type: String,
value: "",
notify: true,
observer: "_value_changed",
},
displayOptionsOnFocus: {
type: Boolean,
value: false,
},
acceptCustomValue: {
type: Boolean,
value: false,
},
_arrow_key_pressed: {
type: Boolean,
value: false,
},
_committing: {
type: Boolean,
value: false,
},
_suggestions: {
type: Array,
value: function() {
return [];
},
},
},
_onFocus: function(e) {
if (this.displayOptionsOnFocus && (
this.value === '' || this.value === undefined)) {
this.value = '';
this._keyup(e);
}
},
_value_changed: function() {
// This event fires when we set this.value or when our parent sets it.
// In the latter case, we need to propagate the change down to our
// child paper-input.
if (this._committing) {
// The event fired because _commit() was called. Don't propagate the
// change or we'll get an infinite loop.
this._committing = false;
} else {
// Our parent set this.value. Propagate the change.
this.$.input.value = this.value;
}
},
_change: function(e) {
// This event fires when the user "finishes" typing in the text box.
// This occurs when the enter key is pressed, or when the text box
// loses focus. We only want our parent to receive a change event when
// the user has finished typing, so we need to handle the case in which
// this event fires because the user clicked one of the suggestions.
e.preventDefault();
e.stopPropagation();
var v = this.$.input.value;
if (this.autocomplete && this.autocomplete.indexOf(v) === -1) {
// If we're autocompleting, don't fire the change event unless we
// chose a suggested value.
} else {
this._commit()
}
},
_commit: function() {
// The user has "finished" typing. This occurs when the user clicks
// a suggestion, or, once the user has typed out an exact match to one
// of the suggestions, they hit the enter key or click out of the text
// box. Set this.value and trigger a "change" event.
this._committing = true;
this._suggestions = [];
this.$.suggestions_box.selected = -1;
this.value = this.$.input.value;
this.fire("change", {"value": this.value});
},
_keyup: function(e) {
if (e.type === "blur") {
// Ignore the blur event if it was caused by clicking a suggestion.
var blurredElem = e.relatedTarget;
if (!blurredElem) {
blurredElem = e.detail.sourceEvent.relatedTarget;
}
if (blurredElem && blurredElem.classList.contains("suggestion")) {
return;
}
}
if (this.autocomplete) {
// Allow the user to scroll through suggestions using arrow keys.
if (e.keyCode === DOWN_ARROW && this._suggestions.length > 0) {
this._arrow_key_pressed = true;
this.$.suggestions_box.selected = this.$.suggestions_box.selected + 1;
} else if (e.keyCode === UP_ARROW && this._suggestions.length > 0) {
this._arrow_key_pressed = true;
this.$.suggestions_box.selected = this.$.suggestions_box.selected - 1;
} else if (e.type === "blur" || e.keyCode === ENTER) {
if (this.$.suggestions_box.selected > -1) {
// If the enter key is pressed while one of the suggestions is
// highlighted, we accept that selection.
this.$.input.value = this.$.suggestions_box.selectedItem.value;
this._commit();
} else if (this.autocomplete.includes(this.$.input.value)) {
// The user manually typed one of the acceptable values.
this._commit();
} else if (this.acceptCustomValue) {
// If the enter key is pressed without highlighting a suggestion
// then accept the current input value.
this._commit();
} else {
// User entered an invalid selection, and custom values are not
// allowed.
this.$.input.value = "";
this._commit();
}
} else {
// The user has entered text. Recompute autocomplete suggestions.
var v = this.$.input.value;
this._suggestions = [];
// Give suggestions on empty input only if displayOptionsOnFocus
// is true.
if (v != "" || this.displayOptionsOnFocus) {
// If possible, treat partially-entered input as a regular
// expression.
var re;
try {
re = new RegExp(v, "i"); // case-insensitive.
} catch (e) {
// If the user enters an invalid expression, just use substring
// match.
re = new Object();
re.test = function(str) {
return str.indexOf(v) != -1;
};
}
// Include anything in the autocomplete list which matches the
// text in the box.
for (var i = 0; i < this.autocomplete.length; ++i) {
if (re.test(this.autocomplete[i])) {
this.push("_suggestions", this.autocomplete[i]);
}
}
}
// If the user types an exact match to one of the suggestions,
// remove the suggestions box.
if (this._suggestions.length === 1 && this._suggestions[0] === v) {
this._suggestions = [];
}
// Ensure that no suggestions are selected after recomputing.
this.$.suggestions_box.selected = -1;
}
}
},
_suggestion_changed: function() {
if (this._arrow_key_pressed) {
// This event fires while we're arrowing through the options. We don't
// want to accept a suggestion while this is happening, so ignore the
// event in that case.
this._arrow_key_pressed = false;
} else {
// The user has clicked on a suggestion. Accept it.
var selected = this.$.suggestions_box.selectedItem.value;
this.$.input.value = selected;
this._commit();
}
},
_show_suggestions: function() {
return !(this._suggestions && this._suggestions.length > 0);
},
});
})()
</script>
</dom-module>