@unicef-polymer/etools-unicef
Version:
eTools UNICEF library of reusable components
1,167 lines (1,155 loc) • 48.4 kB
JavaScript
import { __decorate } from "tslib";
import { html, LitElement } from 'lit';
import '@shoelace-style/shoelace/dist/components/menu-item/menu-item.js';
import '@shoelace-style/shoelace/dist/components/menu/menu.js';
import '@shoelace-style/shoelace/dist/components/input/input.js';
import '@shoelace-style/shoelace/dist/components/tag/tag.js';
import '@shoelace-style/shoelace/dist/components/spinner/spinner.js';
import '@shoelace-style/shoelace/dist/components/popup/popup.js';
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js';
import '../etools-button/etools-button';
import '../etools-icons/etools-icon';
import styles from './styles/sl-autocomplete-styles';
import etoolsStyles from './styles/sl-autocomplete-etools-styles';
import { styleMap } from 'lit/directives/style-map.js';
import { classMap } from 'lit/directives/class-map.js';
import { property, query, state } from 'lit/decorators.js';
import { getTranslation } from './utils/translate';
import { callClickOnEnterPushListener } from '@unicef-polymer/etools-utils/dist/accessibility.util';
import { ifDefined } from 'lit/directives/if-defined.js';
/**
* @summary Advanced dropdown capable of searching and filtering options,
* get data dynamically, scroll by key typing etc.
*
* @since unreleased
* @status unknown
*
* @dependency sl-menu
* @dependency sl-menu-item
* @dependency sl-input
* @dependency sl-tag
* @dependency sl-button
* @dependency sl-spinner
*
*
*/
export class SlAutocomplete extends LitElement {
get selected() {
var _a;
return ((_a = this.selectedValues) === null || _a === void 0 ? void 0 : _a[0]) || null;
}
set selected(value) {
var _a;
this.selectedValues = value !== undefined && value !== null ? [value] : [];
// sl-select is not fired when selected is set through binding
// timeout to wait for selectedItems to be set
if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.length) {
setTimeout(() => this.setSelectedValues());
}
}
get selectedItem() {
var _a;
return (_a = this.selectedItems) === null || _a === void 0 ? void 0 : _a[0];
}
set selectedItem(value) {
this.selectedItems = value ? [value] : [];
}
get open() {
return this._open;
}
set open(value) {
var _a, _b, _c;
if (this.readonly || this.disabled) {
return;
}
this._open = value;
if (this._open) {
this.addOpenListeners();
this.enableInfiniteScroll();
(_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('sl-menu').addEventListener('keydown', this.handleKeyDown);
const parentDialog = this.getParentDialog();
if (parentDialog) {
if (!this.boundary) {
// console.warn('Missing boundary for dropdown in dialog', this);
this.hoist = true;
}
}
setTimeout(() => {
var _a, _b;
if (!this.hideSearch) {
this.searchInput.focus({ preventScroll: true });
}
else {
const selItem = this.shadowRoot.querySelector('sl-menu-item[checked]');
if (selItem && !this.multiple) {
selItem.focus();
}
else {
(_b = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('sl-menu-item')) === null || _b === void 0 ? void 0 : _b.focus({ preventScroll: true });
}
}
}, 0);
}
if (!this._open) {
this.removeOpenListeners();
this.disableInfiniteScroll();
(_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('sl-menu').removeEventListener('keydown', this.handleKeyDown);
if (!this.hideSearch) {
(_c = this.searchInput) === null || _c === void 0 ? void 0 : _c.blur();
if (!this.preserveSearchOnClose) {
setTimeout(() => {
this.search = '';
});
}
}
if (this._autoValidate) {
this.validate();
}
}
this.triggerPopupOpenEvent(this._open);
}
render() {
var _a, _b, _c, _d;
const hasHelpText = !!this.helpText;
const hasClearIcon = this.clearable && this.multiple && !this.disabled && !this.readonly && this.selectedValueCommaList.length > 0;
const isPlaceholderVisible = this.placeholder && this.selectedValueCommaList.length === 0;
// this.filteredOptions should be called only once otherwise it breaks pagination.
const options = (_a = this.filteredOptions) === null || _a === void 0 ? void 0 : _a.slice(0, this.totalOptionsToShow);
return html `
<style>
:host {
width: 100%;
}
sl-popup {
${this.maxWidth ? `--auto-size-available-width: ${this.maxWidth}` : ''}
${this.maxHeight ? `--auto-size-available-height: ${this.maxHeight}` : ''}
}
.dropdown {
min-width: ${this.minWidth};
min-height: ${this.minHeight};
}
</style>
<div
part="form-control"
class=${classMap({
'form-control': true,
'form-control--small': this.size === 'small',
'form-control--medium': this.size === 'medium',
'form-control--large': this.size === 'large',
'form-control--has-label': !this.noLabelFloat,
'form-control--has-help-text': hasHelpText
})}
>
<label
id="label"
part="form-control-label"
class=${classMap({
'form-control__label': true,
'form-control__focused': this.open
})}
aria-hidden=${this.label ? 'false' : 'true'}
>
<slot name="label">${this.label || html ` `}</slot>
</label>
<div part="form-control-input" class="form-control-input">
<sl-popup
class=${classMap({
select: true,
'select--standard': true,
'select--filled': this.filled,
'select--pill': this.pill,
'select--open': this.open,
'select--disabled': this.disabled,
'select--readonly': this.readonly,
'select--invalid': this.invalid,
'select--multiple': this.multiple,
'select--focused': this.hasFocus,
'select--placeholder-visible': isPlaceholderVisible,
'select--top': this.placement === 'top',
'select--bottom': this.placement === 'bottom',
'select--small': this.size === 'small',
'select--medium': this.size === 'medium',
'select--large': this.size === 'large',
'select--transparent': this.transparent
})}
placement=${this.placement}
strategy=${this.hoist ? 'fixed' : 'absolute'}
flip
shift
sync="${ifDefined(this.syncWidth ? 'width' : undefined)}"
?active="${this.open}"
auto-size="vertical"
auto-size-padding="10"
.autoSizeBoundary="${this.boundary}"
.shiftBoundary="${this.boundary}"
.flipBoundary="${this.boundary}"
>
<div
part="combobox"
slot="anchor"
tabindex="${this.readonly ? '-1' : '0'}"
class="select__combobox"
title="${this.selectedLabels.replaceAll(',', ' | ')}"
@mousedown="${this.handleComboboxMouseDown}"
>
<slot part="prefix" name="prefix" class="select__prefix"></slot>
<div
part="display-input"
class="select__display-input"
?is-placeholder=${!this.selectedLabels && this.placeholder}
?disabled=${this.disabled}
?invalid=${this.invalid}
value=${this.selectedLabels}
aria-label="${this.label || this.placeholder || this.id || this.selectedLabels || 'dropdown value'}"
readonly
aria-controls="listbox"
aria-expanded=${this.open ? 'true' : 'false'}
aria-haspopup="listbox"
aria-labelledby="label"
aria-disabled=${this.disabled ? 'true' : 'false'}
aria-readonly=${this.readonly ? 'true' : 'false'}
aria-describedby="help-text"
role="combobox"
tabindex="-1"
>
<div class="outer-text">
<div class="inner-text">${this.selectedLabels || this.placeholder}</div>
</div>
</div>
${this.multiple && ((_b = this.selectedItems) === null || _b === void 0 ? void 0 : _b.length)
? html `
<div part="tags" class="select__tags">
${(_c = this.selectedItems) === null || _c === void 0 ? void 0 : _c.map((option, index) => {
if (index < this.maxOptionsVisible || this.maxOptionsVisible <= 0) {
return html `
<sl-tag
part="tag"
exportparts="
base:tag__base,
content:tag__content,
remove-button:tag__remove-button,
remove-button__base:tag__remove-button__base
"
?pill=${this.pill}
size=${this.size}
?removable=${!this.disabled && !this.readonly && !option.disabled}
@mousedown=${this.handleTagMouseDown}
@sl-remove=${() => this.handleTagRemove(option)}
>
${Object.hasOwn(option, 'itemTemplate') ? option.itemTemplate : option[this.optionLabel]}
</sl-tag>
`;
}
else if (index === this.maxOptionsVisible) {
return html ` <sl-tag size=${this.size}> +${this.selectedItems.length - index} </sl-tag> `;
}
else {
return null;
}
})}
</div>
`
: ''}
<input
class="select__value-input"
type="text"
?disabled=${this.disabled}
?readonly=${this.readonly}
?invalid=${this.invalid}
?required=${this.required}
.value=${this.selectedValueCommaList}
tabindex="-1"
aria-hidden="true"
/>
${hasClearIcon
? html `
<button
part="clear-button"
class="select__clear"
type="button"
aria-label="clear all"
tabindex="-1"
>
<slot name="clear-icon">
<etools-icon
name="cancel"
@mousedown=${this.handleClearMouseDown}
@click=${this.handleClearClick}
></etools-icon>
</slot>
</button>
`
: ''}
<slot name="expand-icon" part="expand-icon" class="select__expand-icon">
<etools-icon name="${this.expandIcon}"></etools-icon>
</slot>
</div>
<div class="dropdown">
<div part="search" id="search" class="search" ?hidden="${this.hideSearch}">
<sl-input
role="presentation"
placeholder=${this.searchPlaceholder || getTranslation(this.language, 'SEARCH')}
.value="${this.search}"
@sl-input=${this.handleSearchChanged}
autocomplete="off"
>
<etools-icon name="search" slot="prefix"></etools-icon>
</sl-input>
</div>
<div
role="list"
aria-expanded=${this.open ? 'true' : 'false'}
aria-multiselectable=${this.multiple ? 'true' : 'false'}
aria-labelledby="label"
part="list"
class="list select__list"
>
<sl-menu>
${
// We need to add it like this instead of hidden because sl-menu adds tabindex="0"
// dynamically to first sl-menu-item and this break tab navigation if hidden
this.enableNoneOption
? html `<sl-menu-item
type="checkbox"
class="noneOption"
@click="${this.preventDeselectByClick}"
@keydown="${this.preventDeselectByEnter}"
?checked=${!((_d = this.selectedItems) === null || _d === void 0 ? void 0 : _d.length)}
value=""
title="${this.noneOptionLabel || getTranslation(this.language, 'NONE')}"
>
${this.noneOptionLabel || getTranslation(this.language, 'NONE')}
</sl-menu-item>`
: ''}
${options === null || options === void 0 ? void 0 : options.map((option) => html `
${option.disabled && option.disabledTooltip
? html `<sl-tooltip hoist content="${option.disabledTooltip}"
>${this.renderMenuItem(option)}</sl-tooltip
>`
: this.renderMenuItem(option)}
`)}
<sl-menu-item
disabled
part="loading-text"
id="loading-text"
class="loading-text"
aria-hidden=${this.loading ? 'false' : 'true'}
?hidden=${!this.loading}
>
<sl-spinner></sl-spinner>
<slot name="loading-text"> ${this.loadingText || getTranslation(this.language, 'LOADING')} </slot>
</sl-menu-item>
<sl-menu-item
disabled
part="no-options-available-text"
id="no-options-available-text"
class="no-options-available-text"
aria-hidden=${!this.noOptionsAvailable ? 'false' : 'true'}
?hidden=${!this.noOptionsAvailable}
>
<slot name="no-options-available-text">
${this.noOptionsAvailableText || getTranslation(this.language, 'NO_OPTIONS_AVAILABLE')}
</slot>
</sl-menu-item>
<sl-menu-item
disabled
part="no-results-text"
id="no-results-text"
class="no-results-text"
aria-hidden=${!this.showNoSearchResultsWarning(options.length) ? 'false' : 'true'}
?hidden=${!this.showNoSearchResultsWarning(options.length)}
>
<slot name="no-results-text">
${this.noResultsText || getTranslation(this.language, 'NO_RESULTS_FOUND_TRY_OTHER_KEYWORDS')}
</slot>
</sl-menu-item>
<div class="infinite-scroll-trigger" ?hidden=${this.loading || this.noMoreItemsToLoad}></div>
</sl-menu>
<div aria-hidden="true" style=${styleMap({ width: `${this.clientWidth}px` })}></div>
</div>
<div class="footer" ?hidden="${!this.multiple || this.hideClose}">
<etools-button id="closeBtn" size="small" variant="text" @click="${() => this.hide()}">
${getTranslation(this.language, 'CLOSE')}
</etools-button>
</div>
</div>
</sl-popup>
</div>
<div class="invalid-message" ?visible=${this.invalid && this.errorMessage}>${this.errorMessage}</div>
</div>
`;
}
constructor() {
super();
this.hasFocus = false;
this.loading = false;
this._open = false;
this.search = '';
this.totalOptionsToShow = 0;
this.language = '';
this.page = 0;
this.prevPage = 0;
this.prevSearch = '';
this.searchHasChanged = false;
this.pageHasChanged = false;
this.noMoreItemsToLoad = false;
this.collectingKeyboardKeysTimeout = undefined;
this.collectedKeyboardKeys = '';
this.placeholder = '—';
this.searchPlaceholder = '';
this.optionValue = 'value';
this.optionLabel = 'label';
this.errorMessage = '';
this.disabled = false;
this.readonly = false;
this.invalid = false;
this.noOptionsAvailableText = '';
this.noResultsText = '';
this.loadingText = '';
this.options = [];
this.multiple = false;
this.maxOptionsVisible = 0;
this.pill = false;
this.size = 'medium';
this.filled = false;
this.placement = 'bottom-start';
this.clearable = true;
this.hoist = false;
this.hideSearch = false;
this.preserveSearchOnClose = false;
this.hideClose = false;
this.enableNoneOption = false;
this.noneOptionLabel = '';
this.shownOptionsLimit = 30;
this.capitalize = false;
this.transparent = false;
this.minWidth = '30px';
this.maxWidth = '';
this.minHeight = '0px';
this.maxHeight = '';
this.syncWidth = true;
/**
* The container relative to which the autosize clipping and shifting of the dropdown occurs.
* Expected value is either a DOM element reference or an array of DOM element references.
*
* EX: document
* .querySelector('app-shell')
* ?.shadowRoot?.querySelector('app-drawer-layout')
* ?.querySelector('app-header-layout')
* ?.querySelector('main')
*
* SL-POUP uses float-ui(https://floating-ui.com/) behind the scences in case we don't provide
* a value for this field it tries to find the clipping and boundary ancetors by it's self.
* This feature does not work when we are using the hoisted dropdown (position: fixed) and
* in this case we have to provide a boundary manually.
*/
this.boundary = undefined;
this.selectedItems = [];
this.selectedValues = [];
// Enable autoValidate only after first focus on input
this._autoValidate = false;
this.expandIcon = 'expand-more'; // arrow-drop-down
if (!this.language) {
this.language = window.EtoolsLanguage || 'en';
}
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
this.handleLanguageChange = this.handleLanguageChange.bind(this);
this.setSelectedOption = this.setSelectedOption.bind(this);
this.handleParentFocus = this.handleParentFocus.bind(this);
this.handleFocusOut = this.handleFocusOut.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
}
connectedCallback() {
var _a;
super.connectedCallback();
this.addEventListener('sl-select', this.setSelectedOption);
this.addEventListener('focusin', this.handleParentFocus);
this.addEventListener('focusout', this.handleFocusOut);
document.addEventListener('language-changed', this.handleLanguageChange);
if (this.multiple) {
callClickOnEnterPushListener((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('#closeBtn'));
}
}
preventDeselectByEnter(e) {
if (!this.multiple && e.key === 'Enter' && e.currentTarget.hasAttribute('checked')) {
e.stopImmediatePropagation();
}
}
renderMenuItem(option) {
return html `<sl-menu-item
type="checkbox"
?checked=${this.isSelected(option)}
value="${option[this.optionValue]}"
tabindex="0"
@click="${this.preventDeselectByClick}"
@keydown="${this.preventDeselectByEnter}"
title="${option[this.optionLabel]}"
disabled="${ifDefined(option.disabled ? true : undefined)}"
>
${Object.hasOwn(option, 'itemTemplate') ? option.itemTemplate : option[this.optionLabel]}
</sl-menu-item>`;
}
preventDeselectByClick(e) {
if (!this.multiple && e.currentTarget.hasAttribute('checked')) {
e.stopImmediatePropagation();
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.removeEventListener('sl-select', this.setSelectedOption);
this.removeEventListener('focusin', this.handleParentFocus);
this.removeEventListener('focusout', this.handleFocusOut);
document.removeEventListener('language-changed', this.handleLanguageChange);
}
updated(changedProperties) {
var _a;
if (changedProperties.has('options') || changedProperties.has('selectedValues')) {
const strSelectedVals = this.selectedValues ? (_a = this.selectedValues) === null || _a === void 0 ? void 0 : _a.map((v) => String(v)) : this.selectedValues;
this.selectedItems = (this.options || []).filter((o) => strSelectedVals === null || strSelectedVals === void 0 ? void 0 : strSelectedVals.includes(String(o[this.optionValue])));
}
if (changedProperties.has('shownOptionsLimit')) {
this.totalOptionsToShow = this.shownOptionsLimit;
}
}
handleKeyDown(e) {
// TO BE DEVELOPED FURTHER
if (this.collectingKeyboardKeysTimeout) {
clearTimeout(this.collectingKeyboardKeysTimeout);
}
if (e.key.match(/(\w|\s)/g) && e.key.length === 1) {
this.collectedKeyboardKeys += e.key;
}
this.collectingKeyboardKeysTimeout = setTimeout(() => {
var _a;
if (this.collectedKeyboardKeys) {
const list = (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('sl-menu');
const foundItems = Array.from((list === null || list === void 0 ? void 0 : list.querySelectorAll('sl-menu-item')) || []).filter((item) => {
var _a;
const text = (_a = item.textContent) === null || _a === void 0 ? void 0 : _a.replace(/(\r\n|\n|\r)/gm, '').trim();
return text === null || text === void 0 ? void 0 : text.toLowerCase().startsWith(this.collectedKeyboardKeys.toLowerCase());
});
// If already selected one with same starting letters then move to next found if any available
let itemToFocusIndex = 0;
const oneElementHasFocusIndex = foundItems.findIndex((item) => item.tabIndex === 0);
if (oneElementHasFocusIndex > -1 && oneElementHasFocusIndex < foundItems.length - 1) {
itemToFocusIndex = oneElementHasFocusIndex + 1;
}
if (foundItems === null || foundItems === void 0 ? void 0 : foundItems[itemToFocusIndex]) {
// According to shoelace the order of calling this is important
list.setCurrentItem(foundItems === null || foundItems === void 0 ? void 0 : foundItems[itemToFocusIndex]);
foundItems === null || foundItems === void 0 ? void 0 : foundItems[itemToFocusIndex].focus({ preventScroll: !(list.scrollHeight > list.clientHeight) });
}
}
this.collectedKeyboardKeys = '';
}, 250);
}
handleParentFocus(event) {
const path = event.composedPath();
const isIconButton = path.some((el) => el instanceof Element && el.tagName.toLowerCase() === 'etools-icon-button');
if (this.disabled || this.readonly || isIconButton) {
return;
}
if (this.autoValidate) {
this._autoValidate = true;
}
if (!this.open) {
this.show();
}
}
handleFocusOut(event) {
const path = event.composedPath();
if (this && !path.includes(this)) {
event.stopImmediatePropagation();
this.hide();
}
}
/**
* Handle language change
*/
handleLanguageChange(e) {
this.language = e.detail.language;
}
/**
* Register document event listeners
*/
addOpenListeners() {
document.addEventListener('focusin', this.handleDocumentFocusIn);
document.addEventListener('mousedown', this.handleDocumentMouseDown);
}
removeOpenListeners() {
document.removeEventListener('focusin', this.handleDocumentFocusIn);
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
}
/**
* Dropdown input mouse down handler. Responsible to open the dropdown popup on input click
*/
handleComboboxMouseDown(event) {
const path = event.composedPath();
const isIconButton = path.some((el) => el instanceof Element && el.tagName.toLowerCase() === 'etools-icon-button');
if (this.disabled || this.readonly || isIconButton) {
return;
}
event.preventDefault();
this.open = !this.open;
}
/**
* Document Mouse Down handler function. On document mouse down it is hiding the dropdown popup
* @param event MouseEvent
*/
handleDocumentMouseDown(event) {
// Close when clicking outside of the select
const path = event.composedPath();
if (this && !path.includes(this)) {
this.hide();
}
}
/**
* Document Focus In handler function. On document focus in it is hiding the dropdown popup
* @param event MouseEvent
*/
handleDocumentFocusIn(event) {
const path = event.composedPath();
if (this && !path.includes(this)) {
this.hide();
}
}
/**
* Clear all click handler function.Will clear entire selection
* @param event MouseEvent
*/
handleClearClick(event) {
event.stopPropagation();
this.clearSelection();
}
/**
* Clear all mouse down handler function. It is used to stop propagation to the elements
* @param event MouseEvent
*/
handleClearMouseDown(event) {
event.stopPropagation();
event.preventDefault();
}
/**
* Tag remove handler function
* @param option - Item that need to be removed
*/
handleTagRemove(option) {
const itemSelectedAtIndex = this.selectedItems.findIndex((x) => (x === null || x === void 0 ? void 0 : x[this.optionValue].toString()) === option[this.optionValue].toString());
if (itemSelectedAtIndex >= 0) {
const itemToBeRemoved = this.selectedItems[itemSelectedAtIndex];
this.selectedItems.splice(itemSelectedAtIndex, 1);
this.setSelectedValues();
this.triggerRemovedOptionsEvent([itemToBeRemoved]);
}
}
/**
* Clear all mouse down handler function. It is used to stop propagation to the elements
* @param event MouseEvent
*/
handleTagMouseDown(event) {
// Prevent open dialog in case we click on X, works together with pointer-events: none on sl-tag.
// If pointer-events: none is removed from sl-tag this will prevent dialog popup to open on click of tag
// as well.
event.stopPropagation();
event.preventDefault();
}
/**
* Trigger remove option event
* @param item - Item that has been removed
*/
triggerRemovedOptionsEvent(item) {
this.dispatchEvent(new CustomEvent('removed-selected-items', {
detail: { value: item },
bubbles: true,
composed: true
}));
}
/**
* Trigger popup open event
* @param boolean - If dialog is opened or closed
*/
triggerPopupOpenEvent(opened) {
if (opened) {
this.dispatchEvent(new CustomEvent('dropdown-opened', {
detail: { value: opened },
bubbles: true,
composed: true
}));
}
if (!opened) {
this.dispatchEvent(new CustomEvent('dropdown-closed', {
detail: { value: opened },
bubbles: true,
composed: true
}));
}
}
/**
* Search change handler function
* @param e SlInputEvent
*/
handleSearchChanged(e) {
var _a;
this.search = (_a = e.target) === null || _a === void 0 ? void 0 : _a.value;
this.totalOptionsToShow = this.shownOptionsLimit;
this.noMoreItemsToLoad = false;
this.enableInfiniteScroll();
this.dispatchEvent(new CustomEvent('search-changed', {
detail: { value: this.search },
bubbles: true,
composed: true
}));
}
/**
* Getter used to return selected options values as a comma separated list.
*/
get selectedValueCommaList() {
var _a;
return ((_a = this.selectedValues) === null || _a === void 0 ? void 0 : _a.join(',')) || '';
}
/**
* Getter used to return select options labels used to display in the select input display
*/
get selectedLabels() {
var _a;
return ((_a = this.selectedItems) === null || _a === void 0 ? void 0 : _a.map((x) => x === null || x === void 0 ? void 0 : x[this.optionLabel]).join(',')) || '';
}
/**
* Getter to return the list of options to show in the dropdown.
* It is responsible to make loadDataMethod function call if defined and
* to filter the options based on the search value
*/
get filteredOptions() {
var _a;
if (typeof this.loadDataMethod === 'function') {
return this.loadOptionsData(this.options, this.search, this.loadDataMethod);
}
if (this.search) {
return ((_a = this.options) === null || _a === void 0 ? void 0 : _a.filter(this.itemContainsSearchString.bind(this))) || [];
}
return this.options || [];
}
get noOptionsAvailable() {
var _a;
return !this.loading && !((_a = this.options) === null || _a === void 0 ? void 0 : _a.length);
}
showNoSearchResultsWarning(totalFilteredItems = 0) {
if (this.noOptionsAvailable) {
return false;
}
return this.options && this.options.length > 0 && totalFilteredItems === 0;
}
/**
* Set selected options. Has logic to resolve multiple selections and single selection
* @param option Option that has been selected
*/
setSelectedOption(e) {
const { detail: { item } } = e;
if (!this.selectedItems) {
this.selectedItems = [];
}
if (!this.selectedValues) {
this.selectedValues = [];
}
if (item.classList.contains('noneOption')) {
this.selectedItems = [];
}
else {
const selectedItem = this.options.find((x) => x[this.optionValue].toString() === item.value.toString());
if (selectedItem) {
const itemSelectedAtIndex = this.selectedItems.findIndex((x) => (x === null || x === void 0 ? void 0 : x[this.optionValue].toString()) === selectedItem[this.optionValue].toString());
if (itemSelectedAtIndex >= 0) {
if (this.multiple) {
this.selectedItems.splice(itemSelectedAtIndex, 1);
}
}
else {
if (this.multiple) {
this.selectedItems = [...this.selectedItems, selectedItem];
}
else {
this.selectedItems = [selectedItem];
}
}
}
}
this.setSelectedValues();
if (!this.multiple) {
this.hide();
}
}
/**
* Set selected values using 'optionValue' from selectedItems and
* triggers and also selection-changed event
*/
setSelectedValues() {
var _a, _b;
this.selectedValues = (this.selectedItems || []).map((x) => x === null || x === void 0 ? void 0 : x[this.optionValue]);
if (this._autoValidate) {
this.validate();
}
this.dispatchEvent(new CustomEvent('selection-changed', {
detail: { value: this.multiple ? this.selectedItems : ((_a = this.selectedItems) === null || _a === void 0 ? void 0 : _a[0]) || undefined },
bubbles: true,
composed: true
}));
this.dispatchEvent(new CustomEvent('etools-selected-item-changed', {
detail: { selectedItem: ((_b = this.selectedItems) === null || _b === void 0 ? void 0 : _b[0]) || undefined },
bubbles: true,
composed: true
}));
this.dispatchEvent(new CustomEvent('etools-selected-items-changed', {
detail: { selectedItems: this.selectedItems },
bubbles: true,
composed: true
}));
// FOR POLYMER SUPORT
this.dispatchEvent(new CustomEvent('selected-changed', {
detail: { value: this.selected },
bubbles: true,
composed: true
}));
this.dispatchEvent(new CustomEvent('selected-values-changed', {
detail: { value: this.selectedValues },
bubbles: true,
composed: true
}));
}
/**
* Clears selected options
*/
clearSelection() {
// filter out selected disabled items, this items should not be removable if they came like this from backend
const itemsToBeRemoved = [...this.selectedItems.filter((x) => !x.disabled)];
// keeps disabled items selected in case of clear all
this.selectedItems = [...this.selectedItems.filter((x) => !!x.disabled)];
this.setSelectedValues();
this.triggerRemovedOptionsEvent(itemsToBeRemoved);
}
/**
* Hide dropdown popup.
*/
show() {
this.open = true;
}
/**
* Show dropdown popup
*/
hide() {
this.open = false;
}
/**
* Function to check if a specific option has been selected.
* It is checking if it is available in selectedItems by matching 'optionValue'
* @param option - The option to check if it has been selected
* @returns
*/
isSelected(option) {
var _a;
return (((_a = this.selectedItems) === null || _a === void 0 ? void 0 : _a.findIndex((x) => { var _a, _b; return ((_a = x === null || x === void 0 ? void 0 : x[this.optionValue]) === null || _a === void 0 ? void 0 : _a.toString()) === ((_b = option[this.optionValue]) === null || _b === void 0 ? void 0 : _b.toString()); })) >
-1);
}
/**
* Validate dropdown selection
* @param selected
* @returns {boolean}
*/
validate() {
if (!this.hasAttribute('required') || this.hasAttribute('readonly')) {
this.invalid = false;
return true;
}
this.invalid = !this.selectedValueCommaList.length;
return !this.invalid;
}
/**
* Reset invalid state
*/
resetInvalidState() {
this.invalid = false;
return this.invalid;
}
/**
* Responsbile to call loadDataMethod function if has been provided in order to fetch data directly from server
* and to directly filter & search via API Requests
* @param options - List of options currently available
* @param search - Search value
* @param loadDataMethod - The load data method to call
* @returns Existing options or empty list depending on some specific cases
*/
loadOptionsData(options, search, loadDataMethod) {
if (search != this.prevSearch && this.totalOptionsToShow !== this.shownOptionsLimit) {
// if search changed reset _shownOptionsCount in order to load the first page for the new search
this.totalOptionsToShow = this.shownOptionsLimit;
return [];
}
this.page = this.totalOptionsToShow / this.shownOptionsLimit || 1;
if (this.noMoreItemsToLoad) {
return options || [];
}
if (search != this.prevSearch || this.page !== this.prevPage) {
this.loading = true;
this.searchHasChanged = this.prevSearch !== search;
this.pageHasChanged = this.page !== this.prevPage;
this.prevSearch = search;
this.prevPage = this.page;
const loadDataMethodReturn = loadDataMethod(this.search, this.page, this.shownOptionsLimit);
// if it is a promise then we try to catch. If it returns error then most probably there is no more items to
// load (the case were total items equals exactly limit * page)
if (loadDataMethodReturn &&
typeof loadDataMethodReturn === 'object' &&
typeof loadDataMethodReturn.then === 'function') {
loadDataMethodReturn.catch(() => {
this.loading = false;
this.noMoreItemsToLoad = true;
});
}
if (this.searchHasChanged) {
// if search is changed we return nothing as options to be shown, options (if any) will be set in loadDataMethod
return [];
}
if (this.pageHasChanged) {
// if page changed return current options so we don't have an empty list until request finishes
return options || [];
}
}
if (this.options !== undefined) {
if (this.searchHasChanged) {
this.searchHasChanged = false;
this.loading = false;
}
else if (this.pageHasChanged) {
this.pageHasChanged = false;
this.loading = false;
if (options.length && options.length < this.totalOptionsToShow) {
this.noMoreItemsToLoad = true;
}
}
return options || [];
}
return [];
}
/**
* Checks if an options contains a search string by first converting the 'optionLabel' and search value to lowercase
* @param item - Item to check against
* @returns Boolean
*/
itemContainsSearchString(item) {
return (item[this.optionLabel] && item[this.optionLabel].toString().toLowerCase().indexOf(this.search.toLowerCase()) > -1);
}
/**
* Function to disable infinite scroll functionality
*/
disableInfiniteScroll() {
if (this.observerInfiniteScroll) {
this.observerInfiniteScroll.disconnect();
this.observerInfiniteScroll = undefined;
}
}
/**
* Function to enable infinite scroll functionality by adding a intersection observer.
*/
enableInfiniteScroll() {
var _a, _b;
this.disableInfiniteScroll();
var options = {
root: (_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('.listInnerWrapper'),
treshold: 1.0
};
this.observerInfiniteScroll = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
this.showMoreOptions();
}
});
}, options);
this.observerInfiniteScroll.observe((_b = this.shadowRoot) === null || _b === void 0 ? void 0 : _b.querySelector('.infinite-scroll-trigger'));
}
/**
* Function to set the number of options to show in dropdown based on the shown options limit and by
* the infinite scroll trigger. Total options to show will increase when the list reaches the end of list
* @returns
*/
showMoreOptions() {
if (!this.options || !this.options.length) {
this.totalOptionsToShow = this.shownOptionsLimit;
return;
}
// If we are not using loadDataMethod. we disable infinite scroll by total number options
if (typeof this.loadDataMethod !== 'function' && this.options.length < this.totalOptionsToShow) {
this.noMoreItemsToLoad = true;
}
this.totalOptionsToShow += this.shownOptionsLimit;
}
/**
* Checks if this dropdown is a child of etools-dialog and returns the etools-dialog element reference if any.
*/
getParentDialog() {
var _a, _b;
return (_b = ((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.getRootNode()).host) === null || _b === void 0 ? void 0 : _b.closest('etools-dialog');
}
}
SlAutocomplete.styles = [styles, etoolsStyles];
__decorate([
query('sl-input')
], SlAutocomplete.prototype, "searchInput", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "hasFocus", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "loading", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "_open", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "search", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "totalOptionsToShow", void 0);
__decorate([
state()
], SlAutocomplete.prototype, "language", void 0);
__decorate([
property({ type: String, attribute: 'label' })
], SlAutocomplete.prototype, "label", void 0);
__decorate([
property({ type: Boolean, attribute: 'no-label-float' })
], SlAutocomplete.prototype, "noLabelFloat", void 0);
__decorate([
property({ type: String, attribute: 'placeholder' })
], SlAutocomplete.prototype, "placeholder", void 0);
__decorate([
property({ type: String, attribute: 'search-placeholder' })
], SlAutocomplete.prototype, "searchPlaceholder", void 0);
__decorate([
property({ type: String, attribute: 'option-value' })
], SlAutocomplete.prototype, "optionValue", void 0);
__decorate([
property({ type: String, attribute: 'option-label' })
], SlAutocomplete.prototype, "optionLabel", void 0);
__decorate([
property({ type: Boolean, attribute: 'required', reflect: true })
], SlAutocomplete.prototype, "required", void 0);
__decorate([
property({ type: String, attribute: 'error-message' })
], SlAutocomplete.prototype, "errorMessage", void 0);
__decorate([
property({ type: Boolean, attribute: 'disabled', reflect: true })
], SlAutocomplete.prototype, "disabled", void 0);
__decorate([
property({ type: Boolean, attribute: 'readonly', reflect: true })
], SlAutocomplete.prototype, "readonly", void 0);
__decorate([
property({ type: Boolean, attribute: 'invalid', reflect: true })
], SlAutocomplete.prototype, "invalid", void 0);
__decorate([
property({ type: String, attribute: 'no-options-available-text' })
], SlAutocomplete.prototype, "noOptionsAvailableText", void 0);
__decorate([
property({ type: String, attribute: 'no-results-text' })
], SlAutocomplete.prototype, "noResultsText", void 0);
__decorate([
property({ type: String, attribute: 'loading-text' })
], SlAutocomplete.prototype, "loadingText", void 0);
__decorate([
property({ type: Number })
], SlAutocomplete.prototype, "options", void 0);
__decorate([
property({ type: Object })
], SlAutocomplete.prototype, "loadDataMethod", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'multiple' })
], SlAutocomplete.prototype, "multiple", void 0);
__decorate([
property({ type: Boolean, attribute: 'max-options-available' })
], SlAutocomplete.prototype, "maxOptionsVisible", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'pill' })
], SlAutocomplete.prototype, "pill", void 0);
__decorate([
property({ type: String, reflect: true, attribute: 'size' })
], SlAutocomplete.prototype, "size", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'filled' })
], SlAutocomplete.prototype, "filled", void 0);
__decorate([
property({ type: String, reflect: true, attribute: 'placement' })
], SlAutocomplete.prototype, "placement", void 0);
__decorate([
property({ type: String, reflect: true, attribute: 'help-text' })
], SlAutocomplete.prototype, "helpText", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'clearable' })
], SlAutocomplete.prototype, "clearable", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'hoist' })
], SlAutocomplete.prototype, "hoist", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'hide-search' })
], SlAutocomplete.prototype, "hideSearch", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'preserve-search-on-close' })
], SlAutocomplete.prototype, "preserveSearchOnClose", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'hide-close' })
], SlAutocomplete.prototype, "hideClose", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'enable-none-option' })
], SlAutocomplete.prototype, "enableNoneOption", void 0);
__decorate([
property({ type: String, attribute: 'none-option-label' })
], SlAutocomplete.prototype, "noneOptionLabel", void 0);
__decorate([
property({ type: Number, attribute: 'shown-options-limit' })
], SlAutocomplete.prototype, "shownOptionsLimit", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'capitalize' })
], SlAutocomplete.prototype, "capitalize", void 0);
__decorate([
property({ type: Boolean, reflect: true, attribute: 'transparent' })
], SlAutocomplete.prototype, "transparent", void 0);
__decorate([
property({ type: String, attribute: 'min-width' })
], SlAutocomplete.prototype, "minWidth", void 0);
__decorate([
property({ type: String, attribute: 'max-width' })
], SlAutocomplete.prototype, "maxWidth", void 0);
__decorate([
property({ type: String, attribute: 'max-height' })
], SlAutocomplete.prototype, "minHeight", void 0);
__decorate([
property({ type: String, attribute: 'max-height' })
], SlAutocomplete.prototype, "maxHeight", void 0);
__decorate([
property({ type: String, attribute: 'sync-width' })
], SlAutocomplete.prototype, "syncWidth", void 0);
__decorate([
property({ type: Object, attribute: 'boundary' })
], SlAutocomplete.prototype, "boundary", void 0);
__decorate([
property({ type: Array })
], SlAutocomplete.prototype, "selectedItems", void 0);
__decorate([
property({ type: Array })
], SlAutocomplete.prototype, "selectedValues", void 0);
__decorate([
property({ type: Boolean, attribute: 'auto-validate' })
], SlAutocomplete.prototype, "autoValidate", void 0);
__decorate([
property({ type: Boolean })
], SlAutocomplete.prototype, "_autoValidate", void 0);
__decorate([
property({ type: Boolean, attribute: 'expand-icon' })
], SlAutocomplete.prototype, "expandIcon", void 0);
__decorate([
property({ type: String })
], SlAutocomplete.prototype, "selected", null);
__decorate([
property({ type: String })
], SlAutocomplete.prototype, "selectedItem", null);
__decorate([
property({ type: Boolean })
], SlAutocomplete.prototype, "open", null);