@metadev/lux
Version:
Lux: Library with User Interface components for Angular.
910 lines (905 loc) • 255 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, ChangeDetectorRef, EventEmitter, forwardRef, Input, Output, ViewChild, Component, Injectable, HostBinding, ElementRef, HostListener, ApplicationRef, Injector, RendererFactory2, TemplateRef, ComponentFactoryResolver, Directive, Renderer2, InjectionToken, PLATFORM_ID, NgModule } from '@angular/core';
import * as i1 from '@angular/forms';
import { FormsModule, NG_VALUE_ACCESSOR, NG_VALIDATORS, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, first, map, filter, takeUntil, withLatestFrom, distinctUntilChanged, switchMap } from 'rxjs/operators';
import * as i1$2 from '@angular/common';
import { DOCUMENT, CommonModule, isPlatformBrowser } from '@angular/common';
import { of, Subject, from, BehaviorSubject, fromEvent } from 'rxjs';
import * as i1$1 from '@angular/router';
import { Router, ActivatedRoute, NavigationEnd, RouterModule } from '@angular/router';
import { HttpClient, HttpClientModule } from '@angular/common/http';
/* eslint-disable no-useless-escape */
// undefined and null functions
const exists = (value) => value !== null && value !== undefined;
const hasValue = (value) => exists(value) && (typeof value === 'string' ? !isEmptyString(value) : true);
// string functions
const isEmptyString = (value) => value.trim() === '';
const isValidEmail = (value) => {
const re = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
return re.test(String(value).toLowerCase().trim());
};
const isValidUrl = (value) => {
const pattern =
// eslint-disable-next-line max-len
/^((([a-z]+?:\/\/)?(((([a-z0-9]([a-z0-9-]*[a-z0-9])*)\.)+[a-z]{2,})|((([0-9]{1,3}\.){3}[0-9]{1,3}))|(localhost))(\:[0-9]+)?))?((\/[a-zA-Z0-9\-_+=~.,:;%]+)*\/?)((\?|;)[a-zA-Z0-9\-_+~.,:;%]+=[a-zA-Z0-9\-_+~.,:;%]+(((&|;)[a-zA-Z0-9\-_+~.,:;%]+=[a-zA-Z0-9\-_+~.,:;%]+)*))?(#[a-zA-Z0-9\-_+~.,:;%]+(=[a-zA-Z0-9\*\-_+~.,:;%]+)?)?$/;
return pattern.test(value);
};
const isValidRelativeUrl = (value) => {
const pattern =
// eslint-disable-next-line max-len
/^((([a-z]+?:\/\/)?(((([a-z0-9]([a-z0-9-]*[a-z0-9])*)\.)+[a-z]{2,})|((([0-9]{1,3}\.){3}[0-9]{1,3}))|(localhost))(\:[0-9]+)?)|([a-zA-Z0-9\-_+=~.,:;%]+))?((\/[a-zA-Z0-9\-_+=~.,:;%]+)*\/?)((\?|;)[a-zA-Z0-9\-_+~.,:;%]+=[a-zA-Z0-9\-_+~.,:;%]+(((&|;)[a-zA-Z0-9\-_+~.,:;%]+=[a-zA-Z0-9\-_+~.,:;%]+)*))?(#[a-zA-Z0-9\-_+~.,:;%]+(=[a-zA-Z0-9\*\-_+~.,:;%]+)?)?$/;
return pattern.test(value);
};
const isValidColor = (value) => {
value = String(value).toLowerCase();
// valid values for CSS color property, yet not valid colors by themselves
if (value === 'currentcolor' ||
value === 'inherit' ||
value === 'initial' ||
value === 'revert' ||
value === 'unset') {
return false;
}
return CSS.supports('color', value);
};
// date functions
const isValidDate = (date) => exists(date) ? !isNaN(date.getTime()) : false;
const normalizeDate = (value) => {
if (typeof value === 'string' && value.length > 10) {
return value.substr(0, 10);
}
return value ? value.toString() : null;
};
const addTimezoneOffset = (date) => {
if (!isValidDate(date)) {
return date;
}
else {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000);
}
};
// number functions
const isValidNumber = (value) => (hasValue(value) ? !Number.isNaN(Number(value)) : false);
const numberOfDecimalDigits = (x) => {
if (isValidNumber(x)) {
const xString = String(Number(x));
if (xString === 'Infinity') {
return 0;
}
const indexOfE = xString.indexOf('e');
if (indexOfE >= 0) {
return 0;
}
const indexOfDecimalPoint = xString.indexOf('.');
if (indexOfDecimalPoint < 0) {
return 0;
}
else {
return xString.length - indexOfDecimalPoint - 1;
}
}
return undefined;
};
const numberOfWholeDigits = (x) => {
if (isValidNumber(x)) {
let xString = String(Number(x));
if (xString.indexOf('-') === 0) {
xString = xString.slice(1, xString.length);
}
if (xString === 'Infinity') {
return Infinity;
}
if (xString.indexOf('0') === 0) {
xString = xString.slice(1, xString.length);
}
const indexOfE = xString.indexOf('e');
if (indexOfE >= 0) {
return Number(xString.slice(indexOfE + 1, xString.length)) + 1;
}
const indexOfDecimalPoint = xString.indexOf('.');
if (indexOfDecimalPoint < 0) {
return xString.length;
}
else {
return indexOfDecimalPoint;
}
}
return undefined;
};
const roundToMultipleOf = (x, modulo) => {
const moduloString = String(modulo);
// approximates the result
// prone to inexactitude because of floating point arithmetic
const approximation = Math.round(x / modulo) * modulo;
const approximationString = String(approximation);
// remove useless decimals
const uselessDecimalsInApproximation = numberOfDecimalDigits(approximationString) -
numberOfDecimalDigits(moduloString);
const resultString = approximationString.slice(0, approximationString.length - uselessDecimalsInApproximation);
return Number(resultString);
};
// other functions
const isInitialAndEmpty = (previousValue, newValue) => {
const isPrevArray = Array.isArray(previousValue);
const isNewArray = Array.isArray(newValue);
return !((isPrevArray ? previousValue.length !== 0 : Boolean(previousValue)) ||
(isNewArray ? newValue.length !== 0 : Boolean(newValue)));
};
const LOST_FOCUS_TIME_WINDOW_MS = 200; // ms
class AutocompleteComponent {
cd = inject(ChangeDetectorRef);
document = inject(DOCUMENT);
appendToContainer = null;
static idCounter = 0;
i0;
completeDiv;
_dataSource;
_placeholder;
_value;
lostFocusHandled = true;
t0 = 0;
showSpinner = false;
touched = false;
completionList = [];
showCompletion = false;
focusItem;
valueChange = new EventEmitter();
dataSourceChange = new EventEmitter();
inputId;
disabled = null;
readonly = null;
label = '';
/** If canAddNewValues, user can type items not present in the data-source. */
canAddNewValues = false;
/** After cleaning the selection should the completion list remain open or closed:
* false: (default) close on filters, to clean a filter and select all.
* true: keep open (when the action most likely is to pick another one).
*/
keepOpenAfterDelete = false;
/** Append dropdown to body or custom selector. Uses position absolute. */
appendTo;
get value() {
return this._value;
}
set value(v) {
const initialAndEmpty = isInitialAndEmpty(this._value, v);
this._value = v;
this.onChange(v);
this.completeLabel();
if (!initialAndEmpty) {
this.valueChange.emit(v);
}
}
get dataSource() {
return this._dataSource;
}
set dataSource(v) {
this._dataSource = v;
this.dataSourceChange.emit(v);
}
required = false;
set placeholder(v) {
this._placeholder = v;
}
get placeholder() {
return this._placeholder ? this._placeholder : '';
}
resolveLabelsFunction = undefined;
populateFunction = undefined;
instance;
// ControlValueAccessor Interface
onChange = (value) => { };
onTouched = () => { };
writeValue(value) {
this.value = value;
}
registerOnChange(onChange) {
this.onChange = onChange;
}
registerOnTouched(onTouched) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched && !this.disabled) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled) {
this.disabled = disabled;
}
// End ControlValueAccessor Interface
// Validator interface
registerOnValidatorChange() { }
validate(control) {
const value = control.value;
if (this.required &&
(value === '' || value === null || value === undefined)) {
return { required: { value, reason: 'Required field.' } };
}
return null;
}
// End of Validator interface
clear() {
this.value = null;
this.toggleCompletion(this.keepOpenAfterDelete, '');
}
completeLabel() {
if (this.value) {
if (this.dataSource) {
this.label = findLabelForId(this.dataSource, this.value) || '';
}
else if (this.instance && this.resolveLabelsFunction) {
this.resolveLabelsFunction(this.instance, [this.value])
.pipe(debounceTime(1), first())
.subscribe((data) => {
this.label = findLabelForId(data, this.value) || '';
});
}
}
else {
this.label = '';
}
}
ngOnInit() {
this.inputId = this.inputId
? this.inputId
: `autocompletelist${AutocompleteComponent.idCounter++}`;
this.completeLabel();
}
ngAfterViewInit() {
this.setSameWidth();
this.handleAppendTo();
}
ngOnDestroy() {
this.removeDropdownFromContainer();
}
handleAppendTo() {
if (!this.appendTo) {
return;
}
const container = this.appendTo === 'body'
? this.document.body
: this.document.querySelector(this.appendTo);
if (container) {
this.appendToContainer = container;
this.appendToContainer.appendChild(this.completeDiv.nativeElement);
}
}
removeDropdownFromContainer() {
if (this.appendToContainer && this.completeDiv) {
const dropdown = this.completeDiv.nativeElement;
if (dropdown.parentElement === this.appendToContainer) {
this.appendToContainer.removeChild(dropdown);
}
}
}
updateDropdownPosition() {
if (!this.appendTo || !this.appendToContainer) {
return;
}
const inputRect = this.i0.nativeElement.getBoundingClientRect();
const dropdown = this.completeDiv.nativeElement;
let top;
let left;
if (this.appendToContainer === this.document.body) {
// For body, use viewport coordinates + scroll
top = inputRect.bottom + window.scrollY;
left = inputRect.left + window.scrollX;
}
else {
// For custom containers, calculate relative position
const containerRect = this.appendToContainer.getBoundingClientRect();
top =
inputRect.bottom -
containerRect.top +
this.appendToContainer.scrollTop;
left =
inputRect.left - containerRect.left + this.appendToContainer.scrollLeft;
}
dropdown.style.top = `${top}px`;
dropdown.style.left = `${left}px`;
dropdown.style.width = `${inputRect.width}px`;
}
onInputResized() {
this.setSameWidth();
}
setSameWidth() {
if (this.appendTo) {
this.updateDropdownPosition();
}
else {
const width = this.i0.nativeElement.getBoundingClientRect().width;
this.completeDiv.nativeElement.style.width = `${width}px`;
}
}
onKeydown(event, label) {
switch (event.key) {
case 'Tab':
if (label) {
this.pickSelectionOrFirstMatch(label);
}
this.showCompletion = false;
break;
}
this.markAsTouched();
}
onKeypress(event, label) {
switch (event.key) {
case 'Intro':
case 'Enter':
this.pickSelectionOrFirstMatch(label);
event.preventDefault();
break;
}
this.markAsTouched();
}
onKeyup(event, label) {
switch (event.key) {
case 'PageDown':
this.focusOnNext(5);
event.preventDefault();
break;
case 'ArrowDown':
this.focusOnNext(1);
event.preventDefault();
break;
case 'PageUp':
this.focusOnPrevious(5);
event.preventDefault();
break;
case 'ArrowUp':
this.focusOnPrevious(1);
event.preventDefault();
break;
case 'Escape':
this.complete(null);
event.preventDefault();
break;
case 'Intro':
case 'Enter':
event.preventDefault();
break;
default:
this.showCompletionList(label);
// event.preventDefault();
}
this.markAsTouched();
}
focusOnNext(offset) {
const list = this.completionList || [];
const index = list.findIndex((it) => this.focusItem && it.key === this.focusItem.key);
const indexNext = index !== -1 && list.length > index + offset
? index + offset
: list.length - 1;
const next = list[indexNext];
this.focusItem = next;
this.ensureItemVisible(index);
}
focusOnPrevious(offset) {
const list = this.completionList || [];
const index = list.findIndex((it) => this.focusItem && it.key === this.focusItem.key);
const indexPrevious = index !== -1 && index > offset ? index - offset : 0;
const next = list[indexPrevious];
this.focusItem = next;
this.ensureItemVisible(index);
}
onLostFocus(label) {
this.lostFocusHandled = false;
this.t0 = performance.now();
// console.log('Init LostFocus');
setTimeout(() => {
// needs to postpone actions some milliseconds to verify if
// lost focus was followed by a list selection -> then cancel
// if not -> make side effect
if (!this.lostFocusHandled) {
// console.log(
// 'Lost focus 2',
// this.lostFocusHandled,
// 'SIDE EFFECT',
// performance.now() - this.t0,
// 'label:',
// label
// );
if (label && this.label !== label) {
this.pickSelectionOrFirstMatch(label);
}
else {
this.lostFocusHandled = true;
}
this.toggleCompletion(false, label);
}
else {
// do nothing (list selection took place)
// console.log(
// 'onlost focus 2',
// this.lostFocusHandled,
// 'nothing',
// performance.now() - this.t0
// );
}
}, LOST_FOCUS_TIME_WINDOW_MS);
}
complete(item) {
if (!this.lostFocusHandled) {
this.lostFocusHandled = true; // prevent a previous lostFocus to trigger a side effect
const ellapsed = performance.now() - this.t0;
if (ellapsed > LOST_FOCUS_TIME_WINDOW_MS) {
console.warn('complete. lostfocus->click timeout of ', LOST_FOCUS_TIME_WINDOW_MS, 'ms exceed: ', performance.now() - this.t0, ' ms');
}
// console.log(
// 'complete. set to true. CANCELED side effect',
// performance.now() - this.t0
// );
}
if (item !== null) {
this.value = item.key;
this.label = item.label;
}
else {
this.value = null;
this.label = '';
}
this.toggleCompletion(false, null);
this.markAsTouched();
}
toggleCompletion(show, label) {
if (show && !this.disabled) {
this.i0.nativeElement.focus();
if (this.appendTo) {
this.updateDropdownPosition();
}
this.showCompletionList(label);
}
else {
this.showCompletion = false;
if (this.canAddNewValues) {
this.syncCustomValue(this.label);
return;
}
}
this.cd.markForCheck();
}
get selectedOption() {
const index = this.completionList.findIndex((i) => i.key === this.focusItem.key);
if (index === -1 || !this.focusItem) {
return null;
}
return `${this.inputId}_${index}`;
}
ensureItemVisible(index) {
const target = this.completeDiv.nativeElement.querySelectorAll('li')[index];
if (target) {
target.scrollIntoView({ block: 'center' });
}
}
syncCustomValue(text) {
this.value = text;
this.label = text;
}
/** Pick selection based on text filtering (ingnores drowdown state) */
pickSelectionOrFirstMatch(text) {
if (this.canAddNewValues) {
this.syncCustomValue(text);
return;
}
const focusIndex = this.completionList.findIndex((it) => this.focusItem && it.key === this.focusItem.key);
if (this.showCompletion &&
focusIndex > 0 &&
this.focusItem &&
this.focusItem.label) {
if (text === this.focusItem.label && this.focusItem.key === this.value) {
// do nothing if value does not change & close dropdow
this.showCompletion = false;
return;
}
// complete selected using selected item on drowdown
this.complete(this.focusItem);
return;
}
const source = (text || '').trim();
if (!source) {
this.showCompletion = false;
// select null value
if (this.value !== null) {
this.value = null;
}
return;
}
this.completionList = [];
this.computeCompletionList(source).subscribe((suggestions) => {
const candidate = suggestions && suggestions.length > 0 ? suggestions[0] : null;
this.complete(candidate);
});
}
showCompletionList(text) {
this.setSameWidth();
const useSpinner = this.hasExternalDataSource();
this.spinnerVisibility(useSpinner, true);
setTimeout(() => {
// for spinner to be shown
this.computeCompletionList(text).subscribe({
next: (cl) => {
this.completionList = cl;
this.focusItem = selectElement(this.completionList, text);
this.showCompletion = true;
this.spinnerVisibility(useSpinner, false);
},
error: () => {
this.spinnerVisibility(useSpinner, false);
},
complete: () => {
this.spinnerVisibility(useSpinner, false);
}
});
}, 1);
}
spinnerVisibility(useSpinner, value) {
if (useSpinner) {
this.showSpinner = value;
}
}
hasExternalDataSource() {
return !this.dataSource && !!this.instance && !!this.populateFunction;
}
computeCompletionList(text) {
const searchText = (text || '').toLowerCase();
if (this.dataSource) {
const ds = (this.dataSource || [])
.filter((it) => ignoreAccentsInclude(it.label, searchText))
.sort((a, b) => a.label.localeCompare(b.label));
return of(decorateDataSource(ds, searchText));
}
else if (this.instance && this.populateFunction) {
return this.populateFunction(this.instance, searchText).pipe(debounceTime(1), first(), map((ds) => {
const dsFiltered = ds
.filter((it) => ignoreAccentsInclude(it.label, searchText))
.sort((a, b) => a.label.localeCompare(b.label));
return decorateDataSource(dsFiltered, searchText);
}));
}
else {
return of([]);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AutocompleteComponent, isStandalone: true, selector: "lux-autocomplete", inputs: { inputId: "inputId", disabled: "disabled", readonly: "readonly", label: "label", canAddNewValues: "canAddNewValues", keepOpenAfterDelete: "keepOpenAfterDelete", appendTo: "appendTo", value: "value", dataSource: "dataSource", required: "required", placeholder: "placeholder", resolveLabelsFunction: "resolveLabelsFunction", populateFunction: "populateFunction", instance: "instance" }, outputs: { valueChange: "valueChange", dataSourceChange: "dataSourceChange" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AutocompleteComponent)
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AutocompleteComponent)
}
], viewQueries: [{ propertyName: "i0", first: true, predicate: ["i0"], descendants: true, static: true }, { propertyName: "completeDiv", first: true, predicate: ["completeDiv"], descendants: true, static: true }], ngImport: i0, template: "<div class=\"lux-autocomplete\" (blur)=\"toggleCompletion(false, i0.value)\">\r\n <div class=\"lux-autocomplete-box\">\r\n <input\r\n #i0\r\n [id]=\"inputId\"\r\n [(ngModel)]=\"label\"\r\n [placeholder]=\"placeholder\"\r\n [attr.disabled]=\"disabled || null\"\r\n [attr.readonly]=\"readonly || null\"\r\n (keydown)=\"onKeydown($event, i0.value)\"\r\n (keypress)=\"onKeypress($event, i0.value)\"\r\n (keyup)=\"onKeyup($event, i0.value)\"\r\n (blur)=\"onLostFocus(i0.value)\"\r\n (focus)=\"toggleCompletion(true, i0.value)\"\r\n (click)=\"toggleCompletion(true, i0.value)\"\r\n (resized)=\"onInputResized()\"\r\n (window:resize)=\"onInputResized()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"showCompletion\"\r\n aria-haspopup=\"true\"\r\n attr.aria-owns=\"{{ inputId + '_list' }}\"\r\n [attr.aria-activedescendant]=\"selectedOption\"\r\n />\r\n @if (canAddNewValues) {\r\n <div class=\"icon-suggestions\"></div>\r\n } @if (showSpinner) {\r\n <div class=\"icon-spinner\"></div>\r\n } @if (!disabled && i0.value && !showCompletion) {\r\n <button\r\n type=\"button\"\r\n class=\"icon-clear\"\r\n aria-label=\"Clear\"\r\n (click)=\"clear()\"\r\n ></button>\r\n } @else {\r\n <div\r\n class=\"icon-dropdown\"\r\n (click)=\"toggleCompletion(!showCompletion, i0.value)\"\r\n ></div>\r\n }\r\n </div>\r\n <div\r\n #completeDiv\r\n [class.showCompletion]=\"showCompletion\"\r\n [class.lux-completion-list-appended]=\"appendTo\"\r\n class=\"lux-completion-list\"\r\n id=\"{{ inputId + '_list' }}\"\r\n >\r\n <ul>\r\n @for (item of completionList; track item.key; let index = $index) {\r\n <li\r\n id=\"{{ inputId + '_' + index }}\"\r\n class=\"lux-completion-item\"\r\n [class.selected]=\"focusItem && item.key === focusItem.key\"\r\n (click)=\"complete(item)\"\r\n [attr.aria-label]=\"item.label\"\r\n >\r\n <span class=\"preserve-white-space\">{{ item.labelPrefix }}</span>\r\n <span class=\"preserve-white-space bold\">{{ item.labelMatch }}</span>\r\n <span class=\"preserve-white-space\">{{ item.labelPostfix }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":focus{z-index:1}input,select,textarea{border:var(--lux-input-border, 1px solid) var(--lux-input-border-color, #495057);margin:var(--lux-input-margin, 0rem);padding:var(--lux-input-padding, 0rem .5rem)}.alert{padding:.2rem 2rem;background:var(--lux-alert-background, #fe2e2e);color:var(--lux-alert-color, white);font-family:Consolas,monospace;width:45%;margin-right:1rem;display:inline-block}.symbol{display:inline-block;align-items:baseline;padding:0rem .5rem;margin-bottom:0;font-weight:400;color:var(--lux-symbol-color, default);text-align:center;white-space:nowrap;background:var(--lux-symbol-background, default);font-size:var(--lux-symbol-font-size, 1rem)}.bordered,.symbol{border:var(--lux-input-border, 1px solid) var(--lux-input-border-color, #495057)}.rounded,.rounded-right,.rounded-middle,.rounded-left{border-radius:var(--lux-border-radius, .25rem)}.rounded-left{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-middle{border-radius:0}.rounded-right{border-top-left-radius:0;border-bottom-left-radius:0}.prefix{border-right:none}.infix{border-left:none;border-right:none}.postfix{border-left:none;display:flex;align-items:center}.monospace{font-family:monospace}.default-font{color:var(--lux-font-color, black)}.lux-autocomplete .lux-autocomplete-box{display:grid;grid-template-columns:1fr max-content}.lux-autocomplete input{padding:var(--lux-autocomplete-input-padding, .1rem 1.5rem .1rem .5rem);text-overflow:ellipsis;grid-row:1;grid-column-start:1;grid-column-end:3}.lux-autocomplete .icon-suggestions{grid-row:1;grid-column:2;width:1.5rem;border:none;background-color:transparent;background:url(/assets/img/suggestions.svg) no-repeat center;background-size:.5rem .5rem;z-index:1;position:relative;left:-1rem}.lux-autocomplete .icon-clear{grid-row:1;grid-column:2;width:var(--lux-autocomplete-icon-width, 1.5rem);border:none;background-color:transparent;background:var(--lux-autocomplete-icon-clear, url(/assets/img/filter-clear.png) no-repeat center);background-size:var(--lux-autocomplete-icon-bg-size, .5rem .5rem);z-index:1}.lux-autocomplete .icon-dropdown{grid-row:1;grid-column:2;width:var(--lux-autocomplete-icon-width, 1.5rem);height:var(--lux-autocomplete-icon-height, 1.3rem);border:none;background-color:transparent;background:var(--lux-autocomplete-icon-dropdown, url(/assets/img/drop-down-arrow.svg) no-repeat center);background-size:var(--lux-autocomplete-icon-bg-size, .5rem .5rem);z-index:1}.lux-autocomplete .icon-spinner{grid-row:1;grid-column:2;width:1.5rem;border:none;background-color:transparent;background:url(/assets/img/spinner.svg) no-repeat center;background-size:1.2rem 1.2rem;z-index:1}.lux-completion-list{visibility:collapse;position:absolute;border:1px solid #ccc;margin:0rem;padding:0rem;max-height:10rem;overflow:auto;z-index:2}.lux-completion-list ul{list-style:none;margin:0rem;padding:0rem}.lux-completion-list.lux-completion-list-appended{position:absolute}.showCompletion{visibility:visible}.lux-completion-item{background-color:#fff;color:#000;padding:.1rem .5rem;cursor:pointer}.selected{background-color:#2e2eac;color:#fff}.disabled{background-color:#f0f0f0;color:#444}.disabled .selected{background-color:#9898ec;color:#fff}.bold{font-weight:700}.preserve-white-space{white-space:pre}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AutocompleteComponent, decorators: [{
type: Component,
args: [{ selector: 'lux-autocomplete', imports: [FormsModule], providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AutocompleteComponent)
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AutocompleteComponent)
}
], template: "<div class=\"lux-autocomplete\" (blur)=\"toggleCompletion(false, i0.value)\">\r\n <div class=\"lux-autocomplete-box\">\r\n <input\r\n #i0\r\n [id]=\"inputId\"\r\n [(ngModel)]=\"label\"\r\n [placeholder]=\"placeholder\"\r\n [attr.disabled]=\"disabled || null\"\r\n [attr.readonly]=\"readonly || null\"\r\n (keydown)=\"onKeydown($event, i0.value)\"\r\n (keypress)=\"onKeypress($event, i0.value)\"\r\n (keyup)=\"onKeyup($event, i0.value)\"\r\n (blur)=\"onLostFocus(i0.value)\"\r\n (focus)=\"toggleCompletion(true, i0.value)\"\r\n (click)=\"toggleCompletion(true, i0.value)\"\r\n (resized)=\"onInputResized()\"\r\n (window:resize)=\"onInputResized()\"\r\n role=\"combobox\"\r\n aria-autocomplete=\"list\"\r\n [attr.aria-expanded]=\"showCompletion\"\r\n aria-haspopup=\"true\"\r\n attr.aria-owns=\"{{ inputId + '_list' }}\"\r\n [attr.aria-activedescendant]=\"selectedOption\"\r\n />\r\n @if (canAddNewValues) {\r\n <div class=\"icon-suggestions\"></div>\r\n } @if (showSpinner) {\r\n <div class=\"icon-spinner\"></div>\r\n } @if (!disabled && i0.value && !showCompletion) {\r\n <button\r\n type=\"button\"\r\n class=\"icon-clear\"\r\n aria-label=\"Clear\"\r\n (click)=\"clear()\"\r\n ></button>\r\n } @else {\r\n <div\r\n class=\"icon-dropdown\"\r\n (click)=\"toggleCompletion(!showCompletion, i0.value)\"\r\n ></div>\r\n }\r\n </div>\r\n <div\r\n #completeDiv\r\n [class.showCompletion]=\"showCompletion\"\r\n [class.lux-completion-list-appended]=\"appendTo\"\r\n class=\"lux-completion-list\"\r\n id=\"{{ inputId + '_list' }}\"\r\n >\r\n <ul>\r\n @for (item of completionList; track item.key; let index = $index) {\r\n <li\r\n id=\"{{ inputId + '_' + index }}\"\r\n class=\"lux-completion-item\"\r\n [class.selected]=\"focusItem && item.key === focusItem.key\"\r\n (click)=\"complete(item)\"\r\n [attr.aria-label]=\"item.label\"\r\n >\r\n <span class=\"preserve-white-space\">{{ item.labelPrefix }}</span>\r\n <span class=\"preserve-white-space bold\">{{ item.labelMatch }}</span>\r\n <span class=\"preserve-white-space\">{{ item.labelPostfix }}</span>\r\n </li>\r\n }\r\n </ul>\r\n </div>\r\n</div>\r\n", styles: [":focus{z-index:1}input,select,textarea{border:var(--lux-input-border, 1px solid) var(--lux-input-border-color, #495057);margin:var(--lux-input-margin, 0rem);padding:var(--lux-input-padding, 0rem .5rem)}.alert{padding:.2rem 2rem;background:var(--lux-alert-background, #fe2e2e);color:var(--lux-alert-color, white);font-family:Consolas,monospace;width:45%;margin-right:1rem;display:inline-block}.symbol{display:inline-block;align-items:baseline;padding:0rem .5rem;margin-bottom:0;font-weight:400;color:var(--lux-symbol-color, default);text-align:center;white-space:nowrap;background:var(--lux-symbol-background, default);font-size:var(--lux-symbol-font-size, 1rem)}.bordered,.symbol{border:var(--lux-input-border, 1px solid) var(--lux-input-border-color, #495057)}.rounded,.rounded-right,.rounded-middle,.rounded-left{border-radius:var(--lux-border-radius, .25rem)}.rounded-left{border-top-right-radius:0;border-bottom-right-radius:0}.rounded-middle{border-radius:0}.rounded-right{border-top-left-radius:0;border-bottom-left-radius:0}.prefix{border-right:none}.infix{border-left:none;border-right:none}.postfix{border-left:none;display:flex;align-items:center}.monospace{font-family:monospace}.default-font{color:var(--lux-font-color, black)}.lux-autocomplete .lux-autocomplete-box{display:grid;grid-template-columns:1fr max-content}.lux-autocomplete input{padding:var(--lux-autocomplete-input-padding, .1rem 1.5rem .1rem .5rem);text-overflow:ellipsis;grid-row:1;grid-column-start:1;grid-column-end:3}.lux-autocomplete .icon-suggestions{grid-row:1;grid-column:2;width:1.5rem;border:none;background-color:transparent;background:url(/assets/img/suggestions.svg) no-repeat center;background-size:.5rem .5rem;z-index:1;position:relative;left:-1rem}.lux-autocomplete .icon-clear{grid-row:1;grid-column:2;width:var(--lux-autocomplete-icon-width, 1.5rem);border:none;background-color:transparent;background:var(--lux-autocomplete-icon-clear, url(/assets/img/filter-clear.png) no-repeat center);background-size:var(--lux-autocomplete-icon-bg-size, .5rem .5rem);z-index:1}.lux-autocomplete .icon-dropdown{grid-row:1;grid-column:2;width:var(--lux-autocomplete-icon-width, 1.5rem);height:var(--lux-autocomplete-icon-height, 1.3rem);border:none;background-color:transparent;background:var(--lux-autocomplete-icon-dropdown, url(/assets/img/drop-down-arrow.svg) no-repeat center);background-size:var(--lux-autocomplete-icon-bg-size, .5rem .5rem);z-index:1}.lux-autocomplete .icon-spinner{grid-row:1;grid-column:2;width:1.5rem;border:none;background-color:transparent;background:url(/assets/img/spinner.svg) no-repeat center;background-size:1.2rem 1.2rem;z-index:1}.lux-completion-list{visibility:collapse;position:absolute;border:1px solid #ccc;margin:0rem;padding:0rem;max-height:10rem;overflow:auto;z-index:2}.lux-completion-list ul{list-style:none;margin:0rem;padding:0rem}.lux-completion-list.lux-completion-list-appended{position:absolute}.showCompletion{visibility:visible}.lux-completion-item{background-color:#fff;color:#000;padding:.1rem .5rem;cursor:pointer}.selected{background-color:#2e2eac;color:#fff}.disabled{background-color:#f0f0f0;color:#444}.disabled .selected{background-color:#9898ec;color:#fff}.bold{font-weight:700}.preserve-white-space{white-space:pre}\n"] }]
}], propDecorators: { i0: [{
type: ViewChild,
args: ['i0', { static: true }]
}], completeDiv: [{
type: ViewChild,
args: ['completeDiv', { static: true }]
}], valueChange: [{
type: Output
}], dataSourceChange: [{
type: Output
}], inputId: [{
type: Input
}], disabled: [{
type: Input
}], readonly: [{
type: Input
}], label: [{
type: Input
}], canAddNewValues: [{
type: Input
}], keepOpenAfterDelete: [{
type: Input
}], appendTo: [{
type: Input
}], value: [{
type: Input
}], dataSource: [{
type: Input
}], required: [{
type: Input
}], placeholder: [{
type: Input
}], resolveLabelsFunction: [{
type: Input
}], populateFunction: [{
type: Input
}], instance: [{
type: Input
}] } });
/** Returns true if text includes substring. No accents considered. Ignorecase */
const ignoreAccentsInclude = (text, substring) => {
text = normalizedString(text).toLowerCase();
substring = normalizedString(substring).toLowerCase();
return text.includes(substring);
};
/** Returns a normalized string with no accents: used for comparison */
const normalizedString = (a) => (a || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '');
const decorateDataSource = (dataSource, subString) => dataSource.map((it) => decorateItem(it, subString));
const decorateItem = (item, tx) => {
const index = normalizedString(item.label)
.toLowerCase()
.indexOf(normalizedString(tx).toLowerCase());
const labelPrefix = index === -1 ? item.label : item.label.substr(0, index);
const labelMatch = index === -1 ? '' : item.label.substr(index, tx.length);
const labelPostfix = index === -1 ? '' : item.label.substr(index + tx.length);
const newItem = {
...item,
labelPrefix,
labelMatch,
labelPostfix
};
return newItem;
};
const findLabelForId = (data, id) => {
const found = data.find((it) => it.key === id);
return found ? found.label : null;
};
const selectElement = (completionList, label) => {
label = (label || '').toLowerCase();
if (!completionList || completionList.length === 0) {
return null;
}
if (completionList.length === 1) {
return completionList[0];
}
const found = completionList.find((it) => it.label.toLowerCase() === label);
return found || completionList[0];
};
/** Language detector based on Navigator preferences */
const languageDetector = () => {
const lang = navigator.language.split('-')[0];
if (lang === 'es' || lang === 'en') {
return lang;
}
return 'en'; // default
};
class AutocompleteListComponent {
static idCounter = 0;
auto;
literals = {
en: {
placeholder: 'new item',
deleteLabelTemplate: 'Delete <<label>>',
addMessage: 'Add'
},
es: {
placeholder: 'nuevo elemento',
deleteLabelTemplate: 'Eliminar <<label>>',
addMessage: 'Añadir'
}
};
internalDataSource = [];
autoPopulate = false;
_value = [];
set value(val) {
if (val === this._value) {
return;
}
const initialAndEmpty = isInitialAndEmpty(this._value, val);
this._value = val;
this.ensureLabelsForIds();
this.populateWith('');
this.onChange(this._value);
if (!initialAndEmpty) {
this.valueChange.emit(this._value);
}
}
get value() {
return this._value;
}
labels = [];
newEntry;
canAdd = false;
touched = false;
_lang = languageDetector();
get lang() {
return this._lang;
}
set lang(l) {
if (l === this._lang) {
return;
}
if (Object.keys(this.literals).includes(l)) {
this._lang = l;
}
else {
this._lang = 'en';
}
}
inputId;
dataSource = [];
placeholder;
disabled = false;
deleteLabelTemplate;
addMessage;
required = false;
resolveLabelsFunction = undefined;
populateFunction = undefined;
instance;
valueChange = new EventEmitter();
// ControlValueAccessor Interface
onChange = (value) => { };
onTouched = () => { };
writeValue(value) {
this.value = value;
}
registerOnChange(onChange) {
this.onChange = onChange;
}
registerOnTouched(onTouched) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched && !this.disabled) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled) {
this.disabled = disabled;
}
// End ControlValueAccessor Interface
// Validator interface
registerOnValidatorChange() { }
validate(control) {
const value = control.value;
if (this.required &&
(value === '' || value === null || value === undefined)) {
return { required: { value, reason: 'Required field.' } };
}
return null;
}
// End of Validator interface
ngOnInit() {
this.inputId = this.inputId
? this.inputId
: `autocompletelist${AutocompleteListComponent.idCounter++}`;
this.autoPopulate =
!!this.resolveLabelsFunction && !!this.populateFunction && this.instance;
this.ensureLabelsForIds();
}
ensureLabelsForIds() {
if (this.autoPopulate && this.resolveLabelsFunction) {
this.resolveLabelsFunction(this.instance, this._value)
.pipe(first())
.subscribe((data) => {
const res = [];
(this._value || []).map((id) => {
const found = data.find((it) => it.key === id);
if (found) {
res.push(found.label);
}
else {
res.push('(unset)');
}
});
this.labels = res;
});
}
else if (this.dataSource) {
const res = [];
(this._value || []).map((id) => {
const found = this.dataSource.find((it) => it.key === id);
if (found) {
res.push(found.label);
}
else {
res.push('(unset)');
}
});
this.labels = res;
}
else {
this.labels = this._value.map((it) => (it ? it.toString() : '(unset)'));
}
}
removeAt(index) {
if (this._value.length > index) {
const key = this._value.splice(index, 1)[0];
const label = this.labels.splice(index, 1)[0];
this.internalDataSource.push({ key, label });
}
this.markAsTouched();
}
onValueChange() {
this.updateCanAdd();
}
onNewEntryChange(event, auto) {
if (event.key === 'Enter' && !!auto.value) {
this.addNew(auto);
}
else if (event.key === 'Delete' || event.key === 'Backspace') {
// todo
}
else {
this.populateWith(auto.label + event.key);
}
this.updateCanAdd();
}
populateWith(searchText) {
if (this.autoPopulate && this.populateFunction && this.instance) {
this.populateFunction(this.instance, searchText)
.pipe(first())
.subscribe((data) => {
this.internalDataSource = data.filter((it) => !(this._value || []).includes(it.key));
});
}
else if (this.dataSource) {
this.internalDataSource = this.dataSource.filter((it) => !(this._value || []).includes(it.key));
}
}
updateCanAdd() {
this.canAdd =
!this.disabled &&
this.auto &&
!!this.auto.value && // has value
!this.value.find((it) => it === this.auto.value); // not already in
}
addNew(auto) {
this.value.push(auto.value);
this.ensureLabelsForIds();
this.newEntry = '';
this.internalDataSource = this.internalDataSource.filter((it) => !this._value.includes(it.key));
this.markAsTouched();
}
getDeleteMessage(label) {
return (this.deleteLabelTemplate ?? this.literals[this.lang].deleteLabelTemplate).replace('<<label>>', label);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AutocompleteListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: AutocompleteListComponent, isStandalone: true, selector: "lux-autocomplete-list", inputs: { value: "value", lang: "lang", inputId: "inputId", dataSource: "dataSource", placeholder: "placeholder", disabled: "disabled", deleteLabelTemplate: "deleteLabelTemplate", addMessage: "addMessage", required: "required", resolveLabelsFunction: "resolveLabelsFunction", populateFunction: "populateFunction", instance: "instance" }, outputs: { valueChange: "valueChange" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AutocompleteListComponent)
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AutocompleteListComponent)
}
], viewQueries: [{ propertyName: "auto", first: true, predicate: ["auto"], descendants: true }], ngImport: i0, template: "<div [id]=\"inputId\">\r\n <ul>\r\n @for (label of labels; track label; let index = $index) {\r\n <li>\r\n <span>{{ label }}</span>\r\n @if (!disabled) {\r\n <button\r\n class=\"remove-item\"\r\n (click)=\"removeAt(index)\"\r\n attr.aria-label=\"{{ getDeleteMessage(label) }}\"\r\n ></button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n @if (!disabled) {\r\n <div class=\"new-entry\">\r\n <lux-autocomplete\r\n #auto\r\n [dataSource]=\"internalDataSource\"\r\n [placeholder]=\"placeholder ?? literals[lang].placeholder\"\r\n [(value)]=\"newEntry\"\r\n (valueChange)=\"onValueChange()\"\r\n (keypress)=\"onNewEntryChange($event, auto)\"\r\n >\r\n </lux-autocomplete>\r\n @if (canAdd) {\r\n <button\r\n type=\"button\"\r\n class=\"add-item\"\r\n aria-label=\"addMessage ?? literals[lang].addMessage\"\r\n (click)=\"addNew(auto)\"\r\n ></button>\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".new-entry{margin-left:1.5rem;display:grid;grid-template-columns:max-content max-content}.new-entry lux-autocomplete{grid-column:1;grid-row:1}.new-entry .add-item{grid-column:2;grid-row:1;border:none;background:var(--lux-autocomplete-list-add-icon, url(/assets/img/create.png) no-repeat .2rem .2rem);background-size:var(--lux-autocomplete-list-add-icon-size, 1.1rem);height:var(--lux-autocomplete-list-add-icon-height, 1.4rem);width:var(--lux-autocomplete-list-add-icon-width, 1.4rem)}.remove-item{margin-left:.5rem;border:none;background:var(--lux-autocomplete-list-remove-icon, url(/assets/img/delete.svg) no-repeat .2rem .1rem);height:var(--lux-autocomplete-list-remove-icon-height, 1.2rem);width:var(--lux-autocomplete-list-remove-icon-width, 1.3rem);position:relative;top:var(--lux-autocomplete-list-remove-icon-top, .2rem);background-size:var(--lux-autocomplete-list-remove-icon-size, 1rem 1rem)}\n"], dependencies: [{ kind: "component", type: AutocompleteComponent, selector: "lux-autocomplete", inputs: ["inputId", "disabled", "readonly", "label", "canAddNewValues", "keepOpenAfterDelete", "appendTo", "value", "dataSource", "required", "placeholder", "resolveLabelsFunction", "populateFunction", "instance"], outputs: ["valueChange", "dataSourceChange"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: AutocompleteListComponent, decorators: [{
type: Component,
args: [{ selector: 'lux-autocomplete-list', imports: [AutocompleteComponent], providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AutocompleteListComponent)
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AutocompleteListComponent)
}
], template: "<div [id]=\"inputId\">\r\n <ul>\r\n @for (label of labels; track label; let index = $index) {\r\n <li>\r\n <span>{{ label }}</span>\r\n @if (!disabled) {\r\n <button\r\n class=\"remove-item\"\r\n (click)=\"removeAt(index)\"\r\n attr.aria-label=\"{{ getDeleteMessage(label) }}\"\r\n ></button>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n @if (!disabled) {\r\n <div class=\"new-entry\">\r\n <lux-autocomplete\r\n #auto\r\n [dataSource]=\"internalDataSource\"\r\n [placeholder]=\"placeholder ?? literals[lang].placeholder\"\r\n [(value)]=\"newEntry\"\r\n (valueChange)=\"onValueChange()\"\r\n (keypress)=\"onNewEntryChange($event, auto)\"\r\n >\r\n </lux-autocomplete>\r\n @if (canAdd) {\r\n <button\r\n type=\"button\"\r\n class=\"add-item\"\r\n aria-label=\"addMessage ?? literals[lang].addMessage\"\r\n (click)=\"addNew(auto)\"\r\n ></button>\r\n }\r\n </div>\r\n }\r\n</div>\r\n", styles: [".new-entry{margin-left:1.5rem;display:grid;grid-template-columns:max-content max-content}.new-entry lux-autocomplete{grid-column:1;grid-row:1}.new-entry .add-item{grid-column:2;grid-row:1;border:none;background:var(--lux-autocomplete-list-add-icon, url(/assets/img/create.png) no-repeat .2rem .2rem);background-size:var(--lux-autocomplete-list-add-icon-size, 1.1rem);height:var(--lux-autocomplete-list-add-icon-height, 1.4rem);width:var(--lux-autocomplete-list-add-icon-width, 1.4rem)}.remove-item{margin-left:.5rem;border:none;background:var(--lux-autocomplete-list-remove-icon, url(/assets/img/delete.svg) no-repeat .2rem .1rem);height:var(--lux-autocomplete-list-remove-icon-height, 1.2rem);width:var(--lux-autocomplete-list-remove-icon-width, 1.3rem);position:relative;top:var(--lux-autocomplete-list-remove-icon-top, .2rem);background-size:var(--lux-autocomplete-list-remove-icon-size, 1rem 1rem)}\n"] }]
}], propDecorators: { auto: [{
type: ViewChild,
args: ['auto']
}], value: [{
type: Input
}], lang: [{