11 changed files with 358 additions and 158 deletions
@ -1,39 +0,0 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
/* eslint-disable */ |
|||
export const POSTAL_CODE_PATTERNS = { |
|||
'United States': '(\\d{5}([\\-]\\d{4})?)', |
|||
'Australia': '[0-9]{4}', |
|||
'Austria': '[0-9]{4}', |
|||
'Belgium': '[0-9]{4}', |
|||
'Brazil': '[0-9]{5}[\\-]?[0-9]{3}', |
|||
'Canada': '^(?!.*[DFIOQU])[A-VXY][0-9][A-Z][ -]?[0-9][A-Z][0-9]$', |
|||
'Denmark': '[0-9]{3,4}', |
|||
'Faroe Islands': '[0-9]{3,4}', |
|||
'Netherlands': '[1-9][0-9]{3}\\s?[a-zA-Z]{2}', |
|||
'Germany': '[0-9]{5}', |
|||
'Hungary': '[0-9]{4}', |
|||
'Italy': '[0-9]{5}', |
|||
'Japan': '\\d{3}-\\d{4}', |
|||
'Luxembourg': '(L\\s*(-|—|–))\\s*?[\\d]{4}', |
|||
'Poland': '[0-9]{2}\\-[0-9]{3}', |
|||
'Spain': '((0[1-9]|5[0-2])|[1-4][0-9])[0-9]{3}', |
|||
'Sweden': '\\d{3}\\s?\\d{2}', |
|||
'United Kingdom': '[A-Za-z]{1,2}[0-9Rr][0-9A-Za-z]? [0-9][ABD-HJLNP-UW-Zabd-hjlnp-uw-z]{2}' |
|||
}; |
|||
/* eslint-enable */ |
|||
|
|||
@ -0,0 +1,58 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2024 The Thingsboard Authors |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
|
|||
--> |
|||
<mat-form-field [formGroup]="countryFormGroup" class="mat-block" subscriptSizing="{{subscriptSizing}}"> |
|||
<mat-label>{{ labelText }}</mat-label> |
|||
<input matInput type="text" |
|||
#countryInput |
|||
formControlName="country" |
|||
(focusin)="onFocus()" |
|||
(blur)="onTouched()" |
|||
[required]="required" |
|||
[matAutocomplete]="countryAutocomplete"> |
|||
<button *ngIf="countryFormGroup.get('country').value && !disabled" |
|||
type="button" |
|||
matSuffix mat-icon-button aria-label="Clear" |
|||
(click)="clear()"> |
|||
<mat-icon class="material-icons">close</mat-icon> |
|||
</button> |
|||
<mat-autocomplete class="tb-autocomplete" |
|||
#countryAutocomplete="matAutocomplete" |
|||
[displayWith]="displayCountryFn"> |
|||
<mat-option *ngFor="let country of filteredCountries | async" [value]="country"> |
|||
<span style="font-size: 20px;padding-right: 4px;">{{country.flag}}</span> |
|||
<span [innerHTML]="country.name | highlight:searchText"></span> |
|||
</mat-option> |
|||
<mat-option *ngIf="!(filteredCountries | async)?.length" [value]="null" class="tb-not-found"> |
|||
<div class="tb-not-found-content" (click)="$event.stopPropagation()"> |
|||
<div *ngIf="!textIsNotEmpty(searchText); else searchNotEmpty"> |
|||
<span translate>contact.no-country-found</span> |
|||
</div> |
|||
<ng-template #searchNotEmpty> |
|||
<span> |
|||
{{ 'contact.no-country-matching' | translate: |
|||
{country: (searchText | truncate: true: 6: '...')} }} |
|||
</span> |
|||
</ng-template> |
|||
</div> |
|||
</mat-option> |
|||
</mat-autocomplete> |
|||
<mat-hint *ngIf="autocompleteHint">{{ autocompleteHint }}</mat-hint> |
|||
<mat-error *ngIf="countryFormGroup.get('country').hasError('required')"> |
|||
{{ requiredText }} |
|||
</mat-error> |
|||
</mat-form-field> |
|||
@ -0,0 +1,209 @@ |
|||
///
|
|||
/// Copyright © 2016-2024 The Thingsboard Authors
|
|||
///
|
|||
/// Licensed under the Apache License, Version 2.0 (the "License");
|
|||
/// you may not use this file except in compliance with the License.
|
|||
/// You may obtain a copy of the License at
|
|||
///
|
|||
/// http://www.apache.org/licenses/LICENSE-2.0
|
|||
///
|
|||
/// Unless required by applicable law or agreed to in writing, software
|
|||
/// distributed under the License is distributed on an "AS IS" BASIS,
|
|||
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|||
/// See the License for the specific language governing permissions and
|
|||
/// limitations under the License.
|
|||
///
|
|||
|
|||
import { Component, ElementRef, EventEmitter, forwardRef, Input, OnInit, Output, ViewChild } from '@angular/core'; |
|||
import { Country, CountryData } from '@shared/models/country.models'; |
|||
import { |
|||
ControlValueAccessor, |
|||
FormBuilder, |
|||
FormGroup, |
|||
NG_VALIDATORS, |
|||
NG_VALUE_ACCESSOR, |
|||
ValidationErrors, |
|||
Validator |
|||
} from '@angular/forms'; |
|||
import { isNotEmptyStr } from '@core/utils'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { debounceTime, distinctUntilChanged, map, share, switchMap, tap } from 'rxjs/operators'; |
|||
import { SubscriptSizing } from '@angular/material/form-field'; |
|||
import { coerceBoolean } from '@shared/decorators/coercion'; |
|||
import { TranslateService } from '@ngx-translate/core'; |
|||
|
|||
interface CountrySearchData extends Country { |
|||
searchText?: string; |
|||
} |
|||
|
|||
@Component({ |
|||
selector: 'tb-country-autocomplete', |
|||
templateUrl: 'country-autocomplete.component.html', |
|||
providers: [ |
|||
CountryData, |
|||
{ |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => CountryAutocompleteComponent), |
|||
multi: true |
|||
}, |
|||
{ |
|||
provide: NG_VALIDATORS, |
|||
useExisting: forwardRef(() => CountryAutocompleteComponent), |
|||
multi: true |
|||
} |
|||
] |
|||
}) |
|||
export class CountryAutocompleteComponent implements OnInit, ControlValueAccessor, Validator { |
|||
|
|||
@Input() |
|||
labelText = this.translate.instant('contact.country'); |
|||
|
|||
@Input() |
|||
requiredText = this.translate.instant('contact.country-required'); |
|||
|
|||
@Input() |
|||
autocompleteHint: string; |
|||
|
|||
@Input() |
|||
disabled: boolean; |
|||
|
|||
@Input() |
|||
@coerceBoolean() |
|||
required = false; |
|||
|
|||
@Input() |
|||
subscriptSizing: SubscriptSizing = 'fixed'; |
|||
|
|||
@ViewChild('countryInput', {static: true}) countryInput: ElementRef; |
|||
|
|||
@Output() |
|||
selectCountryCode = new EventEmitter<string>(); |
|||
|
|||
countryFormGroup: FormGroup; |
|||
|
|||
searchText = ''; |
|||
|
|||
filteredCountries: Observable<Array<Country>>; |
|||
|
|||
onTouched = () => { |
|||
}; |
|||
private propagateChange: (value: any) => void = () => { |
|||
}; |
|||
|
|||
private modelValue: Country; |
|||
|
|||
private allCountries: CountrySearchData[] = this.countryData.allCountries; |
|||
private initSearchData = false; |
|||
private dirty = false; |
|||
|
|||
constructor(private fb: FormBuilder, |
|||
private countryData: CountryData, |
|||
private translate: TranslateService) { |
|||
this.countryFormGroup = this.fb.group({ |
|||
country: [''] |
|||
}); |
|||
} |
|||
|
|||
ngOnInit(): void { |
|||
this.filteredCountries = this.countryFormGroup.get('country').valueChanges.pipe( |
|||
debounceTime(150), |
|||
tap(value => { |
|||
let modelValue: Country; |
|||
if (typeof value === 'string' || !value) { |
|||
modelValue = null; |
|||
} else { |
|||
modelValue = value; |
|||
} |
|||
this.updateView(modelValue); |
|||
if (value === null) { |
|||
this.clear(); |
|||
} |
|||
}), |
|||
map(value => value ? (typeof value === 'string' ? value : value.name) : ''), |
|||
distinctUntilChanged(), |
|||
switchMap(name => of(this.fetchCountries(name))), |
|||
share() |
|||
); |
|||
} |
|||
|
|||
registerOnChange(fn: any): void { |
|||
this.propagateChange = fn; |
|||
} |
|||
|
|||
registerOnTouched(fn: any): void { |
|||
this.onTouched = fn; |
|||
} |
|||
|
|||
setDisabledState(isDisabled: boolean): void { |
|||
this.disabled = isDisabled; |
|||
if (isDisabled) { |
|||
this.countryFormGroup.disable({emitEvent: false}); |
|||
} else { |
|||
this.countryFormGroup.enable({emitEvent: false}); |
|||
} |
|||
} |
|||
|
|||
validate(): ValidationErrors | null { |
|||
return this.countryFormGroup.valid ? null : { |
|||
countryFormGroup: false |
|||
}; |
|||
} |
|||
|
|||
writeValue(country: string) { |
|||
this.dirty = true; |
|||
|
|||
const findCountry = isNotEmptyStr(country) ? this.allCountries.find(item => item.name === country) : null; |
|||
|
|||
this.modelValue = findCountry || null; |
|||
this.countryFormGroup.get('country').patchValue(this.modelValue || '', { emitEvent: false }); |
|||
this.selectCountryCode.emit(this.modelValue ? this.modelValue.iso2 : null); |
|||
} |
|||
|
|||
displayCountryFn(country?: Country): string | undefined { |
|||
return country ? `${country.flag} ${country.name}` : undefined; |
|||
} |
|||
|
|||
onFocus() { |
|||
if (this.dirty) { |
|||
this.countryFormGroup.get('country').updateValueAndValidity({onlySelf: true}); |
|||
this.dirty = false; |
|||
} |
|||
} |
|||
|
|||
textIsNotEmpty(text: string): boolean { |
|||
return (text && text.length > 0); |
|||
} |
|||
|
|||
clear() { |
|||
this.countryFormGroup.get('country').patchValue('', {emitEvent: true}); |
|||
setTimeout(() => { |
|||
this.countryInput.nativeElement.blur(); |
|||
this.countryInput.nativeElement.focus(); |
|||
}, 0); |
|||
} |
|||
|
|||
private fetchCountries(searchText: string): Country[] { |
|||
this.searchText = searchText; |
|||
if (!this.initSearchData) { |
|||
this.allCountries.forEach(country => { |
|||
country.searchText = `${country.name} ${country.iso2}`.toLowerCase(); |
|||
}); |
|||
this.initSearchData = true; |
|||
} |
|||
if (isNotEmptyStr(searchText)) { |
|||
const filterValue = searchText.toLowerCase(); |
|||
return this.allCountries.filter(country => country.searchText.includes(filterValue)); |
|||
} |
|||
return this.allCountries; |
|||
} |
|||
|
|||
private updateView(value: Country | null) { |
|||
if (this.modelValue?.name !== value?.name) { |
|||
this.modelValue = value; |
|||
this.propagateChange(this.modelValue); |
|||
if (value) { |
|||
this.selectCountryCode.emit(value.iso2); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue