@skhemata/skhemata-form
Version:
Skhemata Form Web Component. This web component can be used as base web component when working with forms and inputs.
310 lines (274 loc) • 8.68 kB
text/typescript
import { html, css, CSSResult, property } from '@skhemata/skhemata-base';
import { faAngleDown, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@riovir/wc-fontawesome';
import { SkhemataFormInput } from './SkhemataFormInput';
export class SkhemataFormAutocomplete extends SkhemataFormInput {
static get styles() {
return <CSSResult[]>[
...super.styles,
css`
.field {
margin-bottom: 1rem;
}
option {
text-align: left;
padding: 0.5rem 0rem 0.5rem 1rem;
white-space: nowrap;
}
option:hover {
background-color: whitesmoke;
color: rgb(10, 10, 10);
cursor: pointer;
}
option.is-active {
background-color: rgb(50, 115, 220);
color: rgb(255, 255, 255);
}
.results.dropdown-content {
max-height: 15em;
overflow-y: auto;
}
.dropdown,
.dropdown-trigger,
.dropdown button {
width: 100%;
}
.dropdown button span {
margin-right: auto;
}
.selected-dropdown {
color: rgb(54, 54, 54);
}
.dropdown .button {
color: #d0d0d0;
}
#dropdown-menu {
width: 100%;
padding-top: 0px;
}
#dropdown-menu .dropdown-content {
padding: 0;
}
#dropdown-menu .dropdown-content .dropdown-item {
padding: 5px;
}
`,
];
}
static get scopedElements() {
return {
'fa-icon': FontAwesomeIcon,
};
}
description = '';
selected = '';
mapValue = '';
mapLabel = '';
search = '';
results: any = [];
label = '';
menuOpen = false;
required = false;
name = 'name';
placeholder = 'Select One';
errorMessage = 'Select one';
submitOnSelect = false;
valid = true;
helpClass = '';
private _active: number = 0;
get active(): number {
return this._active;
}
set active(value: number) {
const oldValue = this._active;
this._active = value;
this.requestUpdate('active', oldValue);
this.shadowRoot
?.querySelector('.dropdown-item.is-active')
?.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
reset() {
this.clearError();
this.value = '';
this.selected = '';
this.results = [];
}
validate() {
this.helpClass = '';
if (this.required && this.value.length < 1) {
this.valid = false;
this.helpClass = 'is-danger';
this.requestUpdate();
}
this.dispatchEvent(
new CustomEvent('is-valid', {
detail: { valid: this.valid },
bubbles: true,
composed: true,
})
);
}
clearError() {
this.helpClass = '';
this.valid = true;
this.requestUpdate();
}
handleKeydown(event: any) {
if (event.target.value.length < 2) {
this.reset();
} else {
switch (event.keyCode) {
case 13:
this.value = this.results[this.active].value;
this.selected = this.results[this.active].label;
this.menuOpen = false;
break;
case 40:
this.active =
this.active < this.results.length - 1
? this.active + 1
: this.active;
break;
case 38:
this.active = this.active > 0 ? this.active - 1 : this.active;
break;
default:
this.active = 0;
}
}
}
handleInput(event: any) {
this.clearError();
this.setAttribute('search', event.target.value);
if (this.search.length > 2) {
this.getResults();
}
}
handleSelectValue(selected: string, value: any) {
this.clearError();
this.selected = selected;
this.value = value;
this.menuOpen = false;
}
toggleMenu() {
this.menuOpen = !this.menuOpen;
this.requestUpdate();
}
handleFocusOut() {
this.toggleMenu();
this.results = [];
if (!this.selected) {
this.search = '';
}
}
getResults() {
if (this.api?.url) {
fetch(`${this.api.url}/locale/city/${this.search}`)
.then(data => data.json())
.then(results => {
const mapped =
this.mapLabel && this.mapValue
? results.map((result: any) => ({
value: result[this.mapValue],
label: result[this.mapLabel],
}))
: results;
this.results = mapped;
this.requestUpdate();
});
}
}
async firstUpdated() {
// document.addEventListener('click', e=>this.clickOffDropdown(e));
}
render() {
const field = html`
<div class="field">
${this.label && !this.horizontal
? html`<label class="label">${this.label}</label>`
: null}
<div class="control ${this.valid ? '' : 'has-icons-right'}">
${this.description && !this.horizontal
? html`<p>${this.description}</p>`
: null}
<div class="dropdown ${this.menuOpen ? 'is-active' : ''}">
<div class="dropdown-trigger">
<button
class="button"
aria-haspopup="true"
aria-controls="dropdown-menu"
=${this.toggleMenu}
=${(event: any) => {
if (event.keyCode === '13') this.toggleMenu();
}}
>
<span class="${this.selected ? 'selected-dropdown' : ''}">${this.selected || this.placeholder}</span>
<span class="icon is-small">
<fa-icon .icon=${faAngleDown}></fa-icon>
</span>
</button>
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<div class="dropdown-item">
<div class="control has-icons-right">
<input
class="input ${this.valid ? `` : `is-danger`}"
name="${this.name}"
type="text"
placeholder=""
=${this.handleInput}
=${this.handleKeydown}
.value=${this.search}
/>
<span class="icon is-small is-right">
<fa-icon .icon=${faSearch}></fa-icon>
</span>
</div>
</div>
</div>
<div class="dropdown-content results">
${this.results.map(
(result: any, index: number) =>
html`
<option
class="dropdown-item ${index === this.active
? 'is-active'
: ''}"
value=${result.value}
=${() =>
this.handleSelectValue(result.label, result.value)}
=${(e: any) => {
if (e.keyCode === '13')
this.handleSelectValue(result.label, result.value);
}}
>
${result.label}
</option>
`
)}
</div>
</div>
</div>
</div>
${!this.valid
? html`<p class="help ${this.helpClass}">${this.errorMessage}</p>`
: ``}
</div>
`;
const horizontalFieldLabel = html`
<div class="field-label column is-one-quarter" style="text-align: left">
${this.label ? html`<label class="label">${this.label}</label>` : null}
${this.description ? html`<p>${this.description}</p>` : null}
</div>
`;
const horizontalField = html`
<div class="field is-horizontal">
${this.label || this.description ? horizontalFieldLabel : null}
<div class="field-body column">${field}</div>
</div>
`;
return this.horizontal ? horizontalField : field;
}
}