UNPKG

@firestitch/address

Version:
1,038 lines (1,028 loc) 131 kB
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