`, '') : ``; /* Get the tracking pixels wrapper element */ const wrapper = document.querySelector('#pixels-wrapper'); /* If the wrapper element already exists, append to it. If not, create it with the pixels inside */ if (wrapper) { wrapper.insertAdjacentHTML('beforeend', pixels); } else { document.body.insertAdjacentHTML('beforeend', `
${pixels}
`); } }, /* Tracks events emitted from the timeslots-widget web component */ trackFromEmitted: async (type, event) => { const { reportToWheelhouse, trackWompEvent } = this.helpers; const { scrollLeft, scrollTop } = document.body; let pixel; let provider; let index; let hasTimeslots = 0; const providerCardEl = event.target.closest('.card.provider'); try { if (providerCardEl) { index = Number(providerCardEl.dataset.position); provider = this.state.results.providers[index - 1]; hasTimeslots = providerCardEl.querySelector('timeslots-widget, directory-timeslots') ? 1 : 0; } } catch (err) { console.error('fromEmitted error:\n', err); } switch (type) { case 'phone': if (typeof event.detail === 'string') { await reportToWheelhouse([ { key: "tealium_event", value: "amp_event" }, { key: "event_category", value: "Contact" }, { key: "event_action", value: "Phone" }, { key: "event_label", value: event.detail }, { key: "scroll_x", value: scrollLeft }, { key: "scroll_y", value: scrollTop } ]); /* Internal tracking */ await trackWompEvent({ ec: "Direct Contact", ea: "Phone Click-to-Call", el: event.detail }); if (provider) { await trackWompEvent({ ec: "Provider Click-To-Call", ea: `${provider.Name}, ${index}, timeslots: ${hasTimeslots}`, el: JSON.stringify(provider['@search.features']) }); } /* Telium pixel */ pixel = siteSearch.helpers.createPixel({ event_category: "Direct Contact", event_action: "Phone Click-to-Call", event_label: event.detail, tealium_event: 'womp_directory_event', tealium_event_type: 'event' }); } else { console.error('event.detail value must be a string representing the phone number clicked'); return; } break; case 'timeslot': reportToWheelhouse([ { key: "tealium_event", value: "amp_event" }, { key: "event_category", value: "Schedule Appointment" }, { key: "event_action", value: "Pick a Time" }, { key: "scroll_x", value: scrollLeft }, { key: "scroll_y", value: scrollTop } ]); const action = event.detail?.action; trackWompEvent({ ec: "Directory Journeys", ea: action && action.length ? action : "Provider Directory", el: 'Timeslot Clicks' }); /* Internal tracking */ if (provider) { trackWompEvent({ ec: "Provider Timeslot Click", ea: `${provider.Name}, ${index}, timeslots: ${hasTimeslots}`, el: JSON.stringify(provider['@search.features']) }); } pixel = siteSearch.helpers.createPixel({ event_category: "Directory Journeys", event_action: action && action.length ? action : "Provider Directory", event_label: "Timeslot Clicks", tealium_event: 'womp_directory_event', tealium_event_type: 'event', }); break; default: console.error('Please provide a valid event type [phone, timeslot]'); return; } siteSearch.helpers.trackIt(pixel); }, getGoogleLocation: (elId = "custom-location") => { const value = document.querySelector("#" + elId).value; if (value !== this.state.filters.location) { this.state.updatedLocation = true; } let coords; if (value?.length > 1) { fetch( `https://maps.googleapis.com/maps/api/geocode/json?address=${ value }&key=AIzaSyC9aTi6UWN30wj-e4uJHJ5j0y6szBTQ4BY` ) .then((res) => res.json()) .then((res) => { let validZip = true; if (res.results[0]) { let resultIndex; try { resultIndex = res.results.findIndex(address => address.address_components.findIndex(ac => ac.long_name.toLowerCase().includes('united states') ) > -1 ); } catch (err) { console.error('resultIndex:\n', err); resultIndex = -1; } if (resultIndex > -1) { AMP.setState({locationState: {validZip: 1}}); const geo = res.results[resultIndex].geometry.location; coords = { latitude: geo.lat, longitude: geo.lng, }; console.debug('state:', this.helpers.deepCopy(this.state)); this.state.filters = { ...this.state.filters, coordinates: coords, userProvidedLocation: true, }; document.querySelector("#switch").checked = false; this.handleSubmit(); this.state.filters.location = value; this.state.filters.locationType = "user"; } else { validZip = false; } } else { siteSearch.helpers.getGoogleLocationFallback(); } if (!validZip) { console.error('invalid zip:', validZip); AMP.setState({locationState: {validZip: 0}}); return; } }); } }, getGoogleLocationFallback: (elId = "custom-location") => { const value = document.querySelector("#" + elId).value; if (value !== this.state.filters.location) { this.state.updatedLocation = true; } let coords; if (value?.length > 1) { fetch( `https://maps.googleapis.com/maps/api/geocode/json?components=postal_code%3A${ value }%7Ccountry%3AUS&key=AIzaSyC9aTi6UWN30wj-e4uJHJ5j0y6szBTQ4BY` ) .then((res) => res.json()) .then((res) => { let validZip = true; if (res.results[0]) { let resultIndex; try { resultIndex = res.results.findIndex(address => address.address_components.findIndex(ac => ac.long_name.toLowerCase().includes('united states') ) > -1 ); } catch (err) { console.error('resultIndex bu:\n', err); resultIndex = -1; } if (resultIndex > -1) { AMP.setState({locationState: {validZip: 1}}); const geo = res.results[resultIndex].geometry.location; coords = { latitude: geo.lat, longitude: geo.lng, }; console.debug('state bu:', this.helpers.deepCopy(this.state)); this.state.filters = { ...this.state.filters, coordinates: coords, userProvidedLocation: true, }; document.querySelector("#switch").checked = false; this.handleSubmit(); this.state.filters.location = value; this.state.filters.locationType = "user"; } else { validZip = false; } } else { validZip = false; } if (!validZip) { console.error('invalid bu zip:', validZip); AMP.setState({locationState: {validZip: 0}}); return; } }); } }, /* Scrolls to a specific anchor on the page with a specified offset to account for the fixed nav */ scrollToTarget: (el, headerOffset = 45) => { let offsetPosition = el.getBoundingClientRect().top - headerOffset; window.scrollBy({ top: offsetPosition, behavior: "smooth" }); }, pxInViewport: el => { const bounding = el.getBoundingClientRect(); const offset = bounding.height + bounding.top; return offset < 0 ? 0 : offset; }, waitForGlobal: (key, callback) => { if (window[key]) { callback(); } else { setTimeout(() => { this.helpers.waitForGlobal(key, callback); }, 100); } }, /** * Returns a Promise that resolves once * the required property is available on the object. * Rejects if maxWait occurs. * * @param {String} propName - property name * @param {String} scope - object to test for property * @param {Number} maxWait - ms to wait before rejecting * @param {Number} checkInterval - ms to wait between checks */ waitForProp( propName, scope = window, maxWait = 20000, checkInterval = 150, ) { return new Promise((resolve, reject) => { /* check now, maybe we do not need to wait */ if ( (scope == window && window[propName]) || (scope != window && scope && scope[propName]) ) { resolve(scope[propName]); } const timeout = setTimeout(() => { clearInterval(interval); reject(`could not find ${scope.toString()}['${propName}']`); }, maxWait); const interval = setInterval(() => { if ( (scope == window && window[propName]) || (scope != window && scope && scope[propName]) ) { clearInterval(interval); clearTimeout(timeout); resolve(scope[propName]); } else { return; } }, checkInterval); }); }, showPosition: async (position) => { const { latitude, longitude } = position.coords; await fetch( `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=AIzaSyC9aTi6UWN30wj-e4uJHJ5j0y6szBTQ4BY` ) .then((res) => res.json()) .then((res) => { const parts = res.results[0].address_components.filter((x) => x.types .join("") .match( /locality|administrative_area_level_1|administrative_area_level_3/gi ) ); const newLoc = `${parts[0].long_name}, ${parts[1].short_name}`; /* LocationLabel.innerHTML = `Location: ${newLoc} `; */ this.state.filters.location = newLoc; /** **/ const zipParts = res.results[0].address_components.filter((x) => x.types .join("") .match( /postal_code/gi ) ); const zip = zipParts[0].long_name; const fullLocation = this.state.filters.location + ' ' + zip; const headerLocationLabel = document.querySelector('#geoLocatorLabel span'); headerLocationLabel.innerHTML = fullLocation; document.querySelector('#geoLocatorInput').value = fullLocation; checkLoginStatus(); /** **/ }); this.state.filters = { ...this.state.filters, coordinates: { latitude, longitude, }, locationType: "user", }; this.handleSubmit(); }, handleGeoLocationError: (err) => { console.error('geo:\n', err.message); this.helpers.getGoogleLocation(); }, formatAddress: (type, data) => { if (!data) return ''; let formattedAddress = ''; if (type == 'infoWindow') { const { City, Region, PostalCode, Address } = data; const regex = new RegExp(`${City}, |${Region},|, ${PostalCode}`, 'gi'); formattedAddress = Address.replace(regex, '').trim(); formattedAddress = `${formattedAddress} ${City}, ${Region} ${PostalCode}`; } else if (type == 'providerCard') { formattedAddress = data; let zip = formattedAddress.split(',').pop(); let cityState = formattedAddress.split(',').slice(0,2).join(',').trim(); formattedAddress = formattedAddress.replace(cityState, ''); formattedAddress = formattedAddress.replace(zip, ''); formattedAddress = formattedAddress.slice(2); formattedAddress = `${formattedAddress} ${cityState},${zip}`; /* Following inserts comma before Suite text if Suite text is present. */ if (formattedAddress.toLowerCase().indexOf("suite") >= 0) { let suiteIndex = formattedAddress.toLowerCase().indexOf("suite"); let potentiallyComma = suiteIndex - 2; if (formattedAddress[potentiallyComma] != ",") { formattedAddress = formattedAddress.slice(0, potentiallyComma + 1) + ', ' + formattedAddress.slice(suiteIndex); } } } return formattedAddress; }, formatPhoneNumber: (num, format = 'default') => { /* Filter only numbers from the input */ const cleaned = ('' + num).replace(/\D/g, ''); /* Check if the input is of correct */ const match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/); if (match) { /* Remove the matched extension code. Change this to format for any country code.*/ const intlCode = (match[1] ? '' : ''); switch(format) { case 'hyphenated': return `${intlCode}${match[2]}-${match[3]}-${match[4]}`; default: return `${intlCode}(${match[2]}) ${match[3]}-${match[4]}`; } } return null; }, viewportLessThan: (px = 1000) => { return window.innerWidth < px; }, resolveLocation: (data) => { const hasZip = data.zip && data.zip.length > 1; const hasCity = data.city && data.city.length > 1; const hasState = data.state && data.state.length > 1; const hasNeighborhood = data.neighborhood && data.neighborhood.length > 1; const hasCounty = data.county && data.county.length > 1; if (hasNeighborhood) { return data.neighborhood + (hasState ? `, ${data.state}` : ''); } else if (hasZip) { return data.zip; } else if (hasCity && hasState) { return `${data.city}, ${data.state}`; } else if (hasCounty) { return data.county + (hasState ? `, ${data.state}` : ''); } else { return 'Seattle, WA'; } }, pushHistory: (queryParams = '') => { const apiUrl = new URLSearchParams(queryParams); const url = new URL(window.location); /* Loop through the params we want to add and determine if they should be in the URL pushed to history state */ const paramsToAdd = [ "brand", "languages", "LocationsOnly", "gender", "insuranceaccepted", "primaryspecialties", /** **/ "ProviderOrganization", /** **/ "page", "sortby", "visittypes", "query", "locationname", "userlocation", "practicegroup", "LocationsOnly", "time", "days", "Tier", "distance" ]; for (let param of paramsToAdd) { const { currentLoc, filters, query } = this.state; const { distance, LocationName, LocationsOnly, PrimarySpecialties, tier, visitType } = filters; /** **/ let ProviderOrganization = filters.ProviderOrganization; /** **/ let val, keys; switch (param) { case 'Tier': url.searchParams.delete('tier'); if (!tier) { url.searchParams.delete('Tier'); continue; } val = tier; break; case 'time': val = this.state.filters.AvailableTime ?? 'any'; break; case 'days': if (!this.state.specificDays) { url.searchParams.delete('days'); continue; } val = this.helpers.getAvailability(this.state); break; case 'LocationsOnly': if (!LocationsOnly) { continue; } val = LocationsOnly; break; case 'query': val = query; break; case 'visittypes': keys = Object.keys(visitType).filter(key => visitType[key]?.active === true && visitType[key]?.auto != true); if (keys.length) { /* Create query string from keys */ val = keys.reduce((str, key) => str.length ? str + `,${key}` : key, ''); } else { /* Remove the visitTypes param from the query string */ url.searchParams.delete(param); /* Break out of the loops current iteration */ continue; } break; case 'brand': case 'page': val = this.state.filters[param]; break; case 'locationname': if (LocationName) { val = LocationName; break; } else { url.searchParams.delete(param); continue; } case 'distance': if (distance) { val = distance; break; } else { url.searchParams.delete(param); continue; } case 'userlocation': val = currentLoc; break; case 'gender': case 'insuranceaccepted': case 'languages': const filter = this.state.filters[ param === 'gender' ? 'Gender' : param === 'languages' ? 'Languages' : 'InsuranceAccepted' ]; /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(filter).filter((f) => filter[f]?.active === true && filter[f]?.auto != true); if (keys.length > 0) { val = keys.reduce((str, key, i) => str.length ? str + `,${key}` : key, ''); } else { url.searchParams.delete(param); continue; } break; case 'primaryspecialties': /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(PrimarySpecialties).filter(f => PrimarySpecialties[f]?.active === true && PrimarySpecialties[f]?.auto != true); if (keys.length > 0) { val = keys.reduce((str, key, i) => str.length ? str + `,${key}` : key, ''); } else { url.searchParams.delete(param); continue; } break; /** **/ case "ProviderOrganization": /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(ProviderOrganization).filter(f => ProviderOrganization[f]?.active === true && ProviderOrganization[f]?.auto != true); if (keys.length > 0) { val = keys.reduce((str, key, i) => str.length ? str + `,${key}` : key, ''); } else { url.searchParams.delete(param); continue; } param = 'providerorganization'; break; /** **/ case 'sortby': if (apiUrl.get(param)) { val = apiUrl.get(param); } else { url.searchParams.delete(param); /* Break out of the loops current iteration */ continue; } break; case 'practicegroup': if (apiUrl.get(param)) { val = apiUrl.get(param); } else if (apiUrl.get('PracticeGroup')) { val = sanitizeInput(apiUrl.get('PracticeGroup')); } else { url.searchParams.delete(param); continue; } break; default: break; } url.searchParams.set(param, val); } if (window.history.pushState) { window.history.pushState({}, 'Find a Doctor', url); } else { window.history.replaceState({}, 'Find a Doctor', url); this.state.pushHistory = true; } /* Push the current url to the window history, if it's not the initial load if (this.state.pushHistory) { window.history.pushState({}, "Womp Health Demo", url); } else { window.history.replaceState({}, "Womp Health Demo", url); this.state.pushHistory = true; }*/ }, trackPixels: async () => { const { createPixel, reportToWheelhouse, trackIt } = this.helpers; const { currentLoc, defaultLoc, filters, query } = this.state; const { visitType } = filters; const trackingPixels = []; let search_match = 'Not Set'; try { let match_filters = this.state.results.info.facets.PrimarySpecialties; if (match_filters?.length > 0) { search_match = match_filters.toString(); } else if (query && query.length) { search_match = 'No Match'; } /* Potentially return all Matches */ /* Possibly problematic for searches such as "Joe has Cancer" */ /*let matches = this.state.results.info.matches; if(matches.length > 0) { search_match = matches.toString(); } */ } catch (err) { if (query && query.length) { search_match = 'No Match'; } console.error('failure matching query and PrimarySpecialties:\n', err); } /* Set all the search event params for Tealium */ const searchTrackingParams = { search_type: this.state.pageInit ? 'Passive' : 'Active', search_term: `${search_match}`, medical_group_filter: /** **/ Object.values(filters.ProviderOrganization).some(po => po.active) ? 'Set' : 'Not Set', /** **/ new_patient_availability_filter: filters.clinicVisits ? 'Set' : 'Not Set', telehealth_availability_filter: filters.videoVisits ? 'Set' : 'Not Set', online_scheduling_filter: filters.onlineScheduling ? 'Set' : 'Not Set', search_location_filter: defaultLoc !== currentLoc ? 'Set' : 'Default', event_category: 'Directory Journeys', event_action: 'Provider Directory', event_label: 'Search / Filter', tealium_event: 'womp_directory_event', tealium_event_type: 'event' }; const url = new URL(window.location); const curUrl = new URLSearchParams(window.location.search); /* Set the referrer in state to the current URL. */ this.state.referrer = url; /* If the referrer is set on document, use that. If not, use what we've saved to state. */ const referrer = document.referrer ? document.referrer : this.state.referrer.href; /* WheelhouseDMG tracking */ await reportToWheelhouse([ { key: "tealium_event", value: "amp_screen_view" }, { key: "dom_referrer", value: referrer }, { key: "dom.referrer", value: referrer }, { key: "screen_width", value: `${window.innerWidth}x${window.innerHeight}`} ]); /* Create the pageview pixel and push it to our pixels array */ const pageView = createPixel({ "dom.referrer": referrer, "dom_referrer": referrer }); trackingPixels.push(pageView); let keys; /* track visitType */ keys = Object.keys(visitType).filter(key => visitType[key]?.active === true && visitType[key]?.auto != true); if (keys.length) { /* Set value for clinic visits in pixel */ if ( visitType['clinicVisits']?.active === true || (visitType['clinicVisits']?.active === false && ![null, undefined].includes(visitType.clinicVisits)) ) { if (curUrl.get("visittypes") === null) { searchTrackingParams['new_patient_availability_filter'] = 'Set'; } else if (curUrl.get("visittypes").indexOf('clinicVisits') > -1) { searchTrackingParams['new_patient_availability_filter'] = 'Default'; } else { searchTrackingParams['new_patient_availability_filter'] = 'Set'; } } else { searchTrackingParams['new_patient_availability_filter'] = 'Not Set'; } /* Set value for virtual visits in pixel */ if ( visitType['videoVisits']?.active === true || (visitType['videoVisits']?.active === false && ![null, undefined].includes(visitType.videoVisits)) ) { if (curUrl.get('visittypes') === null) { searchTrackingParams['telehealth_availability_filter'] = 'Set'; } else if (curUrl.get('visittypes').indexOf('videoVisits') > -1) { searchTrackingParams['telehealth_availability_filter'] = 'Default'; } else { searchTrackingParams['telehealth_availability_filter'] = 'Set'; } } else { searchTrackingParams['telehealth_availability_filter'] = 'Not Set'; } /* Set value for virtual visits in pixel */ if ( visitType['onlineScheduling']?.active === true || (visitType['onlineScheduling']?.active === false && ![null, undefined].includes(visitType.onlineScheduling)) ) { if (curUrl.get('visittypes') === null) { searchTrackingParams['online_scheduling_filter'] = 'Set'; sessionStorage.setItem('online_scheduling_filter', 'Set'); } else if (curUrl.get('visittypes').indexOf('clinicVisits%2ConlineScheduling') > -1) { if (sessionStorage.getItem('online_scheduling_filter') === null) { searchTrackingParams['online_scheduling_filter'] = 'Default'; } else { searchTrackingParams['online_scheduling_filter'] = 'Set'; } } else if (curUrl.get('visittypes').indexOf('onlineScheduling') > -1) { if (sessionStorage.getItem('online_scheduling_filter') === null) { searchTrackingParams['online_scheduling_filter'] = 'Default'; } else { searchTrackingParams['online_scheduling_filter'] = 'Set'; } } else { searchTrackingParams['online_scheduling_filter'] = 'Set'; } } else { searchTrackingParams['online_scheduling_filter'] = 'Not Set'; } } else { /* Set the values for virtual and clinic visits on the tracking pixel */ searchTrackingParams['new_patient_availability_filter'] = 'Not Set'; searchTrackingParams['telehealth_availability_filter'] = 'Not Set'; searchTrackingParams['online_scheduling_filter'] = 'Not Set'; } /* track languages, gender, or insuranceaccepted */ let lookup = { Gender: 'provider_gender_filter', Languages: 'provider_language_filter', InsuranceAccepted: 'insurance_filter' }; for (const [key, value] of Object.entries(lookup)) { const tracker = lookup[key]; const tsf_filter = filters[key]; /* Create an array of keys from the filters that are active and that were not autoset by the API */ keys = Object.keys(tsf_filter).filter((f) => tsf_filter[f]?.active === true && tsf_filter[f]?.auto != true); if (keys.length > 0) { searchTrackingParams[tracker] = 'Set'; } else { searchTrackingParams[tracker] = 'Not Set'; } } /* Create our tracking pixel for the search event and add it to the pixels array */ /* A1207376607369937 - Remove extra tracking call everything should now be captured in the pageview event const searchEvent = createPixel(searchTrackingParams); trackingPixels.push(searchEvent); */ /* If the page has timeslots, create an event for Timeslot Enabled Results if (document.querySelectorAll('timeslots-widget, directory-timeslots').length > 0) { const timeslotEnabled = createPixel({ event_category: 'Directory Journeys', event_action: 'Provider Directory', event_label: 'Timeslot Enabled Results', tealium_event: 'womp_directory_event', tealium_event_type: 'event' }); trackingPixels.push(timeslotEnabled); } */ /* Add the created pixels to the DOM to track the events */ trackIt(trackingPixels); /* Track autocomplete if search was initated from the autocomplete el and if the window variable is true */ if (window.wmFromAutoComplete === true) { autocomplete.helpers.trackAutocomplete(this.state.query); window.wmFromAutoComplete = false; } await this.helpers.trackWheelhouseEvents(); }, getSlotsType: () => { const { visitType } = this.state.filters; if (!(visitType.clinicVisits?.active && visitType.videoVisits?.active)) { let tmp = Object.keys(visitType).filter(key => visitType[key]?.active); if (tmp.length) { return tmp[0].replace('Visits', ''); } } return 'all'; }, /* Determine what, if any, timeslots to show for a provider */ checkForTimeslots: (NewClinicVisit, NewVideoVisit, DaysUntilNextAppt) => { let hasTimeslots = false; if (DaysUntilNextAppt <= 90 && (NewClinicVisit == 1 || NewVideoVisit == 1)) { hasTimeslots = true; } return hasTimeslots; }, createProviderLink: (omni) => { let providerLink = `/doctors/profile/${omni.ProfileUrl}`; if (/swedish/i.test(location.origin) && omni.ProviderUniqueUrlSwedish) { providerLink = omni.ProviderUniqueUrlSwedish.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (/pacmed|pacificmedicalcenters/i.test(location.origin) && omni.ProviderUniqueUrlPacmed) { providerLink = omni.ProviderUniqueUrlPacmed.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (/providence/i.test(location.origin) && omni.ProviderUniqueUrlOnesite) { providerLink = omni.ProviderUniqueUrlOnesite; } return providerLink; } }; state = {}; map = { lookup: {}, markers: {}, locations: {}, infoWindow: false, promises: [], selectedNpi: false, selectedLocation: false, bounds: false, icons: {}, toggleVirtual: (isVirtual = false) => { if (isVirtual) { document.querySelector('main').classList.add('no-locations'); document.querySelector('#map').classList.remove('show-map'); } else { document.querySelector('main').classList.remove('no-locations'); document.querySelector('#map').classList.add('show-map'); } }, selectProvider: npi => { const w = window.innerWidth; const npiCard = document.querySelector(`#npi${npi}`); /* Saving some time by not modifying the DOM if the NPI hasn't changed since last click */ if (npi !== this.map.selectedNpi || !npiCard.classList.contains('selected')) { /* Remove the selected classs from the currently selected Npi */ if (this.map.selectedNpi) { document.querySelector(`#npi${this.map.selectedNpi}`).classList.remove('selected'); } /* Highight card for selected Npi */ this.map.selectedNpi = npi; npiCard.classList.add('selected'); } /* We'll scroll to the card regardless, just in case the user changed scroll position since last click */ if (w < 1000) { this.resultsEl.scrollBy({ top: 0, /* 32px is the amount of spacing between the viewports left edge and the selected provider card */ /* only exception is the very first card (has 16px), but this still works for that edge case */ left: npiCard.getBoundingClientRect().left - 32, behavior: 'smooth' }); } else { this.helpers.scrollToTarget(npiCard, 30); } }, createMarkers: () => { this.map.bounds = new google.maps.LatLngBounds(); try { /* Sets Map bounds to the searched for geoIP */ var ePoint1 = new google.maps.LatLng(this.state.results.geoip.coordinates.lat + 0.01, this.state.results.geoip.coordinates.lng + 0.01); var ePoint2 = new google.maps.LatLng(this.state.results.geoip.coordinates.lat - 0.01, this.state.results.geoip.coordinates.lng - 0.01); this.map.bounds.extend(ePoint1); this.map.bounds.extend(ePoint2); } catch (err) { console.error('setting map bounds to coords:\n', err); } const { locations, setMapOnAll, lookup } = this.map; const { createPixel, trackIt } = this.helpers; const ids = Object.keys(locations); const byUUID = {}; const selectFields = [ "id", "Region", "City", "PostalCode", "Address", "Name", "GeocodedCoordinate", "AddressId" ]; const encodedFields = encodeURIComponent(selectFields.join(',')); const encodedIds = encodeURIComponent(ids?.join(',')); if (this.map.infoWindow) { this.map.infoWindow.close() } else { this.map.infoWindow = new google.maps.InfoWindow(); this.map.infoWindow.addListener('closeclick', () => { this.map.resetMarkerIcon(); }); } /* ids?.length && uses short-circuit evalutaion to skip the fetch call if ids is null or not an array for any reason */ /*Using womphealthapi temp because weird data in staging ${this.apiSub} : 'womphealthdevapi' : 'womphealthapi'; */ ids?.length && fetch(`https://womphealthapi.azurewebsites.net/api/OmniData?brand=${this.state.filters.brand}&data=locations&ids=${encodedIds}&select=${encodedFields}`) .then(res => res.json()) .then(res => { res.forEach((loc, i) => { if (!loc) { console.debug(`Location information not available for id=${ids[i]}`); return; } const { Address, City, GeocodedCoordinate, Name, PostalCode, Region, } = loc; const uuid = loc.AddressId; lookup[loc.id] = uuid; if (!byUUID[uuid]) { byUUID[uuid] = loc; } const coordinates = { lat: loc.GeocodedCoordinate.coordinates[1] || 0, lng: loc.GeocodedCoordinate.coordinates[0] || 0 }; const coords = new google.maps.LatLng(coordinates.lat, coordinates.lng); const marker = new google.maps.marker.AdvancedMarkerElement({ content: this.map.icons.default.cloneNode(true), map: this.map.el, position: coords, title: Name, }); marker.addListener('click', () => { try { /* fix for Google Maps and AMP */ window.HTMLElement = window.HTMLElementOrig; const isMobile = this.helpers.viewportLessThan(); this.map.resetMarkerIcon(); const infoWindow = this.map.infoWindow; if (infoWindow) infoWindow.close(); this.map.selectedMarker = marker; this.map.selectedLocation = uuid; marker.content.setAttribute('src','https://pacmed.azureedge.net/7914/active-blue@2x.png'); if (!isMobile) { /* Track pin clicks that open a location's info window */ trackIt(createPixel({ event_category: 'Key Engagements', event_action: 'Maps & Directions Click', event_label: Name, tealium_event: 'womp_directory_event', tealium_event_type: 'event' })); /* Remove the selected class from the currently selected Npi if it's for another location */ if ( this.map.selectedNpi && !this.map.locations[loc.id].some((curr) => curr.Npi === this.map.selectedNpi) ) { document.querySelector(`#npi${this.map.selectedNpi}`).classList.remove('selected'); } const FormattedAddress = this.helpers.formatAddress('infoWindow', { Address: Address, City: City, Region: Region, PostalCode: PostalCode }); let providers = []; for (const [key, value] of Object.entries(this.map.lookup)) { if (this.map.locations[key] === undefined) { continue; } if (value && value == uuid) { providers = [...providers, ...this.map.locations[key]] } } const contentString = /*html*/ `
`; return all + providerHtml; }, '')} ${!providers.length ? '
No providers found as this location
' : ''}
`; /* Set and open info window */ infoWindow.setContent(contentString); setTimeout(() => { this.map.setCenterWithOffset(marker.position, 0, 100, this.map.el); infoWindow.open(this.map.el, marker); /* setTimeout(() => { infoWindow.focus(); }, 20); */ }, 200); } else { this.map.setCenterWithOffset(marker.position, 0, 0, this.map.el); } } catch (err) { console.error('marker click error:\n', err); throw err; } }); marker.content.addEventListener('mouseover', () => { try { marker.content.setAttribute('src','https://pacmed.azureedge.net/7914/active-blue@2x.png'); } catch (err) { console.error('marker mouseover error:\n', err); } }); marker.content.addEventListener('mouseout', () => { try { if (this.map.selectedMarker !== marker) { marker.content.setAttribute('src','https://pacmed.azureedge.net/7914/blue@2x.png'); } } catch (err) { console.error('marker mouseout error:\n', err); } }); this.map.markers[uuid] = marker; try { /* Keep the map within the first 172.5 miles from the center */ if (i == 0 || ((Object.values(this.map.bounds)[0].hi - 2.5 < coordinates.lat && Object.values(this.map.bounds)[0].hi + 2.5 > coordinates.lat) && (Object.values(this.map.bounds)[1].hi - 2.5 < coordinates.lng && Object.values(this.map.bounds)[1].hi + 2.5 > coordinates.lng)) ) { /* extend the bounds to include each marker's position */ this.map.bounds.extend(coords); if (this.map.bounds.getNorthEast().equals(this.map.bounds.getSouthWest())) { var extendPoint1 = new google.maps.LatLng( this.map.bounds.getNorthEast().lat() + 0.01, this.map.bounds.getNorthEast().lng() + 0.01 ); var extendPoint2 = new google.maps.LatLng( this.map.bounds.getNorthEast().lat() - 0.01, this.map.bounds.getNorthEast().lng() - 0.01 ); this.map.bounds.extend(extendPoint1); this.map.bounds.extend(extendPoint2); } } } catch (err) { console.error('map bounds error:\n', err); } /* delay until "Filter Results" animation completes */ setTimeout(() => { this.map.el.fitBounds(this.map.bounds); this.map.setMapOnAll(this.map.el); }, 250); }); }) .catch((err) => console.error('createMarkers error:\n', err)); }, deleteMarkers: () => { if (this.map.markers) { this.map.setMapOnAll(null); this.map.infoWindows = {}; this.map.markers = {}; } }, resetMarkerIcon: () => { if (this.map.selectedMarker) { this.map.selectedMarker.content.setAttribute('src','https://pacmed.azureedge.net/7914/blue@2x.png'); } }, setCenterWithOffset: (latlng, offsetX = 0, offsetY = 0, map) => { if (!latlng || !map) return; const ov = new google.maps.OverlayView(); ov.onAdd = function () { try { const proj = this.getProjection(); const aPoint = proj.fromLatLngToContainerPixel(latlng); if (!aPoint?.x || !aPoint?.y) return; aPoint.x = aPoint.x + offsetX; aPoint.y = aPoint.y + offsetY; const newLatLng = proj.fromContainerPixelToLatLng(aPoint); if (!newLatLng) return; map.panTo(newLatLng); } catch (err) { console.warn('could not center map'); } }; ov.draw = function () {}; ov.setMap(map); }, setMapOnAll: (map) => { const { markers } = this.map; const vals = Object.values(markers); for (const val of vals) { val.setMap(map); } }, build: async () => { async function createMap() { /** * Initialize the map on the page * center is Bellevue * map Id is created in the Google Cloud console * and associated with the apiKey */ try { return new google.maps.Map( document.getElementById('dexMap'), { center: { lat: 47.659159, lng: -122.183832 }, disableDefaultUI: true, gestureHandling: 'greedy', mapId: '545f323259beafed', mapTypeId: google.maps.MapTypeId.ROADMAP, maxZoom: 19, zoom: 8 } ); } catch (err) { console.error('Map Build Error:\n' + err); return undefined; } } await this.helpers.waitForProp('google', window, 10000); await this.helpers.waitForProp('maps', window.google, 10000); await this.helpers.waitForProp('marker', window.google.maps, 10000); console.debug('found google, maps, marker', window.google); await waitForElm('#dexMap'); this.map.el = await createMap(); console.debug('map initialized', this.map.el); /* Set icons object */ const imgIconDefault = document.createElement('img'); imgIconDefault.src = 'https://pacmed.azureedge.net/7914/blue@2x.png'; imgIconDefault.setAttribute('width', '24'); imgIconDefault.setAttribute('height', '35'); this.map.icons = { default: imgIconDefault }; /* this.map.icons = { default: { url: "https://pacmed.azureedge.net/7914/blue@2x.png", scaledSize: new google.maps.Size(24, 35), origin: new google.maps.Point(0,0), anchor: new google.maps.Point(12, 35) }, selected: { url: "https://pacmed.azureedge.net/7914/active-blue@2x.png", scaledSize: new google.maps.Size(24, 35), origin: new google.maps.Point(0,0), anchor: new google.maps.Point(12, 35) } }; */ /* POI marker visibility settings */ /* styles cannot be added here anymore, instead they are created in the Dexcare Google cloud console and associated with the map ID */ /* const styles = { default: [], hide: [ { featureType: "poi.business", stylers: [{ visibility: "off" }], }, { featureType: "poi.medical", stylers: [{ visibility: "off" }], }, { featureType: "transit", elementType: "labels.icon", stylers: [{ visibility: "off" }], } ], }; */ /* Update the map to show location data */ /* this.map.el.setOptions({ styles: styles["hide"] }); */ /* Track Scroll */ /* google.maps.event.addListener(this.map.el, 'dragend', () => { let firstMapEvent = sessionStorage.getItem("fme"); if(!firstMapEvent || firstMapEvent === 'zoom load') { siteSearch.helpers.trackIt(siteSearch.helpers.createPixel({ event_category: 'Key Engagements', event_action: 'Maps & Directions Interaction', event_label: 'Scroll', tealium_event: 'womp_directory_event', tealium_event_type: 'event' })); sessionStorage.setItem("fme", "occured"); } }); */ /* Track Zoom */ /* google.maps.event.addListener(this.map.el, 'zoom_changed', () => { let firstMapEvent = sessionStorage.getItem("fme"); if(!firstMapEvent) { sessionStorage.setItem("fme", "zoom load");} else if(firstMapEvent === 'zoom load') { siteSearch.helpers.trackIt(siteSearch.helpers.createPixel({ event_category: 'Key Engagements', event_action: 'Maps & Directions Interaction', event_label: 'Zoom', tealium_event: 'womp_directory_event', tealium_event_type: 'event' })); sessionStorage.setItem("fme", "occured"); } }); */ google.maps.event.addListener(this.map.el, 'click', () => { const selectedNpi = document.querySelector(`#npi${this.map.selectedNpi}`); if (selectedNpi) { selectedNpi.classList.remove('selected'); } if (this.map.infoWindow) { this.map.infoWindow.close(); this.map.resetMarkerIcon(); } }); } }; MapOldQueryParams = function () { /* Post migration to ProvOmni, providence will still have old links to providence search, using old query params. https://app.asana.com/0/1204100916165836/1205550979625263/f Handle query term replacement to map old providence query params to new ProvOmni params */ let queryString = document.location.search; if (queryString.includes('search-term')) { queryString = queryString.replace('search-term', 'query'); } if (queryString.includes('provider-organization')) { queryString = queryString.replace('provider-organization', 'providerorganization'); } if (queryString.includes('language=')) { queryString = queryString.replace('language=', 'languages='); } if (queryString.includes('insurance=')) { queryString = queryString.replace('insurance=', 'insuranceaccepted='); } if (queryString.includes('&location=')) { queryString = queryString.replace('&location=', '&userlocation='); } let acceptsNew = false; let offersVideo = false; if (queryString.includes('acceptingNewPatients=on')) { acceptsNew = true; queryString = queryString.replace('acceptingNewPatients=on', ''); } if (queryString.includes('virtualCare=on')) { offersVideo = true; queryString = queryString.replace('virtualCare=on', '') } if (acceptsNew && offersVideo) { queryString += '&visittypes=clinicVisits%252CvideoVisits'; } else if (acceptsNew) { queryString += '&visittypes=clinicVisits'; } else if (offersVideo) { queryString += '&visittypes=videoVisits'; } /* if the query string changed, refresh the page */ if (document.location.search != queryString) { document.location.search = queryString } }; containsDisclaimerSearch = (query) => { this.state.disclaimerSearch = false; /* reset */ const regex = new RegExp(this.state.disclaimerSearchKeywords.join( "|" ), "i"); /* true if query or input value includes a disclaimer keyword */ this.state.disclaimerSearch = regex.test(query) || regex.test(this.inputSearchEl.value); }; containsExcludeKeywords = (query) => { this.state.excludeSearch = false; /* reset */ const regex = new RegExp(this.state.excludeSearchKeywords.join( "|" ), "i"); /* true if query or input value includes an exclude keyword */ const exclude = regex.test(query) || regex.test(this.inputSearchEl.value); return exclude; }; handleSubmit = async (event, { ignoreElements = [] } = {}) => { this.MapOldQueryParams(); this.filtersUpdated = false; this.resultsEl.innerHTML = ''; this.paginationEl.style.display = 'none'; this.map.locations = {}; this.map.selectedNpi = false; this.map.selectedLocation = false; this.map.selectedMarker = false; this.map.deleteMarkers(); try { let allDirtyE = document.querySelectorAll('.is-populated'); allDirtyE.forEach((element) => { element.classList.remove('is-populated'); }); } catch (err) { console.error('failed to remove class:\n', err); } try { if (this.state.query !== '') { document.getElementById('searchBox').classList.add('is-populated'); } } catch (err) { console.error('failed to add class:\n', err); } document .querySelectorAll(".dropdown, #searchBox") .forEach((drop) => { if(!ignoreElements.includes(drop.id)) { drop.classList.remove('active'); } }); document.querySelector('#fullsearch').checked = false; if (!this.state.pageInit) { this.state.previousQuery = this.state.query; this.state.query = this.inputSearchEl.value; if (window.location.pathname.includes('provider-not-found')) { let url = new URL(window.location); url.pathname = url.pathname.replace('/provider-not-found', ''); window.history.replaceState({}, 'Find a Doctor', url); } } /* Exclusions and disclaimers */ if (this.containsExcludeKeywords(this.state.query || '')) { this.state.excludeSearch = true; this.state.query = ''; this.inputSearchEl.value = ''; } this.containsDisclaimerSearch(this.state.query || ''); /* Destructing state */ const { query, geolocation, filters, isTest, autoApplied } = this.state; const { formatPhoneNumber, formatAddress, handleGeoLocationError, showPosition, slugify } = this.helpers; /* Update previousSearch state */ if (query.length) { let fromLocal = localStorage.getItem('previousSearches'); let fromSession = sessionStorage.getItem('previousSearches'); /* If localStorage already existed, parse the string ot JSON. If not, create an empty array */ fromLocal !== null ? fromLocal = JSON.parse(fromLocal) : fromLocal = []; /* If sessionStorage already existed, parse the string ot JSON. If not, create an empty array */ fromSession !== null ? fromSession = JSON.parse(fromSession) : fromSession = []; /* Append the newest query to the array of previous searches */ let prevSearchesLocal = [...new Set([query, ...fromLocal])]; let prevSearchesSession = [...new Set([query, ...fromSession])]; /* Write the combined array back to localStorage */ localStorage.setItem('previousSearches', JSON.stringify(prevSearchesLocal.slice(0,5))); /* Write the combined array back to sessionStorage for analytics */ sessionStorage.setItem('previousSearches', JSON.stringify(prevSearchesSession)); } /* Remove autofilters from what is being sent to the API */ if (autoApplied && autoApplied.length) { for (const filter of autoApplied) { const inState = filters[filter.name]; /* If we actually have this value in state, then set active to false */ if (inState && inState[filter.value]) { inState[filter.value].active = false; } } } /* Destructing filters */ const { brand, Languages, Gender, InsuranceAccepted, PrimarySpecialties, coordinates, PracticeGroupId, LocationsOnly, } = filters; let LocationName = filters.LocationName; let PracticeGroup = filters.PracticeGroup; /** **/ let ProviderOrganization = filters.ProviderOrganization; /** **/ /* If certain terms are in search, set clinicVisits to true */ if (/(family medicine|family doctor|internal medicine|family care)/i.test(query)) { const shouldSkipAutoFilter = query != this.state.previousQuery ? false : /(family medicine|family doctor|internal medicine|family care)/i.test(this.state.previousQuery); if (!shouldSkipAutoFilter) { if (!filters.visitType) { filters.visitType = { clinicVisits: { active: true } } } else { filters.visitType = {...filters.visitType, clinicVisits: { active: true }} } } } /* If certain terms are in search, set videoVisits to true */ if (/(video|virtual)/i.test(query)) { if (!filters.visitType) { filters.visitType = { videoVisits: { active: true } }; } else { filters.visitType = { ...filters.visitType, videoVisits: { active: true } }; } } /* If certain requirements are met and search terms exists, turn on 'near me' location toggle */ if (navigator.geolocation && !this.state.nearme && /near me/i.test(query)) { this.state.nearme = true; navigator.geolocation.getCurrentPosition(showPosition, handleGeoLocationError); return; } /* Return to page one when new filters are applied */ if (!this.state.filters.init) { this.state.filters.page = 1; } /* Some variables for our API request */ const post = { search: query, top: 20, skip: (this.state.filters.page - 1) * 20, highlightPreTag: "", highlightPostTag: "", }; /* Starting point for building out the request params + filters */ let queryParams = `&top=${post.top}&skip=${post.skip}`; if (!!this.slugObjDiv && this.state.filters.init) { let slugObj = JSON.parse(this.slugObjDiv?.innerText); if (!!slugObj.name) { this.state.slug = { ...this.state.slug, ...slugObj }; console.log('slug:', this.helpers.deepCopy(this.state.slug)); } } /* Check for a default search (provided in before-render when a slug is present in URL) */ if (!!this.slugObjDiv) { /* do this first: add all weekdays if we have online scheduling */ if (!!this.state.slug.onlineScheduling) { DAYS.forEach((day, i) => { if (i === 0 || i === 6) return; if (!this.state.slug.availabilityDays.includes(day)) { this.state.slug.availabilityDays.push(day); } }); } /* hydrate state.filters with slug values, then clear slug */ if (this.state.slug.availabilityDays.length) { for (const daySlug of Object.entries(this.state.filters.AvailableDays)) { if (this.state.slug.availabilityDays.includes(daySlug[0])) { if (this.state.filters.AvailableDays[daySlug[0]]) { this.state.filters.AvailableDays[daySlug[0]] = { active: true }; document.querySelector(`#${daySlug[0]}`).classList.add('active'); } } } document.querySelector('#any-day').classList.remove('active'); this.state.specificDays = true; } if (!!this.state.slug.availabilityTimeOfDay) { let timeSlug = this.state.slug.availabilityTimeOfDay.toLowerCase(); switch (timeSlug) { case 'afternoon': this.state.filters.AvailableTime = 'afternoon'; break; case 'morning': this.state.filters.AvailableTime = 'morning'; break; default: break; } document.querySelectorAll('#timesList .active')?.forEach((el) => { el.classList.remove('active'); }); let timeSelector = document.querySelector(`#${timeSlug}`); timeSelector?.classList.add('active'); } if ( this.state.slug.availabilityDays.length || !!this.state.slug.availabilityTimeOfDay ) { this.populateDaytimeSelectorFromState(); } if (!!this.state.slug.distance) { this.state.filters.distance = this.state.slug.distance.toString(); } if (!!this.state.slug.gender) { switch (this.state.slug.gender.toLowerCase()) { case 'female': this.state.filters.Gender['Female'] = { active: true }; break; case 'male': this.state.filters.Gender['Male'] = { active: true }; break; case 'non-binary': this.state.filters.Gender['Non-binary'] = { active: true }; break; default: break; } } if (!!this.state.slug.insurance) { const ins = this.state.slug.insurance; this.state.filters.InsuranceAccepted[ins] = { active: true }; } if (this.state.slug.language.length) { for (const lang of this.state.slug.language) { this.state.filters.Languages[lang] = { active: true }; } } if (!!this.state.slug.location) { this.state.filters.location = this.state.slug.location; } if (!!this.state.slug.locationId) { this.state.filters.LocationId = this.state.slug.locationId; } if (!!this.state.slug.medicalGroup) { const group = this.state.slug.medicalGroup; this.state.filters.ProviderOrganization[group] = { active: true }; } if ( !!this.state.slug.acceptingNewPatients || !!this.state.slug.offersVideoVisit || !!this.state.slug.onlineScheduling ) { let cvTest = false; let osTest = false; let vvTest = false; if (!!this.state.slug.acceptingNewPatients) cvTest = true; if (!!this.state.slug.onlineScheduling) osTest = true; if (!!this.state.slug.offersVideoVisit) vvTest = true; if (!this.state.filters.visitType) { this.state.filters.visitType = { clinicVisits: { active: cvTest }, onlineScheduling: { active: osTest }, videoVisits: { active: vvTest } }; } else { this.state.filters.visitType = { ...this.state.filters.visitType, clinicVisits: { active: cvTest }, onlineScheduling: { active: osTest }, videoVisits: { active: vvTest } }; } } if (!!this.state.slug.searchTerms) { this.state.query = this.state.slug.searchTerms; post.search = this.state.slug.searchTerms; this.inputSearchEl.value = this.state.slug.searchTerms; window.sessionStorage.setItem('previousQuery', this.state.slug.searchTerms); } console.debug('filters updated by slug:', this.helpers.deepCopy(this.state.filters)); /* remove slug values after depositing to state.filters */ this.initSlug(); } /* Add visit type, if applicable */ /* const types = Object.keys(visitType).filter(type => visitType[type] === true); if (types.length > 0) { queryParams += types.reduce((str, type) => str + `&${type}=1`, ""); } */ if (filters.visitType?.clinicVisits?.active === true) { queryParams += '&AcceptingNewPatients=1'; } if (filters.visitType?.videoVisits?.active === true) { queryParams += '&VirtualCare=1'; } /* Grab languages, if applicable */ const languages = Object.keys(Languages).filter(lang => lang && Languages[lang]?.active === true); if (languages.length > 0) { queryParams += languages.reduce((str, lang, i) => i === 0 ? str + lang : `${str},${lang}`, "&Languages="); } /* Grab genders, if applicable */ let genders = Object.keys(Gender).filter(gender => Gender[gender]?.active === true); let genderVals = ["Female", "Male", "Non-binary"]; for (const [key, value] of Object.entries(genders)) { if(!genderVals.includes(value)) { delete Gender[value]; let gendersCk = value.charAt(0).toUpperCase() + value.toLowerCase().slice(1); if(!genderVals.includes(gendersCk)) { genders.splice(key, 1); } } } if (genders.length > 0) { queryParams += genders.reduce((str, gender, i) => i === 0 ? str + gender.charAt(0).toUpperCase() + gender.toLowerCase().slice(1) : `${str},${gender.charAt(0).toUpperCase() + gender.toLowerCase().slice(1)}`, "&Gender="); for (const [key, value] of Object.entries(this.state.filters.Gender)) { if(!genderVals.includes(key)) { delete this.state.filters.Gender[key]; } } } try { /* Check for preselected filters */ await this.helpers.fetchWithTimeout(`https://www.providence.org/integrationapi/getinsuranceselectionjson`, { timeout: 2000 }) .then(res => res.json()) .then(res => { if (res.insurance !== null) { console.debug('getinsuranceselectionjson:', res.insurance); queryParams += `&InsuranceAccepted=${res.insurance}`; localStorage.setItem('omniSearchInsurance', res.insurance); } }); } catch (err) { console.error('getinsuranceselection API failure:\n', err); } /* Grab insurances accepted, if applicable */ const insurances = Object.keys(InsuranceAccepted).filter(ins => InsuranceAccepted[ins]?.active === true); if (insurances.length > 0) { let reduced = insurances.reduce((str, ins, i) => i === 0 ? str + ins : `${str},${ins}`, ""); if(reduced.indexOf(',') !== -1){ let currentInsurance = localStorage.getItem('omniSearchInsurance'); if(reduced.split(",")[0] === currentInsurance) { reduced = reduced.split(",").pop(); } else { reduced = reduced.substring(0, reduced.indexOf(',')); } } queryParams += `&InsuranceAccepted=${reduced}`; localStorage.setItem('omniSearchInsurance', reduced); try { await this.helpers.fetchWithTimeout("https://www.providence.org/integrationapi/SetInsuranceSelection?plan=" + reduced, { timeout: 2000 }); } catch (err) { console.error('SetInsuranceSelection API failure:\n', err); } } /* Set filter for LocationName */ if (LocationName && LocationName.length) { LocationName = encodeURIComponent(LocationName); queryParams += `&LocationName=${LocationName}`; } /* Set filter for distance */ if (filters.distance && filters.distance.length) { queryParams += `&distance=${filters.distance}`; } /* Set filter for PracticeGroup */ if (PracticeGroup && PracticeGroup.length) { PracticeGroup = encodeURIComponent(PracticeGroup); queryParams += `&PracticeGroup=${PracticeGroup}`; } /* Set filter for PracticeGroupId */ if (PracticeGroupId && PracticeGroupId.length) { queryParams += `&PracticeGroupId=${this.state.filters.PracticeGroupId}`; } /* If sortby is not false, set param */ if (this.state.filters.sortby) { queryParams += `&sortby=${this.state.filters.sortby}`; } /* Set locations=true if LocationsOnly is set */ if (this.state.filters.LocationsOnly) { queryParams += `&locations=${this.state.filters.LocationsOnly}`; } if (this.state.specificDays && this.state.filters.AvailableDays) { queryParams += `&days=${Object.entries(this.state.filters.AvailableDays).filter(x => x[1].active).map(x => DAYS.indexOf(x[0])).join(",")}`; } if (this.state.filters.AvailableTime) { queryParams += `&time=${this.state.filters.AvailableTime}`; } if (this.state.filters.tier) { queryParams += `&Tier=${this.state.filters.tier}`; } /* Provider cards in the directory should not list their hospital locations as the "primary" address. This occurs because we index a provider record for each location for a provider. We can now filter out those records with IsClinic=true. */ queryParams += '&IsClinic=true'; let degree = sanitizeInput(this.searchParams.get('degree')); if (degree !== null) { queryParams += `&Degrees=${degree.toUpperCase()}`; } /** **/ /* Get medical organizations for ProvidenceOmni */ let organizations = Object.keys(ProviderOrganization).filter(spec => ProviderOrganization[spec]?.active === true); try { for (var i = 0; i < organizations.length; i++) { if(organizations[i] === 'Providence Affiliated Physicians') { let org = []; for (var j = 0; j < organizations.length; j++) { /* Check if valid medical organization */ if(this.state.facets.ProviderOrganization.includes(organizations[j])) { org.push(organizations[j]); } } let po = this.searchParams.get("ProviderOrganization").replaceAll(", ", "!"); po = po.split(','); for (var j = 0; j < po.length; j++) { po[j] = po[j].replace("!", ", "); /* Check if valid medical organization */ if(!this.state.facets.ProviderOrganization.includes(po[j])) { po.splice(j, 1); } } organizations = Array.from([...new Set([...org, ...po])]); break; } } } catch (error) { console.log(error); } /* Check if valid medical organization */ let orgs = []; for (var j = 0; j < organizations.length; j++) { /* Check if valid medical organization */ if(this.state.facets.ProviderOrganization.includes(organizations[j])) { orgs.push(organizations[j]); } } organizations = orgs; if (organizations.length > 0) { const encodedOrganizations = encodeURIComponent(organizations.reduce((str, spec, i) => i === 0 ? str + spec : `${str},${spec}`, "")); queryParams += `&ProviderOrganization=${encodedOrganizations}`; } /** **/ /* Grab primary specialties, if applicable */ const specialties = Object.keys(PrimarySpecialties).filter(spec => PrimarySpecialties[spec]?.active === true); if (specialties.length > 0) { queryParams += specialties.reduce((str, spec, i) => i === 0 ? str + spec : `${str},${spec}`, "&PrimarySpecialties="); } /* Set filter for LocationId */ if (this.state.filters.LocationId && this.state.filters.LocationId.length) { queryParams += `&LocationId=${this.state.filters.LocationId}`; } /* Not 100% necessary but it double-checks that we do indeed have coordinates before adding them to the request. */ if (this.state.filters.location) { queryParams += `&location=${this.state.filters.location}`; } else if (this.state.currentLoc) { queryParams += `&location=${this.state.currentLoc}`; } /* Create the user id if not present */ if (!localStorage.getItem('cid')) { localStorage.setItem('cid', crypto.randomUUID()); } queryParams += `&cid=${localStorage.getItem('cid')}`; /* Add a \ (%5C) to allow for , in queryParams, example - ProviderOrganization */ queryParams = queryParams.replaceAll('%2C%20','%5C%2C%20'); /*Change queryParams lowercase to uppercase for certain values to get results*/ const qStrings = [ 'Gender', 'InsuranceAccepted', 'Languages', 'LocationId', 'LocationName', 'PracticeGroup', 'PracticeGroupId', 'PrimarySpecialties', 'ProviderOrganization', 'userLocation', 'visitTypes' ]; const searchParams = new URLSearchParams(window.location.search); let qStringsLength = qStrings.length; for (let i = 0; i < qStringsLength; i++) { /*replace with correct queryParams*/ queryParams = queryParams.replace(qStrings[i].toLowerCase(), qStrings[i]); /*if not in queryParams check if in url*/ if(searchParams.get(qStrings[i]) && !(queryParams.includes(qStrings[i]) || queryParams.includes(qStrings[i].toLowerCase())) ) { queryParams += `&${qStrings[i]}=${searchParams.get(qStrings[i]).replaceAll(', ','%5C%2C%20').replaceAll('&','%26')}`; }else if(searchParams.get(qStrings[i].toLowerCase()) && !(queryParams.includes(qStrings[i]) || queryParams.includes(qStrings[i].toLowerCase())) ) { queryParams += `&${qStrings[i]}=${searchParams.get(qStrings[i].toLowerCase()).replaceAll(', ','%5C%2C%20').replaceAll('&','%26')}`; } } /* For hospital locations and such we should not use this parameter - in this case all the data should be shown. If this isn't always working we can coordinate with the BE; example if we are missing data and would return nothing they could return a search with the IsClinic flag. */ if ( queryParams.includes('LocationId=') || queryParams.includes('LocationName=') ) { queryParams = queryParams.replace('&IsClinic=true', ''); } /* Sending the actual request */ await fetch( `${this.fetchOmniSearchURL}?type=search&brand=${brand}&search=${post.search}${queryParams}${isTest ? '&test=true' : ''}` ) .then((res) => res.json()) .then((res) => { this.state.results = res; }) .then(() => { const { results, query, filters } = this.state; /* If coordinates are returned from the API, save them to state */ if (this.state.results.geoip) { const { geoip } = this.state.results; const { lat, lng } = geoip.coordinates; this.state.currentLoc = this.helpers.resolveLocation(geoip); localStorage.setItem('omniSearchLocation', this.state.currentLoc); queryParams += `&userLocation=${this.state.currentLoc}`; if (!this.state.defaultLoc) { this.state.defaultLoc = this.state.currentLoc; } this.state.filters.coordinates = { latitude: lat, longitude: lng }; } /* Update local storage with previous searches */ const terms = query.split(/\s+/); let previousTerms = window.sessionStorage.getItem('previousQuery'); previousTerms = previousTerms && previousTerms.length ? previousTerms.split(/\s+/) : []; let inserted = []; let deleted = []; for (let term of terms) { if (!previousTerms.includes(term)) { inserted.push(term); } } for (let term of previousTerms) { if (!terms.includes(term)) { deleted.push(term); } } window.sessionStorage.setItem('previousQuery', this.state.query); /* If we get an error from the API, write the error to the page and stop the rest of the function from running. */ if (results.error) { this.resultsEl.innerHTML = `
${this.state.results.error.message}
`; return; } /* Update state now that an initial search has been completed */ this.state.filters.init = false; /* Grabs the total number of results */ const count = this.state.filters.LocationsOnly ? this.state.results.locations.length : this.state.results.providersCount; /* Calculate how many pages we need for the number of results returned. */ this.state.filters.pages = this.state.filters.LocationsOnly ? Math.ceil(count / post.top) : Math.ceil(this.state.results["@odata.count"] / post.top); /* Add badges for the applied filters */ const filtersElem = document.getElementById('filterButtons'); filtersElem ? filtersElem.innerHTML = '' : ''; const filtersWrapper = document.getElementById('filterWrapper'); const filtersSorted = this.state.results.info.filters.sort((a,b) => a.text ? -1 : 1); const filtersBadged = []; try { if (filtersSorted.length === 0 || (filtersSorted.length === 1 && filtersSorted[0].facets[0].name === 'IsClinic')) { filtersWrapper.classList.remove('has-filter'); } } catch (err) { console.error('couldn’t remove filter:\n', err); } try { const searchRadiusVal = this.state.filters.distance; if (searchRadiusVal) { document.querySelector('#searchRadius').value = searchRadiusVal; } if (searchRadiusVal?.length) { let filterText = `Within ${searchRadiusVal} miles`; if (searchRadiusVal == 1) { filterText = `Within ${searchRadiusVal} mile`; } let distanceFilterEl = { facets: [{ name: "distance", value: filterText }], value: filterText }; this.state.results.info.filters.push(distanceFilterEl); } for (let filter of this.state.results.info.filters) { try { if ( filter.facets[0].name === 'IsClinic' || filter.facets[0].name === 'Degrees' ) { continue; } } catch {} let filterButton = document.createElement('span'); filterButton.classList.add('filter-button'); let filterText = filter.value; /* LocationId becomes LocationName */ if ( filter.facets.length && 'name' in filter.facets[0] && filter.facets[0].name === 'LocationId' && 'info' in this.state.results && 'locations' in this.state.results.info && this.state.results.info.locations?.length ) { const locIndex = this.state.results.info.locations?.findIndex( (loc) => loc.id == filterText ); if (locIndex >= 0) { filterText = this.state.results.info.locations[locIndex].name; } else { this.helpers.removeParamValue('locationid', filterText); const locIdFiltStr = this.state.filters.LocationId; const locIdArr = locIdFiltStr.split(','); const locIdIndex = locIdArr.findIndex((id) => id == filterText); locIdArr.splice(locIdIndex, 1); this.state.filters.LocationId = locIdArr.join(','); continue; } } filterButton.innerHTML = `${filterText} `; const { facets } = filter; /* if the text for a search is returned... */ if (filter.text) { filterButton.setAttribute('data-text', filter.text); for (let facet of facets) { try { const facetFilter = this.state.filters[facet.name]; if (facetFilter) { facetFilter[facet.value] = {...facetFilter[facet.value], text: filter.text, auto: filter.auto == true}; filtersBadged.push(`${facet.name}${facet.value}`); } } catch (error) { throw(error); } } /* otherwise... */ } else { if (filter.value == 1) { continue; } /* everything else should have a length of 1 but let's check to be safe. If the value has already had a badge created, break out of this current loop iteration. */ if (facets.length === 1 && filtersBadged.includes(`${facets[0].name}${facets[0].value}`)) { continue; /* If a badge doesn't already exist, we'll add it to the badged array and keep on in this interation */ } else { filtersBadged.push(`${facets[0].name}${facets[0].value}`); } } /* Create the data attribute that allows us to remove the filter on click */ let dataFacets = filter.facets; if (!(dataFacets && dataFacets.length)) { const { facet, value } = filter; /* If we have facet and value strings, each with a length, create the fallback data */ if (facet?.length && value?.length) { dataFacets = [{ name: facet, value: value }]; } } try { if (dataFacets.length === 1) { filterButton.classList.add(dataFacets[0].name); } } catch (err) { console.error('failed to add filterButton class:\n', err); } /* Set the data-facets attribute */ filterButton.setAttribute('data-facets', JSON.stringify(dataFacets)); /* Create an event listener to handle clicks, removing filters */ filterButton.addEventListener('click', (event) => { const { target } = event; const { dataset } = target; if (dataset.text) { let theRegEx = new RegExp(dataset.text, "gi"); window.sessionStorage.setItem('previousQuery', this.state.query); this.state.query = this.state.query.replaceAll(/ +/gi, " ").replaceAll(theRegEx, "").replaceAll(/ +/gi, " ").trim(); this.inputSearchEl.value = this.state.query; } else if(target.textContent) { let theRegEx = new RegExp(target.textContent, "gi"); window.sessionStorage.setItem('previousQuery', this.state.query); this.state.query = this.state.query.replaceAll(/ +/gi, " ").replaceAll(theRegEx, "").replaceAll(/ +/gi, " ").trim(); this.inputSearchEl.value = this.state.query; } const facets = JSON.parse(dataset.facets); let clearedInsurance = false; let clearInsurancePromise; for (let facet of facets) { const { name: facetName, value } = facet; const state = this.state.filters[facetName]; if (state) { if (typeof state === 'string') { const newState = state.replace(value, '').replace(/^,/,'').replace(',,',','); this.state.filters[facetName] = newState || false; if (!newState) { switch (facetName) { case 'acceptingNewPatients': case 'location': case 'offersVideoVisits': case 'onlineSchedule': this.state.filters[facetName] = false; break; case 'locationId': this.state.filters.LocationId = false; break; default: break; } } } else if (typeof state === 'object') { state[value].active = false; /* this.state.filters[value] = { active: false }; */ /* Catch weird values that have ',' in them */ try { let csv = value.split(','); for (let val of csv) { state[val].active = false; } } catch (err) { console.error('failure removing cvs filter:\n', err); } } } if (facetName == "InsuranceAccepted") { let lsInsurance = localStorage.getItem('omniSearchInsurance') || false; if (lsInsurance) { lsInsurance = lsInsurance.split(',').filter(ins => { ins != value }); lsInsurance.length ? localStorage.setItem('omniSearchInsurance', lsInsurance.join(',')) : localStorage.removeItem('omniSearchInsurance'); clearedInsurance = true; clearInsurancePromise = fetch("https://www.providence.org/integrationapi/clearinsuranceselection"); } } else if (facetName == "distance") { let searchRadiusEl = document.querySelector('#searchRadius'); searchRadiusEl.value = ''; this.state.filters.distance = ''; } } try { let removeParam = JSON.parse(target.getAttribute("data-facets"))[0].name; let url = new URL(window.location); let searchParams = new URLSearchParams(url.search); searchParams.delete(removeParam); searchParams.delete(removeParam.toLowerCase()); url.search = searchParams.toString(); window.history.replaceState({}, 'Find a Doctor', url); } catch (err) { console.error('failure updating url:\n', err); } if (clearedInsurance) { Promise.allSettled([clearInsurancePromise]).then(()=>this.handleSubmit()); } else { this.handleSubmit(); } if (document.querySelectorAll('.filter-button')?.length === 1) { document.getElementById('filterWrapper').classList.remove('has-filter'); } }); /* Add the filter button to the DOM */ filtersElem.prepend(filterButton); filtersWrapper.classList.add('has-filter'); } /* Add badges for Virtual and New Patient visit type selections */ if (this.state.filters.visitType) { const { clinicVisits, onlineScheduling, videoVisits } = this.state.filters.visitType; const hasClinicVisits = clinicVisits && clinicVisits.active; const hasOnlineScheduling = onlineScheduling && onlineScheduling.active; const hasVideoVisits = videoVisits && videoVisits.active; const handleVisitFiltersClick = (event) => { const { value } = event.target.dataset; const { filters } = this.state; const state = filters.visitType; if (state) { state[value].active = false; } if (value == 'onlineScheduling') { if (filters.visitType['clinicVisits'].active != document.querySelector(`.visitType[data-type="clinicVisits"]`).classList.contains('active')) { document.querySelector(`.visitType[data-type="clinicVisits"]`).classList.toggle('active'); } document.querySelectorAll("#daysList a").forEach(day => { siteSearch.updateDaySelectorStateAllDays(day, false) }); if (state) { state['clinicVisits'].active = false; } } try { if (document.querySelectorAll('.filter-button').length === 1) { document.getElementById('filterWrapper').classList.remove('has-filter'); } } catch (err) { console.error('failure removing has-filter:\n', err); } this.handleSubmit(); }; handleVisitFiltersClick.bind(this); if (hasClinicVisits) { let filterButton = document.createElement("span"); filterButton.classList.add("filter-button"); filterButton.innerHTML = `Accepting New Patients `; /* Set the data-facets attribute */ filterButton.setAttribute('data-value', "clinicVisits"); filterButton.addEventListener('click', handleVisitFiltersClick); filtersElem.prepend(filterButton); filtersWrapper.classList.add('has-filter'); } if (hasOnlineScheduling) { let filterButton = document.createElement("span"); filterButton.classList.add("filter-button"); filterButton.innerHTML = `Online Scheduling `; /* Set the data-facets attribute */ filterButton.setAttribute('data-value', "onlineScheduling"); filterButton.addEventListener('click', handleVisitFiltersClick); filtersElem.prepend(filterButton); filtersWrapper.classList.add('has-filter'); } if (hasVideoVisits) { let filterButton = document.createElement("span"); filterButton.classList.add("filter-button"); filterButton.innerHTML = `Video Visits `; /* Set the data-facets attribute */ filterButton.setAttribute('data-value', "videoVisits"); filterButton.addEventListener('click', handleVisitFiltersClick); filtersElem.prepend(filterButton); filtersWrapper.classList.add('has-filter'); } /* Create an event listener to handle clicks, removing filters */ document.querySelectorAll('.visit-filter').forEach(handleVisitFiltersClick); } } catch (err) { console.error('facets failed to load:\n', err); } if (this.state.filters.page > 0) { let baseUrl = 'doctors.pacificmedicalcenters.org'; if(window.location.origin.toLowerCase().includes('swedish')) { baseUrl = 'schedule.swedish.org'; } else if(window.location.origin.toLowerCase().includes('providence')) { baseUrl = 'www.providence.org'; } const canon= document.querySelector('link[rel="canonical"]'); const newCanon = document.createElement('span'); newCanon.innerHTML = ``; canon.parentNode.replaceChild(newCanon, canon); } /* If we're sending a page that is greater than results paginated, reset to page one and redo the search. */ if (results.providersCount > 0 && results.results.length === 0) { this.state.filters.page = 1; return this.handleSubmit(); } else if (this.state.filters.pages === 0) { this.state.filters.page = 1; } /* Determine if we have results or not and display appropriate text. */ let resultsHtml = ''; let secondaryLocationIds = []; if (this.state.description) { resultsHtml += `
${this.state.description}
`; this.state.description = false; } /** * If we have the testing parameter added for internal testing and the type object is present on the API * response, then we're going to render that content above the search results. */ if (isTest && results.info?.type?.length) { resultsHtml += `
Search type(s):
${results.info.type.join(', ')}
`; } /* Pretty self explanatory: if the results count is zero, tell the user there are no results */ if (count == 0) { this.resultsEl.innerHTML = "No results"; return; } /* Otherwise, let's display the total number of results */ else { try { if(results.geoip.zip !== undefined){ resultsHtml += `
Showing ${count.toLocaleString()} Results From ${results.geoip.zip}
This page is intended to provide the most comprehensive information available regarding services offered in your area. Some of the services listed might not be offered at Providence facilities or in Providence affiliated practices. Providence is not responsible for services offered by providers on an independent basis.
' : ''; /* We'll use this to determine if we show the map or not */ let withLocations = 0; /* We'll use this to determine if we show a UC slot - only show for specific searches */ this.state.ucProviders = false; try { let searchTerm = this.state.results.info.search.split(' ').map((s) => s.charAt(0).toUpperCase() + s.substring(1)).join(' '); /* match values from the specialties dropdown */ let matches = ''; if( this.state.results.info.type.includes('PrimarySpecialties')) { matches = this.state.results.info.matches[0]; } if(this.state.filters.page === 1 && this.state.results.info.facets.PrimarySpecialties) { if( this.state.results.info.facets.PrimarySpecialties.includes('Pediatrics') || this.state.results.info.facets.PrimarySpecialties.includes('Internal Medicine') || this.state.results.info.facets.PrimarySpecialties.includes('Family Medicine') ) { this.state.ucProviders = true; } }else if ( this.state.filters.page === 1 && (searchTerm.includes('Pediatrics') || searchTerm.includes('Internal Medicine') || searchTerm.includes('Family Medicine') || matches === 'pediatrics' || matches === 'internal medicine' || matches === 'family medicine' || this.state.filters.visitType.onlineScheduling.active ) ) { this.state.ucProviders = true; } } catch (error) { /* console.log(error); */ } /* If LocationsOnly is set, render the locations for the current page */ if (LocationsOnly) { resultsHtml += this.renderLocations(results.locations); } else { this.state.slotsType = this.helpers.getSlotsType(); /* For each results on the current page, create a card and append it to our results container */ results.results.forEach((result, i) => { const { AcceptingNewPatients, Addresses, AllowsDirectSchedule, AllowsOpenSchedule, AppointmentRequestUrl, DaysUntilFollowUpAppt, DaysUntilNextAppt, Degrees, GeocodedCoordinate, ImageHeight, ImageSourceUrl, ImageWidth, ImageUrl, LocationId, LocationIds, LocationName, LocationNames, LocationsIsClinic, Name, NewClinicVisit, NewVideoVisit, Networks, Npi, Phones, ProfileUrl, PracticeGroup, PrimarySpecialties, ProviderTitle, Rating, RatingCount, ReviewCount, SjhScheduleProviderNum, SubSpecialties, Tier, VirtualCare, acceptingNewPatients, clinicVisits, distance, distances, id, virtual } = result; /* Place the the card as the 3rd result */ if (i === 2) { resultsHtml += /*html*/ ``; } let ProviderOrganization = result.ProviderOrganization; const searchFeatures = result['@search.features']; const width = (150 / ImageHeight) * ImageWidth; let badges = result.badges ? Object.values(result.badges) : []; /* Combine subspecialties into one badge */ if (badges.length) { let SubSpecialtiesNameArr = []; badges.forEach((badge, index) => { if (badge.includes('Subspecialty')) { let noLabel = badge.replace('Subspecialty: ', ''); SubSpecialtiesNameArr.push(noLabel); } }); badges = badges.filter((badge) => !badge.includes('Subspecialty')); if (SubSpecialtiesNameArr.length) { badges.push(`Specialt${SubSpecialtiesNameArr.length > 1 ? 'ies' : 'y'}: ${SubSpecialtiesNameArr.join(', ')}`); } } /* sort badges */ badges = [ ...badges.filter(x => /^Primary Specialty:/i.test(x)), ...badges.filter(x => /^(Specialty:|Specialties:)/i.test(x)), ...badges.filter(x => /^Keywords:/i.test(x)), ...badges.filter(x => !/^(Primary Specialty:|Specialty:|Specialties:|Keywords:)/i.test(x)) ]; const supportsOdhp = (acceptingNewPatients == 1 && AllowsOpenSchedule == 1) || AllowsDirectSchedule == 1; const onlineBooking = supportsOdhp || SjhScheduleProviderNum ? true : false; let AcceptingClinicVisits = AcceptingNewPatients == 1 ? /*html*/ `
Accepting New Patients
` : ''; const hasTimeslots = this.helpers.checkForTimeslots(NewClinicVisit, NewVideoVisit, DaysUntilNextAppt); /* Determining the description to show. If we have a Provider Title, use that. When no provider title, check for a primary specialty. If none, show no text. */ let descr = ""; if (PrimarySpecialties?.length) { descr = PrimarySpecialties.join(', '); if(SubSpecialties?.length) { descr += ', ' + SubSpecialties.join(', '); } } else if (ProviderTitle?.length) { descr = ProviderTitle; } else { descr = ""; } this.map.deleteMarkers(); /* Are any filtered ids hospitals? */ let filteredByHos = false; if (this.state.filters.LocationId?.length) { const locIdFiltStr = this.state.filters.LocationId; const locIdArr = locIdFiltStr.split(','); for (const id of locIdArr) { let iOfLoc = LocationIds.indexOf(id); if (iOfLoc !== -1 && LocationsIsClinic[iOfLoc] === false) { filteredByHos = true; } } } const hasClinic = !LocationsIsClinic.every((loc) => loc === false); for (const locId of LocationIds) { let isClinic; let iOfLoc = LocationIds.indexOf(locId); try { isClinic = LocationsIsClinic[iOfLoc]; } catch (err) { console.error(`Failed to determine if location is CLI for provider: Npi=${Npi}, LocationId=${locId}:`, err); isClinic = true; } let locName = LocationNames[iOfLoc]; /** * Add the location to the map only if location is not virtual AND either * a) we are not filtering by location id and it's a clinic * b) we are filtering by location id and this location is * one of them and it's a clinic * c) we are filtering by a hospital location id, and this * location is a clinic (not a hospital) * d) provider only has hospitals */ if ( (!/virtual/i.test(locName) && !this.state.filters.LocationId && isClinic) || (!/virtual/i.test(locName) && this.state.filters.LocationId && this.state.filters.LocationId.includes(locId) && isClinic) || (!/virtual/i.test(locName) && this.state.filters.LocationId && filteredByHos && isClinic) || (!/virtual/i.test(locName) && !hasClinic) ) { if (!this.map.locations[locId]) { this.map.locations[locId] = []; } this.map.locations[locId].push({ Npi, ImageUrl, ImageSourceUrl, ImageWidth, ImageHeight, Name, ProviderTitle: descr, Rating, PracticeGroup, virtual }); } } /* If we have badges for matched, create element HTML for it */ let badgesEls = ""; if (badges.length) { badges.forEach((badge) => { if (!badge.includes('PracticeGroupId')) {badgesEls += `${badge}`} }); } /* Loop through the LocationNames and Addresses arrays. If these are present, then build out the locs array for listing addresses on the provider detail cards */ let locs = []; let topLocation = false; let secondaryLocations = false; /* Formats address */ if (LocationNames.length && Addresses.length) { for (let i = 0; i < LocationNames.length; i++) { if (Addresses[i]) { locs.push({ name: LocationNames[i], address: formatAddress('providerCard', Addresses[i]), id: LocationIds[i], phone: Phones[i] }); } } /* Code for identifying clinics vs hospitals */ for (let i = 0; i < locs.length; i++) { locs[i].isClinic = result.LocationsIsClinic[i]; locs[i].distance = result.distances[i]; } /* if we have a clinic, filter out non-clinics */ if (locs.length > 1 && hasClinic) { locs = locs.filter((loc) => loc.isClinic); } /* Locations should be sorted by distance Some providers have multiple locations within the same physical location (https://www.providence.org/doctors/general-surgery/mt/missoula/deron-ludwig-1245310200) In those cases, we want to display the first location in the list */ /* if (hasClinic && locs.length > 1) { locs = locs.sort((loc1, loc2) => loc1.distance - loc2.distance); } else { locs = locs.sort((a) => a.name === LocationName ? -1 : 1); } */ function useLoc0() { topLocation = locs[0]; secondaryLocations = locs.length > 1 ? locs.slice(1) : false; } /* Prioritize queried location, otherwise 0 index */ let queriedLocIndex = -1; const locIdFiltStr = this.state.filters.LocationId; const locIdArr = locIdFiltStr?.split(','); if (this.state.filters.LocationId?.length && locIdArr?.length) { /** * return location matching first location id in the list * as long as it a clinic */ for (const id of locIdArr) { const found = locIdArr.findIndex((locId) => locs.some((loc) => loc.id === locId && loc.isClinic)); if (found !== -1) { queriedLocIndex = found; break; } } } if (locs.length > 1) { if (queriedLocIndex !== -1 && locIdArr?.length) { topLocation = locs.filter((loc) => { return loc.id === locIdArr[queriedLocIndex]; })[0]; /* if hasClinic && loc is hospital, topLocation is undefined */ if (!topLocation) { useLoc0(); } else if (locs.length > 1) { secondaryLocations = locs.filter((loc) => { return loc.id !== locIdArr[queriedLocIndex]; }); } } /* handle filtered on location name */ else if (this.state.filters.LocationName?.length) { const locNameFiltStr = this.state.filters.LocationName; queriedLocIndex = LocationNames.indexOf(locNameFiltStr); topLocation = locs.filter((loc) => { return loc.name === LocationNames[queriedLocIndex]; })[0]; /* if hasClinic && loc is hospital, topLocation is undefined */ if (!topLocation) { useLoc0(); } else if (locs.length > 1) { secondaryLocations = locs.filter((loc) => { return loc.name !== LocationNames[queriedLocIndex]; }); } } else { useLoc0(); } } else { topLocation = locs[0]; } } const locAdd = topLocation?.address || formatAddress('providerCard', Addresses[0]) || ''; const locDistance = topLocation?.distance || distance || ''; const locId = topLocation?.id || LocationId || ''; const locName = topLocation?.name || LocationName || ''; const locPhone = topLocation?.phone || Phones[0] || ''; /* We only want to display review/rating data for providers that have "PHS Reviews Eligible Providers" listed as a network */ let rating = ""; if (Networks?.includes('PHS Reviews Eligible Providers')) { /* Create star rating for provider, if rating is present */ if (Rating) { let ratingFull = ""; let lastStar = parseInt(Rating.toString().slice(2,3)) * 1.6; if(Rating > 4) { ratingFull = ` `; } else if(Rating > 3) { ratingFull = ` `; } else if(Rating > 2) { ratingFull = ` `; } else if(Rating > 1) { ratingFull = ` `; } if(ReviewCount > 0) { rating = /*html*/`
${ratingFull} ${Math.round(Rating * 100) / 100}
${RatingCount} Ratings | ${ReviewCount} Reviews
`; } else { rating = /*html*/`
${ratingFull} ${Math.round(Rating * 100) / 100}
${RatingCount} Ratings
`; } } } /* If provider offers virtual visits, create virtual visit html */ let offersVirtual = ""; let virtualBadge = ""; if (VirtualCare && AcceptingNewPatients == 1) { AcceptingClinicVisits = /*html*/ `
Offers Video Visits
Accepting New Patients
Offers Video Visits
Accepting New Patients
`; } else if (VirtualCare) { offersVirtual = /*html*/ `
Offers Video Visits
`; virtualBadge = /*html*/ `
`; } let name = Name; if (Degrees.length) { name = `${Name}, ${Degrees.join(', ')}`; } let providerLink = this.helpers.createProviderLink(result); let wheelhouseData = sessionStorage.getItem('wheelhouseData'); if (!wheelhouseData) { try { const wheelHouseDataRaw = document.querySelector('#wheelhouseJson'); wheelhouseData = wheelHouseDataRaw ? JSON.parse(wheelHouseDataRaw.innerText) : {}; } catch (err) { console.error('couldn’t get wheelhouse data:\n', err); } } else { wheelhouseData = JSON.parse(wheelhouseData); } if (/swedish/i.test(location.origin) && result.ProviderUniqueUrlSwedish) { /* use kyruus profile URL as source of truth for link to to profile */ providerLink = result.ProviderUniqueUrlSwedish; /* some providers still have old profile URL per request from Anita, overwrite with correct www. URL */ if (providerLink.includes('schedule.swedish.org')) { providerLink = providerLink.replace('schedule.swedish.org', 'www.swedish.org'); } } if (!/virtual/i.test(locName)) { withLocations += 1; } /* Determine whether or not to display the 90 days fallback message when timeslots are not available */ const shouldShowAlert = () => { return false; /* - We don't care about followup appointments anymore, only new patient availability if (DaysUntilFollowUpAppt && DaysUntilFollowUpAppt >=0 && DaysUntilFollowUpAppt <= 90) { return true; } else */ if (DaysUntilNextAppt && DaysUntilNextAppt >=0 && DaysUntilNextAppt <= 90) { return true; } else { return false; } }; const wompTrackData = { ec: '', el: `${name}, ${i+1}`, ea: JSON.stringify(result['@search.features']) }; let medicalGroupsAndAffiliations = ''; /** **/ try { ProviderOrganization = ProviderOrganization.filter(org => org.toLowerCase() != 'providence affiliated provider'); if (ProviderOrganization.length === 1) { if (ProviderOrganization[0].toLowerCase() === 'providence') { medicalGroupsAndAffiliations = ''; } else { medicalGroupsAndAffiliations = `
Medical Groups & Affiliations
${ProviderOrganization[0]}
`; } } else if (ProviderOrganization.length > 1) { if (ProviderOrganization[0].toLowerCase() === 'providence') { medicalGroupsAndAffiliations = `
Medical Groups & Affiliations
${ProviderOrganization[1]}
`; } else { medicalGroupsAndAffiliations = `
Medical Groups & Affiliations
${ProviderOrganization[0]}
`; } } } catch (ex) { console.log('Failed to display provider org: ' + ex); } /** **/ let brandedLocNames = ["Providence", "Jude Medical Center", "Mission Heritage", "Jude Heritage", "Joseph Heritage", "Mary High Desert", "Facey", "Joseph Hospital", "Santa Rosa Memorial", "Petaluma Valley Hospital", "Sacred Heart Medical", "Saint Johns Health Cancer", "Doctors of Saint", "Pavilion for Women", "Swedish", "Pacific Medical Centers", "Covenant", "Grace Clinics", "Grace Clinic", "Kadlec"]; let addPO = true; let dpo = ""; if (ProviderOrganization?.length > 0) { /** See provOmniMedicalAffiliations, interesting case where we use the second PO if there are 2 and the first is providence **/ if (ProviderOrganization.length > 1 && ProviderOrganization[0].toLowerCase() === 'providence') { dpo = `dpo-${ProviderOrganization[1].replace(/\s+/g, '')}`; } else { dpo = `dpo-${ProviderOrganization[0].replace(/\s+/g, '')}`; } for (var i = 0; i < brandedLocNames.length; i++) { if (locName.indexOf(brandedLocNames[i]) > -1){ addPO = false; break; } } } else { addPO = false; } /* only link if on the map */ const notLink = locId ? !(locId in this.map.locations) : true; /* If a distance from ZIP exists, include that in the provider card. */ let distanceEl = ''; if (locDistance) { distanceEl = /*html*/`
${parseFloat(Number(locDistance).toFixed(1))} miles away
${secondaryLocations.reduce((html, curr) => { let distanceSec = ''; try { if (curr.distance) { distanceSec = /*html*/`
${parseFloat(Number(curr.distance).toFixed(1))} miles away
`; } } catch (err) { console.error('couldn’t get distances:\n', err); } /* only link if on the map */ const notLink = curr.id ? !(curr.id in this.map.locations) : true; return html + /*html*/ `
`) : ''; } } else { document.querySelector('#custom-location + small.dc')?.remove(); document.querySelector('#location-selection')?.remove(); document.querySelector('#mobile-location-selection')?.remove(); } /* We want to make sure the corrected text from search response is always in the search box. If there's been a spelling correction, update the input value */ let decodedQ = query; try { decodedQ = decodeURIComponent(query); } catch (err) { console.error(`Search query "${query}" is not a valid URI:\n`, err); } if (decodedQ !== results.info.search) { resultsHtml = /*html*/ `
${clinic.hours_today.start} to ${clinic.hours_today.end}
`; document.getElementById('UCSlot').innerHTML = ucHtml; foundLocation = true; } let foundLocation = false; for (let i = 0; i < locations.length; i++) { let loc = locations[i]; if (loc.distance_raw < 10) { let omw = false; /*Reserve My Spot In Line*/ for (let key in omwLocations) { if (key === loc.name) { fillOutLocationCard(loc); document.getElementById("bookingtimes").innerHTML = /*html*/ `
`; break; } else if (loc.booking_waittime !== '' && !omw) { /*example: "4388" - show Walk In Reserve My Spot*/ fillOutLocationCard(loc); document.getElementById('bookingtimes').innerHTML = /*html*/ `
`; } /* Hide the list when no list items match input */ if (filterList) { filterList.style.display = total > 0 ? 'block' : 'none'; filterList.innerHTML = filteredHtml; } }); fireInvoca(); } /* We'll use this to remove auto-applied filters at search time */ const autoApplied = []; /* Iterate over the facets and set filter displays */ try { for (let facet in facetSet) { /* We want to make sure that the facet exists in the facets obj before proceeding */ if (facets[facet]) { facets[facet].forEach((val) => { const filter = info.facets[facet]; let isSelected = false; if (filter && filter.includes(val)) { isSelected = true; } this.state.filters[facet][val] = { ...this.state.filters[facet][val], active: isSelected }; }); } } } catch (err) { console.error('facets failed to load:\n', err); } for (const f of info.filters) { const { facets: fs } = f; if (f.text?.length && fs && fs.length) { console.debug('filter:', f); fs.forEach(filter => { autoApplied.push({ name: filter.name, value: filter.value }); }); } } /* Update autoapplied filters in state */ this.state.autoApplied = autoApplied; /* For each of the filter dropdowns we've added, update the UL to show the available filters */ document.querySelectorAll(".dropdown.facet").forEach((dropdown) => { const facet = this.state.filters[dropdown.id]; if (facet) { /* Use blocklists to explicitly remove specific filter values For example, providence requested we remove multiple provider orgs from the "Medical Group" dropdown */ let blocklists = { 'ProviderOrganization': [ "Providence Affiliated Physicians", " Los Angeles County", " Mission", " St. Joseph", " St. Jude", " St. Mary", /** **/ 'Providence', 'Providence Affiliated Physicians, Los Angeles', 'Axminster Medical Group & Providence Care Network Providers', 'Covenant Health Partners Affiliated', 'Covenant Rural Health Clinics', 'Facey Affiliated Provider', 'PacMed AdvantAge Health Center', 'Providence Affiliated Provider', 'Providence Medical Group', 'Providence Medical Institute', 'Saint John\'s Physician Partners Affiliated Providers', 'St. Joseph Health Medical Group', 'Providence Care Network', 'Providence Medical Associates' /** **/ ] }; let isFiltered = false; let html = "
"; let keys = Object.keys(facet); keys = keys.sort(); let active = 0; /* Add a text input and a list id to lists which can be filtered by text */ if (dropdown.id === "InsuranceAccepted") { html = /*html*/ `
` } else if (dropdown.id === "PrimarySpecialties") { html = /*html*/ `
` } else if (dropdown.id === "ProviderOrganization") { html = /*html*/ `
` } else if (dropdown.id === "Languages") { html = /*html*/ `
` } /* If we have a blocklist for our filter, then remove blocked values before generating UI */ if (dropdown.id in blocklists) { let allowedKeys = keys.filter(function(key) { return !blocklists[dropdown.id].includes(key); }); keys = allowedKeys; } for (let key of keys) { let isActive = false; if (facet[key]?.active === true) { active++; isActive = true; } if (dropdown.id ==="Gender") { const genderVals = ["Female", "Male", "Non-binary"]; if (genderVals.includes(key)) { html += /*html*/ `
`; /* Cap to 5 pages */ if (page <= 4) { start = 1; } else if (pages - page < 4) { start = page - (4 - (pages - page)); } else { start = page - 2; } const firstPage = new URL(document.location); firstPage.searchParams.set('page', 1); if (start > 1) { html += start > 2 ? /*html*/ `1...` : ''; } for (let i = start; i <= start + 4; i++) { if (i > pages) break; if (pages > 4 && i > start + 1 && i < start + 4) { mobileHide = ' mobile-hide'; } else { mobileHide = ''; } if (i === 1) { html += /*html*/ ``; } html += /*html*/ `${i}`; if (i == start + 4 && i < pages) { html += /*html*/ `...`; if (i < pages + 1) { html += /*html*/ `${pages}`; } } } html += /*html*/ `
`; this.paginationEl.innerHTML = html; this.paginationEl.style.display = 'flex'; try { this.paginationEl.classList.remove('first-page'); } catch (err) { console.error('failure removing first-page:\n', err); } try { this.paginationEl.classList.remove('last-page'); } catch (err) { console.error('failure removing last-page:\n', err); } try { this.paginationEl.classList.remove('no-pages'); } catch (err) { console.error('failure removing no-pages:\n', err); } if (pages === 0) { this.paginationEl.classList.add('no-pages'); this.paginationEl.style.display = 'none'; } else if (this.state.filters.page === pages) { this.paginationEl.classList.add('last-page'); } else if (this.state.filters.page === 1) { this.paginationEl.classList.add('first-page'); } }; addWindowListeners = () => { window.addEventListener('resize', this.helpers.debounce(() => { return; const w = window.innerWidth; if (w < 1000) { if (this.mapEl.style.top !== '78px') { this.logo.style.width = '85px'; this.logo.style.height = '50px'; this.mapEl.style.top = '78px'; this.mapEl.style.height = 'calc(100vh - 78px)'; }; } else { const h = document.querySelector('header'); this.logo.style.width = '170px'; this.logo.style.height = '99px'; this.mapEl.style.top = `${h.clientHeight}px`; this.mapEl.style.height = `calc(100vh - ${h.clientHeight}px)`; }; }, 100)); window.onpopstate = (event) => { this.searchParams = prepURLSearchParams(); this.init(); }; }; populateAvailabilityLabel = () => { const label = document.querySelector('#AvailabilityLabel'); if (label) { const activeDays = Object.entries(this.state.filters.AvailableDays).filter(x => this.state.specificDays && x[1].active).map(x => x[0]); let labelText = 'Availability '; if (activeDays.length) { if (activeDays.length !== 7) { labelText += ': ' + activeDays.map(day => { switch (day) { case 'Monday': return 'M'; case 'Tuesday': return 'T'; case 'Wednesday': return 'W'; case 'Thursday': return 'Th'; case 'Friday': return 'F'; case 'Saturday': return 'S'; case 'Sunday': return 'Su'; default: return ''; } }).join(', ') + ' '; } } if (this.state.filters.AvailableTime !== 'any') { let time = this.state.filters.AvailableTime.split(''); time[0] = time[0].toUpperCase(); labelText += ': ' + time.join('') + ' '; } labelText += /*html*/ ``; if (label) label.innerHTML = labelText; } }; populateDaytimeSelectorFromState = () => { const timeSelector = document.querySelector("#time-selector"); const daySelector = document.querySelector("#day-selector"); const checkboxes = document.querySelector("#availability-days-checkboxes"); const dayCheckboxes = document.querySelectorAll(".day-checkbox"); timeSelector.value = this.state.filters.AvailableTime; if (this.state.specificDays) { daySelector.value = "specific-day"; checkboxes.classList.remove("hidden"); } else { daySelector.value = "any-day"; checkboxes.classList.add("hidden"); } dayCheckboxes.forEach(checkbox => { const state = this.state.filters.AvailableDays[checkbox.innerHTML].active; if (!state) { checkbox.classList.remove('active'); } else { checkbox.classList.add('active'); } }); this.populateAvailabilityLabel(); }; updateTimeSelectorState = (time) => { /* revert to any if deselecting */ if (time.id != 'any' && time.classList.contains('active')) { time.classList.remove('active'); document.querySelector('#any')?.classList.add('active'); this.state.filters.AvailableTime = 'any'; } else { const links = document.querySelectorAll('#timesList .active'); links?.forEach((el) => el.classList.remove('active')); time.classList.add('active'); this.state.filters.AvailableTime = time.id; } this.populateAvailabilityLabel(); this.handleSubmit(undefined, { ignoreElements: ['Availability']}); }; updateDaySelectorState = (day) => { let autoChecked = false; if (day.classList.contains('active') && day.id !== "any-day"){ this.state.filters.AvailableDays[day.id].active = false; } if (!day.classList.contains('active')){ day.classList.add('active'); if (day.id !== "any-day") { this.state.filters.AvailableDays[day.id].active = true; } } else if (day.id !== "any-day") { day.classList.remove('active'); /* If this was the last checked box being unchecked, we need to automatically check the "any days" box */ if (!document.querySelectorAll("#daysList .day.active").length) { try { document.querySelector("#any-day").classList.add('active'); this.state.specificDays = false; autoChecked = true; } catch (err) { console.error('adding active to #any-day:\n', err); } } } if (!autoChecked) { if (day.id !== "any-day") { this.state.specificDays = true; try { document.querySelector("#any-day").classList.remove('active'); } catch (err) { console.error('removing active from #any-day:\n', err); } } else { this.state.specificDays = false; document.querySelectorAll("#daysList .day.active").forEach((event) => { event.classList.remove('active'); }); this.state.filters.AvailableDays["Monday"].active = false; this.state.filters.AvailableDays["Tuesday"].active = false; this.state.filters.AvailableDays["Wednesday"].active = false; this.state.filters.AvailableDays["Thursday"].active = false; this.state.filters.AvailableDays["Friday"].active = false; } } this.populateAvailabilityLabel(); this.handleSubmit(undefined, { ignoreElements: ["Availability"]}); }; updateDaySelectorStateAllDays = (day, check) => { if (day.id == "any-day") { if (check && day.classList.contains('active')) { this.state.specificDays = true; day.classList.remove('active'); } else { if (!check && !day.classList.contains('active')) { this.state.specificDays = false; day.classList.add('active'); } } } else { if (day.classList.contains('active') && !check) { day.classList.remove('active'); this.state.filters.AvailableDays[day.id].active = false; } else if (!day.classList.contains('active') && check) { day.classList.add('active'); this.state.filters.AvailableDays[day.id].active = true; } } this.populateAvailabilityLabel(); }; updateDayTimeSelectorState = () => { const timeSelector = document.querySelector("#time-selector"); this.state.filters.AvailableTime = timeSelector.value; const daySelector = document.querySelector("#day-selector"); this.state.specificDays = (daySelector.value === "specific-day"); const checkboxes = document.querySelector("#availability-days-checkboxes"); this.populateDaytimeSelectorFromState(); this.handleSubmit(undefined, { ignoreElements: ["Availability"]}); }; /** **/ /** * @description Convert a JavaScript object into a serialized string * @param {object} obj - JavaScript object capable of serialization * @param {boolean} usePlusSpace - Use '+' instead of '%20' to represent spaces * @returns {string} - Serialized string * * This converts an object into a URL query or URL encoded form body. * The returned string is not prepended by a question mark. * Example with usePlusSpace is truthy: * { a: '', b: '1 2', c: '1 2' } --> 'a&b=1+2&c=1+2' * Example when usePlusSpace is falsy: * { a: '', b: '1 2', c: '1 2' } --> 'a&b=1%202&c=1%202' */ serializeObject(obj, usePlusSpace) { return Object.keys(obj).map((key) => { let value = encodeURIComponent(obj[key] || ''); if (usePlusSpace) value = value.replace(/%20/g, '+'); return encodeURIComponent(key) + '=' + value; }).join('&'); } xhrProvidenceGeolocationAutocomplete(location) { return new Promise((resolve, reject) => { let geocodeUrl; if (this.isStaging) { geocodeUrl = 'https://providencekyruusstaging.azurewebsites.net/api/geolocation/api/autocomplete/'+location+'?take=5'; }else{ geocodeUrl = 'https://providencekyruus.azurewebsites.net/api/geolocation/api/autocomplete/'+location+'?take=5'; } const xhr = new XMLHttpRequest(); xhr.open('GET', geocodeUrl, true); xhr.withCredentials = true; xhr.setRequestHeader("traceparent","00-7c12f8055a29436f95167ba40787dcae-92e09794d75a4f19-01"); xhr.setRequestHeader("x-requested-with","XMLHttpRequest"); if (this.isStaging) { xhr.setRequestHeader("RequestVerificationToken", "E0-x1LbtEMXUSEIfOvRQXj9EY-u2bR1VJpBe-z7eQIzN8eoy0QgjmInrnmLtlJwyuM7x6CdjAD5Q_GREZr0A_WR1uGWT4rJ1QgGUkznTSZo1:DG_76BAidglwME5Li0a2saKhFlCIpsNYFT3iawY9SwQFpiFD-1gwWlOgHFyFANWTe4re6YgvEMiDOW-Y8gGuxTBBqMs8hiwguwKZ3102tF41"); }else{ xhr.setRequestHeader("RequestVerificationToken", "3brXwene3ShS4TLB4IT1rG9vLrzdUgU88z9FHag9cLeRr1rekAkPtkmUkAUv2miRgl8Qpp0HzxxrETfJ6-l7Z3V8Fd0pyZ6nB1RovFdJb041:6E51pGciBcnM_aG91aokWULU6JNKdIR7o7uULQ7B6xAEYUxY2GavtVGNc9qyRnOGwkASFWxBQ12fgXS6n53NTRiIY27SrDAh6js-bSVaOjg1"); } xhr.responseType = 'json'; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { /* IE compatibility */ const json = typeof xhr.response === 'string' ? JSON.parse(xhr.response) : xhr.response; if (json && json.success) resolve(json); else reject('Could not guess user location from zipcode'); } else { reject('xhrProvidenceGeolocationAutocomplete did not succeed: xhr.status=' + xhr.status); } } }; xhr.send(); }) .then((data) => { let results = []; results.push("location-value " + location); data.results.forEach((loc) => { results.push(loc.city + ', ' + loc.state + ' ' + loc.zip + ' ' + loc.lat + ' ' + loc.lng); }); return results; }) .catch((error) => { console.error('Unable to fetch typeahead results for geolocator\n', error); return []; }); } handleGeoUpdate(event) { event.preventDefault(); let location = event.submitter.value; if(location) { let loc1 = location.split(","); let loc2 = loc1[1].split(" "); let city = loc1[0]; let state = loc2[1]; let zip = loc2[2]; let lng = loc2[loc2.length - 1]; let lat = loc2[loc2.length - 2]; let locationName = location.split(' '); /* Remove lat / lng from text value */ locationName.pop(); locationName.pop(); locationName = locationName.join(' '); if(state === 'TX' || state === 'NM') { document.getElementById("urgentCareLink").href = "https://www.providence.org/services/urgent-care"; }else { document.getElementById("urgentCareLink").href = "https://www.providence.org/our-services/urgent-care"; } let myChart = document.getElementsByClassName("my-chart-link"); if(state === 'AK'){ for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://mychartak.providence.org/mychart/Authentication/Login"; } } else { for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://www.providence.org/mychart"; } } document.querySelector('#geoLocatorLabel span').textContent = locationName; document.querySelector('#geo-locator-ck').checked = false; document.querySelector('#geoLocator-form .suggestion').style.setProperty('display', 'none'); this.setGeoProvLocation(zip, city, state); /*Set user Entered zipcode cookie; important to providence // 24 hours */ const setCookie = (id, value, maxage, path, domain, secure) => { let cookieString = id + "=" + value; cookieString += path ? "; path=" + path : "; path=/"; if (maxage !== undefined) cookieString += "; max-age=" + maxage; if (domain) cookieString += "; domain=" + domain; if (secure) cookieString += "; secure"; document.cookie = cookieString; }; let userGeoCookie = getCookie("UserEnteredLocationGeoCoordinates-v3"); if(userGeoCookie !== null) { /* Remove old cookie before we set a new one */ document.cookie = "UserEnteredLocationGeoCoordinates-v3"+'=; Max-Age=-99999999;'; } setCookie('UserEnteredLocationGeoCoordinates-v3', `{"City":"${city}","Latitude":"${lat}","Longitude":"${lng}","PostalCode":"${zip}","StateCode":"${state}","Regions":[],"Version":1}`, 86400); /* localstorage user entered zipcode is only used by us */ localStorage.setItem('UserEnteredLocationGeoCoordinates-v3', `{"City":"${city}","Latitude":"${lat}","Longitude":"${lng}","PostalCode":"${zip}","StateCode":"${state}","Regions":[],"Version":1}`); siteSearch.helpers.getGoogleLocation("geoLocatorSuggestions button:first-of-type"); } /*Set input value to selected value*/ let split = event.submitter.value.split(' '); /* Remove lat / lng from text value */ split.pop(); split.pop(); document.querySelector('#geoLocatorInput').value = split.join(' '); /*Remove options*/ let inputs = document.querySelectorAll('#geoLocatorSuggestions button'); for (let i = 0; i < inputs.length; i++) { inputs[i].remove(); } } /* Set the userlocation on Providence's end */ setGeoProvLocation(postalcodeSaved, citySaved, stateSaved) { return new Promise((resolve, reject) => { const body = { scController: 'DetectUserLocation', scAction: 'SetUserLocation', postalcode: postalcodeSaved, city: citySaved, state: stateSaved }; let geocodeUrl; if (this.isStaging) { geocodeUrl = 'https://preview-providence.provhealth.org/locations'; }else{ geocodeUrl = 'https://www.providence.org/locations'; } const xhr = new XMLHttpRequest(); xhr.open('POST', geocodeUrl); xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest'); xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); xhr.setRequestHeader('Authorization', 'Bearer ' + '7i9p5na47e3xgxz2bk472jgyfm989tt5'); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { checkLoginStatus(); /*console.log("Providence geolocation updated - setGeoProvLocation()");*/ } else { reject('setGeoProvLocation did not succeed: xhr.status=' + xhr.status); } } }; xhr.send(this.serializeObject(body)); }); } guessUserLocation() { return new Promise((resolve, reject) => { const geocodeUrl = 'https://wompservices.wompmobile.com/geoip'; const xhr = new XMLHttpRequest(); xhr.open('GET', geocodeUrl); xhr.responseType = 'json'; xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.status === 200) { /* IE compatibility */ const json = typeof xhr.response === 'string' ? JSON.parse(xhr.response) : xhr.response; if (json && json.success) resolve(json); else reject('Could not guess user location'); } else { reject('guessUserLocation did not succeed: xhr.status=' + xhr.status); } } }; xhr.send(); }); } updateGeoTypeahead(event, currentTarget) { let typeaheadList = document.querySelector('#geoLocator-form .suggestion'); if (!typeaheadList) return console.error('Unable to locate typeahead list for typeahead update'); typeaheadList.querySelectorAll('.geo-btn').forEach(e => e.remove()); this.xhrProvidenceGeolocationAutocomplete(event.target.value) .then((results) => { /* only add the results if they match the current typeahead */ if(document.getElementById('geoLocatorInput').value === results[0].replace('location-value ','')) { /* Remove first validation value */ results.shift(); results.forEach((loc) => { let btn = document.createElement('button'); let split = loc.split(' '); /* Remove lat / lng from text value */ split.pop(); split.pop(); btn.value = loc; btn.textContent = split.join(' '); btn.type = "submit"; btn.style.border = "none"; btn.style.textAlign = "left"; btn.classList.add("geo-btn"); typeaheadList.append(btn); }); } }); } /** **/ init = async () => { this.searchParams = prepURLSearchParams(); /** **/ function getCookie(name) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); if (match) { return match[2]; } } /* Null breaks the parse JSON so we set to blank to fix that */ function setNulltoBlank(value) { if(value === null || value === 'null'){ return ""; }else{ return value; } } try{ /*Add geo locator to utility nav if cookie is set*/ let userDirectedCookie = setNulltoBlank(getCookie("UserDirectedLocationGeoCoordinates-v3")); let userGeoCookie = setNulltoBlank(getCookie("UserEnteredLocationGeoCoordinates-v3")); let directedGeoCookie = setNulltoBlank(getCookie("DetectedGeoLocationGeoCoordinates-v3")); let geoCookie = setNulltoBlank(getCookie("GeoIpLocationGeoCoordinates-v3")); let secGeoCookie = setNulltoBlank(getCookie("SecondaryGeoIpLocationGeoCoordinates-v3")); let geoStorage = localStorage.getItem('UserEnteredLocationGeoCoordinates-v3'); if(userDirectedCookie || userGeoCookie || directedGeoCookie || geoCookie || secGeoCookie || geoStorage){ const userGeoObj = JSON.parse(userDirectedCookie || userGeoCookie || directedGeoCookie || geoCookie || secGeoCookie || geoStorage); if(document.querySelector('#topheader nav')){ let location = userGeoObj.City + ", " + userGeoObj.StateCode + " " + userGeoObj.PostalCode; if(userGeoObj.StateCode === 'TX' || userGeoObj.StateCode === 'NM') { document.getElementById("urgentCareLink").href = "https://www.providence.org/services/urgent-care"; }else { document.getElementById("urgentCareLink").href = "https://www.providence.org/our-services/urgent-care"; } let myChart = document.getElementsByClassName("my-chart-link"); if(userGeoObj.StateCode === 'AK'){ for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://mychartak.providence.org/mychart/Authentication/Login"; } }else { for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://www.providence.org/mychart"; } } let span = document.querySelector('#geoLocatorLabel span'); if(span){ span.textContent = location; document.querySelector('#geoLocatorInput').value = location; } const customStyles = document.querySelector('style'); let css = ` #topheader label#geoLocatorLabel { color: inherit; pointer-events: all; } label#geoLocatorLabel { display: block; } `; if (customStyles) { customStyles.innerHTML += css; } } }else{ this.guessUserLocation() .then((userLocation) => { if (userLocation.city && userLocation.zip && userLocation.state) { let location = userLocation.city + ", " + userLocation.state + " " + userLocation.zip; if(userLocation.state === 'TX' || userLocation.state === 'NM') { document.getElementById("urgentCareLink").href = "https://www.providence.org/services/urgent-care"; }else { document.getElementById("urgentCareLink").href = "https://www.providence.org/our-services/urgent-care"; } let myChart = document.getElementsByClassName("my-chart-link"); if(userLocation.state === 'AK'){ for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://mychartak.providence.org/mychart/Authentication/Login"; } } else { for (let i = 0, le = myChart.length; i < le; i++) { myChart[i].href = "https://www.providence.org/mychart"; } } let label = document.querySelector('#geoLocatorLabel'); let span = document.querySelector('#geoLocatorLabel span'); span.textContent = location; document.querySelector('#geoLocatorInput').value = location; const customStyles = document.querySelector('style'); let css = ` #topheader label#geoLocatorLabel { color: inherit; pointer-events: all; } label#geoLocatorLabel { display: block; } `; if (customStyles) { customStyles.innerHTML += css; } } }) .catch(error => console.log(error)); } } catch (error) { console.error('Add Geo Locator failed\n', error); } /* Geolocator autocomplete */ const geolocator = document.querySelector('#geoLocatorInput'); if (geolocator) { const typeaheadList = document.querySelector('#geoLocator-form .suggestion'); /* Workaround for event.currentTarget === null when debounced is used See: https://github.com/jashkenas/underscore/issues/1905 */ let debouncedUpdateGeoTypeahead = this.helpers.debounce(this.updateGeoTypeahead, 250).bind(this); geolocator.addEventListener('input', function (event) { debouncedUpdateGeoTypeahead(event, event.currentTarget); document.querySelector('#geoLocator-form .suggestion').style.setProperty('display', 'block'); }); geolocator.addEventListener('click', function (event) { if(document.querySelector('#geoLocator-form .suggestion').style.display !== "block"){ /*Select input on click so that the user can overwrite saved location*/ this.setSelectionRange(0, this.value.length); document.querySelector('#geoLocator-form .suggestion').style.setProperty('display', 'block'); } }); let geoLocatorLabel = document.querySelector('#geoLocatorLabel'); geoLocatorLabel.addEventListener('click', function (event) { document.querySelector('#geoLocator-form .suggestion').style.setProperty('display', 'none'); if(window.innerWidth < 1000) { document.querySelector('main').style.setProperty('z-index','0'); } }); } /* Catch geolocator form submission */ let geoForm = document.querySelector('#geoLocator-form'); if(geoForm){ geoForm.addEventListener('submit', this.handleGeoUpdate.bind(this)); } let geoUseCurrentLocation = document.querySelector('#geoUseCurrentLocation'); if(geoUseCurrentLocation) { geoUseCurrentLocation.addEventListener('click', () => { document.querySelector('#geo-locator-ck').checked = false; }); } /** **/ /* Create cookies for linker params, if they don't already exist */ if (typeof cookieLinkerParams != "undefined") cookieLinkerParams(); this.initState = { pageInit: true, pageType: 'Provider Directory', page_subtype: 'Provider Search Results', query: '', /** **/ color: "#00338E", /** **/ disclaimerSearch: false, disclaimerSearchKeywords: [ 'transgender surgery', 'IVF', 'vasectomy', ], excludeSearch: false, excludeSearchKeywords: [ 'abortion', 'physician assisted suicide', 'physician-assisted suicide', 'euthanasia', 'euthenasia' ], pushHistory: false, specificDays: false, filters: { previousPostalCode: false, distance: sanitizeInput(this.searchParams.get('distance')), page: Number(sanitizeInput(this.searchParams.get('page'))) || 1, coordinates: false, Gender: {}, InsuranceAccepted: {}, PrimarySpecialties: {}, /** **/ ProviderOrganization: {}, /** **/ Languages: {}, availability: 'all', visitType: false, brand: sanitizeInput(this.searchParams.get('brand')) || this.brand, sortby: false, init: true, locationType: false, location: false, LocationsOnly: ((this.searchParams.get('LocationsOnly') ?? 'false') === 'true'), AvailableDays: { Sunday: { active: false }, Monday: { active: false }, Tuesday: { active: false }, Wednesday: { active: false }, Thursday: { active: false }, Friday: { active: false }, Saturday: { active: false }, }, AvailableTime: 'any', tier: sanitizeInput(this.searchParams.get('Tier')) || sanitizeInput(this.searchParams.get('tier')) }, filtersUpdated: false, results: false, slug: { acceptingNewPatients: false, availabilityDays: [], availabilityTimeOfDay: '', distance: '', gender: '', insurance: '', language: [], location: '', locationId: '', medicalGroup: '', name: '', offersVideoVisit: false, onlineScheduling: false, searchTerms: '', specialty: '', text: '', title: '', } }; /* Add facets to initState so we don’t lose them on reset */ await fetch(`https://${this.apiSub}.azurewebsites.net/api/OmniSearchFacets?brand=` + this.initState.filters.brand) .then(res => res.json()) .then(res => { this.initState.facets = res['facets']; }); this.state = this.helpers.deepCopy(this.initState); /***** begin create day-time selector *****/ /* populate day-time selector state from URL if it exists */ if(this.searchParams.get('time')) { const time = sanitizeInput(this.searchParams.get('time')).trim(); switch(time) { case 'any': case 'morning': case 'afternoon': this.state.filters.AvailableTime = time; default: break; } } if(this.searchParams.get('days')) { const days = sanitizeInput(this.searchParams.get('days')).split(',').map(x => +x); this.state.specificDays = true; days.forEach(n => { const day = DAYS[n]; if(day && this.state.filters.AvailableDays[day]) { this.state.filters.AvailableDays[day].active = true; } }) } document.querySelectorAll('#timesList a').forEach(time => { time.addEventListener('click', () => this.updateTimeSelectorState(time)); }); document.querySelectorAll('#daysList a').forEach(day => { day.addEventListener('click', () => this.updateDaySelectorState(day)); }); document.querySelector('#time-selector').addEventListener('change', () => { this.updateDayTimeSelectorState(); }); document.querySelector('#day-selector').addEventListener('change', () => { this.updateDayTimeSelectorState(); }); let dayTimeHTML = ''; DAYS.forEach((day, i) => { if (i === 0 || i === 6) return; const isActive = this.state.filters.AvailableDays[day].active; dayTimeHTML += /*html*/ `${day}`; }); document.querySelector('#availability-days-checkboxes') ? document.querySelector('#availability-days-checkboxes').innerHTML = dayTimeHTML : ''; document.querySelectorAll('.day-checkbox').forEach(checkbox => { checkbox.addEventListener('click', (event) => { const state = this.state.filters.AvailableDays[checkbox.innerHTML].active; this.state.filters.AvailableDays[checkbox.innerHTML].active = !state; this.updateDayTimeSelectorState(); }) }); document.querySelector('#day-selector').value = 'any-day'; this.populateDaytimeSelectorFromState(); /***** end create day-time selector *****/ const { searchParams } = this; const lsInsurance = localStorage.getItem('omniSearchInsurance') || false; const lsLocation = localStorage.getItem('omniSearchLocation') || false; const loopable = ['gender', 'languages', 'insuranceaccepted', 'primaryspecialties'/** **/ , "ProviderOrganization" /** **/]; /* const isMobile = this.helpers.viewportLessThan(); */ let tmp; document.addEventListener('timeslot-clicked', event => this.helpers.trackFromEmitted('timeslot', event)); document.addEventListener('phone-clicked', event => this.helpers.trackFromEmitted('phone', event)); /* if (isMobile) { this.logo.style.width = '85px'; this.logo.style.height = '50px'; } */ let hasInsuranceParam = false; const isTest = sanitizeInput(searchParams.get('test')); if (isTest && isTest == "true") { this.state.isTest = true; } /* Loop through params that can have more than one value */ for (let param of loopable) { if (searchParams.get(param)) { if (param === 'insuranceaccepted') { hasInsuranceParam = true; } tmp = sanitizeInput(searchParams.get(param)).split(','); tmp.forEach((val) => { switch(param) { case 'gender': param = 'Gender'; break; case 'languages': param = 'Languages'; break; case 'primaryspecialties': param = 'PrimarySpecialties'; break; case 'insuranceaccepted': param = 'InsuranceAccepted'; break; default: } this.state.filters[param][val] = { active: true }; }); } } if (lsInsurance && !hasInsuranceParam) { const split = lsInsurance.split(','); for (let ins of split) { if (!this.state.filters.InsuranceAccepted) { this.state.filters.InsuranceAccepted = {}; } this.state.filters.InsuranceAccepted[ins] = { active: true } } } /* Select specialty if slugs specialty exists */ this.state.slug.specialty = this.slugSpecialtyDiv?.innerText; if (this.state.slug.specialty) { const lcSpec = this.state.slug.specialty.toLowerCase(); const specKeysArr = this.state.facets.PrimarySpecialties; const specMatch = specKeysArr.find((sp) => sp.toLowerCase() === lcSpec); if (specMatch) { this.state.filters.PrimarySpecialties[specMatch] = { active: true }; } } /* Set slugs title as an h1 header, if it exists */ this.state.slug.title = this.slugTitleDiv?.innerText; if (this.state.slug.title && !document.querySelector('#slugTitleHdr')) { let headerTitle = document.createElement('h1'); headerTitle.innerHTML = this.state.slug.title; headerTitle.id = 'slugTitleHdr'; siteSearch.searchFiltersEl.prepend(headerTitle); let headerTitleMobile = document.createElement('h1'); headerTitleMobile.innerHTML = this.state.slug.title; headerTitleMobile.id = 'slugTitleHdrMobile'; if (this.slugTextDiv) { this.slugTextDiv.prepend(headerTitleMobile); } } /* Set query if not empty */ if (searchParams.get('query')) { window.sessionStorage.setItem('previousQuery', this.state.query); this.state.query = decodeURIComponent(sanitizeInput(searchParams.get('query'))); } else { window.sessionStorage.setItem('previousQuery', ''); } /* If a PracticeGroup param is present, then set state */ if (searchParams.get('practicegroup')) { this.state.filters.PracticeGroup = sanitizeInput(searchParams.get('practicegroup')); } /* If a userlocation param is present, then set state */ if (searchParams.get('userlocation')) { this.state.filters.location = sanitizeInput(searchParams.get('userlocation')); this.state.currentLoc = sanitizeInput(searchParams.get('userlocation')); } else if (lsLocation) { this.state.filters.location = lsLocation } /* If there is a search param of LocationName and it has a length, set that to state */ if (searchParams.get('locationname')) { this.state.filters.LocationName = sanitizeInput(searchParams.get('locationname')); } /* If there is a search param of distance and it has a length, set that to state */ if (searchParams.get('distance')) { this.state.filters.distance = sanitizeInput(searchParams.get('distance')); } /* Check search params for LocationId and PracticeGroupId, and set state accordingly */ if (searchParams.get('locationid')) { this.state.filters.LocationId = sanitizeInput(searchParams.get('locationid')); } if (searchParams.get('practicegroupid')) { this.state.filters.PracticeGroupId = sanitizeInput(searchParams.get('practicegroupid')); } /* Check for / apply visit type filters */ let visitTypes = sanitizeInput(searchParams.get('visittypes')); if (visitTypes) { visitTypes = visitTypes.split(','); document.querySelector('button[data-type="all"]').classList.remove('active'); this.state.filters.visitType = {}; for (let type of visitTypes) { document.querySelector(`button[data-type="${type}"]`).classList.add('active'); this.state.filters.visitType[type] = { active: true }; } } const userState = await fetch('https://wompservices.wompmobile.com/geoip/') .then(res => res.json()) .then(res => res.state ? res.state : 'Undefined') .catch(() => { return 'Undefined'; }); this.state.filters.userState = userState; let brands = { pacmed: 'Pacific Medical', providence: 'Providence', swedish: 'Swedish' }; document.getElementById('OrganizationLabel') ? document.getElementById('OrganizationLabel').innerHTML = /*html*/ `Organization: ${brands[this.state.filters.brand]} ` : ''; /* Handle sort order */ const sortby = sanitizeInput(searchParams.get('sortby')); if (sortby) { switch (sortby) { case 'GeocodedCoordinate': if (searchParams.get('userlocation')) { this.state.filters.sortby = 'GeocodedCoordinate'; const sortByLabel = document.querySelector('#SortByLabel'); sortByLabel ? sortByLabel.innerHTML = /*html*/ `Sort by: Distance ` : ''; } else { this.state.filters.sortby = false; } break; default: this.state.filters.sortby = sortby; break; } } await this.helpers.waitForProp('mapLoaded', window, 10000); console.debug('map loaded'); this.map.build(); this.handleSubmit(); }; }; const dedupeParamValues = (param, str) => { const strArr = str.split(','); const uniqueArray = [...new Set(strArr)]; const hasDups = strArr.length > uniqueArray.length; const newStr = uniqueArray.join(); if (hasDups) { return newStr; } else { return str; } }; const lowercaseCertainParams = (param) => { const qStrings = [ 'gender', 'hos', 'insuranceaccepted', 'languages', 'locationid', 'locationname', 'practicegroup', 'practicegroupid', 'primaryspecialties', 'providerorganization', 'userlocation', 'visittypes' ]; if (param) { const match = qStrings.find((str) => { return str === param.toLowerCase(); }); if (match) { return match; } else { return undefined; } } }; const prepURLSearchParams = () => { const url = new URL(window.location); const searchParams = new URLSearchParams(url.search); /* convert certain param labels to lowercase */ searchParams.forEach((value, key) => { const param = lowercaseCertainParams(key); if (param) { searchParams.delete(key); let newValue = value; if (param === 'locationid') { newValue = dedupeParamValues(key, value); } /* removes hos, which is no longer utilized */ if (param !== 'hos') { searchParams.set(param, newValue); } } }); url.search = searchParams.toString(); window.history.replaceState({}, 'Find a Doctor', url); return searchParams; }; /** * Create environment-dependent config values * * @returns void */ function setEnvironmentData() { const wompHealthApiEnvs = [ { /* Prod api */ subdomain: 'womphealthapi', condition: !/preview|staging|uat\./i.test(window.location.hostname), code: 'prod', }, { /* Special 2nd instance Prod api for testing */ subdomain: 'womphealthapi-swap', condition: false, code: 'pswap', }, { /* UAT (test) api */ subdomain: 'womphealthtestapi', condition: /uat\./i.test(window.location.hostname), code: 'uat', }, { /* Staging (dev) api */ subdomain: 'womphealthdevapi', condition: /preview|staging/i.test(window.location.hostname), code: 'stg', }, ]; /* Which environment are we in? */ const apiEnvObj = wompHealthApiEnvs.find((x) => x.condition); /* URL param will override environment selection. */ const apiCode = window.sessionStorage.getItem('OmniAPI'); const environmentCode = apiCode ? apiCode : apiEnvObj.code; /* Selected enviromnent and scheduling api subdomain */ let apiSubEnvObj = wompHealthApiEnvs.find((x) => x.code === environmentCode); const config = {}; /* Overwrite environment-dependent config values */ config.apiSub = apiSubEnvObj.subdomain; config.isStaging = environmentCode === 'stg'; /* only stg */ config.fetchOmniSearchURL = `https://${config.apiSub}.azurewebsites.net/api/OmniSearch`; config.fetchOmniDataURL = `https://${config.apiSub}.azurewebsites.net/api/OmniData`; return config; } window.config = setEnvironmentData(); Object.assign(window.config, { /* We can add any wData value here using ~~ */ apiOmniAnalytics: 'https://womphealthapi.azurewebsites.net/api/OmniAnalytics', brand: 'providence', }); const siteSearch = new Search(window.config); siteSearch.textInputs.forEach((el) => { if (el.id === 'searchRadius') { el.addEventListener('input', function (event) { const searchRadiusVal = document.querySelector('#searchRadius').value; const newDistance = searchRadiusVal?.length ? searchRadiusVal : ''; siteSearch.state.filters.distance = newDistance; }); el.addEventListener('keydown', function (event) { if (event.keyCode === 13) { siteSearch.handleSubmit(); } }); } if (el.id === 'inputSrch') { el.addEventListener('keydown', function (event) { if (event.keyCode === 13) siteSearch.handleSubmit(); }); } if (el.id === 'custom-location') { el.addEventListener('keydown', function(event) { if (event.keyCode === 13) { siteSearch.helpers.getGoogleLocation(); } }); } el.addEventListener('change', function (event) { const { id, value } = event.target; if (id === 'inputSrch') { return; } else { let val = value?.length ? value : false; siteSearch.state.filters[id] = val; } }); }); siteSearch.form.addEventListener('click', siteSearch.handleSubmit); siteSearch.form2.addEventListener('click', siteSearch.handleSubmit); siteSearch.createClickListener(); siteSearch.createChangeListeners(); siteSearch.createMouseEventListeners(); siteSearch.addWindowListeners(); const script = document.createElement('script'); script.src = `https://maps.googleapis.com/maps/api/js?key=AIzaSyBFhUTBdGySjK63djHCqCZWGK67H5SaPgI&loading=async&libraries=core,maps,marker,places,geometry&callback=mapRequirement${/preview|staging|dev/i.test(location.origin) ? '' : '&v=quarterly'}`; document.body.appendChild(script); siteSearch.init(); try { let mtoggle = localStorage.getItem('maptoggle'); if(mtoggle !== null) { document.getElementById('showmap').checked = mtoggle; if (mtoggle) { setTimeout(() => { setMapOffset(); if (siteSearch.map.bounds) { siteSearch.map.el.fitBounds(siteSearch.map.bounds); siteSearch.map.setMapOnAll(siteSearch.map.el); } }, 100); } } document.getElementById('maptoggle').addEventListener('click', function(event){ localStorage.setItem('maptoggle', !document.getElementById('showmap').checked); if (!document.getElementById('showmap').checked) { setTimeout(() => { setMapOffset(); if (siteSearch.map.bounds) { siteSearch.map.el.fitBounds(siteSearch.map.bounds); siteSearch.map.setMapOnAll(siteSearch.map.el); } }, 100); } }); } catch (err) { console.error('maptoggle:\n', err); } class Autocomplete { constructor(args) { this.useHistory = args.useHistory || false; this.param = args.param; this.src = args.src; this.id = args.id; this.localStorageKey = args.localStorageKey; this.shouldSubmit = args.shouldSubmit || true; this.minChars = args.minChars || 3; this.brand = window.config.brand; this.state = { initValue: '' }; /* Automatically call the init script for the autocomplete */ this.init(); this.helpers.trackAutocomplete = this.helpers.trackAutocomplete.bind(this); } helpers = { debounce(func, delay = 200) { let timeout = null; return function(){ let context = this; let args = arguments; if(document.getElementById('searchBox').classList.length === 0) { document.getElementById('searchBox').classList.add('is-populated'); if(document.getElementById('mobileSearchWrapper').classList.length === 1) { document.getElementById('mobileSearchWrapper').classList.add('is-populated'); } } clearTimeout(timeout); timeout = setTimeout(function(){ func.apply(context, args) }, delay); }; }, slugify(str) { return str .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); }, removeSelected() { document.querySelectorAll('div[data-autocomplete].selected').forEach(div => div.classList.remove('selected')); }, fetchSuggestions: async(val) => { const { src, param, helpers, autocompleteEl } = this; if (!autocompleteEl) console.error('autocompleteEl is ', autocompleteEl); const { slugify } = helpers; const results = await fetch(`${src}&${param}=${val}`).then(res => res.json()) .then(res => { /* If the length is 0, there are no results to the search */ if (res.value.length == 0) { autocompleteEl ? autocompleteEl.innerHTML = 'No suggestions' : ''; return 'success'; } /* Map out the returned results */ const tmp = res.value.reduce((html, r) => r?.text ? html + `
${r.text}
` : html, ''); if (autocompleteEl) autocompleteEl.innerHTML = tmp; return 'success'; }); return results; }, getCID: () => { let cid = localStorage.getItem('cid'); /* If we DON'T have a CID saved to localStorage... */ if (!cid || !cid.length) { /* If crypto's randomUUID func is available, create a CID with it */ if (typeof crypto?.randomUUID == 'function') { cid = crypto.randomUUID(); } /* Else, go old school... */ else { cid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } /* Save the new cid to localStorage */ localStorage.setItem('cid', cid); } /* Send CID to the requestor */ return cid; }, trackAutocomplete: async (str) => { const data = JSON.stringify({ text: this.state.initValue, autocomplete: str }); const { pathname, search } = window.location; await fetch(`${window.config.apiOmniAnalytics}?brand=${this.brand}&t=event&ec=search&ea=autocomplete&el=${data}&cid=${this.helpers.getCID()}&dp=${encodeURIComponent(pathname + search)}`) .then(res => res.json()) .then(res => console.debug('apiOA res:', res)); } }; addListener = { input: () => { const { src, param, helpers, autocompleteEl, getPreviousSearches, minChars, state, inputEl } = this; const { debounce, slugify, fetchSuggestions } = helpers; inputEl.addEventListener('input', debounce(async function(event) { let val = inputEl.value; val = sanitizeInput(val); inputEl.value = val; /* This is the text that the user input */ state.initValue = val; /* If more than minChars characters are input, start querying the API */ if (val.length >= minChars) { await fetchSuggestions(val); } /* Otherwise, show previous and suggested searches */ else { getPreviousSearches(); } })); }, key: () => { const { debounce, removeSelected, trackAutocomplete } = this.helpers; const { inputEl, autocompleteEl, state } = this; inputEl.addEventListener('keydown', debounce(function(event) { const selected = document.querySelector('div[data-autocomplete].selected'); let nextSibling = false; let prevSibling = false; if (selected) { nextSibling = selected.nextElementSibling ? selected.nextElementSibling : false; prevSibling = selected.previousElementSibling ? selected.previousElementSibling : false; if (prevSibling && !prevSibling.hasAttribute('data-autocomplete')) { prevSibling = false; } } const { key } = event; switch(key) { case 'ArrowDown': if (nextSibling) { removeSelected(); nextSibling.classList.add('selected'); inputEl.value = nextSibling.innerText; } else if (!selected) { const first = document.querySelector('div[data-autocomplete]'); inputEl.value = first.innerText; first.classList.add('selected'); } break; case 'ArrowUp': if (prevSibling) { removeSelected(); prevSibling.classList.add('selected'); inputEl.value = prevSibling.innerText; } else { if (selected) { removeSelected(); inputEl.value = state.initValue; } } break; case 'Enter': if (selected) { /* trackAutocomplete(selected.innerText); */ } autocompleteEl.style.display = 'none'; inputEl.blur(); window.wmFromAutoComplete = true; break; default: break; } }, 100)); }, clicks: () => { const { autocompleteEl, inputEl, shouldSubmit, id, getPreviousSearches, helpers } = this; const { removeSelected, fetchSuggestions } = helpers; document.body.addEventListener('click', (event) => { const { target } = event; /* If it has data-autocomplete, it's an autocomplete option. Update the autocomplete */ if (target.hasAttribute('data-autocomplete')) { removeSelected(); /* helpers.trackAutocomplete(target.innerText); */ target.classList.add('selected'); inputEl.value = event.target.innerText; autocompleteEl.style.display = 'none'; if (shouldSubmit) { inputEl.dispatchEvent(new KeyboardEvent('keydown',{'key':'Enter'})); } } /* Else, if the id matches the current id, then its the auto complete element. Run default logic. */ else if (target.id == id) { if (target.value.length > 0) { fetchSuggestions(target.value); } else { getPreviousSearches(); } } /* Otherwise, it's a click outside autocomplete. Hide the autocomplete options. */ else { autocompleteEl.style.display = 'none'; } }); /* If the input is focused on, show the autocomplete box */ inputEl.addEventListener('focus', (event) => { autocompleteEl.style.display = 'block'; }); } }; checkRequiredArgs = () => { const requiredArgs = ["id", "src", "param"]; let missing = false; const missingRequiredArgs = requiredArgs.some(arg => { const tmp = !this[arg]; missing = arg; return tmp; }); if (missingRequiredArgs) { console.error(`Please add key "${missing}" to autocomplete options object`); } else if (this.id == "autocomplete") { console.error('Input id cannot be "autocomplete". Please choose another id.'); return false; } return !missingRequiredArgs; }; getPreviousSearches = async () => { const { src, param, helpers, autocompleteEl, useHistory, localStorageKey } = this; const { slugify } = helpers; let theHtml = await fetch(`${src}&${param}=`).then(res => res.json()) .then(res => { if (!res.value?.length) { return ''; } const tmp = res.value.reduce((html, r) => r?.text ? html + `
${r.text}
` : html, ''); return `
Suggested Searches
${tmp}`; }); /* If we are using histor AND a localStorageKey is set */ if (useHistory && localStorageKey) { this.previousSearches = JSON.parse(localStorage.getItem(localStorageKey)); const { previousSearches } = this; theHtml += '
Previous Searches
'; /* Make sure the data we have in an array before populated the autocomplete */ if (Array.isArray(previousSearches) && previousSearches.length) { theHtml += previousSearches.reduce((html, search, i) => { const id = this.helpers.slugify(search) + 1; const el = `