@firestitch/address
Version:
1,038 lines (1,028 loc) • 131 kB
JavaScript
import { NgClass, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { inject, NgZone, ElementRef, ChangeDetectorRef, EventEmitter, DestroyRef, forwardRef, Component, ChangeDetectionStrategy, Input, Output, ViewChild, HostBinding, Optional, InjectionToken, NgModule, Injectable } from '@angular/core';
import * as i1 from '@angular/forms';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS, FormsModule, NgModel, ControlContainer, NgForm } from '@angular/forms';
import { MatAutocomplete, MatAutocompleteTrigger, MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButton, MatButtonModule } from '@angular/material/button';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatInput, MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import * as i3 from '@firestitch/clear';
import { FsClearModule } from '@firestitch/clear';
import * as i3$2 from '@firestitch/dialog';
import { FsDialogModule } from '@firestitch/dialog';
import * as i2 from '@firestitch/form';
import { FsFormModule } from '@firestitch/form';
import * as i3$1 from '@firestitch/map';
import { FsMap, FsMapComponent, FsMapModule } from '@firestitch/map';
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { guid } from '@firestitch/common';
import { fromEvent, of, from, Subject, Observable } from 'rxjs';
import { filter, map, switchMap, debounceTime, tap, distinctUntilChanged, takeUntil, delay } from 'rxjs/operators';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatFormField, MatLabel, MatHint } from '@angular/material/form-field';
import { MatOption } from '@angular/material/core';
import { controlContainerFactory } from '@firestitch/core';
import { isObject, cloneDeep } from 'lodash-es';
import * as i1$1 from '@firestitch/autocomplete';
import { FsAutocompleteComponent, FsAutocompleteModule } from '@firestitch/autocomplete';
import * as i1$2 from '@firestitch/autocomplete-chips';
import { FsAutocompleteChipsModule } from '@firestitch/autocomplete-chips';
import { CdkScrollable } from '@angular/cdk/scrolling';
var AddressFormat;
(function (AddressFormat) {
AddressFormat["OneLine"] = "oneline";
AddressFormat["TwoLine"] = "twoline";
AddressFormat["Summary"] = "summary";
})(AddressFormat || (AddressFormat = {}));
function addressIsEmpty(value) {
return !value
|| (!value.name
&& !value.street
&& !value.city
&& !value.region
&& !value.zip
&& !value.country
&& !value.address2
&& !value.address3);
}
function createEmptyAddress() {
return {
name: '',
description: '',
country: '',
region: '',
city: '',
street: '',
address2: '',
address3: '',
zip: '',
lat: null,
lng: null,
};
}
function extractUnit(text) {
const primaryUnitRegex = /((unit|apt|#|apartment|building|floor|suite|room|department|po\s*box)\s?#?\d+([,.])?(\w)?([,.])?)/gi;
const secondaryUnitRegex = /-\s?\d+/gi;
const nonWordOrDigitChar = /^[^a-z\d]*|[^a-z\d]*$/gi;
let unit = [
...(text.match(primaryUnitRegex) || []),
...(text.match(secondaryUnitRegex) || []),
][0];
if (unit) {
text = text
.replace(unit, '')
.trim();
unit = unit
.replace(nonWordOrDigitChar, '')
.replace('unit', 'Unit')
.trim();
}
text = text.replace(nonWordOrDigitChar, '').trim();
return {
text,
unit,
};
}
function googlePlaceToFsAddress(result, config) {
const address = createEmptyAddress();
let countryLongName, regionLongName, streetShortName;
address.lat = result.location.lat();
address.lng = result.location.lng();
address.description = result.formattedAddress;
// Finding different parts of address
result.addressComponents.forEach((item) => {
if (item.types.some(type => type === 'country')) {
address.country = item.shortText;
countryLongName = item.longText;
}
if (item.types.some(type => type === 'administrative_area_level_1')) {
address.region = item.shortText;
regionLongName = item.longText;
}
if (item.types.some(type => type === 'locality' || type === 'political')) {
address.city = item.longText;
}
if (item.types.some(type => type === 'postal_code')) {
address.zip = item.longText;
}
});
// Address.Street consists from number and street
const streetNumber = result.addressComponents
.find(el => el.types.some(type => type === 'street_number'));
if (streetNumber) {
address.street = streetNumber.longText + ' ';
streetShortName = streetNumber.longText + ' ';
}
else {
const match = address.description.match(/^[\d-]+/);
if (match) {
address.street = match[0] + ' ';
streetShortName = match[0] + ' ';
}
}
const streetAddress = result.addressComponents
.find(el => el.types.some(type => type === 'route'));
if (streetAddress) {
if (!address.street) {
address.street = streetAddress.longText;
streetShortName = streetAddress.shortText;
}
else {
address.street += streetAddress.longText;
streetShortName += streetAddress.shortText;
}
}
// Checking correct place NAME
if (address.country !== result.displayName
&& countryLongName !== result.displayName
&& address.region !== result.displayName
&& regionLongName !== result.displayName
&& address.city !== result.displayName
&& streetShortName !== result.displayName
&& address.zip !== result.displayName
&& address.street !== result.displayName) {
if (config.name && config.name.visible !== false) {
address.name = result.displayName;
}
}
else {
address.name = '';
}
return address;
}
class FsAddressAutocompleteComponent {
_map = inject(FsMap);
_ngZone = inject(NgZone);
_fm = inject(FocusMonitor);
_elementRef = inject(ElementRef);
_cdRef = inject(ChangeDetectorRef);
static nextId = 0;
format = AddressFormat.TwoLine;
readonly = false;
showClear = true;
suggestions = false;
set config(value) {
this._config = value;
if (this._config) {
this.required =
((this.config.name && this.config.name.required) ||
(this.config.country && this.config.country.required) ||
(this.config.region && this.config.region.required) ||
(this.config.city && this.config.city.required) ||
(this.config.street && this.config.street.required) ||
(this.config.address2 && this.config.address2.required) ||
(this.config.address3 && this.config.address3.required) ||
(this.config.zip && this.config.zip.required));
}
}
get config() {
return this._config;
}
addressChange = new EventEmitter();
addressManual = new EventEmitter();
searchElement;
autoCompleteRef;
autocompleteTrigger;
id = `fs-address-autocomplete-${FsAddressAutocompleteComponent.nextId++}`;
inputAddress = this._defaultInputAddress();
googleSuggestions = [];
googlePlace = null;
onChange;
onTouched;
focused = false;
autocompleteName = `search-${guid('xxxxxxxx')}`;
_config = {};
_address = {};
_searchText = '';
_disabled = false;
_required = false;
_placeholder;
_destroyRef = inject(DestroyRef);
set value(value) {
this._address = value;
this.onChange(this._address);
}
get value() {
return this._address;
}
get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = coerceBooleanProperty(value);
}
get required() {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
}
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
}
get shouldLabelFloat() {
return this.focused;
}
get empty() {
return addressIsEmpty(this.value);
}
ngOnInit() {
this._initGoogleMap();
this._listenUserTyping();
this._listenAutocompleteSelection();
this._registerFocusMonitor();
}
writeValue(value) {
this._address = value;
this.inputAddress = value;
this._cdRef.markForCheck();
}
onContainerClick(event) {
if (event.target.tagName.toLowerCase() !== 'input') {
this.searchElement.nativeElement.focus();
this._elementRef.nativeElement.querySelector('input').focus();
}
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
displayWith = (value) => {
if (value && typeof value === 'object') {
return this.value?.street;
}
else if (!this.empty) {
return '';
}
};
validate(control) {
const validationErrors = {};
const requiredField = [];
const parts = ['name', 'street', 'city', 'region', 'zip', 'country', 'lat', 'lng'];
if (this.required && this.empty) {
validationErrors.required = true;
}
if (!this.empty) {
parts.forEach((part) => {
if (this.config[part] && this.config[part].required && !this.value[part]) {
requiredField.push([part]);
}
});
if (((this.config.lat && this.config.lat.required) ||
(this.config.lng && this.config.lng.required)) &&
(!this.value.lat || !this.value.lat)) {
validationErrors.invalid = 'position on map';
}
if (requiredField.length) {
if (requiredField.length === 1) {
validationErrors.invalid = `The ${requiredField[0]} is required`;
}
else {
const last = requiredField.pop();
validationErrors.invalid = `The ${requiredField.join(', ')} and ${last} are required`;
}
}
}
return validationErrors;
}
clear() {
this.inputAddress = this._defaultInputAddress();
this.value = createEmptyAddress();
this.addressChange.emit(null);
this._clearPredictions();
setTimeout(() => {
this.autocompleteTrigger.openPanel();
});
}
manual(value) {
this.addressManual.emit(value);
}
// Search input can't be null. We implemented required validation to show asterisk if needed
// But general validation placed in another level and not depends of this input
// This hack allow us to show asterisk but disable extra validation
_defaultInputAddress() {
return null;
}
_listenUserTyping() {
this._ngZone.runOutsideAngular(() => {
fromEvent(this.searchElement.nativeElement, 'keydown')
.pipe(filter((event) => event.code === 'Tab'), map(() => this.autocompleteTrigger.activeOption?.value), filter((place) => !!place && this.googleSuggestions.length !== 0), switchMap((place) => this._placeToAddress(place)), takeUntilDestroyed(this._destroyRef))
.subscribe((address) => {
this._selectAddress(address);
this._clearPredictions();
});
fromEvent(this.searchElement.nativeElement, 'keyup')
.pipe(debounceTime(200), filter((event) => {
return event.code !== 'Enter' && event.code !== 'Tab';
}), map((event) => {
return event.target.value;
}), tap((text) => {
if (!text) {
this._clearPredictions();
}
}), filter((value) => !!value), tap((value) => {
this._searchText = value;
if (!value) {
this._address = {
...this._address,
street: value,
};
this._selectAddress(this._address);
}
}), distinctUntilChanged(), switchMap((text) => {
return this._getPlaceSuggestions(text);
}), takeUntilDestroyed(this._destroyRef))
.subscribe((suggestions) => {
this._ngZone.run(() => {
this.googleSuggestions = [
...suggestions,
];
this._cdRef.markForCheck();
});
});
});
}
_clearPredictions() {
this.googleSuggestions = [];
this._cdRef.markForCheck();
}
_selectAddress(address) {
this.value = address;
this.addressChange.emit(address);
}
_placeToAddress(suggestion) {
if (!suggestion || !this.googlePlace) {
return of(null);
}
const place = suggestion.placePrediction.toPlace();
const fetchFieldsRequestOptions = {
fields: [
'displayName',
'location',
'addressComponents',
'formattedAddress',
],
};
return from(place.fetchFields(fetchFieldsRequestOptions))
.pipe(map(({ place }) => {
if (!place) {
return {};
}
return googlePlaceToFsAddress(place, this.config);
}));
}
_listenAutocompleteSelection() {
this.autoCompleteRef.optionSelected
.pipe(map((event) => event.option),
// used to get the value from input when "manual" option selected
filter((option) => {
if (option.value instanceof google.maps.places.AutocompleteSuggestion) {
return true;
}
this.manual(option.value.value);
return false;
}), map((option) => {
return option.value;
}), switchMap((value) => this._placeToAddress(value)), takeUntilDestroyed(this._destroyRef))
.subscribe((address) => {
this._ngZone.run(() => {
this.searchElement.nativeElement.blur();
this.value = address;
const { unit } = extractUnit(this._searchText);
if (unit) {
address.address2 = unit;
}
this.addressChange.emit(address);
this.inputAddress = address;
this._cdRef.markForCheck();
});
});
}
_initGoogleMap() {
this._ngZone.runOutsideAngular(() => {
this._map.loaded$
.pipe(takeUntilDestroyed(this._destroyRef))
.subscribe(() => {
this.googlePlace = new google.maps.places.Place({ id: this.id });
});
});
}
_getPlaceSuggestions(address) {
const { text } = extractUnit(address);
const placesRequest = google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions({ input: text });
return placesRequest
.then((result) => {
return result.suggestions;
})
.catch(() => {
return [];
});
}
_registerFocusMonitor() {
this._fm.monitor(this._elementRef, true)
.pipe(filter(() => !this.disabled), takeUntilDestroyed(this._destroyRef))
.subscribe((origin) => {
this.focused = !!origin;
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsAddressAutocompleteComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: FsAddressAutocompleteComponent, isStandalone: true, selector: "fs-address-autocomplete", inputs: { format: "format", readonly: "readonly", showClear: "showClear", suggestions: "suggestions", config: "config", disabled: "disabled", required: "required", placeholder: "placeholder" }, outputs: { addressChange: "addressChange", addressManual: "addressManual" }, host: { properties: { "id": "this.id", "class.floating": "this.shouldLabelFloat" } }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FsAddressAutocompleteComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FsAddressAutocompleteComponent),
multi: true,
},
], viewQueries: [{ propertyName: "searchElement", first: true, predicate: ["searchInput"], descendants: true, read: ElementRef, static: true }, { propertyName: "autoCompleteRef", first: true, predicate: MatAutocomplete, descendants: true, static: true }, { propertyName: "autocompleteTrigger", first: true, predicate: MatAutocompleteTrigger, descendants: true, static: true }], ngImport: i0, template: "<mat-form-field [floatLabel]=\"empty ? 'auto' : 'always'\">\n <mat-label>\n {{ placeholder }}\n </mat-label>\n <ng-content></ng-content>\n <input\n matInput\n type=\"text\"\n autocomplete=\"off\"\n [(ngModel)]=\"inputAddress\"\n [matAutocomplete]=\"autocomplete\"\n [name]=\"autocompleteName\"\n [disabled]=\"disabled\"\n [fsClear]=\"showClear && !empty && !disabled && !readonly\"\n (cleared)=\"clear()\"\n #searchInput=\"ngModel\">\n <mat-autocomplete\n [displayWith]=\"displayWith\"\n autoActiveFirstOption\n [class]=\"'fs-autocomplete-pane'\"\n #autocomplete=\"matAutocomplete\">\n @for (option of googleSuggestions; track option) {\n <mat-option [value]=\"option\">\n {{ option.placePrediction.text.text }}\n </mat-option>\n }\n @if (!config.hideEnterManually) {\n <div class=\"static-options\">\n <mat-option [value]=\"{ manual: true, value: searchInput.value }\">\n Enter address manually\n </mat-option>\n </div>\n }\n </mat-autocomplete>\n @if (config.hint) {\n <mat-hint>\n {{ config.hint }}\n </mat-hint>\n }\n</mat-form-field>", styles: [".static-options{position:sticky;bottom:0;width:100%;background:#fff;border-top:1px solid #e0e0e0}mat-form-field{width:100%}mat-option ::ng-deep mat-pseudo-checkbox{display:none}:host(.ng-invalid.ng-dirty) ::ng-deep .mat-form-field-outline{color:#f44336}:host(.ng-invalid.ng-dirty) ::ng-deep .mat-form-field-outline-thick{opacity:1}\n"], dependencies: [{ kind: "component", type: MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: MatLabel, selector: "mat-label" }, { kind: "directive", type: MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly"], exportAs: ["matInput"] }, { kind: "ngmodule", type: FormsModule }, { 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: MatAutocompleteTrigger, selector: "input[matAutocomplete], textarea[matAutocomplete]", inputs: ["matAutocomplete", "matAutocompletePosition", "matAutocompleteConnectedTo", "autocomplete", "matAutocompleteDisabled"], exportAs: ["matAutocompleteTrigger"] }, { kind: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2.FsFormNoFsValidatorsDirective, selector: "[ngModel]:not([required]):not([fsFormRequired]):not([fsFormCompare]):not([fsFormDateRange]):not([fsFormEmail]):not([fsFormEmails]):not([fsFormFunction]):not([fsFormGreater]):not([fsFormGreaterEqual]):not([fsFormInteger]):not([fsFormLesser]):not([fsFormMax]):not([fsFormMaxLength]):not([fsFormMin]):not([fsFormMinLength]):not([fsFormNumeric]):not([fsFormPattern]):not([fsFormPhone]):not([fsFormUrl]):not([validate])" }, { kind: "ngmodule", type: FsClearModule }, { kind: "component", type: i3.FsClearComponent, selector: "[fsClear]", inputs: ["ngModel", "visible", "fsClear"], outputs: ["ngModelChange", "cleared"] }, { kind: "component", type: MatAutocomplete, selector: "mat-autocomplete", inputs: ["aria-label", "aria-labelledby", "displayWith", "autoActiveFirstOption", "autoSelectActiveOption", "requireSelection", "panelWidth", "disableRipple", "class", "hideSingleSelectionIndicator"], outputs: ["optionSelected", "opened", "closed", "optionActivated"], exportAs: ["matAutocomplete"] }, { kind: "component", type: MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "directive", type: MatHint, selector: "mat-hint", inputs: ["align", "id"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsAddressAutocompleteComponent, decorators: [{
type: Component,
args: [{ selector: 'fs-address-autocomplete', changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FsAddressAutocompleteComponent),
multi: true,
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => FsAddressAutocompleteComponent),
multi: true,
},
], standalone: true, imports: [
MatFormField,
MatLabel,
MatInput,
FormsModule,
MatAutocompleteTrigger,
FsFormModule,
FsClearModule,
MatAutocomplete,
MatOption,
MatHint,
], template: "<mat-form-field [floatLabel]=\"empty ? 'auto' : 'always'\">\n <mat-label>\n {{ placeholder }}\n </mat-label>\n <ng-content></ng-content>\n <input\n matInput\n type=\"text\"\n autocomplete=\"off\"\n [(ngModel)]=\"inputAddress\"\n [matAutocomplete]=\"autocomplete\"\n [name]=\"autocompleteName\"\n [disabled]=\"disabled\"\n [fsClear]=\"showClear && !empty && !disabled && !readonly\"\n (cleared)=\"clear()\"\n #searchInput=\"ngModel\">\n <mat-autocomplete\n [displayWith]=\"displayWith\"\n autoActiveFirstOption\n [class]=\"'fs-autocomplete-pane'\"\n #autocomplete=\"matAutocomplete\">\n @for (option of googleSuggestions; track option) {\n <mat-option [value]=\"option\">\n {{ option.placePrediction.text.text }}\n </mat-option>\n }\n @if (!config.hideEnterManually) {\n <div class=\"static-options\">\n <mat-option [value]=\"{ manual: true, value: searchInput.value }\">\n Enter address manually\n </mat-option>\n </div>\n }\n </mat-autocomplete>\n @if (config.hint) {\n <mat-hint>\n {{ config.hint }}\n </mat-hint>\n }\n</mat-form-field>", styles: [".static-options{position:sticky;bottom:0;width:100%;background:#fff;border-top:1px solid #e0e0e0}mat-form-field{width:100%}mat-option ::ng-deep mat-pseudo-checkbox{display:none}:host(.ng-invalid.ng-dirty) ::ng-deep .mat-form-field-outline{color:#f44336}:host(.ng-invalid.ng-dirty) ::ng-deep .mat-form-field-outline-thick{opacity:1}\n"] }]
}], propDecorators: { format: [{
type: Input
}], readonly: [{
type: Input
}], showClear: [{
type: Input
}], suggestions: [{
type: Input
}], config: [{
type: Input
}], addressChange: [{
type: Output
}], addressManual: [{
type: Output
}], searchElement: [{
type: ViewChild,
args: ['searchInput', { static: true, read: ElementRef }]
}], autoCompleteRef: [{
type: ViewChild,
args: [MatAutocomplete, { static: true }]
}], autocompleteTrigger: [{
type: ViewChild,
args: [MatAutocompleteTrigger, { static: true }]
}], id: [{
type: HostBinding
}], disabled: [{
type: Input
}], required: [{
type: Input
}], placeholder: [{
type: Input
}], shouldLabelFloat: [{
type: HostBinding,
args: ['class.floating']
}] } });
const Countries = [
{ code: 'AF', name: 'Afghanistan' },
{ code: 'AL', name: 'Albania' },
{ code: 'DZ', name: 'Algeria' },
{ code: 'AS', name: 'American Samoa' },
{ code: 'AD', name: 'Andorra' },
{ code: 'AO', name: 'Angola' },
{ code: 'AI', name: 'Anguilla' },
{ code: 'AQ', name: 'Antarctica' },
{ code: 'AG', name: 'Antigua and Barbuda' },
{ code: 'AR', name: 'Argentina' },
{ code: 'AM', name: 'Armenia' },
{ code: 'AW', name: 'Aruba' },
{ code: 'AU', name: 'Australia' },
{ code: 'AT', name: 'Austria' },
{ code: 'AZ', name: 'Azerbaijan' },
{ code: 'BS', name: 'Bahamas' },
{ code: 'BH', name: 'Bahrain' },
{ code: 'BD', name: 'Bangladesh' },
{ code: 'BB', name: 'Barbados' },
{ code: 'BY', name: 'Belarus' },
{ code: 'BE', name: 'Belgium' },
{ code: 'BZ', name: 'Belize' },
{ code: 'BJ', name: 'Benin' },
{ code: 'BM', name: 'Bermuda' },
{ code: 'BT', name: 'Bhutan' },
{ code: 'BO', name: 'Bolivia' },
{ code: 'BA', name: 'Bosnia and Herzegovina' },
{ code: 'BW', name: 'Botswana' },
{ code: 'BV', name: 'Bouvet Island' },
{ code: 'BR', name: 'Brazil' },
{ code: 'IO', name: 'British Indian Ocean Territory' },
{ code: 'BN', name: 'Brunei Darussalam' },
{ code: 'BG', name: 'Bulgaria' },
{ code: 'BF', name: 'Burkina Faso' },
{ code: 'BI', name: 'Burundi' },
{ code: 'KH', name: 'Cambodia' },
{ code: 'CM', name: 'Cameroon' },
{
code: 'CA', name: 'Canada',
regionLabel: 'Province',
regions: [
{ code: 'AB', name: 'Alberta' },
{ code: 'BC', name: 'British Columbia' },
{ code: 'MB', name: 'Manitoba' },
{ code: 'NB', name: 'New Brunswick' },
{ code: 'NL', name: 'Newfoundland and Labrador' },
{ code: 'NT', name: 'Northwest Territories' },
{ code: 'NS', name: 'Nova Scotia' },
{ code: 'NU', name: 'Nunavut' },
{ code: 'ON', name: 'Ontario' },
{ code: 'PE', name: 'Prince Edward Island' },
{ code: 'QC', name: 'Quebec' },
{ code: 'SK', name: 'Saskatchewan' },
{ code: 'YT', name: 'Yukon Territory' },
],
},
{ code: 'CV', name: 'Cape Verde' },
{ code: 'KY', name: 'Cayman Islands' },
{ code: 'CF', name: 'Central African Republic' },
{ code: 'TD', name: 'Chad' },
{ code: 'CL', name: 'Chile' },
{ code: 'CN', name: 'China' },
{ code: 'CX', name: 'Christmas Island' },
{ code: 'CC', name: 'Cocos (Keeling) Islands' },
{ code: 'CO', name: 'Colombia' },
{ code: 'KM', name: 'Comoros' },
{ code: 'CG', name: 'Congo' },
{ code: 'CD', name: 'Congo, the Democratic Republic of the' },
{ code: 'CK', name: 'Cook Islands' },
{ code: 'CR', name: 'Costa Rica' },
{ code: 'CI', name: 'Cote D\'Ivoire' },
{ code: 'HR', name: 'Croatia' },
{ code: 'CU', name: 'Cuba' },
{ code: 'CY', name: 'Cyprus' },
{ code: 'CZ', name: 'Czech Republic' },
{ code: 'DK', name: 'Denmark' },
{ code: 'DJ', name: 'Djibouti' },
{ code: 'DM', name: 'Dominica' },
{ code: 'DO', name: 'Dominican Republic' },
{ code: 'EC', name: 'Ecuador' },
{ code: 'EG', name: 'Egypt' },
{ code: 'SV', name: 'El Salvador' },
{ code: 'GQ', name: 'Equatorial Guinea' },
{ code: 'ER', name: 'Eritrea' },
{ code: 'EE', name: 'Estonia' },
{ code: 'ET', name: 'Ethiopia' },
{ code: 'FK', name: 'Falkland Islands (Malvinas)' },
{ code: 'FO', name: 'Faroe Islands' },
{ code: 'FJ', name: 'Fiji' },
{ code: 'FI', name: 'Finland' },
{ code: 'FR', name: 'France' },
{ code: 'GF', name: 'French Guiana' },
{ code: 'PF', name: 'French Polynesia' },
{ code: 'TF', name: 'French Southern Territories' },
{ code: 'GA', name: 'Gabon' },
{ code: 'GM', name: 'Gambia' },
{ code: 'GE', name: 'Georgia' },
{ code: 'DE', name: 'Germany' },
{ code: 'GH', name: 'Ghana' },
{ code: 'GI', name: 'Gibraltar' },
{ code: 'GR', name: 'Greece' },
{ code: 'GL', name: 'Greenland' },
{ code: 'GD', name: 'Grenada' },
{ code: 'GP', name: 'Guadeloupe' },
{ code: 'GU', name: 'Guam' },
{ code: 'GT', name: 'Guatemala' },
{ code: 'GN', name: 'Guinea' },
{ code: 'GW', name: 'Guinea-Bissau' },
{ code: 'GY', name: 'Guyana' },
{ code: 'HT', name: 'Haiti' },
{ code: 'HM', name: 'Heard Island and Mcdonald Islands' },
{ code: 'VA', name: 'Holy See (Vatican City State)' },
{ code: 'HN', name: 'Honduras' },
{ code: 'HK', name: 'Hong Kong' },
{ code: 'HU', name: 'Hungary' },
{ code: 'IS', name: 'Iceland' },
{ code: 'IN', name: 'India' },
{ code: 'ID', name: 'Indonesia' },
{ code: 'IR', name: 'Iran, Islamic Republic of' },
{ code: 'IQ', name: 'Iraq' },
{ code: 'IE', name: 'Ireland' },
{ code: 'IL', name: 'Israel' },
{ code: 'IT', name: 'Italy' },
{ code: 'JM', name: 'Jamaica' },
{ code: 'JP', name: 'Japan' },
{ code: 'JO', name: 'Jordan' },
{ code: 'KZ', name: 'Kazakhstan' },
{ code: 'KE', name: 'Kenya' },
{ code: 'KI', name: 'Kiribati' },
{ code: 'KP', name: 'Korea, Democratic People\'s Republic of' },
{ code: 'KR', name: 'Korea, Republic of' },
{ code: 'KW', name: 'Kuwait' },
{ code: 'KG', name: 'Kyrgyzstan' },
{ code: 'LA', name: 'Lao People\'s Democratic Republic' },
{ code: 'LV', name: 'Latvia' },
{ code: 'LB', name: 'Lebanon' },
{ code: 'LS', name: 'Lesotho' },
{ code: 'LR', name: 'Liberia' },
{ code: 'LY', name: 'Libyan Arab Jamahiriya' },
{ code: 'LI', name: 'Liechtenstein' },
{ code: 'LT', name: 'Lithuania' },
{ code: 'LU', name: 'Luxembourg' },
{ code: 'MO', name: 'Macao' },
{ code: 'MK', name: 'Macedonia' },
{ code: 'MG', name: 'Madagascar' },
{ code: 'MW', name: 'Malawi' },
{ code: 'MY', name: 'Malaysia' },
{ code: 'MV', name: 'Maldives' },
{ code: 'ML', name: 'Mali' },
{ code: 'MT', name: 'Malta' },
{ code: 'MH', name: 'Marshall Islands' },
{ code: 'MQ', name: 'Martinique' },
{ code: 'MR', name: 'Mauritania' },
{ code: 'MU', name: 'Mauritius' },
{ code: 'YT', name: 'Mayotte' },
{ code: 'MX', name: 'Mexico' },
{ code: 'FM', name: 'Micronesia, Federated States of' },
{ code: 'MD', name: 'Moldova, Republic of' },
{ code: 'MC', name: 'Monaco' },
{ code: 'MN', name: 'Mongolia' },
{ code: 'MS', name: 'Montserrat' },
{ code: 'MA', name: 'Morocco' },
{ code: 'MZ', name: 'Mozambique' },
{ code: 'MM', name: 'Myanmar' },
{ code: 'NA', name: 'Namibia' },
{ code: 'NR', name: 'Nauru' },
{ code: 'NP', name: 'Nepal' },
{ code: 'NL', name: 'Netherlands' },
{ code: 'AN', name: 'Netherlands Antilles' },
{ code: 'NC', name: 'New Caledonia' },
{ code: 'NZ', name: 'New Zealand' },
{ code: 'NI', name: 'Nicaragua' },
{ code: 'NE', name: 'Niger' },
{ code: 'NG', name: 'Nigeria' },
{ code: 'NU', name: 'Niue' },
{ code: 'NF', name: 'Norfolk Island' },
{ code: 'MP', name: 'Northern Mariana Islands' },
{ code: 'NO', name: 'Norway' },
{ code: 'OM', name: 'Oman' },
{ code: 'PK', name: 'Pakistan' },
{ code: 'PW', name: 'Palau' },
{ code: 'PS', name: 'Palestinian Territory, Occupied' },
{ code: 'PA', name: 'Panama' },
{ code: 'PG', name: 'Papua New Guinea' },
{ code: 'PY', name: 'Paraguay' },
{ code: 'PE', name: 'Peru' },
{ code: 'PH', name: 'Philippines' },
{ code: 'PN', name: 'Pitcairn' },
{ code: 'PL', name: 'Poland' },
{ code: 'PT', name: 'Portugal' },
{ code: 'PR', name: 'Puerto Rico' },
{ code: 'QA', name: 'Qatar' },
{ code: 'RE', name: 'Reunion' },
{ code: 'RO', name: 'Romania' },
{ code: 'RU', name: 'Russian Federation' },
{ code: 'RW', name: 'Rwanda' },
{ code: 'SH', name: 'Saint Helena' },
{ code: 'KN', name: 'Saint Kitts and Nevis' },
{ code: 'LC', name: 'Saint Lucia' },
{ code: 'PM', name: 'Saint Pierre and Miquelon' },
{ code: 'VC', name: 'Saint Vincent and the Grenadines' },
{ code: 'WS', name: 'Samoa' },
{ code: 'SM', name: 'San Marino' },
{ code: 'ST', name: 'Sao Tome and Principe' },
{ code: 'SA', name: 'Saudi Arabia' },
{ code: 'SN', name: 'Senegal' },
{ code: 'ME', name: 'Montenegro' },
{ code: 'RS', name: 'Serbia' },
{ code: 'SC', name: 'Seychelles' },
{ code: 'SL', name: 'Sierra Leone' },
{ code: 'SG', name: 'Singapore' },
{ code: 'SK', name: 'Slovakia' },
{ code: 'SI', name: 'Slovenia' },
{ code: 'SB', name: 'Solomon Islands' },
{ code: 'SO', name: 'Somalia' },
{ code: 'ZA', name: 'South Africa' },
{ code: 'GS', name: 'South Georgia and Sandwich Isles' },
{ code: 'ES', name: 'Spain' },
{ code: 'LK', name: 'Sri Lanka' },
{ code: 'SD', name: 'Sudan' },
{ code: 'SR', name: 'Suriname' },
{ code: 'SJ', name: 'Svalbard and Jan Mayen' },
{ code: 'SZ', name: 'Swaziland' },
{ code: 'SE', name: 'Sweden' },
{ code: 'CH', name: 'Switzerland' },
{ code: 'SY', name: 'Syrian Arab Republic' },
{ code: 'TW', name: 'Taiwan (ROC)' },
{ code: 'TJ', name: 'Tajikistan' },
{ code: 'TZ', name: 'Tanzania, United Republic of' },
{ code: 'TH', name: 'Thailand' },
{ code: 'TL', name: 'Timor-Leste' },
{ code: 'TG', name: 'Togo' },
{ code: 'TK', name: 'Tokelau' },
{ code: 'TO', name: 'Tonga' },
{ code: 'TT', name: 'Trinidad and Tobago' },
{ code: 'TN', name: 'Tunisia' },
{ code: 'TR', name: 'Turkey' },
{ code: 'TM', name: 'Turkmenistan' },
{ code: 'TC', name: 'Turks and Caicos Islands' },
{ code: 'TV', name: 'Tuvalu' },
{ code: 'UG', name: 'Uganda' },
{ code: 'UA', name: 'Ukraine' },
{ code: 'AE', name: 'United Arab Emirates' },
{ code: 'GB', name: 'United Kingdom' },
{
code: 'US',
name: 'United States',
regionLabel: 'State',
regions: [
{ code: 'AK', name: 'Alaska' },
{ code: 'AL', name: 'Alabama' },
{ code: 'AR', name: 'Arkansas' },
{ code: 'AS', name: 'American Samoa' },
{ code: 'AZ', name: 'Arizona' },
{ code: 'CA', name: 'California' },
{ code: 'CO', name: 'Colorado' },
{ code: 'CT', name: 'Connecticut' },
{ code: 'DE', name: 'Delaware' },
{ code: 'FL', name: 'Florida' },
{ code: 'GA', name: 'Georgia' },
{ code: 'HI', name: 'Hawaii' },
{ code: 'IA', name: 'Iowa' },
{ code: 'ID', name: 'Idaho' },
{ code: 'IL', name: 'Illinois' },
{ code: 'IN', name: 'Indiana' },
{ code: 'KS', name: 'Kansas' },
{ code: 'KY', name: 'Kentucky' },
{ code: 'LA', name: 'Louisiana' },
{ code: 'MA', name: 'Massachusetts' },
{ code: 'MD', name: 'Maryland' },
{ code: 'ME', name: 'Maine' },
{ code: 'MI', name: 'Michigan' },
{ code: 'MN', name: 'Minnesota' },
{ code: 'MO', name: 'Missouri' },
{ code: 'MP', name: 'Northern Mariana Islands' },
{ code: 'MS', name: 'Mississippi' },
{ code: 'MT', name: 'Montana' },
{ code: 'NC', name: 'North Carolina' },
{ code: 'ND', name: 'North Dakota' },
{ code: 'NE', name: 'Nebraska' },
{ code: 'NH', name: 'New Hampshire' },
{ code: 'NJ', name: 'New Jersey' },
{ code: 'NM', name: 'New Mexico' },
{ code: 'NV', name: 'Nevada' },
{ code: 'NY', name: 'New York' },
{ code: 'OH', name: 'Ohio' },
{ code: 'OK', name: 'Oklahoma' },
{ code: 'OR', name: 'Oregon' },
{ code: 'PA', name: 'Pennsylvania' },
{ code: 'PR', name: 'Puerto Rico' },
{ code: 'RI', name: 'Rhode Island' },
{ code: 'SC', name: 'South Carolina' },
{ code: 'SD', name: 'South Dakota' },
{ code: 'TN', name: 'Tennessee' },
{ code: 'TX', name: 'Texas' },
{ code: 'UT', name: 'Utah' },
{ code: 'VA', name: 'Virginia' },
{ code: 'VI', name: 'Virgin Islands' },
{ code: 'VT', name: 'Vermont' },
{ code: 'WA', name: 'Washington' },
{ code: 'DC', name: 'Washington (District of Columbia)' },
{ code: 'WI', name: 'Wisconsin' },
{ code: 'WV', name: 'West Virginia' },
{ code: 'WY', name: 'Wyoming' },
],
},
{ code: 'UM', name: 'United States Minor Outlying Islands' },
{ code: 'UY', name: 'Uruguay' },
{ code: 'UZ', name: 'Uzbekistan' },
{ code: 'VU', name: 'Vanuatu' },
{ code: 'VE', name: 'Venezuela' },
{ code: 'VN', name: 'Viet Nam' },
{ code: 'VG', name: 'Virgin Islands, British' },
{ code: 'VI', name: 'Virgin Islands, U.s.' },
{ code: 'WF', name: 'Wallis and Futuna' },
{ code: 'EH', name: 'Western Sahara' },
{ code: 'YE', name: 'Yemen' },
{ code: 'ZM', name: 'Zambia' },
{ code: 'ZW', name: 'Zimbabwe' },
];
var Country;
(function (Country) {
Country["Canada"] = "CA";
Country["UnitedStates"] = "US";
})(Country || (Country = {}));
class FsAddressRegionComponent {
_cdRef = inject(ChangeDetectorRef);
autocompleteModel;
set region(regionCode) {
const region = this.addressCountries
.reduce((accum, addressCountry) => {
return [
...accum,
...(addressCountry.regions || [])
.filter((addressRegion) => (addressRegion.code === regionCode &&
(!this.country || this.country === addressCountry.code))),
];
}, [])[0];
this.regionModel = (region ? region : (regionCode ? { name: regionCode } : null));
}
get region() {
return this.regionModel?.code;
}
disabled = false;
country;
label;
required = false;
regionCountryOrder = [Country.Canada, Country.UnitedStates];
set countries(countryCodes) {
countryCodes = countryCodes || [Country.Canada, Country.UnitedStates];
this._countries = countryCodes
.map((countryCode) => {
return Countries.find((country) => country.code === countryCode);
});
this.updateCountryRegionLabels();
}
get addressCountries() {
return this._countries;
}
regionChange = new EventEmitter();
regionModel;
controlName = `region${guid('xxxxxx')}`;
regionLabel;
countryEnum = Country;
_countries = [];
_destroy$ = new Subject();
constructor() {
this.countries = [Country.Canada, Country.UnitedStates];
}
ngOnInit() {
this.updateCountryRegionLabels();
this._listenControlStateChanges();
}
clear() {
this.regionModel = null;
}
ngOnDestroy() {
this._destroy$.next(null);
this._destroy$.complete();
}
fetch = (keyword) => {
keyword = keyword.toLowerCase();
return of(null)
.pipe(map(() => {
const regions = this._countries
.reduce((accum, country) => {
const countryRegions = (country.regions || [])
.filter((region) => {
const regionName = region.name.toLowerCase().trim();
return regionName.indexOf(keyword) !== -1;
});
if (countryRegions.length) {
console.log(country, keyword, countryRegions);
}
return [
...accum,
...countryRegions
.map((countryRegion) => {
return {
...countryRegion,
country: country.name,
};
}),
];
}, []);
console.log(regions, keyword);
return regions;
}));
};
displayWith = (data) => {
return data?.name;
};
selectUserOption(keyword) {
this.regionModel = {
code: keyword,
name: keyword,
};
this.autocompleteModel.control.markAsDirty();
this.regionChange.emit(keyword);
}
regionChanged() {
this.regionChange.emit(this.regionModel?.code);
}
justUseShow = (keyword) => {
return !!keyword;
};
updateCountryRegionLabels() {
this.regionLabel = this.label ? this.label : Object.keys(this._countries
.reduce((accum, country) => {
return {
...accum,
[country.regionLabel || 'Province']: true,
};
}, {}))
.join('/');
}
// we need this to get updated ng-(invalid/dirty) classes
_listenControlStateChanges() {
this.autocompleteModel
.control
.statusChanges
.pipe(takeUntil(this._destroy$))
.subscribe(() => {
this._cdRef.markForCheck();
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsAddressRegionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.7", type: FsAddressRegionComponent, isStandalone: true, selector: "fs-address-region", inputs: { region: "region", disabled: "disabled", country: "country", label: "label", required: "required", regionCountryOrder: "regionCountryOrder", countries: "countries" }, outputs: { regionChange: "regionChange" }, viewQueries: [{ propertyName: "autocompleteModel", first: true, predicate: FsAutocompleteComponent, descendants: true, read: NgModel, static: true }], ngImport: i0, template: "<fs-autocomplete\n [fetch]=\"fetch\"\n [displayWith]=\"displayWith\"\n [fetchOnFocus]=\"true\"\n [(ngModel)]=\"regionModel\"\n (ngModelChange)=\"regionChanged()\"\n [placeholder]=\"regionLabel\"\n [disabled]=\"disabled\"\n [fsFormRequired]=\"required\"\n [name]=\"controlName\">\n <ng-template\n fsAutocompleteTemplate\n let-data=\"data\">\n <span class=\"country-region\">\n <span>\n {{ data.name }}\n </span>\n @if (!regionModel && addressCountries.length > 1) {\n <span>\n {{ data.country }}\n </span>\n }\n </span>\n </ng-template>\n <ng-template\n fsAutocompleteStatic\n let-keyword\n (selected)=\"selectUserOption($event)\"\n [show]=\"justUseShow\">\n Just Use \"{{ keyword }}\"\n </ng-template>\n <ng-template fsAutocompleteNoResults></ng-template>\n</fs-autocomplete>", styles: ["@charset \"UTF-8\";.country-region{display:inline-flex}.country-region span:not(:last-child):after{content:\",\\a0\"}\n"], dependencies: [{ kind: "ngmodule", type: FsAutocompleteModule }, { kind: "component", type: i1$1.FsAutocompleteComponent, selector: "fs-autocomplete", inputs: ["fetch", "displayWith", "placeholder", "fetchOnFocus", "readonly", "required", "disabled", "formFieldClass", "appearance", "hint", "panelWidth", "panelClass", "showClear"], outputs: ["cleared", "opened", "closed"] }, { kind: "directive", type: i1$1.FsAutocompleteTemplateDirective, selector: "[fsAutocompleteTemplate]" }, { kind: "directive", type: i1$1.FsAutocompleteStaticDirective, selector: "[fsAutocompleteStatic],[fsAutocompleteStaticTemplate]", inputs: ["show", "disable"], outputs: ["selected"] }, { kind: "directive", type: i1$1.FsAutocompleteNoResultsDirective, selector: "[fsAutocompleteNoResults]", inputs: ["show"] }, { kind: "ngmodule", type: FormsModule }, { 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: "ngmodule", type: FsFormModule }, { kind: "directive", type: i2.FsFormRequiredDirective, selector: "[fsFormRequired],[ngModel][required]", inputs: ["fsFormRequired", "required", "fsFormRequiredMessage"] }], viewProviders: [
{
provide: ControlContainer,
useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]],
},
], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.7", ngImport: i0, type: FsAddressRegionComponent, decorators: [{
type: Component,
args: [{ selector: 'fs-address-region', changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [
{
provide: ControlContainer,
useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]],
},
], standalone: true, imports: [
FsAutocompleteModule,
FormsModule,
FsFormModule,
], template: "<fs-autocomplete\n [fetch]=\"fetch\"\n [displayWith]=\"displayWith\"\n [fetchOnFocus]=\"true\"\n [(ngModel)]=\"regionModel\"\n (ngModelChange)=\"regionChanged()\"\n [placeholder]=\"regionLabel\"\n [disabled]=\"disabled\"\n [fsFormRequired]=\"required\"\n [name]=\"controlName\">\n <ng-template\n fsAutocompleteTemplate\n let-data=\"data\">\n <span class=\"country-region\">\n <span>\n {{ data.name }}\n </span>\n @if (!regionModel && addressCountries.length > 1) {\n <span>\n {{ data.country }}\n </span>\n }\n </span>\n </ng-template>\n <ng-template\n fsAutocompleteStatic\n let-keyword\n (selected)=\"selectUserOption($event)\"\n [show]=\"justUseShow\">\n Just Use \"{{ keyword }}\"\n </ng-template>\n <ng-template fsAutocompleteNoResults></ng-template>\n</fs-autocomplete>", styles: ["@charset \"UTF-8\";.country-region{display:inline-flex}.country-region span:not(:last-child):after{content:\",\\a0\"}\n"] }]
}], ctorParameters: () => [], propDecorators: { autocompleteModel: [{
type: ViewChild,
args: [FsAutocompleteComponent, { read: NgModel, static: true }]
}], region: [{
type: Input
}], disabled: [{
type: Input
}], country: [{
type: Input
}], label: [{
type: Input
}], required: [{
type: Input
}], regionCountryOrder: [{
type: Input
}], countries: [{
type: Input
}], regionChange: [{
type: Output
}] } });
function addressFormat(address, options = {}) {
options = {
format: AddressFormat.OneLine,
...options,
};
const parts = ['name', 'street', 'address2', 'address3', 'city', 'region', 'zip', 'country'];
let addressParts = [];
let lines = [];
if (address) {
parts.forEach((part) => {
if (address[part]) {
addressParts.push(address[part]);
}
});
}
if (options.includeFirst) {
addressParts = addressParts.slice(0, options.includeFirst);
}
if (addressParts.length) {
if (options.format === AddressFormat.TwoLine) {
lines = [[addressParts.shift()]];
}
lines.push(addressParts);
}
return lines
.map((line) => {
return line.join(', ');
})
.join