import { ChangeDetectorRef, Renderer2, ViewEncapsulation } from '@angular/core'; import { AfterViewInit, Component, EventEmitter, Input, OnDestroy, OnInit, Output, Self, ViewChild, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { debounceTime, distinctUntilChanged, skip, Subject, takeUntil, } from 'rxjs'; // bootstrap import { NgbPopover, NgbPopoverModule, NgbModule, } from '@ng-bootstrap/ng-bootstrap'; // animation import { inputDropdownAnimation } from './animations'; // components import { CaInputDropdownLoadBrokerComponent } from './components/ca-input-dropdown-load-broker/ca-input-dropdown-load-broker.component'; import { CaInputDropdownLoadDispatchesTtdComponent } from './components/ca-input-dropdown-load-dispatches-ttd/ca-input-dropdown-load-dispatches-ttd.component'; import { CaInputDropdownLoadDispatcherComponent } from './components/ca-input-dropdown-load-dispatcher/ca-input-dropdown-load-dispatcher.component'; import { CaInputDropdownSvgTemplateComponent } from './components/ca-input-dropdown-svg-template/ca-input-dropdown-svg-template.component'; import { CaInputDropdownSvgtextTemplateComponent } from './components/ca-input-dropdown-svgtext-template/ca-input-dropdown-svgtext-template.component'; import { CaInputDropdownSvgtextDispatchTemplateComponent } from './components/ca-input-dropdown-svgtext-dispatch-template/ca-input-dropdown-svgtext-dispatch-template.component'; import { CaInputDropdownGroupsComponent } from './components/ca-input-dropdown-groups/ca-input-dropdown-groups.component'; import { CaInputDropdownMultiselectComponent } from './components/ca-input-dropdown-multiselect/ca-input-dropdown-multiselect.component'; import { CaInputDropdownLabelsComponent } from './components/ca-input-dropdown-labels/ca-input-dropdown-labels.component'; import { CaInputDropdownLoadBrokerContactComponent } from './components/ca-input-dropdown-load-broker-contact/ca-input-dropdown-load-broker-contact.component'; import { CaInputDropdownLoadBrokerShipperComponent } from './components/ca-input-dropdown-load-broker-shipper/ca-input-dropdown-load-broker-shipper.component'; import { CaInputDropdownTextCounterComponent } from './components/ca-input-dropdown-text-counter/ca-input-dropdown-text-counter.component'; import { CaInputDropdownDoubleTextTemplateComponent } from './components/ca-input-dropdown-double-text-template/ca-input-dropdown-double-text-template.component'; import { CaInputDropdownTripleTextTemplateComponent } from './components/ca-input-dropdown-triple-text-template/ca-input-dropdown-triple-text-template.component'; import { CaInputDropdownDefaultTemplateComponent } from './components/ca-input-dropdown-default-template/ca-input-dropdown-default-template.component'; import { CaInputDropdownFuelFranchiseComponent } from './components/ca-input-dropdown-fuel-franchise/ca-input-dropdown-fuel-franchise.component'; import { CaInputDropdownDispatchComponent } from './components/ca-input-dropdown-dispatch/ca-input-dropdown-dispatch.component'; import { CaInputDropdownDetailsTemplateComponent } from './components/ca-input-dropdown-details-template/ca-input-dropdown-details-template.component'; import { CaInputDropdownPayrollTrucksComponent } from './components/ca-input-dropdown-payroll-trucks/ca-input-dropdown-payroll-trucks.component'; import { CaInputDropdownLoadDispatchesTtdComponentItem } from './components/ca-input-dropdown-load-dispatches-ttd/ca-input-dropdown-load-dispatches-ttd-item/ca-input-dropdown-load-dispatches-ttd-item'; import { CaInputDropdownLoadBrokerComponentItem } from './components/ca-input-dropdown-load-broker/ca-input-dropdown-load-broker-item/ca-input-dropdown-load-broker-item.component'; import { CaInputDropdownLoadBrokerShipperItemComponent } from './components/ca-input-dropdown-load-broker-shipper/ca-input-dropdown-load-broker-shipper-item/ca-input-dropdown-load-broker-shipper-item.component'; import { CaInputDropdownLoadDispatcherItemComponent } from './components/ca-input-dropdown-load-dispatcher/ca-input-dropdown-load-dispatcher-item/ca-input-dropdown-load-dispatcher-item.component'; // pipes import { FormControlPipe } from '../ca-input/pipes'; import { DropdownCountPipe, InputDropdownGetIconsPipe, InputDropdownMultiselectClassPipe, } from './pipes'; import { DropdownOptionsPipe } from './pipes/dropdown-options.pipe'; // modules import { AngularSvgIconModule } from 'angular-svg-icon'; // directives import { ControlValueAccessor, FormControl, FormsModule, NgControl, ReactiveFormsModule, } from '@angular/forms'; // models import { CommandsEvent } from '../ca-input/models'; // types import { AbstractControlWithNotFoundType, FormControlWithNotFoundType, } from './types'; import { SelectDropdownValueType } from './types/select-dropdown-value.type'; // svg routes import { InputDropdownSvgRoutes } from './utils/svg-routes/input-dropdown-svg-routes'; // enums import { DropdownStringEnum, DropdownTemplateTypeEnum } from './enums'; // helpers import { uuidv4 } from '../../utils/helpers'; // classes import { EventInputManager } from '../ca-input/base-classes/ca-input-event-manager'; // components import { InputTestComponent } from '../ca-input-test/input-test.component'; import { CaInputDropdownLoadBrokerContactItemComponent } from './components/ca-input-dropdown-load-broker-contact/ca-input-dropdown-load-broker-contact-item/ca-input-dropdown-load-broker-contact-item.component'; // config import { ICaInput } from '../ca-input-test/config'; // interfaces import { IOptionModel } from './interfaces/input-dropdown-option.interface'; @Component({ selector: 'ca-input-dropdown-test', templateUrl: './ca-input-dropdown-test.component.html', styleUrls: ['./ca-input-dropdown-test.component.scss'], encapsulation: ViewEncapsulation.None, providers: [FormControlPipe], animations: [inputDropdownAnimation('showHideDropdownOptions')], imports: [ // Module CommonModule, FormsModule, NgbPopoverModule, ReactiveFormsModule, NgbModule, AngularSvgIconModule, // Component CaInputDropdownSvgtextTemplateComponent, CaInputDropdownSvgtextDispatchTemplateComponent, CaInputDropdownGroupsComponent, CaInputDropdownMultiselectComponent, CaInputDropdownLabelsComponent, CaInputDropdownLoadBrokerComponent, CaInputDropdownLoadDispatchesTtdComponent, CaInputDropdownLoadDispatcherComponent, CaInputDropdownLoadBrokerContactComponent, CaInputDropdownLoadBrokerShipperComponent, CaInputDropdownTextCounterComponent, CaInputDropdownDoubleTextTemplateComponent, CaInputDropdownTripleTextTemplateComponent, CaInputDropdownDefaultTemplateComponent, CaInputDropdownFuelFranchiseComponent, CaInputDropdownDispatchComponent, CaInputDropdownDetailsTemplateComponent, CaInputDropdownPayrollTrucksComponent, CaInputDropdownSvgTemplateComponent, InputTestComponent, CaInputDropdownLoadDispatchesTtdComponentItem, CaInputDropdownLoadBrokerComponentItem, CaInputDropdownLoadBrokerShipperItemComponent, CaInputDropdownLoadDispatcherItemComponent, CaInputDropdownLoadBrokerContactItemComponent, // Pipes FormControlPipe, DropdownCountPipe, InputDropdownMultiselectClassPipe, DropdownOptionsPipe, InputDropdownGetIconsPipe, ], }) export class CaInputDropdownTestComponent implements OnInit, AfterViewInit, OnDestroy, ControlValueAccessor, AfterViewInit { @ViewChild('input') inputRef!: InputTestComponent; @ViewChild('t2') public popoverRef!: NgbPopover; @ViewChild('dropdownComponentRef', { static: false }) dropdownComponentRef: any; // Label input - value that will be showed inside input @Input() label!: keyof IOptionModel; // optionValue input - key value from object - Emit this if you want to set whole object @Input() optionValue!: string; public inputHoveredItem: number = -1; public isDropDownIsOpen: boolean = false; // different templates for body rendering public _template!: string; _canAddNew: boolean = false; @Input() set template(value: string) { this._template = value; } @Input() multiselectTemplate!: string; @Input() inputConfig!: ICaInput; @Input() set canAddNew(value: boolean) { this._canAddNew = value; } // ADD NEW item in options public get canAddNew() { return this._canAddNew; } @Input() canOpenModal!: boolean; // open modal with ADD NEW button // sort-template for different options public _sort!: string; @Input() set sort(value: string) { this._sort = value; } public get sort() { return this._sort; } // currently active item public _activeItem!: IOptionModel | null; @Input() set activeItem(value: IOptionModel | null) { this._activeItem = value; } public get activeItem() { return this._activeItem; } @Input() activeItemColor!: IOptionModel | null; // currently active color in dropdown @Input() labelMode!: 'Label' | 'Color'; // when send SVG, please premmaped object: add 'folder' | 'subfolder' public _options: IOptionModel[] = []; @Input() set options(values: IOptionModel[]) { this._options = values ? [...values] : []; if (this.firstWriteValue) { this.writeValueToFormControl(this.firstWriteValue); } } // MultiSelect Selected Items From Backend @Input() set preloadMultiselectItems(values: IOptionModel[]) { if (this.inputConfig.multiselectDropdown) { if (!values) { this.deleteAllMultiSelectItems(this.inputConfig.label); return; } if (values?.length) { values.forEach((item) => { this.onMultiselectSelect(item); }); } } } @Input() isDetailsPages!: boolean; // only for details pages @Input() isIncorrectValue!: boolean; // applicant review option @Input() isAddressDropdown!: boolean; // only for address dropdown @Output() selectedItem: EventEmitter = new EventEmitter(); @Output() selectedItems: EventEmitter = new EventEmitter(); @Output() selectedItemColor: EventEmitter = new EventEmitter(); @Output() selectedLabelMode: EventEmitter = new EventEmitter(); @Output() closeDropdown: EventEmitter = new EventEmitter(); @Output() saveItem: EventEmitter<{ data: IOptionModel | null; action: string; }> = new EventEmitter<{ data: IOptionModel | null; action: string }>(); @Output() incorrectEvent: EventEmitter = new EventEmitter(); @Output() placeholderIconEvent: EventEmitter = new EventEmitter(); @Output('pagination') paginationEvent: EventEmitter = new EventEmitter(); @Output('activeGroup') activeGroupEvent: EventEmitter = new EventEmitter(); @Output('clearInputEvent') clearInputEvent: EventEmitter = new EventEmitter(); @Output('searchInputEvent') searchInputEvent: EventEmitter = new EventEmitter(); // events public searchInputText = new EventInputManager( null, this.searchInputEvent ); // Copy of Options public originalOptions: IOptionModel[] = []; // Pagination public paginationNumber: number = 0; // Multiselect dropdown options public multiselectItems: IOptionModel[] = []; public isMultiSelectInputFocus: boolean = false; public multiSelectLabel!: string | undefined; public lastActiveMultiselectItem!: IOptionModel | null; // Add mode public isInAddMode: boolean = false; // Dropdown navigation with keyboard private dropdownPosition: number = -1; // Dropdown Cleartimeout public clearTimeoutDropdown: | string | number | ReturnType | undefined; public hoveringIndex!: number; public inputDropdownSvgRoutes = InputDropdownSvgRoutes; public dropdownTemplateTypeEnum = DropdownTemplateTypeEnum; // Destroy private destroy$ = new Subject(); constructor( @Self() public superControl: NgControl, private cdRef: ChangeDetectorRef, private renderer: Renderer2 ) { this.superControl.valueAccessor = this; } get getSuperControl() { return this.superControl.control; } public inputFormControl: FormControl = new FormControl(null); public lastValidOption!: IOptionModel; public firstWriteValue: any; writeValue(value: string | number): void { if (value) this.firstWriteValue = value; this.writeValueToFormControl(value); } public writeValueToFormControlTimer!: ReturnType; public writeValueToFormControl(value: string | number): void { clearTimeout(this.writeValueToFormControlTimer); this.writeValueToFormControlTimer = setTimeout(() => { const findInOptions = this.inputConfig.searchinGroupIndex ? this.getMainGroup() : this._options; let findOption = findInOptions.find( (option) => option[this.optionValue as keyof IOptionModel] === value ); let optionNotFound = false; if (!findOption && value) { if (this.optionValue) { findOption = { [this.optionValue]: this.firstWriteValue, [this.label]: this.firstWriteValue, }; } else { findOption = value as any; } optionNotFound = true; } if (findOption) { this.setDropdownValue({ option: findOption, setNotFoundError: optionNotFound, }); } else { this.setControlValue(); this._activeItem = null; } }, 100); } public setNotFoundInvalidFlag(value: boolean): void { if (this.getSuperControl) ( this.getSuperControl as AbstractControlWithNotFoundType ).notFoundInvalid = value; // Attach dynamic values (this.inputFormControl as FormControlWithNotFoundType).notFoundInvalid = value; // Attach dynamic values } public onTouched = () => {}; public onChange(_: any): void {} public registerOnChange(fn: any): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; } ngOnInit(): void { // Multiselect if (this.inputConfig.multiselectDropdown) { this.multiSelectLabel = this.inputConfig.label; } this.inputFormControl.valueChanges .pipe( distinctUntilChanged(), debounceTime(50), skip(1), takeUntil(this.destroy$) ) .subscribe((searchText) => { if (this.labelMode === 'Color') { return; } this.searchInputText.next(''); if (this.inputRef.inputElement.nativeElement.value) { this.searchInputText.next(searchText || ''); this.cdRef.detectChanges(); } }); } ngAfterViewInit() { if (this.inputConfig.autoFocus) { this.clearTimeoutDropdown = setTimeout(() => { this.popoverRef.open(); }, 450); } } public onScrollDropdown(event: EventTarget | null) { const target = event as HTMLElement; if (target.scrollTop + target.offsetHeight === target.scrollHeight) { this.paginationNumber += 1; this.paginationEvent.emit(this.paginationNumber); } } public onActiveItem(option: IOptionModel, group?: any): void { if ( this.canOpenModal && option?.[this.label].toString()?.toLowerCase() === 'add new' ) { this.popoverRef?.close(); this.searchInputText.next(''); this.selectedItem.emit({ id: 7655, name: DropdownStringEnum.ADD_NEW, canOpenModal: true, }); } else { this.setDropdownValue({ option }); } } public onActiveItemGroup(event: { option: IOptionModel; group: any }) { this.onActiveItem(event.option, event.group); } public onClearSearch(): void { this._activeItem = null; this.inputHoveredItem = -1; this.setControlValue(''); this.selectedItem.emit(null); } public clearDropdownLabel() { this._activeItem = null; this.activeItemColor = null; this.selectedItem.emit(null); this.selectedItemColor.emit(null); this.selectedLabelMode.emit('Label'); } public commandEvent(event: CommandsEvent) { if (event.action === 'Edit Input') { this.selectedLabelMode.emit('Color'); } if (event.action === 'Toggle Dropdown') { this.popoverRef.toggle(); } if (event.action === 'confirm' && event.mode === 'new') { this.addNewItem(); } if (event.action === 'confirm' && event.mode === 'edit') { this.updateItem(); } if (event.action === 'Placeholder Icon Event') { this.placeholderIconEvent.emit(true); } if (event.action === 'cancel') { this.saveItem.emit({ data: this._activeItem, action: 'cancel', }); this.selectedLabelMode.emit('Label'); } } public addNewItem(): void { this._activeItem = { id: parseInt(uuidv4()), name: this.getSuperControl!.value, }; //this.inputConfig.commands!.active = false; this.inputRef.isVisibleCommands = false; this.inputRef.isFocusInput = false; this.saveItem.emit({ data: this._activeItem, action: 'new' }); if (this.inputConfig.dropdownLabel) { this.selectedLabelMode.emit('Label'); this.inputRef.isTouchedInput = true; } } public updateItem(): void { if (this.inputConfig.dropdownLabel) { this._activeItem = { ...this._activeItem, name: this.getSuperControl?.value, colorId: this.activeItemColor ? this.activeItemColor.id : this._activeItem?.colorId, color: this.activeItemColor ? this.activeItemColor.name : this._activeItem?.color, code: this.activeItemColor ? this.activeItemColor.code : this._activeItem?.code, }; this.selectedLabelMode.emit('Label'); } else { this._activeItem = { ...this._activeItem, name: this.getSuperControl?.value, }; } this.saveItem.emit({ data: this._activeItem, action: 'edit', }); } public addNewConfig() { this.inputConfig = { ...this.inputConfig, commands: { active: true, type: 'confirm-cancel', firstCommand: { popup: { name: 'Confirm', backgroundColor: '#3074d3', }, name: 'confirm', svg: InputDropdownSvgRoutes.specConfirmSvg, }, secondCommand: { popup: { name: 'Cancel', backgroundColor: '#2f2f2f', }, name: 'cancel', svg: InputDropdownSvgRoutes.xClearSvg, }, }, placeholder: '', }; this.popoverRef?.close(); this.isInAddMode = true; this.clearTimeoutDropdown = setTimeout(() => { this.isInAddMode = false; }, 200); } public onIncorrectInput(event: boolean) { this.incorrectEvent.emit(event); } public identity(index: number, item: IOptionModel): number | undefined { return item.id; } public toggleNestedList(option: IOptionModel): void { if (option.open) { option.open = false; return; } this._options.filter((item) => (item.open = false)); option.open = !option.open; if (option.open) { this.activeGroupEvent.emit(option); } } public onBlurInput(event: boolean) { this.closeDropdown.emit(event); } public onClearInputEvent(event: boolean) { this.clearInputEvent.emit(event); if (event) { this.popoverRef?.close(); // label dropdown if (this.inputConfig.dropdownLabel) { this.clearDropdownLabel(); } // normal dropdown else { this.onClearSearch(); } } } public handleHiddenDropdown() { this.isDropDownIsOpen = false; this.inputFormControl.patchValue(this.dropdownValue || ''); this.inputRef.inputElement.nativeElement.blur(); this.searchInputText.next(''); this.inputRef.inputElement.nativeElement.value = ''; } public handleOpenDropdown() { this.isDropDownIsOpen = true; this.searchInputText.next(''); this.inputRef.inputElement.nativeElement.value = ''; } public showHideDropdown(action: boolean) { if (this.inputConfig.multiselectDropdown) { this.isMultiSelectInputFocus = action; } if (this.labelMode !== 'Color') { this.getSuperControl!.setValue(null); this.popoverRef?.close(); } // Details pages if ( this.inputConfig.customClass?.includes('details-pages') && !action ) { this.selectedItem.emit(this._activeItem); } } public getMainGroup() { let mainGroup: IOptionModel[] = []; this._options.map((groups) => { mainGroup.push( ...groups[ this.inputConfig.searchinGroupIndex as keyof IOptionModel ] ); }); return mainGroup; } public dropDownKeyNavigation({ keyCode, data, }: { keyCode: number; data: any; }) { // Navigate down if (keyCode === 40) { this.dropdownNavigation(1); } // Navigate up if (keyCode === 38) { this.dropdownNavigation(-1); } // Press 'enter' if (keyCode === 13) { let selectedItem; if (this.inputConfig.searchinGroupIndex) { let mainGroup = this.getMainGroup(); selectedItem = mainGroup[this.inputHoveredItem]; } else { selectedItem = this.inputHoveredItem > -1 ? this.dropdownComponentRef.options[ this.inputHoveredItem ] : { [this.label]: '' }; } if ( this.canOpenModal && selectedItem?.[this.label].toString()?.toLowerCase() === 'add new' ) { this.selectedItem.emit({ id: 7655, name: DropdownStringEnum.ADD_NEW, canOpenModal: true, }); } else if( selectedItem[this.label] ){ this.setDropdownValue({ option: selectedItem }); } } if (keyCode === 9) { if (!this.popoverRef.isOpen()) this.popoverRef?.open(); } } public setDropdownValue({ option, optionValue, label, setNotFoundError, }: SelectDropdownValueType) { this.setNotFoundInvalidFlag(setNotFoundError || false); this.lastValidOption = option; this.selectedItem.emit(option); const optValue = optionValue ? optionValue : this.optionValue; const lbel = label ? label : this.label; const controlValue = optValue ? option[optValue as keyof IOptionModel] : option; const inputValue = option[lbel as keyof IOptionModel] || controlValue; this._activeItem = option; this.setControlValue(controlValue, inputValue); this.popoverRef?.close(); this.searchInputText.next(''); } // ---------------------------------- Multiselect Dropdown ---------------------------------- public onMultiselectSelect(option: IOptionModel): void { this.isMultiSelectInputFocus = false; //this.inputConfig.label = undefined; if (this.multiselectItems.some((item) => item.id === option.id)) { return; } this._options = this._options.map((item) => { if (item.id === option.id) { return { ...item, active: true, }; } else { if (!item.active) { return { ...item, active: false, }; } else { return { ...item, active: true, }; } } }); this.multiselectItems = this._options.filter((item) => item.active); this.selectedItems.emit(this.multiselectItems); this._options = this._options.sort( (x, y) => Number(y.active) - Number(x.active) ); this.originalOptions = [...this._options]; this.lastActiveMultiselectItem = this._options .filter((item) => item.active) .slice(-1)[0]; if (this.inputRef) { this.inputRef.isFocusInput = false; this.inputRef.inputElement.nativeElement.blur(); } } public removeMultiSelectItem(index: number) { this._options = this.originalOptions.map((item) => { if (item.id === this.multiselectItems[index].id) { return { ...this.multiselectItems[index], active: false, }; } return item; }); this._options = this._options.sort( (x, y) => Number(y.active) - Number(x.active) ); this.originalOptions = this._options; this.multiselectItems.splice(index, 1); if (!this.multiselectItems.length) { this.lastActiveMultiselectItem = null; } else { this.lastActiveMultiselectItem = this._options .filter((item) => item.active) .slice(-1)[0]; } this.selectedItems.emit( this.multiselectItems.map((item) => { return { ...item }; }) ); } public deleteAllMultiSelectItems(currentLabel?: string) { this.multiselectItems = []; this._options = this._options.map((item) => { return { ...item, active: false, }; }); this.originalOptions = this._options; this.selectedItems.emit(null); this.lastActiveMultiselectItem = null; } public toggleMultiselectDropdown() { if (this.inputConfig.isDisabled) { return; } this.isMultiSelectInputFocus = !this.isMultiSelectInputFocus; if (this.isMultiSelectInputFocus) { this.clearTimeoutDropdown = setTimeout(() => { this.popoverRef.open(); }, 150); } else { this.inputRef.isFocusInput = false; this.popoverRef?.close(); } } // ---------------------------------- End ---------------------------------- public dropdownValue: string | undefined = ''; public setControlValue(value?: string, dropdownInputValue?: string) { this.onChange(value); // Notify Angular form about the change if (value) this.onTouched(); // Mark as touched this.dropdownValue = dropdownInputValue || value; this.inputFormControl.patchValue(dropdownInputValue || value); } /** * Navigate through dropdown with keyboard arrows */ private dropdownNavigation(step: number) { const nextStep = this.inputHoveredItem + step; if (nextStep > this.dropdownComponentRef.dropdownOption.length - 1) this.inputHoveredItem = 0; else if (nextStep < 0) this.inputHoveredItem = this.dropdownComponentRef.dropdownOption.length - 1; else this.inputHoveredItem = nextStep; } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); clearTimeout(this.clearTimeoutDropdown as ReturnType); } }