diff --git a/src/digital-components/modal/modal.vue b/src/digital-components/modal/modal.vue index e1f3fb462..0004be47e 100644 --- a/src/digital-components/modal/modal.vue +++ b/src/digital-components/modal/modal.vue @@ -90,10 +90,12 @@ export default { openModal() { const modal = Modal.getOrCreateInstance(document.getElementById(this.modalId)); modal.show(); + this.$emit("isModalOpened", true); }, closeModal() { const modal = Modal.getInstance(document.getElementById(this.modalId)); modal.hide(); + this.$emit("isModalOpened", false); }, }, computed: { diff --git a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js index 5cade9ddb..26a81341a 100644 --- a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.spec.js @@ -7,7 +7,6 @@ import { mount, shallowMount } from "@vue/test-utils"; import { getMountOptions } from "@/helpers/unit-test-helper.js"; import { storeMutations } from "@/constants/store-mutations"; import store from "@/store"; -import { createImportSpecifier } from "typescript"; let autocompleteElement; describe("address-questions.vue", () => { @@ -134,22 +133,19 @@ describe("address-questions.vue", () => { }); test("Should set this.showAddressFields to true when the model is prepopulated", async () => { - // Arrange - // Act - const newAddressModel = { - streetAddress: "foo", - city: "foo", - state: "foo", - zipCode: "55555", - }; - const wrapper = shallowMount(addressQuestions, { - propsData: { - modelValue: newAddressModel, + // Arrange / Act + const { wrapper } = setupMocks({ + props: { + modelValue: { + streetAddress: "foo", + city: "foo", + state: "foo", + zipCode: "55555", + }, }, }); - // Act - wrapper.vm.setupAddressLookup(); + await wrapper.vm.$nextTick(); // Assert expect(wrapper.vm.showAddressFields).toBe(true); diff --git a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue index 9842d42c3..20434da4d 100644 --- a/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue +++ b/src/layouts/address-lookup/customer-questions/address-questions/address-questions.vue @@ -122,13 +122,15 @@ export default { }, data() { return { - showAddressFields: false, displayVerificationWarning: false, displayNoMatchWarning: false, alertHeadlineVerificationWarning: "", alertCopyVerificationWarning: "", alertHeadlineNoMatchWarning: "", alertCopyNoMatchWarning: "", + autocomplete: null, + autocompleteListener: null, + showAddressFields: false, matchFound: null, // null = no attempted match, true = match was found, false = match was not found }; }, @@ -203,182 +205,194 @@ export default { return this.captureApartmentNumberOrBusinessName; }, }, + addressField1: { + get: function () { + return document.getElementById("autocomplete"); + }, + }, }, methods: { - setupAddressLookup() { - this.showAddressFields = false; - if ( - this.addressModel.streetAddress && - this.addressModel.city && - this.addressModel.state && - this.addressModel.zipCode - ) { - this.showAddressFields = true; - return; - } - - const addressField1 = document.getElementById("autocomplete"); - const self = this; - + loadGooglePlacesAutocompleteScript() { + // Load the Google Places Autocomplete script const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY; - this.$loadScript( `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype` - ) - .then(() => { - // Script is loaded, initialize the autocomplete textbox - const autocomplete = new window.google.maps.places.Autocomplete(addressField1, { - componentRestrictions: { country: ["us"] }, - fields: ["address_components"], - types: ["geocode"], - }); + ).then(() => { + // When loaded trigger the setup + this.initializeAutocomplete(); + }); + }, + initializeAutocomplete() { + // Initialize the Google Places Autocomplete + this.autocomplete = new window.google.maps.places.Autocomplete(this.addressField1, { + componentRestrictions: { country: ["us"] }, + fields: ["address_components"], + types: ["geocode"], + }); - // Standard place_changed event handling - const autocompleteListener = window.google.maps.event.addListener( - autocomplete, - "place_changed", - fillInAddress + // Set up the Autocomplete place_changed event to call our method to fill in the address + this.autocompleteListener = window.google.maps.event.addListener( + this.autocomplete, + "place_changed", + this.fillInAddress + ); + + // When the Street Address textbox receives focus, + // append the search results list container to the bottom of the textbox + // and disable browser autofill + this.addressField1.addEventListener("focus", (e) => { + // Make place results box stick to the input on scroll + const streetAddressField = document.getElementById("streetAddressField"); + const autocompleteResultsContainer = + document.getElementsByClassName("pac-container")[0]; + if (autocompleteResultsContainer) { + streetAddressField.appendChild(autocompleteResultsContainer); + } + + // Unfortunately this is the only place we can set the autocomplete attribute without the + // Google Places object resetting it to "off" which does nothing to prevent browser autofill + this.addressField1.setAttribute("autocomplete", "do-not-autofill"); + }); + + this.addressField1.addEventListener("keydown", (e) => { + const event = new Event("place_changed"); + + // When either of the two enter keys or the tab key are pressed + if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") { + // Grab the selected item + const selectedItem = document.querySelector( + ".pac-container .pac-item-selected" ); - addressField1.addEventListener("focus", () => { - // Wrapping the addressField1 element in the Google Address Autocomplete object - // will cause "autocomplete='off'" which Chrome completely ignores. This event - // handler will set the value to something arbitrary so autofill doesn't work. - // https://stackoverflow.com/a/30976223 - addressField1.setAttribute("autocomplete", "do-not-autofill"); + if (selectedItem !== null) { + // If an item was selected then fill in the address with the selected item + // by triggering the "place_changed" event of the Autocomplete object + this.autocomplete.dispatchEvent(event); + } else { + // Otherwise fill-in the address using first item from the list. + this.fillInAddressUsingFirstItem(); + } + } else { + return; + } + }); - // Make place results box stick to the input on scroll - const streetAddressField = document.getElementById("streetAddressField"); - const autocompleteResultsContainer = - document.getElementsByClassName("pac-container")[0]; - if (autocompleteResultsContainer) { - streetAddressField.appendChild(autocompleteResultsContainer); - } - }); + this.addressField1.addEventListener("change", () => { + // If a match has been previously attempted then do nothing + if (this.matchFound !== null) { + return; + } - addressField1.addEventListener("keydown", (e) => { - const autocomplete = document.getElementById("autocomplete"); - const event = new Event("place_changed"); + // Get the address that the user clicked on (if any) + const clickedAddress = document.querySelector(".pac-container .pac-item:hover"); - if (e.code === "Enter" || e.code === "NumpadEnter" || e.code === "Tab") { - const selectedItem = document.querySelector( - ".pac-container .pac-item-selected" - ); - if (selectedItem !== null) { - // Fill-in the address using selected item in the list. - autocomplete.dispatchEvent(event); - //fillInAddress(selectedItem.textContent); - } else { - // Fill-in the address using first item in the list. - fillInAddressUsingFirstItem(); + // If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field) + if (clickedAddress === null) { + // Fill-in the address using first item in the list. + this.fillInAddressUsingFirstItem(); + } + }); + }, + fillInAddress(place) { + if (!place) { + place = this.autocomplete.getPlace(); + } + + if (place && place.address_components) { + this.matchFound = true; + + const self = this; + this.$nextTick(function () { + self.showAddressFields = true; + + for (const component of place.address_components) { + const componentType = component.types[0]; + + switch (componentType) { + case "street_number": { + self.addressModel.streetAddress = component.long_name; + break; + } + case "route": { + self.addressModel.streetAddress += " " + component.short_name; + break; + } + case "locality": { + self.addressModel.city = component.long_name; + break; + } + case "administrative_area_level_1": { + self.addressModel.state = component.short_name; + break; + } + case "postal_code": { + self.addressModel.zipCode = component.long_name; + break; } - } else { - return; - } - }); - - addressField1.addEventListener("change", () => { - // If a match has been previously attempted then do nothing - if (self.matchFound !== null) { - return; - } - - // Get the address that the user clicked on (if any) - const clickedAddress = document.querySelector( - ".pac-container .pac-item:hover" - ); - - // If the Street Address field changed without clicking (i.e. by pressing Tab, or clicking outside the field) - if (clickedAddress === null) { - // Fill-in the address using first item in the list. - fillInAddressUsingFirstItem(); - } - }); - - function fillInAddressUsingFirstItem() { - // Fill-in the address using first item in the list. - const item = document.querySelector(".pac-container .pac-item"); - if (item != null) { - const firstResult = item.textContent; - const geocoder = new window.google.maps.Geocoder(); - geocoder.geocode( - { - address: firstResult, - }, - function (results, status) { - if (status === window.google.maps.GeocoderStatus.OK) { - fillInAddress(results[0]); - self.displayVerificationWarning = true; - } - } - ); - } else { - self.matchFound = false; } } - function fillInAddress(place) { - if (!place) { - place = autocomplete.getPlace(); - } - - if (place && place.address_components) { - self.matchFound = true; - - self.$nextTick(function () { - self.showAddressFields = true; - - for (const component of place.address_components) { - const componentType = component.types[0]; - - switch (componentType) { - case "street_number": { - self.addressModel.streetAddress = component.long_name; - break; - } - case "route": { - self.addressModel.streetAddress += - " " + component.short_name; - break; - } - case "locality": { - self.addressModel.city = component.long_name; - break; - } - case "administrative_area_level_1": { - self.addressModel.state = component.short_name; - break; - } - case "postal_code": { - self.addressModel.zipCode = component.long_name; - break; - } - } - } - - // after showing the address fields, disable the address autocomplete - window.google.maps.event.removeListener(autocompleteListener); - window.google.maps.event.clearInstanceListeners(autocomplete); - addressField1.onchange = null; - const pacContainer = document.querySelector(".pac-container"); - if (pacContainer) { - pacContainer.remove(); - } - }); - } - } - }) - .catch(() => { - // Failed to fetch script - console.log("Unable to load Google Places API script"); + // After filling in the address fields, disable the address autocomplete + this.unloadAutocomplete(); }); + } + }, + fillInAddressUsingFirstItem() { + // Fill-in the address using first item in the list. + const item = document.querySelector(".pac-container .pac-item"); + if (item != null) { + const firstResult = item.textContent; + const geocoder = new window.google.maps.Geocoder(); + const self = this; + geocoder.geocode( + { + address: firstResult, + }, + function (results, status) { + if (status === window.google.maps.GeocoderStatus.OK) { + self.fillInAddress(results[0]); + self.displayVerificationWarning = true; + } + } + ); + } else { + this.matchFound = false; + } }, resetAlerts() { this.displayVerificationWarning = false; }, + unloadAutocomplete() { + if (this.autocompleteListener && this.autocomplete) { + window.google.maps.event.removeListener(this.autocompleteListener); + this.autocompleteListener = null; + + window.google.maps.event.clearInstanceListeners(this.autocomplete); + this.autocomplete = null; + + this.addressField1.onchange = null; + + const pacContainer = document.querySelector(".pac-container"); + if (pacContainer) { + pacContainer.remove(); + } + } + }, }, mounted() { - this.setupAddressLookup(); + // If we already have a full address, show it + this.showAddressFields = + (this.addressModel.streetAddress ?? "") !== "" && + (this.addressModel.city ?? "") !== "" && + (this.addressModel.state ?? "") !== "" && + (this.addressModel.zipCode ?? "") !== ""; + + if (!this.showAddressFields) { + this.loadGooglePlacesAutocompleteScript(); + } + }, + unmounted() { + this.unloadAutocomplete(); }, watch: { matchFound: { @@ -398,7 +412,7 @@ export default { this.addressModel.state = ""; this.addressModel.zipCode = ""; } - this.showAddressFields = true; + this.displayVerificationWarning = false; // Only deep watch the Address Model after a failed match @@ -414,11 +428,6 @@ export default { } }, }, - modelValue: { - handler() { - this.setupAddressLookup(); - }, - }, }, components: { textboxQuestion, diff --git a/src/layouts/license-plate-lookup/license-plate-lookup.vue b/src/layouts/license-plate-lookup/license-plate-lookup.vue index 03b56a4cb..a3579318d 100644 --- a/src/layouts/license-plate-lookup/license-plate-lookup.vue +++ b/src/layouts/license-plate-lookup/license-plate-lookup.vue @@ -301,12 +301,18 @@ export default { ); await this.dispatchStoreAction(storeActions.SAVE_EMAIL, this.email, false); + await this.dispatchStoreAction( storeActions.SAVE_SERVICE_LOCATION, { - zipCode: this.serviceZipCode, + address: "", + address2: "", + city: "", state: resultMap.serviceZipValidationResponse.state, + zipCode: this.serviceZipCode, zipCodeCtu: resultMap.serviceZipValidationResponse.zipCodeCtu, + appointmentType: "", + isVehicleProtected: null, }, false ); diff --git a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue index 82dd2980a..cce1a4d5b 100644 --- a/src/layouts/service-location/appointment-type-question/appointment-type-question.vue +++ b/src/layouts/service-location/appointment-type-question/appointment-type-question.vue @@ -95,8 +95,10 @@ export default { diff --git a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue index 39f64d890..eebaaab07 100644 --- a/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue +++ b/src/layouts/service-location/mobile-location-modal-questions/mobile-location-modal-questions.vue @@ -32,24 +32,27 @@ :footerButtonText="modalFooterText" :onModalOpenedCallback="onModalOpened" :onModalClosedCallback="onModalClosed" + @isModalOpened="setModalStatus" @footer-button-event="setMobileLocation"> - - - - + @@ -81,6 +84,8 @@ export default { return { internalModel: deepClone(this.modelValue), displayInvalidZipAlert: false, + addressQuestionsKey: 0, + isModalOpened: false, }; }, setup(props) { @@ -184,6 +189,9 @@ export default { modalFooterText() { return this.getCmsContent(this.modalWidgetName, "FooterText"); }, + modal() { + return this.$refs[this.modalName]; + }, addressModel: { get: function () { return this.modelValue.addressQuestions; @@ -192,48 +200,23 @@ export default { }, methods: { openModal() { - this.$refs[this.modalName].openModal(); - }, - closeModal() { - this.$refs[this.modalName].closeModal(); + this.modal.openModal(); }, onModalOpened() { this.internalModel = deepClone(this.modelValue); }, + setModalStatus(isOpened) { + this.isModalOpened = isOpened; + }, + closeModal() { + this.modal.closeModal(); + }, onModalClosed() { this.displayInvalidZipAlert = false; this.internalModel = deepClone(this.modelValue); - this.resetValidation(); - }, - resetComponent(updatedServiceZipCodeInfo) { - // Reset the validation form, setting the initial values - // for the state and zipCode to those that were entered - // on the service-zip-modal-question component - this.$refs[this.modalName].resetForm({ - values: { - autocomplete: updatedServiceZipCodeInfo.streetAddress, - city: updatedServiceZipCodeInfo.city, - state: updatedServiceZipCodeInfo.state, - zipCode: updatedServiceZipCodeInfo.zipCode, - isVehicleProtected: updatedServiceZipCodeInfo.isVehicleProtected, - }, - }); }, resetModalButtonStyle() { - this.$refs[this.modalName].resetButtonStyle(); - }, - resetValidation() { - this.$refs.addressQuestions.resetAlerts(); - - this.$refs[this.modalName].resetForm({ - values: { - autocomplete: this.internalModel.addressQuestions.streetAddress, - city: this.internalModel.addressQuestions.city, - state: this.internalModel.addressQuestions.state, - zipCode: this.internalModel.addressQuestions.zipCode, - isVehicleProtected: this.internalModel.isVehicleProtected, - }, - }); + this.modal.resetButtonStyle(); }, async setMobileLocation() { if ( @@ -283,14 +266,6 @@ export default { this.internalModel = deepClone(newValue); this.handleChange(newValue); - - this.resetComponent({ - streetAddress: newValue.addressQuestions.streetAddress, - city: newValue.addressQuestions.city, - state: newValue.addressQuestions.state, - zipCode: newValue.addressQuestions.zipCode, - isVehicleProtected: newValue.isVehicleProtected, - }); }, deep: true, }, diff --git a/src/layouts/service-location/service-location.vue b/src/layouts/service-location/service-location.vue index 8f63e5df7..b44f1c64c 100644 --- a/src/layouts/service-location/service-location.vue +++ b/src/layouts/service-location/service-location.vue @@ -122,10 +122,10 @@ import { errorMessages } from "@/constants/error-messages"; // DEFINE VALIDATION RULES defineRule("mobile-location-required", (value) => { if ( - value.addressQuestions.streetAddress == "" || - value.addressQuestions.city == "" || - value.addressQuestions.state == "" || - value.addressQuestions.zipCode == "" || + value.addressQuestions.streetAddress == null || + value.addressQuestions.city == null || + value.addressQuestions.state == null || + value.addressQuestions.zipCode == null || value.isVehicleProtected == null ) { return errorMessages.MOBILE_LOCATION_REQUIRED; diff --git a/src/layouts/vin-lookup/vin-lookup.vue b/src/layouts/vin-lookup/vin-lookup.vue index 3a2b79a8c..c1f2dd7ec 100644 --- a/src/layouts/vin-lookup/vin-lookup.vue +++ b/src/layouts/vin-lookup/vin-lookup.vue @@ -325,9 +325,14 @@ export default { await this.dispatchStoreAction( storeActions.SAVE_SERVICE_ZIP_CODE_INFO, { - zipCode: this.serviceZipCode, + address: "", + address2: "", + city: "", state: resultMap.zipCodeData.state, + zipCode: this.serviceZipCode, zipCodeCtu: resultMap.zipCodeData.zipCodeCtu, + appointmentType: "", + isVehicleProtected: null, }, false ); @@ -352,8 +357,8 @@ export default { { address: vehicleRegistrationInfo.address, city: vehicleRegistrationInfo.city, - zipCode: vehicleRegistrationInfo.zipCode, state: vehicleRegistrationInfo.state, + zipCode: vehicleRegistrationInfo.zipCode, zipCodeCtu: zipCodeData.zipCodeCtu, }, false @@ -362,9 +367,14 @@ export default { await this.dispatchStoreAction( storeActions.SAVE_SERVICE_ZIP_CODE_INFO, { - zipCode: this.serviceZipCode, + address: null, + address2: null, + city: null, state: zipCodeData.state, + zipCode: this.serviceZipCode, zipCodeCtu: zipCodeData.zipCodeCtu, + appointmentType: null, + isVehicleProtected: null, }, false ); diff --git a/src/store/index.js b/src/store/index.js index 6c95aa81b..83220bc3b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -253,9 +253,15 @@ export const mutations = { state.order.serviceLocation.appointmentType = serviceLocationInfo.appointmentType; state.order.serviceLocation.isVehicleProtected = serviceLocationInfo.isVehicleProtected; - if (serviceLocationInfo.provider) { - state.order.serviceLocation.provider = serviceLocationInfo.provider; - } + state.order.serviceLocation.provider = { + providerNumber: serviceLocationInfo.provider?.providerNumber, + address: { + streetAddress: serviceLocationInfo.provider?.address?.streetAddress, + city: serviceLocationInfo.provider?.address?.city, + state: serviceLocationInfo.provider?.address?.state, + zip: serviceLocationInfo.provider?.address?.zip, + }, + }; }, updateSchedule(state, scheduleInfo) { if (scheduleInfo) { @@ -362,7 +368,15 @@ export const mutations = { state.order.serviceLocation.appointmentType = null; }, resetServiceLocationProvider(state) { - state.order.serviceLocation.provider = null; + state.order.serviceLocation.provider = { + providerNumber: null, + address: { + streetAddress: null, + city: null, + state: null, + zip: null, + }, + }; }, resetServiceLocationMobileAddress(state) { state.order.serviceLocation.address = null;