@teipublisher/pb-components
Version:
Collection of webcomponents underlying TEI Publisher
348 lines (320 loc) • 10.2 kB
JavaScript
import { LitElement, html, css } from 'lit-element';
import { pbMixin, waitOnce } from './pb-mixin';
import { translate } from './pb-i18n';
import '@polymer/paper-input/paper-input.js';
import '@polymer/iron-ajax';
import '@polymer/iron-icon';
import '@cwmr/paper-autocomplete/paper-autocomplete-suggestions.js';
function _query(datasource, query) {
const queryResult = [];
datasource.forEach(item => {
let objText;
let objValue;
if (typeof item === 'object') {
objText = item.text;
objValue = item.value;
} else {
objText = item.toString();
objValue = objText;
}
if (objText.toLowerCase().indexOf(query) > -1) {
// NOTE: the structure of the result object matches with the current template. For custom templates, you
// might need to return more data
const resultItem = {};
resultItem.text = objText;
resultItem.value = objValue;
queryResult.push(resultItem);
}
});
return queryResult;
}
/**
* Provides an input with attached autocomplete. The autocomplete suggestions can be read
* either from a static list or a remote endpoint to which the current user input is sent.
*
* @cssprop --pb-search-label-color - Color of the label and underline
* @cssprop --pb-search-input-color - Text color for input field
* @cssprop --pb-search-focus-color - Color for label and underline if input has focus
* @cssprop --pb-search-suggestions-color - Color for the labels shown in the suggestions dropdown
* @cssprop --pb-search-suggestions-background - Background for the suggestions dropdown
* @slot - default unnamed slot
*/
export class PbAutocomplete extends pbMixin(LitElement) {
static get properties() {
return {
...super.properties,
/**
* Name of the form field which will be submitted
*/
name: {
type: String,
},
/**
* Value of the form field which will be submitted
*/
value: {
type: String,
},
/**
* Placeholder to display if field is empty
*/
placeholder: {
type: String,
attribute: 'placeholder',
},
/**
* Optional URL to query for suggestions. If relative, it is interpreted
* relative to the endpoint defined on a surrounding `pb-page`.
*
* Upon autocomplete, the current input by the user will be sent with a query parameter
* `query`. The name/values of form controls nested within `pb-autocomplete` will also be
* appended to the request as parameters. This allows the server side code to distinguish
* different states.
*/
source: {
type: String,
},
/**
* If set, the entire list of possible suggestions will be preloaded upon initialization of the
* component.
*/
preload: {
type: Boolean,
},
/**
* A static list of suggestions. Use instead of `source`. May either be a flat array of strings,
* or an array containing objects of the form `{"text": "", "value": ""}, in which case "value" denotes
* the value to be used when the enclosing form is submitted, and "text" is the label to be displayed.
*/
suggestions: {
type: Array,
},
/**
* By default suggestions are filtered by prefix, i.e. only suggestions starting with the prefix
* typed by the user are shown. Set this property to true to search for the user-provided string
* anywhere within the suggestion text.
*/
substring: {
type: Boolean,
},
/**
* An icon to display next to the input.
*/
icon: {
type: String,
},
};
}
constructor() {
super();
this.placeholder = 'search.placeholder';
this.suggestions = [];
this.lastSelected = null;
this.preload = false;
this.substring = false;
this._hiddenInput = null;
this._initialized = false;
}
connectedCallback() {
super.connectedCallback();
}
firstUpdated() {
const inIronForm = this.closest('iron-form,pb-search,pb-custom-form');
if (!inIronForm) {
this._hiddenInput = document.createElement('input');
this._hiddenInput.type = 'hidden';
this._hiddenInput.name = this.name;
this.appendChild(this._hiddenInput);
}
const autocomplete = this.shadowRoot.getElementById('autocomplete');
autocomplete.addEventListener('autocomplete-change', this._autocomplete.bind(this));
if (this.preload && this.source) {
if (this.substring) {
autocomplete.queryFn = _query;
}
waitOnce('pb-page-ready', () => {
this._sendRequest();
});
} else if (this.value) {
if (this.source) {
waitOnce('pb-page-ready', () => {
// console.log('send autocomplete request for remote source %s on value %s', this.source, this.value);
this._sendRequest(this.value);
});
} else {
const input = this.shadowRoot.getElementById('search');
const value = this.suggestions.find(suggestion => {
if (suggestion.text) {
return suggestion.value === this.value;
}
return suggestion === this.value;
});
if (value) {
input.value = value.text || value;
if (this._hiddenInput) {
this._hiddenInput.value = value.value || value;
}
}
if (this._hiddenInput) {
this._hiddenInput.value = this.value;
}
}
}
}
render() {
return html`
<custom-style>
<style>
:host {
--suggestions-item: {
color: var(--pb-search-suggestions-color, black);
}
--suggestions-wrapper: {
background: var(--pb-search-suggestions-background, white);
}
}
</style>
</custom-style>
<slot></slot>
<paper-input
id="search"
type="search"
name="query"
="${this._setInput}"
label="${translate(this.placeholder)}"
always-float-label
>
${this.icon ? html`<iron-icon icon="${this.icon}" slot="prefix"></iron-icon>` : null}
</paper-input>
<paper-autocomplete-suggestions
id="autocomplete"
for="search"
.source="${this.suggestions}"
?remote-source="${!this.preload && this.source}"
-selected="${this._autocompleteSelected}"
></paper-autocomplete-suggestions>
<iron-ajax
id="autocompleteLoader"
verbose
handle-as="json"
method="get"
with-credentials
="${this._updateSuggestions}"
></iron-ajax>
`;
}
static get styles() {
return css`
:host {
--paper-input-container-color: var(--pb-search-label-color, var(--paper-grey-500, #303030));
--paper-input-container-input-color: var(
--pb-search-input-color,
var(--pb-color-primary, #000000)
);
--paper-input-container-focus-color: var(
--pb-search-focus-color,
var(--paper-grey-500, #303030)
);
display: flex;
align-items: center;
}
::slotted {
display: block;
margin-left: 10px;
}
`;
}
_autocomplete(ev) {
const search = this.shadowRoot.getElementById('search');
this._sendRequest(search.value);
}
_sendRequest(query) {
const loader = this.shadowRoot.getElementById('autocompleteLoader');
loader.url = this.toAbsoluteURL(this.source);
const params = this._getParameters();
params.query = query;
loader.params = params;
// console.log('send request for %s with %o', loaderId, params);
loader.generateRequest();
}
_updateSuggestions() {
const loader = this.shadowRoot.getElementById('autocompleteLoader');
if (this._initialized) {
const autocomplete = this.shadowRoot.getElementById('autocomplete');
if (loader.lastResponse) {
this.suggestions = loader.lastResponse;
autocomplete.suggestions(this.suggestions);
}
} else if (loader.lastResponse) {
const suggestions = loader.lastResponse;
const input = this.shadowRoot.getElementById('search');
const value = suggestions.find(suggestion => {
if (suggestion.text) {
return suggestion.value === this.value;
}
return suggestion === this.value;
});
if (value) {
input.value = value.text || value;
if (this._hiddenInput) {
this._hiddenInput.value = value.value || value;
}
} else if (this._hiddenInput) {
this._hiddenInput.value = this.value;
}
if (this.preload) {
this.suggestions = suggestions;
}
}
this._initialized = true;
}
_getParameters() {
const params = {};
const inputs = this.querySelectorAll('[name]');
inputs.forEach(input => {
params[input.name] = input.value;
});
return params;
}
_autocompleteSelected(ev) {
const { text, value } = ev.detail;
this.lastSelected = text;
const input = this.shadowRoot.getElementById('search');
input.value = text;
this.value = value;
if (this._hiddenInput) {
this._hiddenInput.value = this.value;
}
this.emitTo('pb-autocomplete-selected', { text, value });
}
_setInput(ev) {
const input = this.shadowRoot.getElementById('search');
this.value = input.value;
if (this._hiddenInput) {
this._hiddenInput.value = this.value;
}
if (ev.keyCode === 13) {
const entry = this.suggestions.find(suggestion => {
if (suggestion.text) {
return suggestion.value === this.value;
}
return suggestion === this.value;
});
if (!entry) {
return;
}
if (entry.value) {
this.emitTo('pb-autocomplete-selected', {
text: entry.text,
value: entry.value,
});
} else {
this.emitTo('pb-autocomplete-selected', {
text: entry,
value: entry,
});
}
}
}
}
customElements.define('pb-autocomplete', PbAutocomplete);