@teipublisher/pb-components
Version:
Collection of webcomponents underlying TEI Publisher
275 lines (254 loc) • 9.88 kB
JavaScript
import { LitElement, html, css } from 'lit-element';
import { pbMixin, waitOnce } from './pb-mixin.js';
import { registry } from "./urls.js";
import { translate } from "./pb-i18n.js";
import '@polymer/paper-input/paper-input.js';
import '@polymer/paper-checkbox';
import '@polymer/iron-ajax';
import '@polymer/iron-form';
import '@polymer/paper-button';
import '@polymer/iron-icon';
import '@cwmr/paper-autocomplete';
/**
* Implements a basic search form, which can be extended with additional inputs.
*
* @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
* @slot - beforeInput renders content before the actual search input field
* @slot - searchButton allows to plug a component that acts as submit button. Must support the 'click' event
* @slot - resetButton allows to plug a component that acts as reset button. Must support the 'click' event
* @fires pb-load - Fired to perform the actual search with parameters passed to the request
* @fires pb-paginate - When received, triggers the search again with the new value of the start property
* @fires pb-search-resubmit - When received, triggers the search again
*/
export class PbSearch extends pbMixin(LitElement) {
static get properties() {
return {
...super.properties,
action: {
type: String
},
name: {
type: String
},
value: {
type: String
},
start: {
type: Number
},
placeHolder: {
type: String,
attribute: 'place-holder'
},
redirect: {
type: Boolean
},
currentDoc: {
type: String,
attribute: 'current-doc'
},
submitOnLoad: {
type: Boolean,
attribute: 'submit-on-load'
},
subforms: {
type: String
},
disableAutocomplete: {
type: Boolean,
attribute: 'disable-autocomplete'
},
/**
* 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-search` or subforms will also be
* appended to the request as parameters. This allows the server side code to distinguish
* different states.
*/
source: {
type: String
}
};
}
constructor() {
super();
this.name = 'query';
this.value = '';
this.redirect = false;
this.submitOnLoad = false;
this.placeHolder = 'search.placeholder';
this.disableAutocomplete = false;
this.start = 1;
}
connectedCallback() {
super.connectedCallback();
this.subscribeTo('pb-search-resubmit', this._doSearch.bind(this));
this.subscribeTo('pb-paginate', (ev) => {
this.start = ev.detail.params.start;
this._doSearch(true);
});
registry.subscribe(this, (state) => {
this.value = state.query || '';
this.start = state.start || 1;
if (this.submitOnLoad) {
this.emitTo('pb-load', {
"url": this.action,
"params": state
});
}
});
}
firstUpdated() {
if (!this.disableAutocomplete) {
const autocomplete = this.shadowRoot.getElementById('autocomplete');
autocomplete.addEventListener('autocomplete-change', this._autocomplete.bind(this));
}
const ironform = this.shadowRoot.getElementById('ironform');
ironform.addEventListener('iron-form-response', (event) =>
event.detail.completes.then((r) => this.emitTo('pb-search', r.parseResponse()))
);
waitOnce('pb-page-ready', (options) => {
const loader = this.shadowRoot.getElementById('autocompleteLoader');
const url = this.source || "api/search/autocomplete";
if (this.minApiVersion('1.0.0')) {
loader.url = `${options.endpoint}/${url}`;
} else {
loader.url = `${options.endpoint}/modules/autocomplete.xql`;
}
});
if (this.submitOnLoad) {
const params = registry.state;
registry.replace(this, params);
this.emitTo('pb-load', {
"url": this.action,
"params": params
});
}
this.addEventListener('click', (e) => {
const root = e.target.closest('[slot]');
if (!root) {
return;
}
if (root.slot === 'searchButton'){
this._doSearch();
}
if (root.slot === 'resetButton'){
this._reset();
}
});
}
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>
<iron-form id="ironform" allow-redirect="${this.redirect}">
<form id="searchPageForm" method="get" action="${this.action}" accept="text/html">
<slot name="beforeInput"></slot>
<paper-input id="search" type="search" name="query" @keyup="${this._handleEnter}" label="${translate(this.placeHolder)}"
value="${this.value}" always-float-label>
<iron-icon icon="search" @click="${this._doSearch}" slot="prefix"></iron-icon>
</paper-input>
<paper-autocomplete-suggestions id="autocomplete" for="search" source="${this._suggestions}" remote-source></paper-autocomplete-suggestions>
<slot></slot>
<slot name="searchButton"></slot>
<slot name="resetButton"></slot>
</form>
</iron-form>
<iron-ajax
id="autocompleteLoader"
verbose
handle-as="json"
method="get"
with-credentials
@response="${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));
}
a{
padding:1rem;
color:var(--pb-reset-color);
}
.buttons{
margin-top:1rem;
}
form {
margin: 0;
}
`;
}
_doSearch(pagination = false) {
let json = this.shadowRoot.getElementById('ironform').serializeForm();
json = this._paramsFromSubforms(json);
// remove unnecessary param added by autocomplete
delete json['autocomplete-custom-template'];
// always start on first result after submitting new search
json.start = pagination ? this.start : 1;
if (this.redirect) {
window.location.href = `${this.action}?${new URLSearchParams(json)}`;
} else {
registry.commit(this, json);
this.emitTo('pb-load', {
"url": this.action,
"params": json
});
}
}
_paramsFromSubforms(params) {
if (this.subforms) {
document.querySelectorAll(this.subforms).forEach((form) => {
if (form.serializeForm) {
Object.assign(params, form.serializeForm());
}
});
}
return params;
}
_handleEnter(e) {
if (e.keyCode === 13) {
this._doSearch();
}
}
_doSubmit() {
this.shadowRoot.getElementById('ironform').submit();
}
_reset(){
this.shadowRoot.getElementById('ironform').reset();
}
_autocomplete(ev) {
const params = this.shadowRoot.getElementById('ironform').serializeForm();
const loader = this.shadowRoot.getElementById('autocompleteLoader');
loader.params = params;
loader.generateRequest();
}
_updateSuggestions() {
const autocomplete = this.shadowRoot.getElementById('autocomplete');
const loader = this.shadowRoot.getElementById('autocompleteLoader');
if (loader.lastResponse) {
autocomplete.suggestions(loader.lastResponse);
}
}
}
customElements.define('pb-search', PbSearch);