UNPKG

@metadev/lux

Version:

Lux: Library with User Interface components for Angular.

924 lines (919 loc) 253 kB
import * as i0 from '@angular/core'; import { EventEmitter, forwardRef, Input, Output, ViewChild, Component, HostBinding, Injectable, HostListener, Inject, TemplateRef, Directive, InjectionToken, PLATFORM_ID, NgModule } from '@angular/core'; import * as i1 from '@angular/forms'; import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { debounceTime, first, map, filter, takeUntil, withLatestFrom, distinctUntilChanged, switchMap } from 'rxjs/operators'; import * as i1$1 from '@angular/common'; import { DOCUMENT, isPlatformBrowser, CommonModule } from '@angular/common'; import { of, Subject, fromEvent, from, BehaviorSubject } from 'rxjs'; import * as i1$2 from '@angular/router'; import { NavigationEnd, RouterModule } from '@angular/router'; import * as i1$3 from '@angular/common/http'; import { 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))); }; /** Language detector based on Navigator preferences */ const languageDetector = () => { const lang = navigator.language.split('-')[0]; if (lang === 'es' || lang === 'en') { return lang; } return 'en'; // default }; const LOST_FOCUS_TIME_WINDOW_MS = 200; // ms class AutocompleteComponent { cd; 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; 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; constructor(cd) { this.cd = cd; } // 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(); } onInputResized() { this.setSameWidth(); } setSameWidth() { 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(); 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: "19.2.1", ngImport: i0, type: AutocompleteComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.1", type: AutocompleteComponent, isStandalone: false, selector: "lux-autocomplete", inputs: { inputId: "inputId", disabled: "disabled", readonly: "readonly", label: "label", canAddNewValues: "canAddNewValues", keepOpenAfterDelete: "keepOpenAfterDelete", 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)\">\n <div class=\"lux-autocomplete-box\">\n <input\n #i0\n [id]=\"inputId\"\n [(ngModel)]=\"label\"\n [placeholder]=\"placeholder\"\n [attr.disabled]=\"disabled || null\"\n [attr.readonly]=\"readonly || null\"\n (keydown)=\"onKeydown($event, i0.value)\"\n (keypress)=\"onKeypress($event, i0.value)\"\n (keyup)=\"onKeyup($event, i0.value)\"\n (blur)=\"onLostFocus(i0.value)\"\n (focus)=\"toggleCompletion(true, i0.value)\"\n (click)=\"toggleCompletion(true, i0.value)\"\n (resized)=\"onInputResized()\"\n (window:resize)=\"onInputResized()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"showCompletion\"\n aria-haspopup=\"true\"\n attr.aria-owns=\"{{ inputId + '_list' }}\"\n [attr.aria-activedescendant]=\"selectedOption\"\n />\n <div *ngIf=\"canAddNewValues\" class=\"icon-suggestions\"></div>\n <div *ngIf=\"showSpinner; else noLoading\" class=\"icon-spinner\"></div>\n <ng-template #noLoading>\n <button\n *ngIf=\"!disabled && i0.value && !showCompletion; else dropdown\"\n type=\"button\"\n class=\"icon-clear\"\n aria-label=\"Clear\"\n (click)=\"clear()\"\n ></button>\n <ng-template #dropdown>\n <div\n class=\"icon-dropdown\"\n (click)=\"toggleCompletion(!showCompletion, i0.value)\"\n ></div>\n </ng-template>\n </ng-template>\n </div>\n <div\n #completeDiv\n [class.showCompletion]=\"showCompletion\"\n class=\"lux-completion-list\"\n id=\"{{ inputId + '_list' }}\"\n >\n <ul>\n <li\n *ngFor=\"let item of completionList; let index = index\"\n id=\"{{ inputId + '_' + index }}\"\n class=\"lux-completion-item\"\n [class.selected]=\"focusItem && item.key === focusItem.key\"\n (click)=\"complete(item)\"\n [attr.aria-label]=\"item.label\"\n >\n <span class=\"preserve-white-space\">{{ item.labelPrefix }}</span>\n <span class=\"preserve-white-space bold\">{{ item.labelMatch }}</span>\n <span class=\"preserve-white-space\">{{ item.labelPostfix }}</span>\n </li>\n </ul>\n </div>\n</div>\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}.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: "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"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: AutocompleteComponent, decorators: [{ type: Component, args: [{ standalone: false, selector: 'lux-autocomplete', 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)\">\n <div class=\"lux-autocomplete-box\">\n <input\n #i0\n [id]=\"inputId\"\n [(ngModel)]=\"label\"\n [placeholder]=\"placeholder\"\n [attr.disabled]=\"disabled || null\"\n [attr.readonly]=\"readonly || null\"\n (keydown)=\"onKeydown($event, i0.value)\"\n (keypress)=\"onKeypress($event, i0.value)\"\n (keyup)=\"onKeyup($event, i0.value)\"\n (blur)=\"onLostFocus(i0.value)\"\n (focus)=\"toggleCompletion(true, i0.value)\"\n (click)=\"toggleCompletion(true, i0.value)\"\n (resized)=\"onInputResized()\"\n (window:resize)=\"onInputResized()\"\n role=\"combobox\"\n aria-autocomplete=\"list\"\n [attr.aria-expanded]=\"showCompletion\"\n aria-haspopup=\"true\"\n attr.aria-owns=\"{{ inputId + '_list' }}\"\n [attr.aria-activedescendant]=\"selectedOption\"\n />\n <div *ngIf=\"canAddNewValues\" class=\"icon-suggestions\"></div>\n <div *ngIf=\"showSpinner; else noLoading\" class=\"icon-spinner\"></div>\n <ng-template #noLoading>\n <button\n *ngIf=\"!disabled && i0.value && !showCompletion; else dropdown\"\n type=\"button\"\n class=\"icon-clear\"\n aria-label=\"Clear\"\n (click)=\"clear()\"\n ></button>\n <ng-template #dropdown>\n <div\n class=\"icon-dropdown\"\n (click)=\"toggleCompletion(!showCompletion, i0.value)\"\n ></div>\n </ng-template>\n </ng-template>\n </div>\n <div\n #completeDiv\n [class.showCompletion]=\"showCompletion\"\n class=\"lux-completion-list\"\n id=\"{{ inputId + '_list' }}\"\n >\n <ul>\n <li\n *ngFor=\"let item of completionList; let index = index\"\n id=\"{{ inputId + '_' + index }}\"\n class=\"lux-completion-item\"\n [class.selected]=\"focusItem && item.key === focusItem.key\"\n (click)=\"complete(item)\"\n [attr.aria-label]=\"item.label\"\n >\n <span class=\"preserve-white-space\">{{ item.labelPrefix }}</span>\n <span class=\"preserve-white-space bold\">{{ item.labelMatch }}</span>\n <span class=\"preserve-white-space\">{{ item.labelPostfix }}</span>\n </li>\n </ul>\n </div>\n</div>\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}.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"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], 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 }], 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]; }; 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: "19.2.1", ngImport: i0, type: AutocompleteListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.1", type: AutocompleteListComponent, isStandalone: false, 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\">\n <ul>\n <li *ngFor=\"let label of labels; let index = index\">\n <span>{{ label }}</span>\n <button\n *ngIf=\"!disabled\"\n class=\"remove-item\"\n (click)=\"removeAt(index)\"\n attr.aria-label=\"{{ getDeleteMessage(label) }}\"\n ></button>\n </li>\n </ul>\n <div *ngIf=\"!disabled\" class=\"new-entry\">\n <lux-autocomplete\n #auto\n [dataSource]=\"internalDataSource\"\n [placeholder]=\"placeholder ?? literals[lang].placeholder\"\n [(value)]=\"newEntry\"\n (valueChange)=\"onValueChange()\"\n (keypress)=\"onNewEntryChange($event, auto)\"\n >\n </lux-autocomplete>\n <button\n *ngIf=\"canAdd\"\n type=\"button\"\n class=\"add-item\"\n aria-label=\"addMessage ?? literals[lang].addMessage\"\n (click)=\"addNew(auto)\"\n ></button>\n </div>\n</div>\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: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: AutocompleteComponent, selector: "lux-autocomplete", inputs: ["inputId", "disabled", "readonly", "label", "canAddNewValues", "keepOpenAfterDelete", "value", "dataSource", "required", "placeholder", "resolveLabelsFunction", "populateFunction", "instance"], outputs: ["valueChange", "dataSourceChange"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: AutocompleteListComponent, decorators: [{ type: Component, args: [{ standalone: false, selector: 'lux-autocomplete-list', providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => AutocompleteListComponent) }, { provide: NG_VALIDATORS, multi: true, useExisting: forwardRef(() => AutocompleteListComponent) } ], template: "<div [id]=\"inputId\">\n <ul>\n <li *ngFor=\"let label of labels; let index = index\">\n <span>{{ label }}</span>\n <button\n *ngIf=\"!disabled\"\n class=\"remove-item\"\n (click)=\"removeAt(index)\"\n attr.aria-label=\"{{ getDeleteMessage(label) }}\"\n ></button>\n </li>\n </ul>\n <div *ngIf=\"!disabled\" class=\"new-entry\">\n <lux-autocomplete\n #auto\n [dataSource]=\"internalDataSource\"\n [placeholder]=\"placeholder ?? literals[lang].placeholder\"\n [(value)]=\"newEntry\"\n (valueChange)=\"onValueChange()\"\n (keypress)=\"onNewEntryChange($event, auto)\"\n >\n </lux-autocomplete>\n <button\n *ngIf=\"canAdd\"\n type=\"button\"\n class=\"add-item\"\n aria-label=\"addMessage ?? literals[lang].addMessage\"\n (click)=\"addNew(auto)\"\n ></button>\n </div>\n</div>\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: [{ type: Input }], inputId: [{ type: Input }], dataSource: [{ type: Input }], placeholder: [{ type: Input }], disabled: [{ type: Input }], deleteLabelTemplate: [{ type: Input }], addMessage: [{ type: Input }], required: [{ type: Input }], resolveLabelsFunction: [{ type: Input }], populateFunction: [{ type: Input }], instance: [{ type: Input }], valueChange: [{ type: Output }] } }); class LuxBreadcrumbComponent { route; activedRoute; breadcrumbs; subs = []; imagePath = '../assets/img/arrow-forward.svg'; constructor(route, activedRoute) { this.route = route; this.activedRoute = activedRoute; } ngOnInit() { this.subs.push(this.route.events .pipe(filter((event) => event instanceof NavigationEnd)) .subscribe((_) => { this.breadcrumbs = []; this.addBreadcrumbs(this.activedRoute.snapshot.root, true, null); })); } ngOnDestroy() { this.subs.forEach((s) => s.unsubscribe()); this.subs = []; } addBreadcrumbs(activedRouteSnapshot, isRoot, urlPrefix) { const routeConfig = activedRouteSnapshot.routeConfig; let url = urlPrefix || ''; url += routeConfig ? '/' + this.getUrl(activedRouteSnapshot) : ''; const label = routeConfig ? this.getLabel(activedRouteSnapshot) : isRoot ? 'Home' : ''; if (label && url !== '/') { const breadcrumb = { label, url }; this.breadcrumbs.push(breadcrumb); } if (activedRouteSnapshot.children.length) { this.addBreadcrumbs(activedRouteSnapshot.children[0], false, url); } } getUrl(activedRouteSnapshot) { if (!activedRouteSnapshot.url[0]) { return ''; } const id = activedRouteSnapshot.params.id; return id ? `${activedRouteSnapshot.url[0]}/${id}` : activedRouteSnapshot.routeConfig.path || ''; } getLabel(activedRouteSnapshot) { const routeConfig = activedRouteSnapshot.routeConfig; if (!rout