(function () { "use strict"; if (window.__medspaClinicLoaderLoaded) return; window.__medspaClinicLoaderLoaded = true; // =========================================================================== // CONFIG — the only values you should ever need to edit // =========================================================================== // API root. Must end with a trailing slash. var apiUrl = "https://api.medspacircle.com/api/v1/"; // Booking site root. Clinic and branch slugs are appended to this, producing // e.g. https://go.medspacircle.com/clinics/my-clinic/my-branch/treatments // Must end with a trailing slash. var linkUrl = "https://go.medspacircle.com/clinics/"; // Google Font loaded for the card styling. Set to null to skip loading it. var FONT_FAMILY = "Montserrat"; var MOUNT_ID = "MCLoader"; var MOUNT_TIMEOUT = 15000; // ms to wait for #MCLoader to appear var SCHEDULE_DAYS_AHEAD = 60; // how far ahead to request availability // =========================================================================== // DATE UTILITIES — moment replacement // =========================================================================== var MONTHS_SHORT = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; var MONTHS_LONG = ["January","February","March","April","May","June","July", "August","September","October","November","December"]; var DAYS_SHORT = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]; var DAYS_LONG = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"]; /** * Parse from components so we get LOCAL time, matching moment's behaviour. * Handles "YYYY-MM-DD", "YYYY-MM-DD HH:mm:ss" and "YYYY-MM-DDTHH:mm:ss". */ function parseDate(input) { if (input instanceof Date) return isNaN(input.getTime()) ? null : input; if (typeof input === "number") { var n = new Date(input); return isNaN(n.getTime()) ? null : n; } if (typeof input !== "string") return null; var m = input.match( /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?)?/ ); if (m) { var d = new Date( parseInt(m[1], 10), parseInt(m[2], 10) - 1, parseInt(m[3], 10), m[4] ? parseInt(m[4], 10) : 0, m[5] ? parseInt(m[5], 10) : 0, m[6] ? parseInt(m[6], 10) : 0 ); return isNaN(d.getTime()) ? null : d; } var fallback = new Date(input); return isNaN(fallback.getTime()) ? null : fallback; } function ordinal(n) { var s = ["th", "st", "nd", "rd"]; var v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function pad2(n) { return n < 10 ? "0" + n : String(n); } var TOKEN_RE = /\[([^\]]*)\]|YYYY|YY|MMMM|MMM|MM|M|DD|Do|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g; function formatToken(token, d) { var h24 = d.getHours(); var h12 = h24 % 12 === 0 ? 12 : h24 % 12; switch (token) { case "YYYY": return String(d.getFullYear()); case "YY": return pad2(d.getFullYear() % 100); case "MMMM": return MONTHS_LONG[d.getMonth()]; case "MMM": return MONTHS_SHORT[d.getMonth()]; case "MM": return pad2(d.getMonth() + 1); case "M": return String(d.getMonth() + 1); case "DD": return pad2(d.getDate()); case "Do": return ordinal(d.getDate()); case "D": return String(d.getDate()); case "dddd": return DAYS_LONG[d.getDay()]; case "ddd": return DAYS_SHORT[d.getDay()]; case "HH": return pad2(h24); case "H": return String(h24); case "hh": return pad2(h12); case "h": return String(h12); case "mm": return pad2(d.getMinutes()); case "m": return String(d.getMinutes()); case "ss": return pad2(d.getSeconds()); case "s": return String(d.getSeconds()); case "A": return h24 < 12 ? "AM" : "PM"; case "a": return h24 < 12 ? "am" : "pm"; default: return token; } } /** * Drop-in replacement for the old moment-based helper. Same signature, * same default format, same output. */ function formatDateMoment(inputDate, newformat) { newformat = newformat || "MMM Do, ddd"; var d = parseDate(inputDate); if (!d) return String(inputDate); return newformat.replace(TOKEN_RE, function (match, escaped) { if (escaped !== undefined) return escaped; // [literal text] return formatToken(match, d); }); } // =========================================================================== // SAFE STORAGE — localStorage throws SecurityError when cookies are blocked // =========================================================================== function lsGet(key) { try { return window.localStorage.getItem(key); } catch (e) { return null; } } function lsSet(key, value) { try { window.localStorage.setItem(key, value); } catch (e) { /* quota exceeded or cookies blocked — tracking degrades, widget still works */ } } var TRACKING_KEYS = [ "ref", "mcpr", "utm_source", "utm_medium", "utm_campaign", "utm_id", "utm_term", "utm_content" ]; /** * Persist tracking params from the landing URL so they survive the journey to * go.medspacircle.com. LAST-TOUCH: a new campaign overwrites the stored one. * A key absent from the current URL is left alone, so a visitor who arrives * via a campaign and then browses to the booking page keeps their attribution. */ function captureTrackingParams() { if (!window.location.search) return; try { var params = new URLSearchParams(window.location.search); TRACKING_KEYS.forEach(function (key) { var value = params.get(key); if (value) lsSet(key, value); }); } catch (e) { console.warn("Tracking capture failed", e); } } // =========================================================================== // MOUNT DETECTION // =========================================================================== function whenDomReady(cb) { if (document.readyState !== "loading") { cb(); } else { document.addEventListener("DOMContentLoaded", cb, { once: true }); } } function waitForElement(id, timeout, cb, onFail) { var el = document.getElementById(id); if (el) return cb(el); var done = false; var timer = setTimeout(function () { if (done) return; done = true; observer.disconnect(); onFail(); }, timeout); var observer = new MutationObserver(function () { var found = document.getElementById(id); if (found && !done) { done = true; clearTimeout(timer); observer.disconnect(); cb(found); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); } // =========================================================================== // STYLES // =========================================================================== function injectStyles() { if (document.getElementById("mcloader-styles")) return; var styleElement = document.createElement("style"); styleElement.id = "mcloader-styles"; styleElement.textContent = ` .mcloader-card-list { display: flex; justify-content: center; flex-wrap: wrap; gap: 16px; list-style: none; padding: 0; margin: 0; } .mcloader-card { box-sizing: border-box; width: 100%; max-width: 480px; min-width: 280px; flex: 1 1 280px; background-color: #fff; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); overflow: hidden; margin: 10px 10px 20px 10px; text-align: center; font-family: "Montserrat", sans-serif; transition: box-shadow .3s ease, transform .3s ease; } .mcloader-card:hover { box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3); transform: scale(1.05); } .mcloader-card img { width: 100%; height: auto; border-radius: 10px 10px 0 0; } .mcloader-card-content { padding: 20px; color: black; text-align: left; } @media (max-width: 768px) { .mcloader-card { flex: 1 1 100%; max-width: 100%; min-width: 300px; } .mcloader-card-list { justify-content: center; } } h3.mcloader { margin: 0 0 10px; font-size: 20px; font-weight: bold; color: black; } p.mcloader { margin-top: 10px; font-size: 14px; color: black; } a.mcloader { text-decoration: none; } .loading-container { display: flex; justify-content: center; align-items: center; } .loading-spinner { width: 50px; height: 50px; border-radius: 50%; border: 4px solid rgba(255, 255, 255, 0.3); border-top: 4px solid #3498db; animation: mcloader-spin 1s linear infinite; } @keyframes mcloader-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .loading-spinner { animation-duration: 3s; } .mcloader-card { transition: none; } } button.mcloader { box-sizing: border-box; padding: 0; margin: 0; border: none; background-color: transparent; cursor: pointer; } .paddingtop10 { padding-top: 10px; } .rectangular-button { margin-top: 10px; display: inline-block; width: 100%; height: 40px; line-height: 40px; text-align: center; font-size: 16px; font-weight: bold; color: #FFFFFF; background-color: #ca2c5b; border-radius: 10px; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); transition: background-color 0.3s ease; font-family: "Montserrat", sans-serif; } .rectangular-button[disabled] { opacity: .6; cursor: default; } .square-image-container { justify-content: center; align-items: center; } .mcloader_logo { width: auto !important; height: 250px !important; margin-top: 10px; } .mcloader-medspa, .mcloader-address { text-align: left !important; } .mcloader-medspa { font-weight: bold; } .mcloader-address { margin-bottom: 10px; } .mcloadervia { margin-top: 5px; padding-top: 5px; font-size: 0.75em; text-align: center; } #NewLeadsMCLoader { margin: 0 auto; width: 400px; max-width: 100%; } .formnewleads { text-align: left; } .invalid-feedback-show { color: #FFFFFF; background: #ca2c5b; margin-bottom: 5px; padding: 0px 13px; } .rownewleads p { font-size: 115%; } .timeslotbox { padding-top: 15px; } .timeslot { max-width: 100px; margin: 2px !important; } /* #MCTimeSlots is a flex item inside .mcloader-card-list. Without a full-width basis it shrinks to fit and its content sits left. */ #MCTimeSlots { flex: 1 1 100%; width: 100%; text-align: center; } .MCTimeSlotsStacked { width: 100%; text-align: center; } #MCTimeSlots h2 { text-align: center; } .timeslotbox_date { text-align: center; font-weight: bold; } .timeslotbox { text-align: center; } .timeslotbox .timeslot { display: inline-block; vertical-align: top; width: auto; min-width: 90px; margin: 4px !important; padding: 0 12px; } .timeslotbox p { text-align: center; margin: 12px auto 0; max-width: 480px; } `; document.head.appendChild(styleElement); } function loadGoogleFont(fontFamily) { if (document.getElementById("mcloader-font")) return; var linkElement = document.createElement("link"); linkElement.id = "mcloader-font"; linkElement.rel = "stylesheet"; linkElement.href = "https://fonts.googleapis.com/css2?family=" + encodeURIComponent(fontFamily) + "&display=swap"; document.head.appendChild(linkElement); } // =========================================================================== // iOS DIALOG POLYFILL // =========================================================================== function checkBrowserCompatibility() { var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); if (!isIOS) return; var match = navigator.userAgent.match(/OS (\d+)_(\d+)/); var iOSVersion = match ? parseFloat(match[1] + "." + match[2]) : null; if (!iOSVersion || iOSVersion >= 14.9) return; if (typeof HTMLDialogElement !== "undefined") return; var polyfillCSS = document.createElement("link"); polyfillCSS.rel = "stylesheet"; polyfillCSS.href = "https://cdn.jsdelivr.net/npm/dialog-polyfill@0.5.6/dist/dialog-polyfill.css"; document.head.appendChild(polyfillCSS); var polyfillJS = document.createElement("script"); polyfillJS.src = "https://cdn.jsdelivr.net/npm/dialog-polyfill@0.5.6/dist/dialog-polyfill.js"; polyfillJS.onload = function () { var dialogs = document.querySelectorAll("dialog"); Array.prototype.forEach.call(dialogs, function (dialog) { if (typeof dialogPolyfill !== "undefined") { dialogPolyfill.registerDialog(dialog); } }); }; document.head.appendChild(polyfillJS); } // =========================================================================== // NETWORK // =========================================================================== var MAX_RETRIES = 2; var RETRY_BASE_DELAY = 600; function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } /** * Retries on transient failures only: network errors and 5xx. A load balancer * returns 502/503 while a target is draining, failing a health check, or * being replaced during a deploy — brief, random, and invisible in logs unless * you look for it. One retry usually lands on a healthy target. * * 4xx is NOT retried: a 404 clinic slug or a 400 payload will fail identically * every time, and retrying only delays the error message. */ async function fetchData(url, fetchOptions, attempt) { attempt = attempt || 0; var controller = new AbortController(); var timeoutId = setTimeout(function () { controller.abort(); }, 30000); try { var options = Object.assign({}, fetchOptions || {}, { signal: controller.signal }); var response = await fetch(url, options); if (!response.ok) { var err = new Error("HTTP error! status: " + response.status); err.status = response.status; throw err; } return await response.json(); } catch (error) { var isTransient = error.status === undefined // network failure / DNS / connection reset ? error.name !== "AbortError" : error.status >= 500; if (isTransient && attempt < MAX_RETRIES) { clearTimeout(timeoutId); var delay = RETRY_BASE_DELAY * Math.pow(2, attempt); console.warn( "Transient failure, retrying in " + delay + "ms (" + (attempt + 1) + "/" + MAX_RETRIES + "):", url ); await sleep(delay); return fetchData(url, fetchOptions, attempt + 1); } console.error("Error fetching data:", url, error); throw error; } finally { clearTimeout(timeoutId); } } // =========================================================================== // MAIN // =========================================================================== function boot(targetDiv) { var slug = targetDiv.getAttribute("data-slug-id"); if (!slug) { console.error("MCLoader: data-slug-id attribute is missing"); targetDiv.innerHTML = '
' + "

Booking widget is not configured.

"; return; } var withmap = targetDiv.getAttribute("data-with-map"); var mapSettings = targetDiv.getAttribute("data-map-settings") || ""; var mapApiKey = targetDiv.getAttribute("data-map-api-key"); var mapZoomLevel = targetDiv.getAttribute("data-map-zoom-level"); var stackingView = targetDiv.getAttribute("data-stacking-view"); var locationTitle = targetDiv.getAttribute("data-location-title"); var showSchedule = targetDiv.getAttribute("data-schedule") || "true"; var showFullBook = targetDiv.getAttribute("data-show-full-book") || "false"; var theslug = []; var themapSettings = []; var theUrlsToCall = []; var branchData = []; var theMarkerLists = []; var arr_dropdown = []; injectStyles(); if (FONT_FAMILY) loadGoogleFont(FONT_FAMILY); checkBrowserCompatibility(); if (withmap === "true") { targetDiv.innerHTML = '
' + '
' + '
'; } else { targetDiv.innerHTML = '
' + '
'; } // ------------------------------------------------------------------------- // UTM / REFERRAL // ------------------------------------------------------------------------- function appendUtmParamsAndReferral() { var parts = []; TRACKING_KEYS.forEach(function (key) { var value = lsGet(key); if (value) parts.push(key + "=" + encodeURIComponent(value)); }); return parts.length ? "?" + parts.join("&") : ""; } function bookingHref(clinicSlug, branchSlug, extraParams) { var refcode = appendUtmParamsAndReferral(); var url = linkUrl + clinicSlug + "/" + branchSlug + "/treatments" + refcode; if (extraParams) { url += (refcode ? "&" : "?") + extraParams; } return url; } function escapeHtml(str) { return String(str == null ? "" : str) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } // ------------------------------------------------------------------------- // CARD RENDERING // ------------------------------------------------------------------------- function renderScheduleButtons(fs, clinicSlug, branch) { if (!fs || !fs.data || !fs.data.date) return ""; var html = ""; for (var j = 0; j < fs.data.date.length; j++) { var raw = fs.data.date[j]; html += '
"; } return html; } function renderCard(branch, clinicSlug, fs, index) { var scheduleCount = fs && fs.data && fs.data.date ? fs.data.date.length : 0; var fschedules = showSchedule === "true" ? renderScheduleButtons(fs, clinicSlug, branch) : ""; // FIX: original showed FULLY BOOKED when slots WERE available. var hasAvailability = scheduleCount > 0; var html = '
' + '
' + '
' + '
' + '
' + '
' + escapeHtml(branch.name) + "
" + '
' + escapeHtml(branch.address) + "
" + fschedules; if (!hasAvailability && showFullBook === "true") { html += "FULLY BOOKED."; if (branch.cs_phone_number) { html += "
Please call us at " + escapeHtml(branch.cs_phone_number); } } else { // FIX: closing , not a second opening . html += ''; } html += "
"; return html; } function fetchSchedulesFor(branches) { var starttime = createDate(); var endtime = createDate(SCHEDULE_DAYS_AHEAD); return Promise.all( branches.map(function (branch) { return fetchData( apiUrl + "branches/" + branch.slug + "/schedules?start_time=" + encodeURIComponent(starttime) + "&end_time=" + encodeURIComponent(endtime) + "&is_pause=false&usage_for=wordpress" ).catch(function () { return null; // one bad branch must not kill the whole render }); }) ); } // ------------------------------------------------------------------------- // CITY DROPDOWN // ------------------------------------------------------------------------- async function getCityList(clinicApiUrls) { try { var results = await Promise.all( clinicApiUrls.map(function (url) { return fetchData(url).catch(function () { return null; }); }) ); arr_dropdown = []; results.forEach(function (item, i) { if (item && Array.isArray(item.data) && item.data.length && item.data[0].clinic) { arr_dropdown.push({ rs_slug: item.data[0].clinic.slug, rs_name: item.data[0].clinic.name }); } else { console.warn("Missing or malformed data at index", i, item); } }); if (arr_dropdown.length <= 1) return; var selector = document.getElementById("MCLoaderHereSelector"); if (!selector) return; var html = '
' + "

Please select location:

" + '
 
'; selector.innerHTML = html; document.getElementById("chooseClinicDD") .addEventListener("change", function () { process(this.value); }); } catch (error) { console.error("Error building city list:", error); } } // ------------------------------------------------------------------------- // SINGLE VIEW // ------------------------------------------------------------------------- async function process(chosenSlug) { branchData = []; var branchList = document.getElementById("branchList"); if (branchList) { branchList.innerHTML = '
'; } var targetDiv2 = document.getElementById("MCLoaderHere"); if (!targetDiv2) return; try { var data = await fetchData( apiUrl + "clinics/" + chosenSlug + "/branches?is_active=true" ); var activeBranches = (data.data || []).filter(function (b) { return b.is_active; }); var schedulesResults = showSchedule === "true" || showFullBook === "true" ? await fetchSchedulesFor(activeBranches) : []; var htmlContent = '
'; activeBranches.forEach(function (branch, i) { htmlContent += renderCard(branch, chosenSlug, schedulesResults[i], i); branchData.push({ branch_name: branch.name, map_lat: branch.map_lat, map_long: branch.map_long, branch_address: branch.address }); }); htmlContent += '
'; targetDiv2.innerHTML = htmlContent; theMapInitiator(); bindDateButtons(); document.dispatchEvent(new CustomEvent("medspacircle:ready")); } catch (error) { console.error("Error loading clinic data:", error); targetDiv2.innerHTML = '
' + "

Unable to load clinic information. Please refresh your browser.

" + '

Book online instead

' + "
"; } } // ------------------------------------------------------------------------- // STACKED VIEW // ------------------------------------------------------------------------- async function processStack(slugs) { var locationTitleArray = locationTitle ? locationTitle.split(",") : []; var targetDiv2 = document.getElementById("MCLoaderHere"); if (!targetDiv2) return; try { var clinicsData = await Promise.all( slugs.map(function (s) { return fetchData(apiUrl + "clinics/" + s + "/branches?is_active=true"); }) ); var allClinicsSchedules = await Promise.all( clinicsData.map(function (data) { var active = (data.data || []).filter(function (b) { return b.is_active; }); if (showSchedule === "true" || showFullBook === "true") { return fetchSchedulesFor(active); } return Promise.resolve([]); }) ); var htmlContentArray = []; var index = 0; for (var x = 0; x < slugs.length; x++) { var activeBranches = (clinicsData[x].data || []).filter(function (b) { return b.is_active; }); var clinicSchedules = allClinicsSchedules[x]; branchData[x] = []; htmlContentArray[x] = '

' + '
' + '
'; for (var i = 0; i < activeBranches.length; i++) { var branch = activeBranches[i]; htmlContentArray[x] += renderCard(branch, slugs[x], clinicSchedules[i], index); branchData[x].push({ branch_name: branch.name, map_lat: branch.map_lat, map_long: branch.map_long, branch_address: branch.address }); index++; } htmlContentArray[x] += "
"; } htmlContentArray.push( '
' + '
 
' ); targetDiv2.innerHTML = htmlContentArray.join(""); theMapInitiatorStack(slugs, themapSettings); for (var y = 0; y < slugs.length; y++) { var el = document.getElementById("cityname" + y); if (el && locationTitleArray[y] !== undefined) { el.textContent = locationTitleArray[y].trim(); } } bindDateButtons(); document.dispatchEvent(new CustomEvent("medspacircle:ready")); } catch (error) { console.error("Error loading stacked clinic data:", error); targetDiv2.innerHTML = '
' + "

Unable to load clinic information. Please refresh your browser.

"; } } // ------------------------------------------------------------------------- // DATE BUTTONS + TIME SLOTS // ------------------------------------------------------------------------- function bindDateButtons() { var buttons = document.querySelectorAll(".date-button"); Array.prototype.forEach.call(buttons, function (button) { button.addEventListener("click", function () { Array.prototype.forEach.call(buttons, function (btn) { btn.style.backgroundColor = ""; }); this.style.backgroundColor = "blue"; }); button.addEventListener("click", handleClickDate); }); } async function handleClickDate(event) { var btn = event.currentTarget; var dataDate = btn.getAttribute("data-date"); var dataClinicSlug = btn.getAttribute("data-clinic-slug"); var dataBranchSlug = btn.getAttribute("data-branch-slug"); var dataBranchName = btn.getAttribute("data-branch-name"); var dataTargetLoad = btn.getAttribute("data-target-load"); var idelement; if (dataTargetLoad != null) { Array.prototype.forEach.call( document.querySelectorAll(".MCTimeSlotsStacked"), function (element) { element.innerHTML = ""; } ); idelement = document.getElementById(dataTargetLoad); } else { idelement = document.getElementById("MCTimeSlots"); } if (!idelement) return; idelement.innerHTML = '
'; idelement.scrollIntoView({ behavior: "smooth", block: "start" }); var originalBtnText = btn.innerHTML; btn.innerHTML = "Please wait"; btn.disabled = true; try { var data = await fetchData( apiUrl + "branches/" + dataBranchSlug + "/time_slots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ datetimes: dataDate + " 00:00:00" }) } ); var dataSlotsHtml; if (data.data && data.data.length) { var slots = ""; for (var j = 0; j < data.data.length; j++) { var label = formatDateMoment(data.data[j].start_time, "hh:mm A"); var href = bookingHref( dataClinicSlug, dataBranchSlug, "date=" + encodeURIComponent(dataDate) ); slots += '' + escapeHtml(label) + " "; } dataSlotsHtml = slots + '

Exact time slots will be confirmed once you ' + "select your treatment, as duration varies by service

"; } else { dataSlotsHtml = '

FULLY BOOKED: Please select another date. Thank you.

'; } var fdatedisplay = formatDateMoment(dataDate, "ddd, MMM Do, YYYY"); idelement.innerHTML = '
 
' + "

" + escapeHtml(dataBranchName) + "

" + '
' + escapeHtml(fdatedisplay) + "
" + '
' + dataSlotsHtml + "
"; setTimeout(function () { fetchGlParameter(appendGlToLinks); }, 175); } catch (error) { console.error("Error fetching time slots:", error); idelement.innerHTML = '

Error loading time slots. Please try again.

'; } finally { btn.innerHTML = originalBtnText; btn.disabled = false; } } // ------------------------------------------------------------------------- // DATE HELPER FOR API RANGE // ------------------------------------------------------------------------- function createDate(daysToAdd) { var today = new Date(); if (daysToAdd) today.setDate(today.getDate() + daysToAdd); return ( today.getFullYear() + "-" + pad2(today.getMonth() + 1) + "-" + pad2(today.getDate()) + " " + pad2(today.getHours()) + ":" + pad2(today.getMinutes()) + ":" + pad2(today.getSeconds()) ); } // ------------------------------------------------------------------------- // GOOGLE MAPS // ------------------------------------------------------------------------- function isGoogleMapsLoaded() { return ( typeof google === "object" && typeof google.maps === "object" && typeof google.maps.Map === "function" && typeof google.maps.Marker === "function" ); } function hideMapDiv() { var mapElements = document.getElementsByClassName("googlemaploader"); for (var i = 0; i < mapElements.length; i++) { mapElements[i].style.display = "none"; } } function waitForGoogleMaps(callback, maxAttempts, attempt) { maxAttempts = maxAttempts || 50; attempt = attempt || 1; if (isGoogleMapsLoaded()) return callback(); if (attempt >= maxAttempts) { console.error("Google Maps API failed to load after maximum attempts"); return hideMapDiv(); } setTimeout(function () { waitForGoogleMaps(callback, maxAttempts, attempt + 1); }, 100); } var mapsScriptInjected = false; function loadGoogleMapsAPI(onReady) { if (isGoogleMapsLoaded()) return onReady(); if (mapsScriptInjected) return waitForGoogleMaps(onReady); mapsScriptInjected = true; if (!mapApiKey) { console.error("MCLoader: data-map-api-key missing"); return hideMapDiv(); } var script = document.createElement("script"); script.src = "https://maps.googleapis.com/maps/api/js?key=" + encodeURIComponent(mapApiKey) + "&libraries=places&loading=async"; script.async = true; script.defer = true; script.onload = function () { waitForGoogleMaps(onReady); }; script.onerror = function () { console.error("Failed to load Google Maps API"); hideMapDiv(); }; document.head.appendChild(script); } function addMarker(map, map_lat, map_long, title, address) { var marker = new google.maps.Marker({ position: { lat: parseFloat(map_lat), lng: parseFloat(map_long) }, map: map, title: title, animation: google.maps.Animation.DROP }); var infoWindow = new google.maps.InfoWindow({ content: '' + escapeHtml(title) + "
" + escapeHtml(address) + "
" }); marker.addListener("click", function () { infoWindow.open(map, marker); }); theMarkerLists.push(marker); return marker; } function centerOf(list) { var total_lat = 0, total_long = 0, count = 0; for (var i = 0; i < list.length; i++) { if (list[i].map_lat) { total_lat += parseFloat(list[i].map_lat); total_long += parseFloat(list[i].map_long); count++; } } return count ? { lat: total_lat / count, lng: total_long / count, count: count } : { lat: 40.7128, lng: -74.006, count: 0 }; } function applyZoom(map, bounds) { if ( mapZoomLevel === undefined || mapZoomLevel === null || mapZoomLevel === "auto" || mapZoomLevel === "" ) { map.fitBounds(bounds); } else { map.setZoom(parseInt(mapZoomLevel, 10)); } } function theMapInitiator() { if (withmap !== "true") { var els = document.getElementsByClassName("googlemaploader"); for (var i = 0; i < els.length; i++) els[i].removeAttribute("style"); return; } loadGoogleMapsAPI(function initMap() { var container = document.getElementById("map"); if (!container) return; try { var center = centerOf(branchData); var map = new google.maps.Map(container, { center: { lat: center.lat, lng: center.lng }, zoom: 10 }); theMarkerLists = []; branchData.forEach(function (b) { if (b.map_lat) { addMarker(map, b.map_lat, b.map_long, b.branch_name, b.branch_address); } }); var bounds = new google.maps.LatLngBounds(); theMarkerLists.forEach(function (m) { bounds.extend(m.getPosition()); }); if (theMarkerLists.length) applyZoom(map, bounds); } catch (error) { console.error("Error initializing Google Maps:", error); hideMapDiv(); } }); } function theMapInitiatorStack(slugs, settings) { if (withmap !== "true") { var els = document.getElementsByClassName("googlemaploader"); for (var i = 0; i < els.length; i++) els[i].removeAttribute("style"); return; } var stray = document.getElementById("map"); if (stray) stray.remove(); loadGoogleMapsAPI(function initMapStack() { try { for (var x = 0; x < slugs.length; x++) { var container = document.getElementById("map" + x); if (!container) continue; var list = branchData[x] || []; var center = centerOf(list); var map = new google.maps.Map(container, { center: { lat: center.lat, lng: center.lng }, zoom: 10 }); var markers = []; var bounds = new google.maps.LatLngBounds(); for (var i = 0; i < list.length; i++) { if (list[i].map_lat) { var marker = addMarker( map, list[i].map_lat, list[i].map_long, list[i].branch_name, list[i].branch_address ); markers.push(marker); bounds.extend(marker.getPosition()); } } if (markers.length) applyZoom(map, bounds); if (settings && settings[x] === "false") { container.style.display = "none"; } } } catch (error) { console.error("Error initializing stacked Google Maps:", error); hideMapDiv(); } }); } // ------------------------------------------------------------------------- // GOOGLE ANALYTICS CROSS-DOMAIN // ------------------------------------------------------------------------- var measurementId = null; function getMeasurementIdFromScriptTag() { var scripts = document.querySelectorAll("script[src]"); for (var i = 0; i < scripts.length; i++) { var src = scripts[i].getAttribute("src") || ""; if (src.indexOf("gtag/js?id=") !== -1) { var idMatch = src.match(/id=(G-[a-zA-Z0-9]+)/); if (idMatch) return idMatch[1]; } } return null; } function fetchGlParameter(callback) { if (!measurementId) measurementId = getMeasurementIdFromScriptTag(); if (!measurementId || typeof window.gtag !== "function") return; try { window.gtag("get", measurementId, "linker", function (glParam) { if (glParam) callback(glParam); }); } catch (e) { console.warn("gtag linker unavailable", e); } } function appendGlToLinks(glParam) { if (!glParam) return; var links = document.querySelectorAll('a[href^="' + linkUrl + '"]'); Array.prototype.forEach.call(links, function (link) { try { var url = new URL(link.href); url.searchParams.set("_gl", glParam); link.href = url.toString(); } catch (e) { /* ignore malformed href */ } }); } // ------------------------------------------------------------------------- // START // ------------------------------------------------------------------------- if (slug.indexOf(",") !== -1) { theslug = slug.split(",").map(function (s) { return s.trim(); }); themapSettings = mapSettings.split(",").map(function (s) { return s.trim(); }); } else { theslug = [slug.trim()]; themapSettings = [mapSettings.trim()]; } theslug.forEach(function (s) { theUrlsToCall.push(apiUrl + "clinics/" + s + "/branches?is_active=true"); }); if (stackingView === "true") { processStack(theslug); } else { process(theslug[0]); getCityList(theUrlsToCall); } setTimeout(function () { fetchGlParameter(appendGlToLinks); }, 175); } // =========================================================================== // ENTRY POINT — wait for DOM, then for #MCLoader to exist // =========================================================================== // Capture immediately — before any DOM wait — so attribution is stored even // if the visitor leaves before the widget mounts. captureTrackingParams(); whenDomReady(function () { waitForElement(MOUNT_ID, MOUNT_TIMEOUT, boot, function () { console.error( "MCLoader element not found after " + MOUNT_TIMEOUT + "ms. " + 'Add
to the page.' ); }); }); })();