import { CommonModule } from '@angular/common'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, Self, ViewChild, ViewEncapsulation, ChangeDetectorRef, HostListener, } from '@angular/core'; import { catchError, debounceTime, distinctUntilChanged, filter, of, Subject, switchMap, takeUntil, tap, } from 'rxjs'; import { UntypedFormGroup, NgControl, ControlValueAccessor, FormsModule, } from '@angular/forms'; // Config import { ICaInput } from '../ca-input/config/ca-input.config'; // Components import { CaInputDropdownComponent } from '../ca-input-dropdown/ca-input-dropdown.component'; // Modules import { AngularSvgIconModule } from 'angular-svg-icon'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; // enums import { InputAddressCommandsStringEnum } from './enums/input-address-commands-string.enum'; import { InputAddressStopTypesStringEnum } from './enums/input-address-stop-types-string.enum'; import { InputAddressTypeStringEnum } from './enums/input-address-type-string.enum'; import { InputAddressLayersStringEnum } from './enums/input-address-layers-string.enum'; import { eInputBasicString } from './enums/input-address-basic-string.enum'; import { eSharedString } from '../../enums'; // models import { AddressData } from './models/address-data.model'; import { AddressList } from './models/address-list.model'; import { CommandsHandler } from './models/commands-handler.model'; import { AutocompleteSearchLayer } from '../../models/autocomplete-search-layer.model'; import { CaAppTooltipV2Component } from '../ca-app-tooltip-v2/ca-app-tooltip-v2.component'; import { HttpClientModule } from '@angular/common/http'; import { InputAddressCommandsString } from './models/input-address-commands-string.model'; import { SentAddressData } from './models/sent-address-data.model'; import { AddressListResponse } from '../../models/address-list-response.model'; @Component({ selector: 'app-ca-input-address-dropdown', templateUrl: './ca-input-address-dropdown.component.html', styleUrls: [ './ca-input-address-dropdown.component.scss', '../ca-input/ca-input.component.scss', ], encapsulation: ViewEncapsulation.None, imports: [ // Modules CommonModule, FormsModule, NgbModule, ReactiveFormsModule, AngularSvgIconModule, HttpClientModule, // Components CaInputDropdownComponent, CaAppTooltipV2Component, ] }) export class CaInputAddressDropdownComponent implements OnInit, ControlValueAccessor, OnDestroy { @ViewChild('inputDropdown') inputDropdown!: CaInputDropdownComponent; @Input() set placeholderType(value: string) { this.checkSearchLayers(value); } @Input() public set activeAddress(value: AddressList | null) { if (value) { this._activeAddress = value; let isValid = true; if ( this.searchLayers?.[0] === InputAddressLayersStringEnum.ADDRESS ) { const address = value?.address; if (address) isValid = this.checkAddressValidation(address); } if (!value || !isValid) this.getSuperControl!.setErrors({ invalid: true }); else this.getSuperControl!.setErrors(null); } } @Input() public set receivedAddressData(value: AddressData | null) { if (value) { this.currentAddressData = { address: value.address!, valid: value.address && value.longLat ? true : false, longLat: value.longLat, isParking: this.isParkingAddressSelected, }; if (this.currentAddressData!.valid) { this.getSuperControl!.setErrors(null); } this.selectedAddress.emit(this.currentAddressData!); } } @Input() public set receivedAddressList(value: AddressListResponse | null) { this._receivedAddressList = value; if (value?.addresses) { let parkingAddressesList: any = []; this.addresList = value.addresses.map( (item: string, indx: number) => { const parkingAddress = this.template === eSharedString.PARKING_LOWERCASE && this._parkingList?.length ? this.filterParkingByCity(item) : null; if (parkingAddress?.length) parkingAddressesList.push({ name: item, id: indx, parkingAddress, }); return { name: item, id: indx, }; } ); this.addresList = [...parkingAddressesList, ...this.addresList]; } } @Input() public set parkingList(value: any[]) { // leave any need backend for this this._parkingList = value; } @Output() public sentAddressValue: EventEmitter = new EventEmitter(); @Input() public inputConfig!: ICaInput; @Input() public commandHandler!: CommandsHandler; @Input() public isRouting: boolean = false; @Input() public closedBorder: boolean = false; @Input() public incorrectValue!: boolean; @Input() public hideEmptyLoaded: boolean = false; @Input() public addresList!: AddressList[]; @Input() public template!: string; @Output() selectedAddress: EventEmitter = new EventEmitter(); @Output() sentAddressData: EventEmitter = new EventEmitter(); @Output() closeDropdown: EventEmitter = new EventEmitter(); @Output() commandEvent: EventEmitter = new EventEmitter< AddressData | {} >(); @Output() changeFlag: EventEmitter = new EventEmitter(); @Output() incorrectEvent: EventEmitter = new EventEmitter(); @HostListener('document:keydown', ['$event']) handleKeyboardEvent(event: KeyboardEvent) { const key = event.key; if ( this.inputConfig.name == InputAddressTypeStringEnum.ROUTING_ADDRESS ) { if (key === InputAddressCommandsStringEnum.ENTER) { if (this.currentAddressData) { this.onCommands( event, InputAddressCommandsStringEnum.CONFIRM ); } } else if (key === InputAddressCommandsStringEnum.ESCAPE) { this.clearInput(event); } } } //Address data private searchLayers!: AutocompleteSearchLayer[]; public currentAddressData: AddressData | null = null; //Confg public addressExpanded: boolean = false; public chosenFromDropdown: boolean = false; private allowValidation: boolean = false; public stopType: string = InputAddressStopTypesStringEnum.EMPTY; private requestSent: boolean = false; public _receivedAddressList: AddressListResponse | null = null; public _activeAddress: AddressList | null = null; public _parkingList: any = null; //leave any for now private destroy$ = new Subject(); public addressForm!: UntypedFormGroup; public isParkingAddressSelected: boolean = false; constructor( @Self() public superControl: NgControl, private ref: ChangeDetectorRef ) { this.superControl.valueAccessor = this; } writeValue(_: any): void {} public registerOnChange(fn: any): void { this.onChange = fn; } public onChange(_: any): void {} public registerOnTouched(_: any): void {} ngOnInit(): void { this.initChangesListener(); this.inputConfig = { ...this.inputConfig, textTransform: 'capitalizedcase', }; } public initChangesListener(): void { this.getSuperControl?.valueChanges ?.pipe( distinctUntilChanged(), takeUntil(this.destroy$), tap((term) => { this.inputConfig = { ...this.inputConfig, loadingSpinner: { ...this.inputConfig.loadingSpinner, isLoading: true, size: eInputBasicString.SMALL, color: eInputBasicString.WHITE, }, }; if (!term) { this.inputConfig = { ...this.inputConfig, loadingSpinner: { ...this.inputConfig.loadingSpinner, isLoading: false, }, }; this.addresList = []; } else if ( term !== this.currentAddressData?.address.address && this.inputConfig.name === InputAddressTypeStringEnum.ROUTING_ADDRESS ) { this.currentAddressData = null; } if ( this.inputConfig.name !== InputAddressTypeStringEnum.ROUTING_ADDRESS && this.allowValidation && this.inputDropdown.inputRef.isFocusInput ) { this.requestSent = false; const addressData = { address: {}, valid: false, longLat: {}, }; this.selectedAddress.emit(addressData); } this.allowValidation = true; }), filter((term: string) => { return term?.length >= 3; }), debounceTime(500), switchMap((query: string) => { const params = { query: query, searchLayers: this.searchLayers, closedBorder: this.closedBorder, }; this.sentAddressData.next(params); return of(this.receivedAddressList).pipe( catchError(() => of([])) ); }) ) .subscribe(() => { let isValid = true; if ( this.searchLayers?.[0] === InputAddressLayersStringEnum.ADDRESS ) { const address = this._activeAddress?.address; if (address) isValid = this.checkAddressValidation(address); } if (!this._activeAddress || !isValid) this.getSuperControl!.setErrors({ invalid: true }); else this.getSuperControl!.setErrors(null); this.inputConfig = { ...this.inputConfig, loadingSpinner: { ...this.inputConfig.loadingSpinner, isLoading: false, }, }; this.ref.detectChanges(); }); } get getSuperControl() { return this.superControl.control as FormControl; } public onCloseDropdown(event: boolean): void { let isValid = true; setTimeout(() => { if ( this.searchLayers?.[0] === InputAddressLayersStringEnum.ADDRESS ) { const address = this._activeAddress?.address; if (address) isValid = this.checkAddressValidation(address); if (!isValid) this.getSuperControl!.setErrors({ invalid: true }); } if (!this.requestSent && isValid) this.getSuperControl!.setErrors({ invalid: true }); if ( this.getSuperControl!.value === this._activeAddress?.address && isValid ) this.getSuperControl!.setErrors(null); }, 200); this.closeDropdown.emit(event); } public getAddressData(address: string): void { this.requestSent = true; this.sentAddressValue.emit(address); } public onSelectDropdown(event: AddressList | null): void { this._activeAddress = event ? { ...event, address: event?.name } : null; this.isParkingAddressSelected = this._activeAddress?.parkingAddress?.length ? true : false; if (event?.name) { if ( this.searchLayers?.[0] === InputAddressLayersStringEnum.ADDRESS ) { const isValid = this.checkAddressValidation(event.name); if (isValid) { this.getAddressData(event.name); this.getSuperControl!.setValue(event.name); this.getSuperControl!.setErrors(null); } else { this.getSuperControl!.setErrors({ invalid: true, }); } } else { this.getAddressData(event.name); this.getSuperControl!.setValue(event.name); this.getSuperControl!.setErrors(null); } this.chosenFromDropdown = true; } else { this.onClearInputEvent(); this.currentAddressData = null; this.addresList = []; } this.inputDropdown?.popoverRef?.close(); } public onCommands( event: KeyboardEvent, type: InputAddressCommandsString ): void { event.preventDefault(); event.stopPropagation(); if ( (type === InputAddressCommandsStringEnum.CONFIRM && this.currentAddressData) || type === InputAddressCommandsStringEnum.CANCEL ) { this.currentAddressData!.type = type; this.commandEvent.emit(this.currentAddressData ?? {}); this.closeAddress(); this.clearInput(event); } } public addressExpand(): void { if (!this.addressExpanded) this.addressExpanded = true; } public closeAddress(): void { this.addressExpanded = false; } public clearInput(event: KeyboardEvent): void { this.currentAddressData = null; this.addresList = []; this.getSuperControl!.setValue(null); this._activeAddress = null; this.inputDropdown?.inputRef?.clearInput(event); this.chosenFromDropdown = false; } private checkSearchLayers(value: string): void { this.searchLayers = value === InputAddressTypeStringEnum.LONG_ADDRESS ? [InputAddressLayersStringEnum.ADDRESS] : value === InputAddressTypeStringEnum.SHORT_ADDRESS ? [InputAddressLayersStringEnum.LOCALITY] : []; } public changeStopType(): void { let flag = false; if (this.stopType === InputAddressStopTypesStringEnum.EMPTY) { this.stopType = InputAddressStopTypesStringEnum.LOADED; flag = true; } else { this.stopType = InputAddressStopTypesStringEnum.EMPTY; } this.changeFlag.emit(flag); if (!this.chosenFromDropdown) { this.inputDropdown?.inputRef?.input.nativeElement.focus(); setTimeout(() => { this.inputDropdown.inputRef.isFocusInput = true; }, 500); } } public onIncorrectInput(event: boolean): void { this.incorrectEvent.emit(event); } public onClearInputEvent(): void { if (this.inputConfig.isRequired) { setTimeout(() => { this.getSuperControl!.setErrors({ required: true }); }, 300); } const addressData = { address: {}, valid: false, longLat: {}, }; this.selectedAddress.emit(addressData); } private checkAddressValidation(address: string): boolean { const streetNum = /\d/; return streetNum.test(address) ? true : false; } private filterParkingByCity(searchString: string): any { const formattedSearchString = searchString .trim() .replace(/\s*,\s*/g, ', '); const isParking = this._parkingList.filter((parking: any) => { const { city, stateShortName, country } = parking.address || {}; const formattedAddress = `${city}, ${stateShortName}, ${country}`; if (formattedAddress === formattedSearchString) parking.address.isParking = true; return formattedAddress === formattedSearchString; }); return isParking; } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } }