import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnDestroy, OnInit, Output, QueryList, SimpleChanges, ViewChild, ViewChildren } from "@angular/core"; import {AbstractControl, UntypedFormArray, UntypedFormControl, UntypedFormGroup, ValidatorFn} from "@angular/forms"; import {HelperFunctions} from "../../utils/HelperFunctions.class"; import {BehaviorSubject, Subscription} from "rxjs"; import {EnvProperties} from "../../utils/properties/env-properties"; import {properties} from "../../../../environments/environment"; import {ClickEvent} from "../../utils/click/click-outside-or-esc.directive"; import {LayoutService} from "../../dashboard/sharedComponents/sidebar/layout.service"; import {MobileDropdownComponent} from "../../utils/mobile-dropdown/mobile-dropdown.component"; import {MatDatepicker} from "@angular/material/datepicker"; export type InputType = 'text' | 'URL' | 'logoURL' | 'autocomplete' | 'autocomplete_soft' | 'textarea' | 'select' | 'chips' | 'year-range' | 'date'; export interface Option { icon?: string, iconClass?: string, value: any, label: string, tooltip?: string, disabled?: boolean, hidden?: boolean } export interface Placeholder { label: string, static?: boolean, tooltip?: string } export interface YearRange { from: ControlConfiguration, to: ControlConfiguration } export interface ControlConfiguration { control: string, placeholder: string } declare var UIkit; /** * Autocomplete soft allows values that are not listed in options list. In order to work as expected * avoid providing options with different label and value. * * */ @Component({ selector: '[dashboard-input], [input]', template: `
{{ placeholderInfo.label }}
{{ getLabel(formControl.value) }}
{{ noValueSelected }}
{{ getLabel(formControl.value) }}
{{ placeholderInfo?.static ? placeholderInfo.label : getLabel(formAsControl.value) }}
{{ getLabel(formAsControl.value) }}
{{ noValueSelected }}
{{ getLabel(formControl.value) }}
{{ getLabel(chip.value) }}
+ {{ (formAsArray.length - visibleChips) }} more
-
{{ selectADate }}
{{ formAsControl.getRawValue() | date: 'dd-MM-yyyy' }}
{{ placeholderInfo.label }}
{{ errors?.error }} Please provide a valid URL (e.g. https://example.com) Note: Prefer urls like "https://example.com/my-secure-image.png" instead of "http://example.com/my-image.png". Browsers may not load non secure content. ` }) export class InputComponent implements OnInit, OnDestroy, AfterViewInit, OnChanges { private static INPUT_COUNTER: number = 0; /** Basic information */ @Input('formInput') formControl: AbstractControl; @Input('type') type: InputType = 'text'; @Input() password: boolean = false; @Input() validators: ValidatorFn[] | ValidatorFn; @Input() disabled: boolean = false; @Input() disabledIcon: string = 'lock'; @Input() value: any | any[]; @Output() valueChange = new EventEmitter(); @Input() hint: string; @Input() tooltip: boolean = false; @Input() searchable: boolean = false; /** Text */ @ViewChildren('input') input: QueryList; /** Textarea options */ @ViewChild('textArea') textArea: ElementRef; @Input('rows') rows: number = 3; /** Select | Autocomplete | chips available options */ @Input() selectArrow: string = 'arrow_drop_down'; @Input() selectedIndex: number = 0; @Input() selectable: boolean = false; @Input() noValueSelected: string = 'No option selected'; /** Chips && Autocomplete*/ public filteredOptions: Option[] = []; public searchControl: UntypedFormControl; public activeElement: BehaviorSubject = new BehaviorSubject(null); /** Use modifier's class(es) to change view of your Input */ @Input() inputClass: string = 'flat'; /** Icon on the input */ @Input() icon: string = null; /** Chip options */ @Input() addExtraChips: boolean = false; @Input() showOptionsOnEmpty: boolean = true; @Input() visibleChips: number = 1; @Input() separators: string[] = []; @Input() noWrap: boolean = false; /** Year Range Configuration */ @Input() yearRange: YearRange; public activeIndex: 0 | 1 | null = null; /** Date Configuration*/ @Input() selectADate: string = 'Select a date'; public selectedDate: Date; @Input() visibleRows: number = -1; @Input() extendEnter: () => void = null; @Output() focusEmitter: EventEmitter = new EventEmitter(); /** LogoUrl information */ public secure: boolean = true; /** Internal basic information */ public id: string; public placeholderInfo: Placeholder = {label: '', static: true}; public required: boolean = false; public focused: boolean = false; public opened: boolean = false; public mobile: boolean = false; public properties: EnvProperties = properties; private initValue: any; private optionsArray: Option[] = []; private optionsBreakpoint: number = 6; private subscriptions: any[] = []; @ViewChild('inputBox') inputBox: ElementRef; @ViewChild('optionBox') optionBox: ElementRef; @ViewChild('calendarBox') calendarBox: ElementRef; @ViewChild('mobileDropdown') mobileDropdown: MobileDropdownComponent; @ViewChild('searchInput') searchInput: ElementRef; @ViewChildren('chip') chips: QueryList; @ViewChild('datepicker') datepicker: MatDatepicker; @Input() set placeholder(placeholder: string | Placeholder) { if (this.type === 'year-range') { this.placeholderInfo = null; } else if (typeof placeholder === 'string') { this.placeholderInfo = {label: placeholder, static: false}; } else { if (placeholder.static && (this.type === 'autocomplete' || this.hint)) { placeholder.static = false; console.debug('Static placeholder is not available in this type of input and if hint is available.'); } this.placeholderInfo = placeholder; } } @Input() set options(options: (Option | string | number) []) { this.optionsArray = options.map(option => { if (option === null) { return { label: this.noValueSelected, value: '' }; } else if (typeof option === 'string' || typeof option === 'number') { return { label: option.toString(), value: option }; } else { return option; } }); if (!this.tooltip) { this.tooltip = this.optionsArray.length > 0; } if (this.type === "select") { if (this.optionsArray.length > this.optionsBreakpoint) { this.type = 'autocomplete'; this.showOptionsOnEmpty = true; this.icon = this.selectArrow; } this.selectable = true; } } constructor(private elementRef: ElementRef, private cdr: ChangeDetectorRef, private layoutService: LayoutService) { if (elementRef.nativeElement.hasAttribute('dashboard-input') && this.properties.environment === "development") { console.warn("'dashboard-input' selector is deprecated; use 'input' instead."); } } @HostListener('window:keydown.arrowUp', ['$event']) arrowUp(event: KeyboardEvent) { if (this.opened && this.optionBox) { event.preventDefault(); if (this.selectedIndex > 0) { this.selectedIndex--; this.optionBox.nativeElement.scrollBy(0, -34); } } } @HostListener('window:keydown.arrowDown', ['$event']) arrowDown(event: KeyboardEvent) { if (this.opened && this.optionBox) { event.preventDefault(); if (this.selectedIndex < (this.filteredOptions.length - 1)) { this.selectedIndex++; this.optionBox.nativeElement.scrollBy(0, 34); } } } @HostListener('window:keydown.arrowLeft', ['$event']) arrowLeft(event: KeyboardEvent) { if (this.type === 'chips' && this.focused) { if (this.activeElement.getValue()) { event.preventDefault(); let index = this.chips.toArray().indexOf(this.activeElement.getValue()); if (index > 0) { this.activeElement.next(this.chips.get(index - 1)); return; } } } } @HostListener('window:keydown.arrowRight', ['$event']) arrowRight(event: KeyboardEvent) { if (this.type === 'chips' && this.focused) { if (this.activeElement.getValue()) { event.preventDefault(); let index = this.chips.toArray().indexOf(this.activeElement.getValue()); if (index < this.chips.length - 1) { this.activeElement.next(this.chips.get(index + 1)); return; } } } } @HostListener('window:keydown.enter', ['$event']) enter(event: KeyboardEvent) { if (this.extendEnter) { this.extendEnter(); } if (this.opened && this.optionBox) { event.preventDefault(); if (this.filteredOptions[this.selectedIndex]) { this.selectOption(this.filteredOptions[this.selectedIndex], event); } this.open(false); event.stopPropagation(); } else { this.focus(false, event); } } @HostListener('keydown', ['$event']) onKeyDown(event: KeyboardEvent) { if (this.separators.includes(event.key) || this.separators.includes(event.key.toLowerCase())) { event.preventDefault(); this.add(event, true); } } click(event: ClickEvent) { this.focus(!event.clicked); } ngOnInit() { this.subscriptions.push(this.layoutService.isMobile.subscribe(mobile => { this.mobile = mobile; this.cdr.detectChanges(); })); InputComponent.INPUT_COUNTER++; this.id = 'input-' + InputComponent.INPUT_COUNTER; if (!this.formControl) { if (Array.isArray(this.value)) { this.formControl = new UntypedFormArray([]); this.value.forEach(value => { this.formAsArray.push(new UntypedFormControl(value, this.validators)); }); } else { this.formControl = new UntypedFormControl(this.value, this.validators); } if (this.disabled) { this.formControl.disable(); } } this.activeElement.subscribe(element => { if (element) { element.nativeElement.scrollIntoView({behavior: 'smooth'}); } }) } ngAfterViewInit() { this.reset(); } ngOnChanges(changes: SimpleChanges) { if (this.formControl) { if (changes.value && changes.value.currentValue !== changes.value.previousValue) { this.formControl.setValue(this.value); } if (changes.validators) { this.updateValidators(); } if (changes.formControl || changes.validators || changes.options) { this.reset(); } if (changes.disabled) { if (this.disabled) { this.formControl.disable(); } else { this.formControl.enable(); } } } } ngOnDestroy(): void { this.unsubscribe(); } getFormByName(name: string): UntypedFormControl { if (this.formControl instanceof UntypedFormGroup) { return this.formControl.get(name); } else { return null; } } get formAsGroup(): UntypedFormGroup { if (this.formControl instanceof UntypedFormGroup) { return this.formControl; } else { return null; } } get formAsControl(): UntypedFormControl { if (this.formControl instanceof UntypedFormControl) { return this.formControl; } else { return null; } } get formAsArray(): UntypedFormArray { if (this.formControl instanceof UntypedFormArray) { return this.formControl; } else { return null; } } get yearRangeActive(): boolean { if (this.yearRange) { return this.formAsGroup && (this.getFormByName(this.yearRange.from.control)?.value || this.getFormByName(this.yearRange.to.control)?.value); } return false; } get errors(): any { if (this.formAsGroup) { return (this.formAsGroup.errors ? this.formAsGroup.errors : (this.getFormByName(this.yearRange.from.control).errors ? this.getFormByName(this.yearRange.from.control).errors : this.getFormByName(this.yearRange.to.control).errors)); } else if (this.formAsControl) { return this.formAsControl.errors; } else if (this.searchControl) { return this.searchControl.errors; } else { return null; } } reset() { this.secure = true; this.unsubscribe(); this.initValue = HelperFunctions.copy(this.formControl.value); if (this.type === 'logoURL') { this.secure = (!this.initValue || this.initValue.includes('https://')); } if (this.optionsArray?.length > 0) { this.filteredOptions = this.filter(''); this.cdr.detectChanges(); } if (this.type === 'chips' || this.type === 'autocomplete') { if (!this.searchControl) { this.searchControl = new UntypedFormControl('', this.validators); } this.subscriptions.push(this.searchControl.valueChanges.subscribe(value => { this.filteredOptions = this.filter(value); this.cdr.detectChanges(); if (this.focused) { this.open(true); setTimeout(() => { if (this.searchInput) { this.searchInput.nativeElement.focus(); this.searchInput.nativeElement.value = value; } }, 0); } })); } if (this.formAsControl?.validator || this.formAsArray?.validator) { let validator = this.formControl.validator({} as AbstractControl); this.required = (validator && validator.required); } if (this.type === 'date') { this.selectedDate = this.formAsControl.getRawValue() ? new Date(this.formAsControl.getRawValue()) : null; } this.subscriptions.push(this.formControl.valueChanges.subscribe(value => { if (this.formControl.enabled) { if (this.type !== 'year-range') { value = (value === '') ? null : value; if (this.type === 'logoURL') { this.secure = (!value || value.includes('https://')); } if (this.initValue === value || (this.initValue === '' && value === null)) { this.formControl.markAsPristine(); } else { this.formControl.markAsDirty(); } if (this.type === 'autocomplete_soft') { this.filteredOptions = this.filter(value); this.cdr.detectChanges(); if (this.focused) { this.open(true); } } if (this.type === 'date') { this.selectedDate = value ? new Date(value) : null; } } if ((this.value && value && this.value !== value) || (!this.value && value) || this.value && !value) { this.valueChange.emit(this.formControl.value); } } })); if (this.formAsGroup) { let fromControl = this.formAsGroup.get(this.yearRange.from.control); this.subscriptions.push(fromControl.valueChanges.subscribe(value => { let from = this.initValue[this.yearRange.from.control]; if (from === value || (from === '' && value === null)) { fromControl.markAsPristine(); } else { fromControl.markAsDirty(); } if (fromControl.valid) { if (this.activeIndex === 0 && value) { this.activeIndex = 1; this.input.get(this.activeIndex).nativeElement.focus(); } } })); let toControl = this.formAsGroup.get(this.yearRange.to.control); this.subscriptions.push(toControl.valueChanges.subscribe(value => { let to = this.initValue[this.yearRange.to.control]; if (to === value || (to === '' && value === null)) { toControl.markAsPristine(); } else { toControl.markAsDirty(); } })); } if (this.input) { this.input.forEach(input => { input.nativeElement.disabled = this.formControl.disabled; }); } } unsubscribe() { this.subscriptions.forEach(subscription => { if (subscription instanceof Subscription) { subscription.unsubscribe(); } }); } updateValidators() { if (this.formAsArray) { this.formAsArray.controls.forEach(control => { control.setValidators(this.validators); control.updateValueAndValidity(); }) } else { this.formControl.setValidators(this.validators); this.formControl.updateValueAndValidity(); } } remove(index: number, event) { if (this.focused) { this.formAsArray.removeAt(index); this.formAsArray.markAsDirty(); this.focus(true); this.searchControl.setValue(''); event.stopPropagation(); } } private filter(value: string): Option[] { let options = this.optionsArray.filter(option => !option.hidden); if (this.type === "chips") { options = options.filter(option => !this.formAsArray.value.find(value => HelperFunctions.equals(option.value, value))); } if ((!value || value.length == 0)) { this.selectedIndex = 0; return (this.showOptionsOnEmpty) ? options : []; } const filterValue = value.toString().toLowerCase(); options = options.filter(option => (option.label && option.label.toLowerCase().indexOf(filterValue) != -1)); this.selectedIndex = options.findIndex(option => option.value === this.formControl.value); if (this.selectedIndex === -1) { this.selectedIndex = 0; } return options; } add(event, addChips = false) { if (addChips && this.searchControl.value) { this.splitSearchControl(); } else if (!this.focused) { this.searchControl.setValue(''); } } splitSearchControl() { let values = [this.searchControl.value]; this.separators.forEach(separator => { values = ([] as string[]).concat(...values.map(value => { if (Array.isArray(value)) { return ([] as string[]).concat(...value.map(element => element.split(separator))); } else { return value.split(separator); } })); }); values.forEach(value => { let control = new UntypedFormControl(value.trim(), this.validators); if (control.valid) { this.formAsArray.push(control); this.formAsArray.markAsDirty(); } }); if (this.formAsArray.dirty) { this.activeElement.next(this.chips.last); this.searchControl.setValue(''); } } getLabel(value: any): string { let option = this.optionsArray.find(option => HelperFunctions.equals(option.value, value)); return (option) ? option.label : (value); } getTooltip(value: any): string { let option = this.optionsArray.find(option => HelperFunctions.equals(option.value, value)); return (option) ? (option.tooltip ? option.tooltip : option.label) : (value); } focus(value: boolean, event = null) { if (!this.activeIndex) { this.activeIndex = 0; } if (this.focused) { this.formControl.markAsTouched(); } if (this.formControl.enabled) { this.focused = value; this.cdr.detectChanges(); if (this.focused) { if (this.input?.length > 0) { this.input.get(this.activeIndex).nativeElement.focus(); } else if (this.textArea) { this.textArea.nativeElement.focus(); } else if (this.searchInput) { this.searchInput.nativeElement.focus(); this.activeElement.next(this.chips.last); } if (this.selectArrow || this.datepicker) { this.open(!this.opened); } else if (this.type !== 'autocomplete' || this.showOptionsOnEmpty || !this.formControl.value) { this.open(true); } } else { this.activeIndex = null; this.open(false); if (this.input) { this.input.forEach(input => { input.nativeElement.blur(); }) } else if (this.textArea) { this.textArea.nativeElement.blur(); } else if (this.searchInput) { this.searchInput.nativeElement.blur(); } if (this.searchControl) { this.add(event, this.addExtraChips); } } this.focusEmitter.emit(this.focused); } } open(value: boolean) { this.opened = value && this.formControl.enabled; this.cdr.detectChanges(); if (this.optionBox) { if (this.opened) { this.selectedIndex = this.filteredOptions.findIndex(option => option.value === this.formControl.value); if (this.selectedIndex === -1 && this.type !== 'autocomplete_soft') { this.selectedIndex = 0; } UIkit.dropdown(this.optionBox.nativeElement).show(); } else { UIkit.dropdown(this.optionBox.nativeElement).hide(); this.focused = false; } } else if (this.calendarBox) { if (this.opened) { UIkit.dropdown(this.calendarBox.nativeElement).show(); } else { UIkit.dropdown(this.calendarBox.nativeElement).hide(); this.focused = false; } } else if (this.mobileDropdown) { if (this.opened) { this.mobileDropdown.open(); if (this.searchInput) { this.searchInput.nativeElement.blur(); } } else { this.mobileDropdown.close(); } } } resetSearch(event: any) { event.stopPropagation(); this.searchControl.setValue(''); this.focus(true, event); } resetValue(event: any) { event.stopPropagation(); this.formControl.setValue(''); this.focus(true, event); } selectOption(option: Option, event) { if (this.formControl.enabled) { if (this.formAsControl) { this.formAsControl.setValue(option.value); } else if (this.formAsArray) { this.formAsArray.push(new UntypedFormControl(option.value)); this.formAsArray.markAsDirty(); event.stopPropagation(); this.focus(true); this.searchControl.setValue(''); } } if (this.mobileDropdown) { this.mobileDropdown.close(); } } dateChanged(event: Date) { this.focus(false); this.formAsControl.setValue(event.getTime()); } }