diff --git a/addon/components/utils/address-form.hbs b/addon/components/utils/address-form.hbs
index dd6163fe..82b6ab8d 100644
--- a/addon/components/utils/address-form.hbs
+++ b/addon/components/utils/address-form.hbs
@@ -59,16 +59,14 @@
{{t "upf_utils.address_form.line_1"}}
{{#if this.useGoogleAutocomplete}}
-
-
-
+
{{else}}
-
+
\ No newline at end of file
diff --git a/addon/components/utils/address-form.ts b/addon/components/utils/address-form.ts
index 780f0602..b093ee99 100644
--- a/addon/components/utils/address-form.ts
+++ b/addon/components/utils/address-form.ts
@@ -2,12 +2,9 @@ import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action, get, set } from '@ember/object';
import { isEmpty } from '@ember/utils';
-import { isTesting } from '@embroider/macros';
-import { getOwner } from '@ember/application';
-
-import { Loader } from '@googlemaps/js-api-loader';
import { CountryData, countries } from '@upfluence/oss-components/utils/country-codes';
import { next } from '@ember/runloop';
+import { type AutocompletionAddress } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
type FocusableInput =
| 'first-name'
@@ -32,9 +29,6 @@ interface UtilsAddressFormArgs {
}
type ProvinceData = { code: string; name: string };
-type GAddressComponent = google.maps.GeocoderAddressComponent;
-type GPlaceResult = google.maps.places.PlaceResult;
-type GAutoComplete = google.maps.places.Autocomplete;
type AddressKey = 'address' | 'line';
const BASE_VALIDATED_ADDRESS_FIELDS: string[] = ['city', 'countryCode', 'zipcode'];
@@ -63,27 +57,6 @@ export default class extends Component {
return this.args.useGoogleAutocomplete ?? true;
}
- @action
- initAutoCompletion(): void {
- if (isTesting()) return;
- this.appendContainerLocally();
- const loader = new Loader({
- apiKey: getOwner(this).resolveRegistration('config:environment').google_map_api_key,
- version: 'weekly'
- });
-
- loader.importLibrary('places').then(({ Autocomplete }) => {
- const input = document.querySelector('[data-control-name="address-form-address1"] input') as HTMLInputElement;
- const options = {
- fields: ['address_components'],
- strictBounds: false,
- types: ['address']
- };
- const autocomplete = new Autocomplete(input, options);
- this.initInputListeners(autocomplete, input);
- });
- }
-
@action
selectCountryCode(code: { id: string }): void {
set(this.args.address, 'countryCode', code.id);
@@ -123,6 +96,20 @@ export default class extends Component {
this.args.onChange?.(this.args.address, this.checkAddressValidity());
}
+ @action
+ onAddressSelected(address: AutocompletionAddress): void {
+ set(this.args.address, `${this.addressKey}1`, address.address1);
+ if (address.address2) {
+ set(this.args.address, `${this.addressKey}2`, address.address2);
+ }
+ this.applyCountry(address.country);
+ set(this.args.address, 'city', address.city);
+ set(this.args.address, 'state', address.state);
+ set(this.args.address, 'zipcode', address.zipcode);
+
+ this.onFieldUpdate();
+ }
+
private checkAddressValidity(): boolean {
if (!isEmpty(this.provincesForCountry) && isEmpty(get(this.args.address, 'state'))) return false;
@@ -146,70 +133,6 @@ export default class extends Component {
});
}
- private initInputListeners(autocomplete: GAutoComplete, input: HTMLElement): void {
- autocomplete.addListener('place_changed', () => {
- const place = autocomplete.getPlace();
- this.fillInAddress(place);
- });
- input.addEventListener('focusout', (event) => {
- if ((event.target).value !== get(this.args.address, `${this.addressKey}1`)) {
- (event.target).value = get(this.args.address, `${this.addressKey}1`) ?? '';
- }
- });
- }
-
- private fillInAddress(place: GPlaceResult): void {
- let address1: string = '';
- let address2: string = '';
- let zipcode: string = '';
- let city: string = '';
-
- const mapper: { [key: string]: (comp: GAddressComponent) => void } = {
- street_number: (comp) => {
- address1 = `${comp.long_name} ${address1}`;
- },
- route: (comp) => {
- address1 += comp.long_name;
- },
- subpremise: (comp) => {
- address2 = comp.long_name;
- },
- postal_code: (comp) => {
- zipcode = `${comp.long_name}${zipcode}`;
- },
- postal_code_suffix: (comp) => {
- zipcode = `${zipcode}-${comp.long_name}`;
- },
- locality: (comp) => {
- city = comp.long_name;
- },
- postal_town: (comp) => {
- city = comp.long_name;
- },
- administrative_area_level_1: (comp) => {
- set(this.args.address, 'state', comp.long_name || '');
- },
- country: (comp) => {
- const selectedCountry = this.countries.find((country) => country.alpha2 === comp.short_name);
-
- if (!selectedCountry) return;
- this.applyCountry(selectedCountry);
- }
- };
-
- (place.address_components ?? []).reverse().forEach((component) => {
- const componentType: string = component.types[0];
-
- mapper[componentType]?.(component);
- });
-
- set(this.args.address, `${this.addressKey}1`, address1);
- set(this.args.address, `${this.addressKey}2`, address2);
- set(this.args.address, 'zipcode', zipcode);
- set(this.args.address, 'city', city);
- this.onFieldUpdate();
- }
-
private validatedAddressFieldsHandler(): void {
this.validatedAddressFields.push(`${this.addressKey}1`);
@@ -222,21 +145,6 @@ export default class extends Component {
}
}
- private appendContainerLocally(): void {
- const observer = new MutationObserver((mutationList: any) => {
- for (const mutation of mutationList) {
- if (mutation.type === 'childList') {
- const pacContainer = mutation.addedNodes[0];
- if (!pacContainer?.classList.contains('pac-container')) return;
-
- document.querySelector('[data-control-name="address-form-address1"]')?.append(pacContainer);
- observer.disconnect();
- }
- }
- });
- observer.observe(document.body, { childList: true });
- }
-
private openCountrySelect(): void {
next(() => {
(document.querySelector('[data-control-name="address-form-country"] .upf-input') as HTMLInputElement)?.click();
diff --git a/addon/components/utils/address-inline.hbs b/addon/components/utils/address-inline.hbs
index 4d33c3a2..1f0df464 100644
--- a/addon/components/utils/address-inline.hbs
+++ b/addon/components/utils/address-inline.hbs
@@ -1,16 +1,12 @@
{{#if this.useGoogleAutocomplete}}
-
-
-
+
{{else}}
-
-
-
+
{{/if}}
\ No newline at end of file
diff --git a/addon/components/utils/address-inline.ts b/addon/components/utils/address-inline.ts
index f1feb81c..745568eb 100644
--- a/addon/components/utils/address-inline.ts
+++ b/addon/components/utils/address-inline.ts
@@ -1,9 +1,6 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
-import { isTesting } from '@embroider/macros';
-import { getOwner } from '@ember/application';
-
-import { Loader } from '@googlemaps/js-api-loader';
+import { type AutocompletionAddress } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
interface UtilsAddressInlineArgs {
value: ShippingAddress;
@@ -22,34 +19,22 @@ export type ShippingAddress = {
} | null;
};
-type GAddressComponent = google.maps.GeocoderAddressComponent;
-type GAutoComplete = google.maps.places.Autocomplete;
-type GPlaceResult = google.maps.places.PlaceResult;
-
export default class extends Component {
get useGoogleAutocomplete(): boolean {
return this.args.useGoogleAutocomplete ?? true;
}
@action
- initAutoCompletion(): void {
- if (isTesting()) return;
- this.appendContainerLocally();
- const loader = new Loader({
- // @ts-ignore
- apiKey: getOwner(this).resolveRegistration('config:environment').google_map_api_key,
- version: 'weekly'
- });
-
- loader.importLibrary('places').then(({ Autocomplete }) => {
- const input = document.querySelector('[data-control-name="address-inline"] input') as HTMLInputElement;
- const options = {
- fields: ['address_components'],
- strictBounds: false,
- types: ['address']
- };
- const autocomplete = new Autocomplete(input, options);
- this.initInputListeners(autocomplete, input);
+ onAddressSelected(address: AutocompletionAddress): void {
+ this.args.onChange({
+ address: address.formattedAddress,
+ resolved_address: {
+ line_1: address.address1,
+ zipcode: address.zipcode,
+ city: address.city,
+ state: address.state,
+ country_code: address.country.alpha2
+ }
});
}
@@ -57,78 +42,4 @@ export default class extends Component {
onChange(value: string): void {
this.args.onChange({ address: value, resolved_address: null });
}
-
- private appendContainerLocally(): void {
- const observer = new MutationObserver((mutationList: any) => {
- for (const mutation of mutationList) {
- if (mutation.type === 'childList') {
- const pacContainer = mutation.addedNodes[0];
- if (!pacContainer?.classList.contains('pac-container')) return;
-
- document.querySelector('[data-control-name="address-inline"]')?.append(pacContainer);
- observer.disconnect();
- }
- }
- });
- observer.observe(document.body, { childList: true });
- }
-
- private initInputListeners(autocomplete: GAutoComplete, input: HTMLInputElement): void {
- autocomplete.addListener('place_changed', () => {
- const place = autocomplete.getPlace();
- this.updateAddress(place, input);
- });
- }
-
- private updateAddress(place: GPlaceResult, input: HTMLInputElement): void {
- let address1: string = '';
- let zipcode: string = '';
- let city: string = '';
- let state: string = '';
- let country: string = '';
-
- const mapper: { [key: string]: (comp: GAddressComponent) => void } = {
- street_number: (comp) => {
- address1 = `${comp.long_name} ${address1}`;
- },
- route: (comp) => {
- address1 += comp.long_name;
- },
- postal_code: (comp) => {
- zipcode = `${comp.long_name}${zipcode}`;
- },
- postal_code_suffix: (comp) => {
- zipcode = `${zipcode}-${comp.long_name}`;
- },
- locality: (comp) => {
- city = comp.long_name;
- },
- postal_town: (comp) => {
- city = comp.long_name;
- },
- administrative_area_level_1: (comp) => {
- state = comp.long_name ?? '';
- },
- country: (comp) => {
- country = comp.short_name;
- }
- };
-
- (place.address_components ?? []).reverse().forEach((component) => {
- const componentType: string = component.types[0];
-
- mapper[componentType]?.(component);
- });
-
- this.args.onChange({
- address: input.value,
- resolved_address: {
- line_1: address1,
- zipcode,
- city,
- state,
- country_code: country
- }
- });
- }
}
diff --git a/addon/modifiers/setup-autocomplete.ts b/addon/modifiers/setup-autocomplete.ts
new file mode 100644
index 00000000..7537ba2c
--- /dev/null
+++ b/addon/modifiers/setup-autocomplete.ts
@@ -0,0 +1,173 @@
+import Modifier, { type ArgsFor, type PositionalArgs, type NamedArgs } from 'ember-modifier';
+import { registerDestructor } from '@ember/destroyable';
+import { assert } from '@ember/debug';
+import { getOwner } from '@ember/application';
+import { isTesting } from '@embroider/macros';
+
+import { Loader } from '@googlemaps/js-api-loader';
+
+import { parseAddressComponents } from '@upfluence/ember-upf-utils/utils/address-parser';
+import { MockLoader } from '@upfluence/ember-upf-utils/utils/google-maps-mock';
+import { CountryData } from '@upfluence/oss-components/utils/country-codes';
+
+type GooglePlaceResult = google.maps.places.PlaceResult;
+type GoogleAutocomplete = google.maps.places.Autocomplete;
+type GoogleAutocompleteOptions = google.maps.places.AutocompleteOptions;
+export type AutocompletionAddress = {
+ address1: string;
+ address2?: string;
+ city: string;
+ state: string;
+ zipcode: string;
+ country: CountryData;
+ formattedAddress: string;
+};
+
+interface SetupAutocompleteSignature {
+ Element: HTMLElement;
+ Args: {
+ Named: {
+ callback(result: AutocompletionAddress): void;
+ loader?: Loader;
+ };
+ };
+}
+
+const AUTOCOMPLETE_CONTAINER_CLASS = 'autocomplete-input-container';
+const AUTOCOMPLETE_OPTIONS: GoogleAutocompleteOptions = {
+ fields: ['address_components'],
+ strictBounds: false,
+ types: ['address']
+};
+
+function cleanup(instance: SetupAutocompleteModifier): void {
+ if (instance.createdWrapper && instance.targetInput && instance.targetElement) {
+ const parent = instance.targetElement.parentNode;
+ if (parent && instance.targetElement.contains(instance.targetInput)) {
+ parent.insertBefore(instance.targetInput, instance.targetElement);
+ instance.targetElement?.remove();
+ }
+ }
+
+ instance.targetElement = null;
+ instance.targetInput = null;
+ instance.result = null;
+ instance.createdWrapper = false;
+ document.querySelector('.pac-container')?.remove();
+}
+
+export default class SetupAutocompleteModifier extends Modifier {
+ targetElement: HTMLElement | null = null;
+ targetInput: HTMLInputElement | null = null;
+ result: AutocompletionAddress | null = null;
+ createdWrapper: boolean = false;
+
+ private callback: ((result: AutocompletionAddress) => void) | null = null;
+
+ constructor(owner: unknown, args: ArgsFor) {
+ super(owner, args);
+ registerDestructor(this, cleanup);
+ }
+
+ modify(
+ element: HTMLElement,
+ _: PositionalArgs,
+ { callback, loader }: NamedArgs
+ ): void {
+ const input: HTMLInputElement | null = this.getInputElement(element);
+ if (!input) return;
+
+ this.targetInput = input;
+
+ assert(
+ '[modifier][setup-autocomplete] The callback is mandatory and must be a function',
+ typeof callback === 'function'
+ );
+ this.callback = callback;
+
+ if (!this.targetElement) {
+ this.setupTargetElement(element, input);
+ this.setupAutoComplete(loader);
+ }
+ }
+
+ private setupTargetElement(element: HTMLElement, input: HTMLInputElement): void {
+ if (element === input) {
+ this.targetElement = this.createWrapperForInput(input);
+ } else {
+ this.targetElement = element;
+ this.targetElement.classList.add(AUTOCOMPLETE_CONTAINER_CLASS);
+ }
+ }
+
+ private createWrapperForInput(input: HTMLInputElement): HTMLElement {
+ const parentNode = input.parentNode;
+ assert('[modifier][setup-autocomplete] Input element must have a parent node', parentNode !== null);
+
+ if (parentNode instanceof HTMLElement && parentNode.classList.contains(AUTOCOMPLETE_CONTAINER_CLASS)) {
+ return parentNode;
+ }
+
+ const wrapper = document.createElement('div');
+ wrapper.classList.add(AUTOCOMPLETE_CONTAINER_CLASS);
+
+ parentNode.insertBefore(wrapper, input);
+ wrapper.appendChild(input);
+
+ this.createdWrapper = true;
+
+ return wrapper;
+ }
+
+ private getInputElement(element: HTMLElement): HTMLInputElement | null {
+ if (this.isTextInput(element)) return element as HTMLInputElement;
+
+ const inputElement = element.querySelector('input[type="text"]');
+ assert(
+ '[modifier][setup-autocomplete] No input[type="text"] element found in the provided element or its children',
+ inputElement !== null
+ );
+ return inputElement as HTMLInputElement;
+ }
+
+ private isTextInput(element: HTMLElement): element is HTMLInputElement {
+ return element.tagName === 'INPUT' && (element as HTMLInputElement).type === 'text';
+ }
+
+ private setupAutoComplete(loader?: Loader): Promise {
+ const loaderInstance: Loader | MockLoader = isTesting()
+ ? loader ?? new MockLoader({ apiKey: 'test-key' })
+ : new Loader({
+ apiKey: getOwner(this).resolveRegistration('config:environment').google_map_api_key,
+ version: 'weekly'
+ });
+
+ // @ts-ignore
+ return loaderInstance.importLibrary('places').then(({ Autocomplete }: google.maps.PlacesLibrary) => {
+ this.initializeAutocomplete(Autocomplete);
+ });
+ }
+
+ private initializeAutocomplete(
+ AutocompleteConstructor: new (
+ input: HTMLInputElement,
+ options?: google.maps.places.AutocompleteOptions
+ ) => GoogleAutocomplete
+ ): void {
+ assert('[modifier][setup-autocomplete] Target input is not initialized', this.targetInput !== null);
+
+ const autocomplete = new AutocompleteConstructor(this.targetInput, AUTOCOMPLETE_OPTIONS);
+ autocomplete.addListener('place_changed', () => {
+ const place = autocomplete.getPlace();
+ this.handlePlaceChanged(place);
+ });
+ }
+
+ private handlePlaceChanged(place: GooglePlaceResult): void {
+ if (!place.address_components) return;
+
+ const formattedAddress = this.targetInput?.value ?? '';
+ this.result = parseAddressComponents(place.address_components, formattedAddress);
+ this.callback?.(this.result);
+ }
+}
diff --git a/addon/utils/address-parser.ts b/addon/utils/address-parser.ts
new file mode 100644
index 00000000..049bc775
--- /dev/null
+++ b/addon/utils/address-parser.ts
@@ -0,0 +1,73 @@
+import { countries } from '@upfluence/oss-components/utils/country-codes';
+
+import { type AutocompletionAddress } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
+
+const ADDRESS_COMPONENT_TYPES = [
+ 'street_number',
+ 'route',
+ 'subpremise',
+ 'postal_code',
+ 'postal_code_suffix',
+ 'locality',
+ 'postal_town',
+ 'administrative_area_level_1',
+ 'country'
+] as const;
+
+type GoogleAddressComponent = google.maps.GeocoderAddressComponent;
+type AddressComponentType = (typeof ADDRESS_COMPONENT_TYPES)[number];
+
+export function parseAddressComponents(
+ components: GoogleAddressComponent[],
+ formattedAddress: string = ''
+): AutocompletionAddress {
+ const defaultCountry = countries.find((country) => country.alpha2 === 'US')!;
+ const result: AutocompletionAddress = {
+ address1: '',
+ address2: '',
+ city: '',
+ state: '',
+ zipcode: '',
+ country: defaultCountry,
+ formattedAddress
+ };
+
+ const mapper: Record void> = {
+ street_number: (comp: GoogleAddressComponent): void => {
+ result.address1 = `${comp.long_name} ${result.address1}`.trim();
+ },
+ route: (comp: GoogleAddressComponent): void => {
+ result.address1 += comp.long_name;
+ },
+ subpremise: (comp: GoogleAddressComponent): void => {
+ result.address2 = comp.long_name;
+ },
+ postal_code: (comp: GoogleAddressComponent): void => {
+ result.zipcode = `${comp.long_name}${result.zipcode}`;
+ },
+ postal_code_suffix: (comp: GoogleAddressComponent) => {
+ result.zipcode = `${result.zipcode}-${comp.long_name}`;
+ },
+ locality: (comp: GoogleAddressComponent): void => {
+ result.city = comp.long_name;
+ },
+ postal_town: (comp: GoogleAddressComponent): void => {
+ result.city = comp.long_name;
+ },
+ administrative_area_level_1: (comp: GoogleAddressComponent) => {
+ result.state = comp.long_name;
+ },
+ country: (comp: GoogleAddressComponent): void => {
+ result.country = countries.find((country) => country.alpha2 === comp.short_name) ?? defaultCountry;
+ }
+ };
+
+ (components ?? []).reverse().forEach((component) => {
+ const componentType: AddressComponentType = component.types[0] as AddressComponentType;
+ mapper[componentType]?.(component);
+ });
+
+ if (result.address2 === '') delete result['address2'];
+
+ return result;
+}
diff --git a/addon/utils/google-maps-mock.ts b/addon/utils/google-maps-mock.ts
new file mode 100644
index 00000000..b654971a
--- /dev/null
+++ b/addon/utils/google-maps-mock.ts
@@ -0,0 +1,160 @@
+export type MockGoogleAddressComponent = {
+ types: string[];
+ long_name: string;
+ short_name: string;
+};
+
+export type MockPlaceResult = {
+ address_components?: MockGoogleAddressComponent[];
+ formatted_address?: string;
+};
+
+export class MockAutocomplete {
+ private listeners: Map = new Map();
+ private mockPlace: MockPlaceResult = {};
+
+ constructor(public input: HTMLInputElement, public options?: Record) {}
+
+ addListener(eventName: string, handler: Function): void {
+ if (!this.listeners.has(eventName)) {
+ this.listeners.set(eventName, []);
+ }
+ this.listeners.get(eventName)!.push(handler);
+ }
+
+ getPlace(): MockPlaceResult {
+ return this.mockPlace;
+ }
+
+ setPlace(place: MockPlaceResult): void {
+ this.mockPlace = place;
+ }
+
+ triggerEvent(eventName: string): void {
+ const handlers = this.listeners.get(eventName) || [];
+ handlers.forEach((handler) => handler());
+ }
+
+ simulatePlaceSelection(place: MockPlaceResult): void {
+ this.setPlace(place);
+ this.triggerEvent('place_changed');
+ }
+}
+
+export class MockLoader {
+ private mockAutocompleteInstance: MockAutocomplete | null = null;
+
+ constructor(public config?: Record) {}
+
+ async importLibrary(libraryName: string): Promise {
+ if (libraryName === 'places') {
+ const self = this;
+ return {
+ Autocomplete: class {
+ constructor(input: HTMLInputElement, options?: Record) {
+ self.mockAutocompleteInstance = new MockAutocomplete(input, options);
+ return self.mockAutocompleteInstance;
+ }
+ }
+ };
+ }
+ return {};
+ }
+
+ getMockAutocompleteInstance(): MockAutocomplete | null {
+ return this.mockAutocompleteInstance;
+ }
+}
+
+export function createSampleAddressComponents(
+ overrides: Partial<{
+ streetNumber: string;
+ route: string;
+ subpremise: string;
+ city: string;
+ state: string;
+ zipcode: string;
+ country: string;
+ countryCode: string;
+ }>
+): MockGoogleAddressComponent[] {
+ const defaults = {
+ streetNumber: '1600',
+ route: 'Amphitheatre Parkway',
+ subpremise: '',
+ city: 'Mountain View',
+ state: 'California',
+ zipcode: '94043',
+ country: 'United States',
+ countryCode: 'US'
+ };
+
+ const values = { ...defaults, ...overrides };
+ const components: MockGoogleAddressComponent[] = [];
+
+ if (values.streetNumber) {
+ components.push({
+ types: ['street_number'],
+ long_name: values.streetNumber,
+ short_name: values.streetNumber
+ });
+ }
+
+ if (values.route) {
+ components.push({
+ types: ['route'],
+ long_name: values.route,
+ short_name: values.route
+ });
+ }
+
+ if (values.subpremise) {
+ components.push({
+ types: ['subpremise'],
+ long_name: values.subpremise,
+ short_name: values.subpremise
+ });
+ }
+
+ if (values.city) {
+ components.push({
+ types: ['locality'],
+ long_name: values.city,
+ short_name: values.city
+ });
+ }
+
+ if (values.state) {
+ components.push({
+ types: ['administrative_area_level_1'],
+ long_name: values.state,
+ short_name: values.state
+ });
+ }
+
+ if (values.zipcode) {
+ components.push({
+ types: ['postal_code'],
+ long_name: values.zipcode,
+ short_name: values.zipcode
+ });
+ }
+
+ components.push({
+ types: ['country'],
+ long_name: values.country,
+ short_name: values.countryCode
+ });
+
+ return components;
+}
+
+export function createMockPlaceResult(
+ addressComponents: MockGoogleAddressComponent[],
+ formattedAddress?: string
+): MockPlaceResult {
+ return {
+ address_components: addressComponents,
+ formatted_address: formattedAddress
+ };
+}
diff --git a/app/modifiers/setup-autocomplete.js b/app/modifiers/setup-autocomplete.js
new file mode 100644
index 00000000..d36f0c9b
--- /dev/null
+++ b/app/modifiers/setup-autocomplete.js
@@ -0,0 +1 @@
+export { default } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
diff --git a/app/styles/components/utils/address-form.less b/app/styles/components/utils/address-form.less
index 687e2a28..7acd6e81 100644
--- a/app/styles/components/utils/address-form.less
+++ b/app/styles/components/utils/address-form.less
@@ -1,45 +1,47 @@
-.google-autocomplete-input-container {
+.autocomplete-input-container {
position: relative;
- .pac-container {
- background-color: var(--color-white);
- border: 1px solid var(--color-border-default);
- border-radius: var(--border-radius-sm);
- box-shadow: var(--box-shadow-md);
- margin-top: var(--spacing-px-6);
- z-index: 1300;
- top: 36px !important;
- left: 0 !important;
+ .pac-target-input {
+ width: 100%;
+ }
+}
- &:after {
- display: none;
- }
+.pac-container {
+ background-color: var(--color-white);
+ border: 1px solid var(--color-border-default);
+ border-radius: var(--border-radius-sm);
+ box-shadow: var(--box-shadow-md);
+ margin-top: var(--spacing-px-6);
+ z-index: 1300;
- .pac-item {
- display: flex;
- align-items: center;
- font-size: var(--font-size-sm);
- color: var(--color-gray-400);
- padding: var(--spacing-px-3) var(--spacing-px-12);
- border: none;
- gap: var(--spacing-px-6);
- }
+ &:after {
+ display: none;
+ }
- .pac-item-query {
- color: var(--color-gray-600);
- }
+ .pac-item {
+ display: flex;
+ align-items: center;
+ font-size: var(--font-size-sm);
+ color: var(--color-gray-400);
+ padding: var(--spacing-px-3) var(--spacing-px-12);
+ border: none;
+ gap: var(--spacing-px-6);
+ }
+
+ .pac-item-query {
+ color: var(--color-gray-600);
+ }
- .pac-icon-marker {
- font-family: 'Font Awesome 5 Pro';
- font-weight: 400;
- margin: 0;
- width: auto;
- height: auto;
- background-image: none;
+ .pac-icon-marker {
+ font-family: 'Font Awesome 7 Pro';
+ font-weight: 400;
+ margin: 0;
+ width: auto;
+ height: auto;
+ background-image: none;
- &::before {
- content: '\f3c5';
- }
+ &::before {
+ content: '\f3c5';
}
}
}
diff --git a/app/utils/address-parser.js b/app/utils/address-parser.js
new file mode 100644
index 00000000..979d20b6
--- /dev/null
+++ b/app/utils/address-parser.js
@@ -0,0 +1 @@
+export { parseAddressComponents } from './address-parser';
diff --git a/app/utils/serialize-params.js b/app/utils/serialize-params.js
deleted file mode 100644
index 980afb15..00000000
--- a/app/utils/serialize-params.js
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from '@upfluence/ember-upf-utils/utils/serialize-params';
diff --git a/package.json b/package.json
index 7de1cc00..a3c9b875 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"ember-cli-htmlbars": "^5.7.2",
"ember-cli-less": "^2.0.6",
"ember-cli-typescript": "^4.2.1",
+ "ember-modifier": "^3.2.7",
"ember-named-blocks-polyfill": "^0.2.4",
"moment": "^2.29.4",
"tinycolor2": "^1.4.1"
@@ -68,6 +69,7 @@
"@types/ember__component": "^3.16.6",
"@types/ember__controller": "^3.16.6",
"@types/ember__debug": "^3.16.5",
+ "@types/ember__destroyable": "^4.0.5",
"@types/ember__engine": "^3.16.3",
"@types/ember__error": "^3.16.1",
"@types/ember__object": "^3.12.6",
@@ -84,7 +86,7 @@
"@types/rsvp": "^4.0.4",
"@types/sinon": "^10.0.6",
"@typescript-eslint/parser": "^5.0.0",
- "@upfluence/oss-components": "^3.81.3",
+ "@upfluence/oss-components": "^3.88.15",
"ember-cli": "~3.28.6",
"ember-cli-code-coverage": "^3.0.0",
"ember-cli-dependency-checker": "^3.2.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 54580fce..2b2dd8e4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
ember-cli-typescript:
specifier: ^4.2.1
version: 4.2.1
+ ember-modifier:
+ specifier: ^3.2.7
+ version: 3.2.7(@babel/core@7.26.0)
ember-named-blocks-polyfill:
specifier: ^0.2.4
version: 0.2.5
@@ -99,6 +102,9 @@ importers:
'@types/ember__debug':
specifier: ^3.16.5
version: 3.16.12
+ '@types/ember__destroyable':
+ specifier: ^4.0.5
+ version: 4.0.5
'@types/ember__engine':
specifier: ^3.16.3
version: 3.16.9
@@ -148,8 +154,8 @@ importers:
specifier: ^5.0.0
version: 5.62.0(eslint@7.32.0)(typescript@4.9.5)
'@upfluence/oss-components':
- specifier: ^3.81.3
- version: 3.81.3(@babel/core@7.26.0)(@ember/test-helpers@2.9.4(@babel/core@7.26.0)(ember-source@3.28.12(@babel/core@7.26.0)))(ember-source@3.28.12(@babel/core@7.26.0))(qunit@2.23.1)(typescript@4.9.5)(webpack@5.97.1)
+ specifier: ^3.88.15
+ version: 3.88.15(@babel/core@7.26.0)(@ember/test-helpers@2.9.4(@babel/core@7.26.0)(ember-source@3.28.12(@babel/core@7.26.0)))(ember-source@3.28.12(@babel/core@7.26.0))(qunit@2.23.1)(typescript@4.9.5)(webpack@5.97.1)
ember-cli:
specifier: ~3.28.6
version: 3.28.6(babel-core@6.26.3)(handlebars@4.7.8)(underscore@1.13.7)
@@ -955,6 +961,15 @@ packages:
'@glint/template':
optional: true
+ '@embroider/macros@1.19.6':
+ resolution: {integrity: sha512-yPf8lD/gRZmcxms66CCKuZuvWMJ0g/hdCE6P8FZsyewR3So6pxgdgOFp0zk2w5d34jS1ejBtxLNREZbBTELpzw==}
+ engines: {node: 12.* || 14.* || >= 16}
+ peerDependencies:
+ '@glint/template': ^1.0.0
+ peerDependenciesMeta:
+ '@glint/template':
+ optional: true
+
'@embroider/shared-internals@1.8.3':
resolution: {integrity: sha512-N5Gho6Qk8z5u+mxLCcMYAoQMbN4MmH+z2jXwQHVs859bxuZTxwF6kKtsybDAASCtd2YGxEmzcc1Ja/wM28824w==}
engines: {node: 12.* || 14.* || >= 16}
@@ -967,6 +982,10 @@ packages:
resolution: {integrity: sha512-zi0CENFD1e0DH7c9M/rNKJnFnt2c3+736J3lguBddZdmaIV6Cb8l3HQSkskSW5O4ady+SavemLKO3hCjQQJBIw==}
engines: {node: 12.* || 14.* || >= 16}
+ '@embroider/shared-internals@3.0.2':
+ resolution: {integrity: sha512-/SusdG+zgosc3t+9sPFVKSFOYyiSgLfXOT6lYNWoG1YtnhWDxlK4S8leZ0jhcVjemdaHln5rTyxCnq8oFLxqpQ==}
+ engines: {node: 12.* || 14.* || >= 16}
+
'@embroider/test-setup@0.48.1':
resolution: {integrity: sha512-MmYTgQMDVDrZPvxeT27LTUD/BOum21ip1tEYv5H/StSeTZyZQ861Q+8HXQUFTVF/HFjGAB1c/BAgnw+8hO1ueA==}
engines: {node: 12.* || 14.* || >= 16}
@@ -988,18 +1007,31 @@ packages:
'@glint/template':
optional: true
+ '@embroider/util@1.13.5':
+ resolution: {integrity: sha512-rHhGUzAQ5iOr5Swvk7yaarVe5SJtcjK2t/C8ts9agWfhTq4DVfy8+axF0KOf1jALRiJao3l9ALRGd6letKw2ZQ==}
+ engines: {node: 12.* || 14.* || >= 16}
+ peerDependencies:
+ '@glint/environment-ember-loose': ^1.0.0
+ '@glint/template': ^1.0.0
+ ember-source: '*'
+ peerDependenciesMeta:
+ '@glint/environment-ember-loose':
+ optional: true
+ '@glint/template':
+ optional: true
+
'@eslint/eslintrc@0.4.3':
resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==}
engines: {node: ^10.12.0 || >=12.0.0}
- '@floating-ui/core@1.6.9':
- resolution: {integrity: sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==}
+ '@floating-ui/core@1.7.3':
+ resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
- '@floating-ui/dom@1.6.13':
- resolution: {integrity: sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==}
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
- '@floating-ui/utils@0.2.9':
- resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==}
+ '@floating-ui/utils@0.2.10':
+ resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@formatjs/ecma402-abstract@2.2.4':
resolution: {integrity: sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==}
@@ -1045,9 +1077,9 @@ packages:
typescript:
optional: true
- '@fortawesome/fontawesome-pro@git+https://git@github.com:upfluence/fontawesome-pro.git#2ae6c0044b34d06543101723202e117125257264':
- resolution: {commit: 2ae6c0044b34d06543101723202e117125257264, repo: git@github.com:upfluence/fontawesome-pro.git, type: git}
- version: 6.7.2
+ '@fortawesome/fontawesome-pro@git+https://git@github.com:upfluence/fontawesome-pro.git#c639fe415de762c946eaebf4cc4e815ce3ede420':
+ resolution: {commit: c639fe415de762c946eaebf4cc4e815ce3ede420, repo: git@github.com:upfluence/fontawesome-pro.git, type: git}
+ version: 7.0.1
engines: {node: '>=6'}
'@glimmer/component@1.1.2':
@@ -1244,6 +1276,9 @@ packages:
'@types/ember__debug@3.16.12':
resolution: {integrity: sha512-+k+9qNmTaLw6xQCvcZm1DrQ6D2n9uob/8RVAK6jxFkxyPNbdt66z3fn7V/NHIURcBVhGVgf1qr5x62bHW0PIdg==}
+ '@types/ember__destroyable@4.0.5':
+ resolution: {integrity: sha512-spJyZxpvecssbXkaOQYcbnlWgb+TasFaKrgAYVbykZY6saMwUdMOGDDoW6uP/y/+A8Jj/fUIatPWJLepeSfgww==}
+
'@types/ember__engine@3.16.9':
resolution: {integrity: sha512-Dab779R+nuGoprrOV1qzomUSEGM9eqXxFB1q5ArK00IDf+B6qkws2rJg6pB7PBSdiBhnhAq8tAJ/WNy4eC/jwQ==}
@@ -1419,8 +1454,8 @@ packages:
resolution: {integrity: sha512-KvSlbSLXM/UTLSxuWRi3rK7qrXVwolFtXiCqo2rQnXV0hS2mAKtK3+bnATX2BRdjNdNyc8da7I2RNmKshb7ZSQ==, tarball: https://npm.pkg.github.com/download/@upfluence/hyperevents/0.3.9/6bc9a5f792a8ee995aa638a54d57a1f94da7b20d}
engines: {node: 10.* || >= 12}
- '@upfluence/oss-components@3.81.3':
- resolution: {integrity: sha512-gewaOtG8c5KrWTYf/Wgk3T0WdXWrkew13P6mDchvfeKHlNEs/W5GE/aPjq7tk8t3ZEMVJhWpUOK15svW30N2fA==, tarball: https://npm.pkg.github.com/download/@upfluence/oss-components/3.81.3/a514087f6a9dc558ffaed2eea21ee27beb0389ef}
+ '@upfluence/oss-components@3.88.15':
+ resolution: {integrity: sha512-Ut4mhwl+272//V7VWpwM9oHhaQYBsgRU06LXcs5UcR/ZHiLIo/KJ3RSfxwzl1LehmHigdXrg6ohucTuHAbuTIw==, tarball: https://npm.pkg.github.com/download/@upfluence/oss-components/3.88.15/4c2b1c0d8e430c007ab11fb6111ea3a0b59ff6a3}
engines: {node: 12.* || 14.* || >= 16}
peerDependencies:
qunit: ^2.x
@@ -1828,6 +1863,10 @@ packages:
resolution: {integrity: sha512-4YNPkuVsxAW5lnSTa6cn4Wk49RX6GAB6vX+M6LqEtN0YePqoFczv1/x0EyLK/o+4E1j9jEuYj5Su7IEPab5JHQ==}
engines: {node: '>= 12.*'}
+ babel-import-util@3.0.1:
+ resolution: {integrity: sha512-2copPaWQFUrzooJVIVZA/Oppx/S/KOoZ4Uhr+XWEQDMZ8Rvq/0SNQpbdIyMBJ8IELWt10dewuJw+tX4XjOo7Rg==}
+ engines: {node: '>= 12.*'}
+
babel-loader@8.4.1:
resolution: {integrity: sha512-nXzRChX+Z1GoE6yWavBQg6jDslyFF3SDjl2paADuoQtQW10JqShJt62R6eJQ5m/pjJFDT8xgKIWSP85OY8eXeA==}
engines: {node: '>= 8.9'}
@@ -2097,6 +2136,7 @@ packages:
bootstrap@3.4.1:
resolution: {integrity: sha512-yN5oZVmRCwe5aKwzRj6736nSmKDX7pLYwsXiCj/EYmo16hODaBiT4En5btW/jhBF/seV+XMx3aYwukYC3A49DA==}
engines: {node: '>=6'}
+ deprecated: This version of Bootstrap is no longer supported. Please upgrade to the latest version.
bower-config@1.4.3:
resolution: {integrity: sha512-MVyyUk3d1S7d2cl6YISViwJBc2VXCkxF5AUFykvN0PQj5FsUiMNSgAYTso18oRFfyZ6XEtjrgg9MAaufHbOwNw==}
@@ -2910,6 +2950,15 @@ packages:
supports-color:
optional: true
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
@@ -4779,6 +4828,9 @@ packages:
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+ lodash@4.17.23:
+ resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==}
+
log-symbols@2.2.0:
resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==}
engines: {node: '>=4'}
@@ -5802,11 +5854,20 @@ packages:
resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
deprecated: https://github.com/lydell/resolve-url#deprecated
+ resolve.exports@2.0.3:
+ resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==}
+ engines: {node: '>=10'}
+
resolve@1.22.10:
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
engines: {node: '>= 0.4'}
hasBin: true
+ resolve@1.22.11:
+ resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+ engines: {node: '>= 0.4'}
+ hasBin: true
+
responselike@1.0.2:
resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==}
@@ -5938,6 +5999,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ semver@7.7.3:
+ resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+ engines: {node: '>=10'}
+ hasBin: true
+
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
@@ -7873,6 +7939,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@embroider/macros@1.19.6':
+ dependencies:
+ '@embroider/shared-internals': 3.0.2
+ assert-never: 1.4.0
+ babel-import-util: 3.0.1
+ ember-cli-babel: 7.26.11
+ find-up: 5.0.0
+ lodash: 4.17.23
+ resolve: 1.22.11
+ semver: 7.7.3
+ transitivePeerDependencies:
+ - supports-color
+
'@embroider/shared-internals@1.8.3':
dependencies:
babel-import-util: 1.4.1
@@ -7912,6 +7991,24 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@embroider/shared-internals@3.0.2':
+ dependencies:
+ babel-import-util: 3.0.1
+ debug: 4.4.3
+ ember-rfc176-data: 0.3.18
+ fs-extra: 9.1.0
+ is-subdir: 1.2.0
+ js-string-escape: 1.0.1
+ lodash: 4.17.23
+ minimatch: 3.1.2
+ pkg-entry-points: 1.1.1
+ resolve-package-path: 4.0.3
+ resolve.exports: 2.0.3
+ semver: 7.7.3
+ typescript-memoize: 1.1.1
+ transitivePeerDependencies:
+ - supports-color
+
'@embroider/test-setup@0.48.1':
dependencies:
lodash: 4.17.21
@@ -7931,6 +8028,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ '@embroider/util@1.13.5(ember-source@3.28.12(@babel/core@7.26.0))':
+ dependencies:
+ '@embroider/macros': 1.19.6
+ broccoli-funnel: 3.0.8
+ ember-cli-babel: 7.26.11
+ ember-source: 3.28.12(@babel/core@7.26.0)
+ transitivePeerDependencies:
+ - supports-color
+
'@eslint/eslintrc@0.4.3':
dependencies:
ajv: 6.12.6
@@ -7945,16 +8051,16 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@floating-ui/core@1.6.9':
+ '@floating-ui/core@1.7.3':
dependencies:
- '@floating-ui/utils': 0.2.9
+ '@floating-ui/utils': 0.2.10
- '@floating-ui/dom@1.6.13':
+ '@floating-ui/dom@1.7.4':
dependencies:
- '@floating-ui/core': 1.6.9
- '@floating-ui/utils': 0.2.9
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
- '@floating-ui/utils@0.2.9': {}
+ '@floating-ui/utils@0.2.10': {}
'@formatjs/ecma402-abstract@2.2.4':
dependencies:
@@ -8031,7 +8137,7 @@ snapshots:
optionalDependencies:
typescript: 4.9.5
- '@fortawesome/fontawesome-pro@git+https://git@github.com:upfluence/fontawesome-pro.git#2ae6c0044b34d06543101723202e117125257264': {}
+ '@fortawesome/fontawesome-pro@git+https://git@github.com:upfluence/fontawesome-pro.git#c639fe415de762c946eaebf4cc4e815ce3ede420': {}
'@glimmer/component@1.1.2(@babel/core@7.26.0)':
dependencies:
@@ -8316,6 +8422,8 @@ snapshots:
'@types/ember__engine': 3.16.9
'@types/ember__object': 3.12.13
+ '@types/ember__destroyable@4.0.5': {}
+
'@types/ember__engine@3.16.9':
dependencies:
'@types/ember__object': 3.12.13
@@ -8547,13 +8655,13 @@ snapshots:
- supports-color
- webpack
- '@upfluence/oss-components@3.81.3(@babel/core@7.26.0)(@ember/test-helpers@2.9.4(@babel/core@7.26.0)(ember-source@3.28.12(@babel/core@7.26.0)))(ember-source@3.28.12(@babel/core@7.26.0))(qunit@2.23.1)(typescript@4.9.5)(webpack@5.97.1)':
+ '@upfluence/oss-components@3.88.15(@babel/core@7.26.0)(@ember/test-helpers@2.9.4(@babel/core@7.26.0)(ember-source@3.28.12(@babel/core@7.26.0)))(ember-source@3.28.12(@babel/core@7.26.0))(qunit@2.23.1)(typescript@4.9.5)(webpack@5.97.1)':
dependencies:
'@ember/render-modifiers': 2.1.0(@babel/core@7.26.0)(ember-source@3.28.12(@babel/core@7.26.0))
'@embroider/macros': 1.16.10
- '@embroider/util': 1.13.2(ember-source@3.28.12(@babel/core@7.26.0))
- '@floating-ui/dom': 1.6.13
- '@fortawesome/fontawesome-pro': git+https://git@github.com:upfluence/fontawesome-pro.git#2ae6c0044b34d06543101723202e117125257264
+ '@embroider/util': 1.13.5(ember-source@3.28.12(@babel/core@7.26.0))
+ '@floating-ui/dom': 1.7.4
+ '@fortawesome/fontawesome-pro': git+https://git@github.com:upfluence/fontawesome-pro.git#c639fe415de762c946eaebf4cc4e815ce3ede420
babel-plugin-debug-macros: 0.3.4(@babel/core@7.26.0)
bootstrap: 3.4.1
broccoli-funnel: 3.0.8
@@ -8571,7 +8679,7 @@ snapshots:
ember-truth-helpers: 3.1.1
money-formatter: 0.1.4
qunit: 2.23.1
- resolve: 1.22.10
+ resolve: 1.22.11
transitivePeerDependencies:
- '@babel/core'
- '@ember/test-helpers'
@@ -9118,6 +9226,8 @@ snapshots:
babel-import-util@3.0.0: {}
+ babel-import-util@3.0.1: {}
+
babel-loader@8.4.1(@babel/core@7.26.0)(webpack@4.47.0):
dependencies:
'@babel/core': 7.26.0
@@ -10633,6 +10743,10 @@ snapshots:
dependencies:
ms: 2.1.3
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
decimal.js@10.4.3: {}
decode-uri-component@0.2.2: {}
@@ -13452,6 +13566,8 @@ snapshots:
lodash@4.17.21: {}
+ lodash@4.17.23: {}
+
log-symbols@2.2.0:
dependencies:
chalk: 2.4.2
@@ -14541,12 +14657,20 @@ snapshots:
resolve-url@0.2.1: {}
+ resolve.exports@2.0.3: {}
+
resolve@1.22.10:
dependencies:
is-core-module: 2.16.1
path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0
+ resolve@1.22.11:
+ dependencies:
+ is-core-module: 2.16.1
+ path-parse: 1.0.7
+ supports-preserve-symlinks-flag: 1.0.0
+
responselike@1.0.2:
dependencies:
lowercase-keys: 1.0.1
@@ -14688,6 +14812,8 @@ snapshots:
semver@7.6.3: {}
+ semver@7.7.3: {}
+
send@0.19.0:
dependencies:
debug: 2.6.9
diff --git a/tests/dummy/app/controllers/application.js b/tests/dummy/app/controllers/application.ts
similarity index 58%
rename from tests/dummy/app/controllers/application.js
rename to tests/dummy/app/controllers/application.ts
index cdf40f82..81df42ed 100644
--- a/tests/dummy/app/controllers/application.js
+++ b/tests/dummy/app/controllers/application.ts
@@ -1,6 +1,7 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
+import { type AutocompletionAddress } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
export default class ApplicationController extends Controller {
@tracked selectedItems = ['toto'];
@@ -20,6 +21,8 @@ export default class ApplicationController extends Controller {
zipcode: '10016'
};
@tracked shippingAddress = { address: '69 Avenue Victor Hugo, Paris, France', resolved_address: null };
+ @tracked inputValue: string = '';
+ @tracked ossInputValue: string = '';
constructor() {
super(...arguments);
@@ -27,9 +30,7 @@ export default class ApplicationController extends Controller {
}
@action
- onSocialMediaHandlerChanged(socialNetwork, handle, formattedUrl) {
- console.log(socialNetwork, handle, formattedUrl);
- }
+ onSocialMediaHandlerChanged() {}
@action
onBlobSwitch() {
@@ -37,18 +38,29 @@ export default class ApplicationController extends Controller {
}
@action
- onChange(address, isValid) {
- console.log(address, isValid);
- }
+ onChange() {}
@action
- onLogoChange(icon, color) {
+ onLogoChange(icon: string, color: string) {
this.selectedColor = color;
this.selectedIcon = icon;
}
@action
- onChangeAddress(value) {
+ onChangeAddress(value: any) {
this.shippingAddress = value;
+ console.log('Auto-complete shipping address', value);
+ }
+
+ @action
+ onAutoComplete(value: AutocompletionAddress) {
+ this.inputValue = value.formattedAddress;
+ console.log('Auto-complete oss/input-container address', value);
+ }
+
+ @action
+ onOssAutoComplete(value: AutocompletionAddress) {
+ this.ossInputValue = value.formattedAddress;
+ console.log('Auto-complete native input address', value);
}
}
diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs
index 77dd90bc..7d3f529d 100644
--- a/tests/dummy/app/templates/application.hbs
+++ b/tests/dummy/app/templates/application.hbs
@@ -25,4 +25,6 @@
+
+
\ No newline at end of file
diff --git a/tests/integration/components/u-edit/shared-triggers/modals/image-upload-test.ts b/tests/integration/components/u-edit/shared-triggers/modals/image-upload-test.ts
index fdf0a957..ac27bbd9 100644
--- a/tests/integration/components/u-edit/shared-triggers/modals/image-upload-test.ts
+++ b/tests/integration/components/u-edit/shared-triggers/modals/image-upload-test.ts
@@ -43,7 +43,7 @@ module('Integration | Component | u-edit/shared-triggers/modals/image-upload', f
hbs``
);
await click('[data-control-name="close-modal-button"]');
- assert.true(this.onClose.calledOnceWithExactly());
+ assert.true(this.onClose.calledOnce);
});
test("the alert info isn't rendered", async function (assert) {
diff --git a/tests/integration/components/u-edit/shared-triggers/modals/pdf-upload-test.ts b/tests/integration/components/u-edit/shared-triggers/modals/pdf-upload-test.ts
index cb4cc1dc..35dd7250 100644
--- a/tests/integration/components/u-edit/shared-triggers/modals/pdf-upload-test.ts
+++ b/tests/integration/components/u-edit/shared-triggers/modals/pdf-upload-test.ts
@@ -43,7 +43,7 @@ module('Integration | Component | u-edit/shared-triggers/modals/pdf-upload', fun
hbs``
);
await click('[data-control-name="close-modal-button"]');
- assert.true(this.onClose.calledOnceWithExactly());
+ assert.true(this.onClose.calledOnce);
});
test('the alert info is rendered', async function (assert) {
diff --git a/tests/integration/components/utils/address-form-test.ts b/tests/integration/components/utils/address-form-test.ts
index 9e32e585..e8f4dbd2 100644
--- a/tests/integration/components/utils/address-form-test.ts
+++ b/tests/integration/components/utils/address-form-test.ts
@@ -315,7 +315,7 @@ module('Integration | Component | utils/address-form', function (hooks) {
@onChange={{this.onChange}} />`
);
- assert.dom('.google-autocomplete-input-container[data-control-name="address-form-address1"]').exists();
+ assert.dom('.autocomplete-input-container[data-control-name="address-form-address1"]').exists();
});
module('When @hideNameAttrs is true', () => {
diff --git a/tests/integration/components/utils/address-inline-test.ts b/tests/integration/components/utils/address-inline-test.ts
index 7ec990b6..aba351b1 100644
--- a/tests/integration/components/utils/address-inline-test.ts
+++ b/tests/integration/components/utils/address-inline-test.ts
@@ -46,11 +46,11 @@ module('Integration | Component | utils/address-inline', function (hooks) {
});
});
- test('when @useGoogleAutocomplete is true, it renders the google autocomplete input', async function (assert) {
+ test('when @useGoogleAutocomplete is true, it renders the autocomplete input', async function (assert) {
await render(
hbs``
);
- assert.dom('.google-autocomplete-input-container [data-control-name="address-inline"]').exists();
+ assert.dom('.autocomplete-input-container[data-control-name="address-inline"]').exists();
});
});
diff --git a/tests/integration/modifiers/setup-autocomplete-test.ts b/tests/integration/modifiers/setup-autocomplete-test.ts
new file mode 100644
index 00000000..852213c0
--- /dev/null
+++ b/tests/integration/modifiers/setup-autocomplete-test.ts
@@ -0,0 +1,248 @@
+import { module, test } from 'qunit';
+import { setupRenderingTest } from 'ember-qunit';
+import { render, find, settled, setupOnerror } from '@ember/test-helpers';
+import { hbs } from 'ember-cli-htmlbars';
+import { type AutocompletionAddress } from '@upfluence/ember-upf-utils/modifiers/setup-autocomplete';
+
+import {
+ createMockPlaceResult,
+ createSampleAddressComponents,
+ MockLoader
+} from '@upfluence/ember-upf-utils/utils/google-maps-mock';
+
+module('Integration | Modifier | setup-autocomplete', function (hooks) {
+ setupRenderingTest(hooks);
+
+ hooks.beforeEach(function () {
+ this.mockLoader = new MockLoader({ apiKey: 'test-key' });
+ });
+
+ module('Element setup', () => {
+ test('it works with a text input element directly', async function (assert) {
+ await render(
+ hbs``
+ );
+
+ assert.dom('input[type="text"]').exists();
+ const input = find('input[type="text"]');
+ assert.dom(input?.parentElement).hasClass('autocomplete-input-container');
+ });
+
+ test('it works with input inside a container element', async function (assert) {
+ await render(hbs`
+
+
+
+ `);
+
+ assert.dom('div').hasClass('autocomplete-input-container');
+ assert.dom('input[type="text"]').exists();
+ });
+ });
+
+ module('Callback functionality', () => {
+ test('callback is called with parsed address data', async function (assert) {
+ assert.expect(1);
+
+ this.handleAddress = (result: AutocompletionAddress) => {
+ assert.ok(result, 'callback receives address result');
+ };
+
+ await render(hbs`
+
+ `);
+
+ const mockAutocomplete = this.mockLoader.getMockAutocompleteInstance();
+ const mockPlace = createMockPlaceResult(createSampleAddressComponents({}));
+ mockAutocomplete?.simulatePlaceSelection(mockPlace);
+ await settled();
+ });
+
+ test('callback receives all expected address fields', async function (assert) {
+ assert.expect(6);
+
+ this.handleAddress = (result: AutocompletionAddress) => {
+ assert.ok('address1' in result, 'result has address1');
+ assert.ok('city' in result, 'result has city');
+ assert.ok('state' in result, 'result has state');
+ assert.ok('zipcode' in result, 'result has zipcode');
+ assert.ok('country' in result, 'result has country');
+ assert.ok('formattedAddress' in result, 'result has formattedAddress');
+ };
+
+ await render(hbs`
+
+ `);
+
+ const mockAutocomplete = this.mockLoader.getMockAutocompleteInstance();
+ const mockPlace = createMockPlaceResult(createSampleAddressComponents({}));
+ mockAutocomplete?.simulatePlaceSelection(mockPlace);
+ await settled();
+ });
+ });
+
+ module('Cleanup', () => {
+ test('pac-container is removed on teardown', async function (assert) {
+ await render(hbs`
+
+ `);
+
+ const pacContainer = document.createElement('div');
+ pacContainer.classList.add('pac-container');
+ document.body.appendChild(pacContainer);
+
+ assert.ok(document.querySelector('.pac-container'));
+
+ await render(hbs``);
+
+ assert.notOk(document.querySelector('.pac-container'));
+ });
+
+ test('wrapper is properly unwrapped during cleanup', async function (assert) {
+ await render(
+ hbs``
+ );
+
+ assert.dom('#test-input').exists();
+ const wrapper = find('#test-input')!.parentElement;
+ assert.dom(wrapper).hasClass('autocomplete-input-container');
+
+ const wrapperParent = wrapper?.parentElement;
+ assert.dom(wrapperParent).exists();
+
+ await render(hbs``);
+
+ assert.dom('.autocomplete-input-container').doesNotExist();
+ assert.dom('#new-content').exists();
+ });
+
+ test('cleanup handles already removed elements gracefully', async function (assert) {
+ await render(
+ hbs``
+ );
+
+ assert.dom('input[type="text"]').exists();
+ const input = find('input[type="text"]');
+
+ const wrapper = input?.parentElement;
+ if (wrapper?.classList.contains('autocomplete-input-container')) {
+ wrapper.remove();
+ }
+
+ await render(hbs``);
+ assert.ok(true, 'cleanup handled gracefully without errors');
+ });
+
+ test('wrapper is not created when modifier is on container element', async function (assert) {
+ await render(hbs`
+
+
+
+ `);
+
+ const input = find('#container-input') as HTMLInputElement;
+ const container = input.parentElement;
+
+ assert.dom(container).hasClass('autocomplete-input-container');
+ assert.strictEqual(container?.tagName, 'DIV', 'parent is the original div container');
+
+ await render(hbs``);
+ assert.ok(true, 'cleanup completed without trying to unwrap');
+ });
+ });
+
+ module('Edge cases', () => {
+ test('works with pre-filled input value', async function (assert) {
+ this.value = '123 Main Street';
+
+ await render(hbs`
+
+ `);
+
+ assert.dom('input[type="text"]').hasValue('123 Main Street');
+ });
+
+ test('preserves input attributes', async function (assert) {
+ await render(hbs`
+
+ `);
+
+ assert.dom('input[type="text"]').hasAttribute('id', 'address-input');
+ assert.dom('input[type="text"]').hasClass('custom-input');
+ assert.dom('input[type="text"]').hasAttribute('placeholder', 'Enter address');
+ });
+
+ test('handles international addresses correctly', async function (assert) {
+ assert.expect(4);
+
+ this.handleAddress = (result: AutocompletionAddress) => {
+ assert.strictEqual(result.address1, '10 Downing Street');
+ assert.strictEqual(result.city, 'London');
+ assert.strictEqual(result.zipcode, 'SW1A 2AA');
+ assert.strictEqual(result.country.alpha2, 'GB');
+ };
+
+ await render(hbs`
+
+ `);
+
+ const mockAutocomplete = this.mockLoader.getMockAutocompleteInstance();
+ const mockPlace = createMockPlaceResult([
+ { types: ['street_number'], long_name: '10', short_name: '10' },
+ { types: ['route'], long_name: 'Downing Street', short_name: 'Downing St' },
+ { types: ['postal_town'], long_name: 'London', short_name: 'London' },
+ { types: ['postal_code'], long_name: 'SW1A 2AA', short_name: 'SW1A 2AA' },
+ { types: ['country'], long_name: 'United Kingdom', short_name: 'GB' }
+ ]);
+
+ mockAutocomplete?.simulatePlaceSelection(mockPlace);
+ await settled();
+ });
+
+ module('for error management', () => {
+ test('handles missing callback gracefully', async function (assert) {
+ assert.expect(1);
+ setupOnerror((error: Error) => {
+ assert.equal(
+ error.message,
+ 'Assertion Failed: [modifier][setup-autocomplete] The callback is mandatory and must be a function'
+ );
+ });
+
+ await render(hbs``);
+ });
+
+ test('handles missing input element gracefully', async function (assert) {
+ assert.expect(1);
+ setupOnerror((error: Error) => {
+ assert.equal(
+ error.message,
+ 'Assertion Failed: [modifier][setup-autocomplete] No input[type="text"] element found in the provided element or its children'
+ );
+ });
+
+ await render(hbs``);
+ });
+
+ test('handles missing input element in its children gracefully', async function (assert) {
+ assert.expect(1);
+ setupOnerror((error: Error) => {
+ assert.equal(
+ error.message,
+ 'Assertion Failed: [modifier][setup-autocomplete] No input[type="text"] element found in the provided element or its children'
+ );
+ });
+
+ await render(
+ hbs``
+ );
+ });
+ });
+ });
+});
diff --git a/tests/unit/utils/address-parser-test.ts b/tests/unit/utils/address-parser-test.ts
new file mode 100644
index 00000000..da43a375
--- /dev/null
+++ b/tests/unit/utils/address-parser-test.ts
@@ -0,0 +1,181 @@
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { parseAddressComponents } from '@upfluence/ember-upf-utils/utils/address-parser';
+import { createSampleAddressComponents } from '@upfluence/ember-upf-utils/utils/google-maps-mock';
+
+module('Unit | Utility | address-parser', function (hooks) {
+ setupTest(hooks);
+
+ module('parseAddressComponents', function () {
+ test('parses a complete US address correctly', function (assert) {
+ const components = createSampleAddressComponents({
+ streetNumber: '1600',
+ route: 'Amphitheatre Parkway',
+ city: 'Mountain View',
+ state: 'California',
+ zipcode: '94043',
+ country: 'United States',
+ countryCode: 'US'
+ });
+
+ const result = parseAddressComponents(components, '1600 Amphitheatre Parkway, Mountain View, CA 94043');
+
+ assert.strictEqual(result.address1, '1600 Amphitheatre Parkway', 'address1 is correct');
+ assert.strictEqual(result.city, 'Mountain View', 'city is correct');
+ assert.strictEqual(result.state, 'California', 'state is correct');
+ assert.strictEqual(result.zipcode, '94043', 'zipcode is correct');
+ assert.strictEqual(result.country.alpha2, 'US', 'country code is correct');
+ assert.strictEqual(result.formattedAddress, '1600 Amphitheatre Parkway, Mountain View, CA 94043');
+ });
+
+ test('parses address with apartment/suite number (subpremise)', function (assert) {
+ const components = createSampleAddressComponents({
+ streetNumber: '123',
+ route: 'Main Street',
+ subpremise: 'Apt 4B',
+ city: 'New York',
+ state: 'New York',
+ zipcode: '10001',
+ countryCode: 'US'
+ });
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.address1, '123 Main Street');
+ assert.strictEqual(result.address2, 'Apt 4B');
+ assert.strictEqual(result.city, 'New York');
+ });
+
+ test('handles address without street number', function (assert) {
+ const components = [
+ { types: ['route'], long_name: 'Broadway', short_name: 'Broadway' },
+ { types: ['locality'], long_name: 'New York', short_name: 'New York' },
+ { types: ['administrative_area_level_1'], long_name: 'New York', short_name: 'NY' },
+ { types: ['postal_code'], long_name: '10001', short_name: '10001' },
+ { types: ['country'], long_name: 'United States', short_name: 'US' }
+ ];
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.address1, 'Broadway');
+ assert.strictEqual(result.city, 'New York');
+ });
+
+ test('handles postal_code_suffix for extended zip codes', function (assert) {
+ const components = [
+ { types: ['street_number'], long_name: '123', short_name: '123' },
+ { types: ['route'], long_name: 'Main St', short_name: 'Main St' },
+ { types: ['locality'], long_name: 'Portland', short_name: 'Portland' },
+ { types: ['administrative_area_level_1'], long_name: 'Oregon', short_name: 'OR' },
+ { types: ['postal_code'], long_name: '97201', short_name: '97201' },
+ { types: ['postal_code_suffix'], long_name: '1234', short_name: '1234' },
+ { types: ['country'], long_name: 'United States', short_name: 'US' }
+ ];
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.zipcode, '97201-1234');
+ });
+
+ test('uses postal_town as city when locality is not available', function (assert) {
+ const components = [
+ { types: ['street_number'], long_name: '10', short_name: '10' },
+ { types: ['route'], long_name: 'Downing Street', short_name: 'Downing St' },
+ { types: ['postal_town'], long_name: 'London', short_name: 'London' },
+ { types: ['postal_code'], long_name: 'SW1A 2AA', short_name: 'SW1A 2AA' },
+ { types: ['country'], long_name: 'United Kingdom', short_name: 'GB' }
+ ];
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.city, 'London');
+ assert.strictEqual(result.country.alpha2, 'GB');
+ });
+
+ test('postal_town takes precedence over locality when both present', function (assert) {
+ const components = [
+ { types: ['route'], long_name: 'Test Street', short_name: 'Test St' },
+ { types: ['postal_town'], long_name: 'Postal Town', short_name: 'Postal Town' },
+ { types: ['locality'], long_name: 'Actual City', short_name: 'Actual City' },
+ { types: ['postal_code'], long_name: '12345', short_name: '12345' },
+ { types: ['country'], long_name: 'United States', short_name: 'US' }
+ ];
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.city, 'Postal Town');
+ });
+
+ test('handles international address (France)', function (assert) {
+ const components = createSampleAddressComponents({
+ streetNumber: '5',
+ route: 'Avenue Anatole France',
+ city: 'Paris',
+ state: 'Île-de-France',
+ zipcode: '75007',
+ country: 'France',
+ countryCode: 'FR'
+ });
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.address1, '5 Avenue Anatole France');
+ assert.strictEqual(result.city, 'Paris');
+ assert.strictEqual(result.country.alpha2, 'FR');
+ });
+
+ test('defaults to US when country code is not recognized', function (assert) {
+ const components = [
+ { types: ['route'], long_name: 'Unknown Street', short_name: 'Unknown St' },
+ { types: ['locality'], long_name: 'Unknown City', short_name: 'Unknown City' },
+ { types: ['country'], long_name: 'Unknown Country', short_name: 'XX' }
+ ];
+
+ const result = parseAddressComponents(components, '');
+
+ assert.strictEqual(result.country.alpha2, 'US');
+ });
+
+ test('handles empty address components array', function (assert) {
+ const result = parseAddressComponents([], '123 Test St');
+
+ assert.strictEqual(result.address1, '');
+ assert.strictEqual(result.city, '');
+ assert.strictEqual(result.state, '');
+ assert.strictEqual(result.zipcode, '');
+ assert.strictEqual(result.country.alpha2, 'US');
+ assert.strictEqual(result.formattedAddress, '123 Test St');
+ });
+
+ test('omits address2 when not present', function (assert) {
+ const components = createSampleAddressComponents({
+ streetNumber: '123',
+ route: 'Main St',
+ city: 'Test City',
+ zipcode: '12345',
+ countryCode: 'US'
+ });
+
+ const result = parseAddressComponents(components, '');
+
+ assert.notOk('address2' in result);
+ });
+
+ test('preserves formatted address parameter', function (assert) {
+ const components = createSampleAddressComponents({});
+ const formattedAddress = '1600 Amphitheatre Parkway, Mountain View, CA 94043, USA';
+
+ const result = parseAddressComponents(components, formattedAddress);
+
+ assert.strictEqual(result.formattedAddress, formattedAddress);
+ });
+
+ test('handles missing formatted address parameter', function (assert) {
+ const components = createSampleAddressComponents({});
+
+ const result = parseAddressComponents(components);
+
+ assert.strictEqual(result.formattedAddress, '');
+ });
+ });
+});