`, '') : ``; /* 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(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 (ex) { console.log(ex); 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.log(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.log('invalid zip'); 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); }; }, 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(); }, showError: (error) => { console.log("Error when trying to get users location:"); console.log(error.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 = 969) => { 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: async (queryParams = '') => { const { filters, query, pageType, page_subtype, defaultLoc, currentLoc, pushHistory } = this.state; const { createPixel, trackIt, reportToWheelhouse } = this.helpers; const trackingPixels = []; /* Set all the search event params for Tealium */ const searchTrackingParams = { search_type: this.state.pageInit ? 'Passive' : 'Active', search_term: query && query.length ? 'Set' : 'Not Set', 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', 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' }; /* Create a new URLSearchParams object from the provided query string, as well as a URL object from the current page location */ const apiUrl = new URLSearchParams(queryParams); const url = new URL(window.location); /* 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); /* 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" ]; for (let param of paramsToAdd) { const { pageType, page_subtype, filters, query, currentLoc } = this.state; const { visitType, InsuranceAccepted, PrimarySpecialties, LocationName, PracticeGroup, LocationsOnly, tier } = 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, ''); /* Set value for clinic visits in pixel */ if (visitType['clinicVisits']?.active === true || (visitType['clinicVisits']?.active === false && ![null, undefined].includes(visitType.clinicVisits))) { searchTrackingParams['new_patient_availability_filter'] = 'Set'; } else { searchTrackingParams['new_patient_availability_filter'] = 'Default'; }; /* Set value for virtual visits in pixel */ if (visitType['videoVisits']?.active === true || (visitType['videoVisits']?.active === false && ![null, undefined].includes(visitType.videoVisits))) { searchTrackingParams['telehealth_availability_filter'] = 'Set'; } else { searchTrackingParams['telehealth_availability_filter'] = 'Default'; }; } else { /* Remove the visitTypes param from the query string */ url.searchParams.delete(param); /* Set the values for virtual and clinic visits on the tracking pixel */ searchTrackingParams['new_patient_availability_filter'] = 'Default'; searchTrackingParams['telehealth_availability_filter'] = 'Default'; /* 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 "userLocation": val = currentLoc; break; case "Languages": case "Gender": case "InsuranceAccepted": let lookup = { Languages: 'provider_language_filter', Gender: 'provider_gender_filter', InsuranceAccepted: 'insurance_filter' }; const tracker = lookup[param]; const filter = this.state.filters[param]; /* 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, ''); searchTrackingParams[tracker] = "Set"; } else { searchTrackingParams[tracker] = "Not Set"; 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; } 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 { url.searchParams.delete(param); continue; } break; default: break; }; url.searchParams.set(param, val); }; /* Push the current url to the window history, if it's not the initial load */ if (pushHistory) { window.history.pushState({}, "Womp Health Demo", url); } else { window.history.replaceState({}, "Womp Health Demo", url); this.state.pushHistory = true; } /* Create pagination links based on url state */ this.paginate(); /* Create our tracking pixel for the search event and add it to the pixels array */ 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); /* Set this to false. Mainly being used for handling the search box update in state. */ this.state.pageInit = false; /* 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 <= 30 && (NewClinicVisit == 1 || NewVideoVisit == 1)) { hasTimeslots = true; } return hasTimeslots; }, createProviderLink: (omni) => { let providerLink = `/doctors/profile/${omni.ProfileUrl}`; if (/swedish/gi.test(location.origin) && omni.ProviderUniqueUrlSwedish) { providerLink = omni.ProviderUniqueUrlSwedish.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (/pacmed|pacificmedicalcenters/gi.test(location.origin) && omni.ProviderUniqueUrlPacmed) { providerLink = omni.ProviderUniqueUrlPacmed.replace(/https:\/\/[a-z\.]*\.org/gi, ''); } if (/providence/gi.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 < 969) { 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 (error) { console.log("Error: Could not get geoIP coordinates"); } const { locations, el, 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(',')); /* ids?.length && uses short-circuit evalutaion to skip the fetch call if ids is null or not an array for any reason */ ids?.length && fetch(`https://${this.apiSub}.azurewebsites.net/api/WompHealthData?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 { pageType, page_subtype } = this.state; const { GeocodedCoordinate, City, Region, PostalCode, Address, Name } = loc; const uuid = loc.AddressId; lookup[loc.id] = uuid; if (!byUUID[uuid]) { byUUID[uuid] = loc; } const coordinates = { lat: loc.GeocodedCoordinate.coordinates[1], lng: loc.GeocodedCoordinate.coordinates[0] }; const coords = new google.maps.LatLng(coordinates.lat, coordinates.lng); const marker = new google.maps.Marker({ position: coords, map: el, icon: this.map.icons.default }); marker.addListener('click', () => { try { const isMobile = this.helpers.viewportLessThan(); this.map.resetMarkerIcon(); if (this.map.infoWindow) this.map.infoWindow.close(); this.map.selectedMarker = marker; this.map.selectedLocation = uuid; marker.setIcon(this.map.icons.selected); 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', { City: City, Region: Region, PostalCode: PostalCode, Address: Address }); 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 */ const infoWindow = new google.maps.InfoWindow({ content: contentString }); this.map.infoWindow = infoWindow; this.map.infoWindow.open(el, marker); } else { this.map.el.setCenterWithOffset(marker.getPosition(), 0, 150); }; } catch (err) { console.log(err); throw err; } }); this.map.markers[uuid] = marker; /* Keep the map within the first 172.5 miles from the center */ if(i == 0 || ((this.map.bounds.Oa.hi - 2.5 < coordinates.lat && this.map.bounds.Oa.hi + 2.5 > coordinates.lat) && (this.map.bounds.mb.hi - 2.5 < coordinates.lng && this.map.bounds.mb.hi + 2.5 > coordinates.lng) )) { /* extend the bounds to include each marker's position */ this.map.bounds.extend(coords); const { bounds } = this.map; if (bounds.getNorthEast().equals(bounds.getSouthWest())) { var extendPoint1 = new google.maps.LatLng(bounds.getNorthEast().lat() + 0.01, bounds.getNorthEast().lng() + 0.01); var extendPoint2 = new google.maps.LatLng(bounds.getNorthEast().lat() - 0.01, bounds.getNorthEast().lng() - 0.01); this.map.bounds.extend(extendPoint1); this.map.bounds.extend(extendPoint2); }; } this.map.el.fitBounds(this.map.bounds); this.map.setMapOnAll(this.map.el); }); }) .catch(err => console.log(err)); }, deleteMarkers: () => { if (this.map.markers) { this.map.setMapOnAll(null); this.map.infoWindows = {}; this.map.markers = {}; }; }, resetMarkerIcon: () => { const { markers, icons, selectedMarker } = this.map; if (selectedMarker) { selectedMarker.setIcon(icons.default); }; }, setOffset: () => { const top = this.helpers.pxInViewport(this.headerEl); this.mapEl.style.top = `${top}px`; this.mapEl.style.height = `calc(100vh - ${top}px)`; }, setMapOnAll: map => { const { markers, bounds } = this.map; let vals = Object.values(markers); for (let val of vals) { val.setMap(map); } }, build: () => { this.helpers.waitForGlobal('google', () => { console.log('found google'); /* Add a prototype function to allow px offsets for map center */ google.maps.Map.prototype.setCenterWithOffset = function(latlng, offsetX = 0, offsetY = 0) { var map = this; var ov = new google.maps.OverlayView(); ov.onAdd = function() { var proj = this.getProjection(); var aPoint = proj.fromLatLngToContainerPixel(latlng); aPoint.x = aPoint.x+offsetX; aPoint.y = aPoint.y+offsetY; map.panTo(proj.fromContainerPixelToLatLng(aPoint)); }; ov.draw = function() {}; ov.setMap(this); }; /* Create the position object for centering the map and populate it with the coordinates we passed in. */ const position = { lat: 39.50, lng: -98.35 }; /* Set icons object */ this.map.icons = { default: { url: "https://www.pacificmedicalcenters.org/wp-content/plugins/wp-store-locator/img/markers/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://www.pacificmedicalcenters.org/wp-content/plugins/wp-store-locator/img/markers/dark-blue@2x.png", scaledSize: new google.maps.Size(24, 35), origin: new google.maps.Point(0,0), anchor: new google.maps.Point(12, 35) } }; /* Initialize the map on the page */ this.map.el = new google.maps.Map(document.getElementById("wm_map"), { mapTypeId: google.maps.MapTypeId.ROADMAP, gestureHandling: "greedy", disableDefaultUI: true, }); console.log('map initialized'); const {el: map} = this.map; /* POI marker visibility settings */ 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 */ map.setOptions({ styles: styles["hide"] }); /* Set map offset from top of screen */ const w = window.innerWidth; google.maps.event.addListener(map, '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('gender=')) { queryString = queryString.replace('gender=', 'Gender='); } 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 } }; handleSubmit = async (e, { ignoreElements = [] } = {}) => { this.MapOldQueryParams(); 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-dirty"); allDirtyE.forEach((element) => { element.classList.remove('is-dirty'); }); } catch (error) { console.log(""); } try { if(this.state.query !== '') { document.getElementById("searchBox").classList.add("is-dirty"); } } catch (error) { console.log(error); } document .querySelectorAll(".dropdown") .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.input.value; }; /* Destructing state */ const { query, geolocation, filters, isTest, autoApplied } = this.state; const { formatPhoneNumber, formatAddress, showError, showPosition, slugify, trackIt, createPixel } = 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 { previousPostalCode, distance, brand, Languages, Gender, InsuranceAccepted, PrimarySpecialties, visitType, coordinates, PracticeGroup, LocationId, PracticeGroupId, LocationsOnly, } = filters; let LocationName = filters.LocationName; /** **/ let ProviderOrganization = filters.ProviderOrganization; /** **/ /* If certain terms are in search, set clinicVisits to true */ if (/(family medicine|family doctor|internal medicine|family care)/gi.test(query)) { const shouldSkipAutoFilter = query != this.state.previousQuery ? false : /(family medicine|family doctor|internal medicine|family care)/gi.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)/gi.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/gi.test(query)) { this.state.nearme = true; navigator.geolocation.getCurrentPosition(showPosition, showError); 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}`; /* Check for a default search (provided in before-render when a slug is present in URL) */ const defaultSearch = document.querySelector('#defaultSearch'); const slugText = document.querySelector('#slugText'); if(this.state.filters.init && defaultSearch) { const slugSearchTerm = defaultSearch.querySelector('#slugSearchTerm'); const slugLocation = defaultSearch.querySelector('#slugLocation'); const searchString = slugSearchTerm.innerText; const locationString = slugLocation.innerText; this.state.filters.location = locationString; this.state.query = searchString; post.search = searchString; this.input.value = searchString; window.sessionStorage.setItem('previousQuery', searchString); } else if(!this.state.filters.init && slugText) { /* If additional searches are performed, remove the slug & slug description text */ slugText.remove(); } /* 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 (visitType.clinicVisits?.active == true) { queryParams += '&AcceptingNewPatients=1'; } if (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 */ const genders = Object.keys(Gender).filter(gender => Gender[gender]?.active === true); if (genders.length > 0) { queryParams += genders.reduce((str, gender, i) => i === 0 ? str + gender : `${str},${gender}`, "&Gender="); }; try { /* Check for preselected filters */ await this.helpers.fetchWithTimeout(`https://www.providence.org/integrationapi/getinsuranceselectionjson`, { timeout: 2000 }) .then(res => res.json()) .then(res => { console.log("getinsuranceselectionjson " + res.insurance); if(res.insurance !== null) { queryParams += `&InsuranceAccepted=${res.insurance}`; localStorage.setItem('omniSearchInsurance', res.insurance); } }); } catch (err) { console.error('Insurance selection API not responding:\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('Insurance selection API not responding.'); } }; /** **/ /* 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 LocationName */ if (LocationName && LocationName.length) { LocationName = encodeURIComponent(LocationName); queryParams += `&LocationName=${LocationName}`; } /* Set filter for PracticeGroup */ if (PracticeGroup && PracticeGroup.length) { queryParams += `&PracticeGroup=${PracticeGroup}`; } /* Set filter for LocationId */ if (LocationId && LocationId.length) { queryParams += `&LocationId=${this.state.filters.LocationId}`; } /* Set filter for PracticeGroupId */ if (PracticeGroupId && PracticeGroupId.length) { queryParams += `&PracticeGroupId=${this.state.filters.PracticeGroupId}`; } /* 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}`; } /* If sortby is not false, set param */ if (this.state.filters.sortby) { queryParams += `&sortby=${this.state.filters.sortby}`; }; /* Create the user id if not present */ if (!localStorage.getItem('cid')) { localStorage.setItem('cid', crypto.randomUUID()); }; queryParams += `&cid=${localStorage.getItem('cid')}`; /* 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 = this.searchParams.get("degree"); if(degree !== null) { queryParams += `&Degrees=${degree.toUpperCase()}`; } /* Add a \ (%5C) to allow for , in queryParams, example - ProviderOrganization */ queryParams = queryParams.replaceAll("%2C%20","%5C%2C%20"); /* Sending the actual request */ await fetch( `${this.fetchUrl}&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("filter-buttons"); 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")) { document.getElementById("filterWrapper").classList.remove("hasFilter"); } } catch (error) { console.log("Couldn't remove filter"); } try { for (let filter of siteSearch.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"); filterButton.innerHTML = `${filter.value} `; 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 (e) { throw(e); } } /* 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 }]; } } /* Set the data-facets attribute */ filterButton.setAttribute('data-facets', JSON.stringify(dataFacets)); /* Create an event listener to handle clicks, removing filters */ filterButton.addEventListener('click', e => { const { target } = e; 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(); document.getElementById("query").value = this.state.query; }; const facets = JSON.parse(dataset.facets); let clearedInsurance = false; let clearInsurancePromise; for (let facet of facets) { const { name, value } = facet; const state = this.state.filters[name]; if (state) { if (typeof state === 'string') { this.state.filters[name] = false; } else if (typeof state === 'object') { state[value].active = false; /* Catch weird values that have ',' in them */ try { let csv = value.split(","); for (let v of csv) { state[v].active = false; } } catch (error) { console.log("Error removing cvs filter."); } } } if (name == "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"); } } } if (clearedInsurance) { Promise.allSettled([clearInsurancePromise]).then(()=>this.handleSubmit()); } else { this.handleSubmit(); } if(document.querySelectorAll('.filter-button').length === 1) { document.getElementById("filterWrapper").classList.remove("hasFilter"); } }); /* Add the filter button to the DOM */ filtersElem.appendChild(filterButton); if(!filtersWrapper.classList.contains("hasFilter")){ filtersWrapper.classList.add("hasFilter"); } } /* Add badges for Virtual and New Patient visit type selections */ if (this.state.filters.visitType) { const { clinicVisits, videoVisits } = this.state.filters.visitType; const hasClinicVisits = clinicVisits && clinicVisits.active; const hasVideoVisits = videoVisits && videoVisits.active; const handleVisitFiltersClick = e => { const { value } = e.target.dataset; const { filters } = this.state; const state = filters.visitType; if (state) { state[value].active = false; } try { if(document.querySelectorAll('.filter-button').length === 1) { document.getElementById("filterWrapper").classList.remove("hasFilter"); } } catch (error) { console.log(error); } 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.appendChild(filterButton); if(!filtersWrapper.classList.contains("hasFilter")){ filtersWrapper.classList.add("hasFilter"); } } 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.appendChild(filterButton); if(!filtersWrapper.classList.contains("hasFilter")){ filtersWrapper.classList.add("hasFilter"); } } /* Create an event listener to handle clicks, removing filters */ document.querySelectorAll('.visit-filter').forEach(handleVisitFiltersClick); } } catch (e) { console.log(`Facets failed to load`); } 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 = ""; 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"; /* Set map to center on Bellevue */ this.map.el.setCenterWithOffset(new google.maps.LatLng(47.659159, -122.183832), 0, 0); 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}
Showing ${count.toLocaleString()} Results From ${results.geoip.state}
`; }else { resultsHtml += `
Showing ${count.toLocaleString()} Results
`; } } catch (error) { resultsHtml += `
Showing ${count.toLocaleString()} Results
`; console.log(error); } } /* We'll use this to detmine if we show the map or not */ let withLocations = 0; /* 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 { ImageUrl, ImageSourceUrl, ImageWidth, ImageHeight, Name, PrimarySpecialties, ProviderTitle, Addresses, PracticeGroup, LocationNames, LocationName, Rating, virtual, distance, distances, Npi, id, clinicVisits, NewClinicVisit, NewVideoVisit, GeocodedCoordinate, LocationId, LocationIds, RatingCount, ReviewCount, Degrees, Phones, ProfileUrl, AcceptingNewPatients, VirtualCare, SjhScheduleProviderNum, AllowsOpenSchedule, AllowsDirectSchedule, LocationsIsClinic, acceptingNewPatients, AppointmentRequestUrl, DaysUntilNextAppt, DaysUntilFollowUpAppt, Tier } = result; let ProviderOrganization = result.ProviderOrganization; const searchFeatures = result['@search.features']; const width = (150 / ImageHeight) * ImageWidth; const badges = result.badges ? Object.values(result.badges) : []; const supportsOdhp = (acceptingNewPatients == 1 && AllowsOpenSchedule == 1) || AllowsDirectSchedule == 1; const onlineBooking = supportsOdhp || SjhScheduleProviderNum ? true : false; let AcceptingClinicVisits = AcceptingNewPatients == 1 ? `
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(', '); } else if (ProviderTitle?.length) { descr = ProviderTitle; } else { descr = ""; }; this.map.deleteMarkers(); for (const location of LocationIds) { let isClinic; try { let iOfLoc = LocationIds.indexOf(location); isClinic = LocationsIsClinic[iOfLoc]; } catch (ex) { console.log(`Failed to determine if location is CLI for provider: Npi=${Npi}, LocationId=${location}; ` + ex); isClinic = true; } if (isClinic) { if (!this.map.locations[location]) { this.map.locations[location] = []; } this.map.locations[location].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}`}}); } /* If a location name is present in the results, choose the proper text and create element HTML for it */ let locationName = ""; if (LocationName) { locationName = /*html*/`
${LocationName}
`; } else if (LocationNames.length) { if (result["@search.highlights"] && result["@search.highlights"].LocationNames) { locationName = /*html*/`
${result["@search.highlights"].LocationNames[0]}
`; }; }; let locationNames = false; if (LocationNames.length > 1) { locationNames = LocationNames.filter(loc => loc !== locationName); }; /* 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 primaryLocation = 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]), phone: Phones[i] }); } } /* Code for identifying clinics vs hospitals */ for (let i = 0; i < locs.length; i++) { locs[i].isClinic = result.LocationsIsClinic[i]; } let hasClinic = false; let tempLocArr = []; if (locs.length > 1) { locs.forEach(item => { if (item.isClinic == true) { hasClinic = true; } }); if (hasClinic) { for (let i = 0; i < locs.length; i++) { if (locs[i].isClinic) { tempLocArr.push(locs[i]); } } locs = tempLocArr; } } locs = locs.sort((a) => a.name === LocationName ? -1 : 1); primaryLocation = locs[0]; secondaryLocations = locs.length > 1 ? locs.slice(1) : false; }; /* If a practice group is present in the result, choose the proper text and create element HTML for it */ let practiceGroup = ""; if (PracticeGroup.length) { if (result["@search.highlights"] && result["@search.highlights"].PracticeGroup) { practiceGroup = /*html*/`${result["@search.highlights"].PracticeGroup[0]}`; }; }; /* Create star rating for provider, if rating is present */ let rating = ""; 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 = `
Offers Video Visits
Accepting New Patients
Offers Video Visits
Accepting New Patients
`; } else if (VirtualCare) { offersVirtual = /*html*/ `
Offers Video Visits
`; virtualBadge = /*html*/`
` }; /* If a distance from ZIP exists, include that in the provider card. */ let distanceEl = ""; if (distance) { distanceEl = /*html*/`
${parseFloat(Number(distance).toFixed(1))} miles away
`; }; 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 (error) { console.log("Couldn't get wheelhouse data"); } } else{ wheelhouseData = JSON.parse(wheelhouseData); } const locName = primaryLocation?.name ? primaryLocation.name : LocationName ? LocationName : ''; const providerPhone = primaryLocation?.phone ?? Phones[0]; if (locName.indexOf("Swedish") > -1 || (/swedish/gi.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/gi.test(locName)) { withLocations += 1; }; /* Determine whether or not to display the 90 days fallback message when timeslots are not available */ const shouldShowAlert = () => { 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 distanceSec = ""; try { if (distances) { distanceSec = /*html*/`
${parseFloat(Number(distances[0]).toFixed(1))} miles away
`; }; } catch (error) { console.log("Couldn't get distances"); } 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; } resultsHtml += /*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 { console.log(`Search query "${query}" is not a valid URI.`); } if (decodedQ !== results.info.search) { resultsHtml = /*html*/ `
`; } /* Hide the list when no list items match input */ 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 { console.log(`Facets failed to load`); } for (const f of info.filters) { const { facets: fs } = f; if (f.text?.length && fs && fs.length) { console.log(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*/ `
` } /* 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; }; html += /*html*/ `
'; dropdown.lastElementChild.innerHTML = html; if (dropdown.id === "InsuranceAccepted") { createFilterInputListener(facet, keys, "insuranceInput", "insuranceList"); } else if(dropdown.id === "PrimarySpecialties") { createFilterInputListener(facet, keys, "specialtyInput", "specialtiesList"); } else if(dropdown.id === "ProviderOrganization") { createFilterInputListener(facet, keys, "organizationInput", "organizationList"); } else if(dropdown.id === "Languages") { createFilterInputListener(facet, keys, "languageInput", "languagesList"); } if (active > 0) { dropdown.querySelector("button").classList.add("filtered"); } else { dropdown.querySelector("button").classList.remove("filtered"); }; } }); /* Set query in search box */ if (this.state.query.length) { document.querySelector("#query").value = decodeURIComponent( this.state.query ); }; /* Set location information in location dropdown */ if (this.state.currentLoc?.length) { try { document.querySelector( "#LocationLabel" ).innerHTML = `Location: ${this.state.currentLoc} `; document.querySelector( 'input[name="location"]' ).value = this.state.currentLoc; /** **/ this.textInputs.forEach(input => { if (['custom-location'].includes(input.id)) { input.value = this.state.currentLoc; } if (['geoLocatorInput'].includes(input.id)) { checkLoginStatus(); } }); /** **/ } catch (err) { console.error(err); } } /* Set the visit type toggle */ document.querySelectorAll('#visitTypeFilters button').forEach(button => { const type = button.dataset.type; const { visitType } = this.state.filters; if (!visitType) { type === 'all' ? button.classList.add('active') : button.classList.remove('active'); } else { type === 'all' ? button.classList.remove('active') : visitType[type]?.active ? button.classList.add('active') : button.classList.remove('active'); } }); }; paginate = () => { const { filters } = this.state; const { pages, page } = filters; let start; let html = ``; if(pages === 1 && page === pages) { html = /*html*/`
`; } else { html = /*html*/`
`; } /* Let's make sure we display 5 results any time there are 5 or more 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); var nextPageUrl = new URL(document.location); var previousPageUrl = new URL(document.location); try { nextPageUrl.searchParams.set('page', filters.page + 1); previousPageUrl.searchParams.set('page', filters.page - 1); } catch (error) { console.log("Error setting pagination"); nextPageUrl.searchParams.set('page', 1); previousPageUrl.searchParams.set('page', 1); } if (start > 1) { html += start > 2 ? /*html*/ `1...` : ``; }; const ithPage = new URL(document.location); let mobileHide = ""; for (let i = start; i <= start + 4; i++) { if(pages > 4 && i > start + 1 && i < start + 4) { mobileHide = " mobile-hide" } else { mobileHide = ""; } if (i > pages) break; ithPage.searchParams.set('page', i); if(i === 1) { html += /*html*/ ``; } html += /*html*/ `${i}`; if (i == start + 4 && i < pages) { ithPage.searchParams.set('page', pages); html += /*html*/ `...`; if (i < pages + 1) { ithPage.searchParams.set('page', pages); html += /*html*/`${pages}`; }; }; }; html += `
`; this.paginationEl.innerHTML = html; this.paginationEl.style.display = "block"; try { this.paginationEl.classList.remove("first-page"); } catch (error) { } try { this.paginationEl.classList.remove("last-page"); } catch (error) { } if(filters.page === pages){ this.paginationEl.classList.add(`last-page`); } else if(filters.page === 1) { this.paginationEl.classList.add(`first-page`); } }; addWindowListeners = () => { window.addEventListener('resize', this.helpers.debounce(() => { return; const w = window.innerWidth; if (w < 969) { 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 = new URLSearchParams(document.location.search); this.init(); }; window.addEventListener('scroll', () => this.map.setOffset()); }; populateTimeFromState = () => { const activeDays = Object.entries(this.state.filters.AvailableDays).filter(x => this.state.specificDays && x[1].active).map(x => x[0]); let labelText = "Availability "; const label = document.querySelector("#AvailabilityLabel"); 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 += ``; 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"); } }); const activeDays = Object.entries(this.state.filters.AvailableDays).filter(x => this.state.specificDays && x[1].active).map(x => x[0]); let labelText = "Availability "; const label = document.querySelector("#AvailabilityLabel"); 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 += ``; label.innerHTML = labelText; }; updateTimeSelectorState = (time) => { document.querySelectorAll("#timesList .active").forEach(e => { e.classList.remove("active"); }); time.classList.add("active"); this.state.filters.AvailableTime = time.id; this.populateTimeFromState(); this.handleSubmit(undefined, { ignoreElements: ["Availability"]}); }; updateDaySelectorState = (day) => { 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(day.id !== "any-day") { this.state.specificDays = true; try { document.querySelector("#any-day").classList.remove("active"); } catch (error) { console.log(error); } } else { this.state.specificDays = false; document.querySelectorAll("#daysList .day.active").forEach(e => { e.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.populateTimeFromState(); this.handleSubmit(undefined, { ignoreElements: ["Availability"]}); }; 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 = []; data.results.forEach((loc) => { results.push(loc.city + ', ' + loc.state + ' ' + loc.zip); }); 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]; document.querySelector('#geoLocatorLabel span').textContent = location; 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 //this.setCookie('UserEnteredLocationGeoCoordinates-v3', `{"City":"${city}","Latitude":"undefined","Longitude":"undefined","PostalCode":"${zip}","StateCode":"${state}","Regions":[],"Version":1}`, 1800); // 30 min */ /* localstorage user entered zipcode is only used by us */ localStorage.setItem('UserEnteredLocationGeoCoordinates-v3', `{"City":"${city}","Latitude":"undefined","Longitude":"undefined","PostalCode":"${zip}","StateCode":"${state}","Regions":[],"Version":1}`); siteSearch.helpers.getGoogleLocation("geoLocatorSuggestions input:first-of-type"); } /*Set input value to selected value*/ document.querySelector('#geoLocatorInput').value = event.submitter.value; /*Remove options*/ let inputs = document.querySelectorAll('#geoLocatorSuggestions input'); 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('input').forEach(e => e.remove()); this.xhrProvidenceGeolocationAutocomplete(event.target.value) .then((results) => { results.forEach((loc) => { let btn = document.createElement('input'); btn.value = loc; btn.type = "submit"; btn.style.border = "none"; btn.style.textAlign = "left"; typeaheadList.append(btn); }); }); } /** **/ init = async () => { /** **/ function getCookie(name) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); if (match) { return match[2]; } } try{ /*Add geo locator to utility nav if cookie is set*/ let userGeoCookie = getCookie("UserEnteredLocationGeoCoordinates-v3"); let geoCookie = getCookie("GeoIpLocationGeoCoordinates-v3"); let geoStorage = localStorage.getItem('UserEnteredLocationGeoCoordinates-v3'); if(userGeoCookie || geoCookie || geoStorage){ const userGeoObj = JSON.parse(userGeoCookie || geoCookie || geoStorage); if(document.querySelector('#topheader nav')){ let location = userGeoObj.City + ", " + userGeoObj.StateCode + " " + userGeoObj.PostalCode; 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; 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'); }); } /* 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.state = { pageInit: true, pageType: 'Provider Directory', page_subtype: 'Provider Search Results', query: "", /** **/ color: "#00338e", /** **/ pushHistory: false, specificDays: false, filters: { previousPostalCode: false, distance: this.searchParams.get("distance") || false, page: Number(this.searchParams.get("page")) || 1, coordinates: false, Gender: {}, InsuranceAccepted: {}, PrimarySpecialties: {}, /** **/ ProviderOrganization: {}, /** **/ Languages: {}, availability: "all", visitType: false, /** **/ brand: this.searchParams.get("brand") || "providence", /** **/ 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: this.searchParams.get("Tier") || this.searchParams.get("tier") }, results: false, }; this.map.build(); this.map.setOffset(); /* Create day-time selector */ /* populate day-time selector state from URL if it exists */ if(this.searchParams.get("time")) { const time = decodeURIComponent(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 = decodeURIComponent(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 += ` ${day} `; }); document.querySelector("#availability-days-checkboxes").innerHTML = dayTimeHTML; document.querySelectorAll(".day-checkbox").forEach(checkbox => { checkbox.addEventListener("click", e => { 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 day-time selector */ const { searchParams, state, map, helpers } = this; const { createPixel, trackIt, trackFromEmitted } = helpers; const lsInsurance = localStorage.getItem('omniSearchInsurance') || false; const lsLocation = localStorage.getItem('omniSearchLocation') || false; const loopable = ["Gender", "Languages", "PrimarySpecialties"/** **/ , "ProviderOrganization" /** **/]; const isMobile = this.helpers.viewportLessThan(); let tmp; document.addEventListener('timeslot-clicked', event => trackFromEmitted('timeslot', event)); document.addEventListener('phone-clicked', event => trackFromEmitted('phone', event)); /* if (isMobile) { this.logo.style.width = '85px'; this.logo.style.height = '50px'; }; */ let hasInsuranceParam = false; const isTest = 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 = decodeURIComponent(searchParams.get(param)).split(","); tmp.forEach((val) => { 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 } } } /* Set query if not empty */ if (searchParams.get("query")) { this.state.query = decodeURIComponent(searchParams.get("query")); window.sessionStorage.setItem('previousQuery', this.state.query); } else { window.sessionStorage.setItem('previousQuery', ""); }; /* If a PracticeGroup param is present, then set state */ if (searchParams.get('PracticeGroup')) { this.state.filters.PracticeGroup = decodeURIComponent(searchParams.get('PracticeGroup')); } /* If a userLocation param is present, then set state */ if (searchParams.get('userLocation')) { this.state.filters.location = decodeURIComponent(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 = decodeURIComponent(searchParams.get("LocationName")); } /* Check search params for LocationId and PracticeGroupId, and set state accordingly */ if (searchParams.get("LocationId")) { this.state.filters.LocationId = decodeURIComponent(searchParams.get("LocationId")); } if (searchParams.get("PracticeGroupId")) { this.state.filters.PracticeGroupId = decodeURIComponent(searchParams.get("PracticeGroupId")); } await fetch(`https://${this.apiSub}.azurewebsites.net/api/WompHealthSearchFacets?brand=` + this.state.filters.brand) .then(res => res.json()) .then(res => { this.state.facets = res["facets"]; }); /* Check for / apply visit type filters */ let visitTypes = searchParams.get("visitTypes"); if (visitTypes) { visitTypes = decodeURIComponent(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").innerHTML = `Organization: ${brands[this.state.filters.brand]} `; /* Handle sort order */ const sortby = searchParams.get("sortby"); if (sortby) { switch (sortby) { case "GeocodedCoordinate": if (searchParams.get("userLocation")) { siteSearch.state.filters.sortby = "GeocodedCoordinate"; const sortByLabel = document.querySelector("#SortByLabel"); sortByLabel.innerHTML = `Sort by: Distance `; } else { siteSearch.state.filters.sortby = false; }; break; default: siteSearch.state.filters.sortby = sortby; break; }; }; this.handleSubmit(); }; }; const siteSearch = new Search( `https://${/staging/gi.test(window.location.hostname) ? 'womphealthdevapi' : 'womphealthapi'}.azurewebsites.net/api/WompHealthSearch?` ); siteSearch.textInputs.forEach((el) => { if (el.id === 'query') { el.addEventListener("keydown", function (e) { if (e.keyCode === 13) siteSearch.handleSubmit(); }); }; if (el.id === "custom-location") { el.addEventListener("keydown", function(e) { if (e.keyCode === 13) { siteSearch.helpers.getGoogleLocation(); } }); } el.addEventListener("change", function (e) { const { id, value } = e.target; if (id === "query") { 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.addWindowListeners(); siteSearch.init(); try { let mtoggle = localStorage.getItem("maptoggle"); if(mtoggle !== null) { document.getElementById("showmap").checked = mtoggle; } document.getElementById("maptoggle").addEventListener("click", function(e){ localStorage.setItem("maptoggle", !document.getElementById("showmap").checked); }); } catch (error) { console.log(error); } 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.isStaging = /staging/gi.test(window.location.hostname); this.apiSub = this.isStaging ? 'womphealthdevapi' : 'womphealthapi'; /** **/ this.brand = 'providence'; /** **/ 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-dirty"); if(document.getElementById('mobileSearchWrapper').classList.length === 1) { document.getElementById("mobileSearchWrapper").classList.add("is-dirty"); } } 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; 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.innerHTML = 'No suggestions'; return 'success'; }; /* Map out the returned results */ const tmp = res.value.reduce((html, r) => r?.text ? html + `
${r.text}
` : html, ''); 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(`https://${this.apiSub}.azurewebsites.net/api/WompHealthAnalytics?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.log(res)); } }; addListener = { input: () => { const { src, param, helpers, autocompleteEl, getPreviousSearches, minChars, state, inputEl } = this; const { debounce, slugify, fetchSuggestions } = helpers; inputEl.addEventListener('input', debounce(async function(e) { const val = inputEl.value; /* 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(e) { 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 } = e; 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', (e) => { const { target } = e; /* 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 = e.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', (e) => { 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 = `