function onShadowReady(root, selector, callback) { const existing = root.querySelector(selector); if (existing) return callback(existing); const observer = new MutationObserver(() => { const el = root.querySelector(selector); if (el) { observer.disconnect(); callback(el); } }); observer.observe(root, { childList: true, subtree: true }); } function jarvisGoBack() { if (!window.$shedulerShadowRoot) return; onShadowReady( window.$shedulerShadowRoot, "#app > div.flex.flex-col.flex-grow.mt-6 > form > div.z-50.flex-grow.py-6.mt-6.bg-color.shadow-2xl > div > div > div a", (backButton) => { console.log("Back button clicked by script:", backButton); backButton.click(); } ); } function isIOS() { return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; } function validateUSPhoneNumber(input) { if (!input) { return { valid: false, message: "Phone number is required." } } let digits = input.replace(/\D/g, ''); if (digits.length === 11 && digits.startsWith('1')) { digits = digits.slice(1) } if (digits.length !== 10) { return { valid: false, message: "Please enter a valid 10-digit US phone number." } } const areaCode = digits.slice(0, 3) const exchangeCode = digits.slice(3, 6) if (!/^[2-9]/.test(areaCode)) { return { valid: false, message: "Invalid area code." } } // Exchange code must start 2–9 if (!/^[2-9]/.test(exchangeCode)) { return { valid: false, message: "Invalid phone number format" }; } // Block N11 exchanges (911, 411 etc) if (/^[2-9]11$/.test(exchangeCode)) { return { valid: false, message: "Invalid exchange code" }; } // Optional: block obviously fake numbers if (/^(\d)\1{9}$/.test(digits)) { return { valid: false, message: "Invalid phone number" }; } return { valid: true, message: "", normalized: digits, // 2125551234 formatted: `(${areaCode}) ${exchangeCode}-${digits.slice(6)}` } } function showPhoneError(message) { const root = window.$shedulerShadowRoot; if (!root) return; const phoneInput = root.querySelector("#mobile_phone input"); if (!phoneInput) return; // Style the input phoneInput.style.borderColor = "red"; phoneInput.style.borderWidth = "2px"; // Remove existing error (avoid duplicates) let existingError = phoneInput.parentElement.querySelector(".phone-error-msg"); if (existingError) { existingError.remove(); } // Create error element const error = document.createElement("div"); error.className = "phone-error-msg"; error.textContent = message; // Style it (keep it subtle but clear) error.style.color = "#d32f2f"; error.style.fontSize = "12px"; error.style.marginTop = "4px"; error.style.fontFamily = "inherit"; // Attach below input phoneInput.parentElement.appendChild(error); // Optional: auto-clear on user input phoneInput.addEventListener("input", () => { phoneInput.style.borderColor = ""; error.remove(); }, { once: true }); } var load_jarvis = () => { function getCommentFromURL() { // Get the full URL const url = window.location.href; // Create a URL object const urlObj = new URL(url); // Get the search parameters const searchParams = new URLSearchParams(urlObj.search); // Get the 'comment' parameter if it exists const comment = searchParams.get("comment"); // Return the decoded comment or null if not present return comment ? decodeURIComponent(comment) : null; } function isBookAppointmentButton(el) { return el.textContent?.toLowerCase().includes("book"); } var LocationName = window.jarvisConfig.officeId; // Change as needed var bookingForm = document.getElementById("booking-form"); if ( LocationName === "" || LocationName === null || LocationName === undefined ) { bookingForm.style.display = "block"; var loading_icon_temp = document.getElementById("loading-spinner-tmp"); loading_icon_temp.style.display = "none"; //hide when form shows } else { bookingForm.style.display = "none"; try { /* const urlParams = new URLSearchParams(window.location.search); var commentPrefill = ""; if (urlParams.has("comment")) { = urlParams.get("comment"); }*/ // var $shedulerShadowRoot; //now window.$shedulerShadowRoot var phoneNumber; var customerFirstName; var customerLastName; var customerEmail; /* intercept XHR to get submitted phone number */ const originalXhrSend = XMLHttpRequest.prototype.send; const originalXhrOpen = XMLHttpRequest.prototype.open; // Intercept the open method to capture the URL XMLHttpRequest.prototype.open = function ( method, url, async, user, password ) { this._url = url; // Store the URL for logging return originalXhrOpen.apply(this, arguments); }; // Unified extraction logic for XHR and fetch // Global cache for robust date/time extraction window.jarvisBookingCache = { date: "", day: "", time: "", location: "" }; function processCustomerData(inputData) { if (!inputData) return; customerFirstName = inputData.first_name || customerFirstName || ""; customerLastName = inputData.last_name || customerLastName || ""; customerEmail = inputData.email || document.getElementById("Booking-Email")?.value || customerEmail || ""; phoneNumber = inputData.mobile_phone || phoneNumber || ""; // Strategy 1: Extract Date/Time from the API payload directly try { let rawStart = inputData.start_time || inputData.appointment_date || ""; if (!rawStart) { const jsonStr = JSON.stringify(inputData); const isoMatch = jsonStr.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/); if (isoMatch) rawStart = isoMatch[0]; } if (rawStart) { const d = new Date(rawStart); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); const tMatch = rawStart.match(/T(\d{2}):(\d{2})/); if (tMatch && !rawStart.includes("Z") && !rawStart.match(/[+-]\d{2}:\d{2}$/)) { let hrs = parseInt(tMatch[1], 10); const ampm = hrs >= 12 ? 'PM' : 'AM'; hrs = hrs % 12 || 12; window.jarvisBookingCache.time = `${hrs}:${tMatch[2]} ${ampm}`; } else { window.jarvisBookingCache.time = d.toLocaleTimeString("en-US", { hour: 'numeric', minute: '2-digit' }); } } } } catch(e) {} if (phoneNumber) { const urlObj = new URL(window.location.href); if (urlObj.searchParams.get("confirmation") !== "1") { urlObj.searchParams.delete("comment"); urlObj.searchParams.delete("variant"); urlObj.searchParams.set("confirmation", "1"); window.history.pushState({}, "", urlObj.toString()); } } } // Intercept the send method XMLHttpRequest.prototype.send = function (body) { if (this._url && typeof this._url === "string" && this._url.includes("schedule.jarvisanalytics.com/graphql")) { try { if (body && typeof body === "string") { const parsedBody = JSON.parse(body); if (parsedBody && parsedBody.query) { const inputData = parsedBody.variables?.input; processCustomerData(inputData); } } } catch (e) { console.log("Error parsing XHR body:", e); } } return originalXhrSend.apply(this, arguments); }; /* Intercept fetch function */ LocationName; window.fetch = new Proxy(window.fetch, { apply: async function (target, thisArg, argumentsList) { const [url, options = {}] = argumentsList; if (url == "https://schedule.jarvisanalytics.com/graphql" && options.body) { try { const parsedBody = JSON.parse(options.body); const inputData = parsedBody.variables?.input; processCustomerData(inputData); } catch (e) { console.log("Error parsing fetch body:", e); } } // You can also log the response by handling the promise const response = await target.apply(thisArg, argumentsList); return response; }, }); const winShadowRoot = window.$shedulerShadowRoot; /* end of interception */ //var canUpdatePhoneNumberListener = isThreeStepForm || false; //onThreestep form, we can update the phonenumber listener at start var canObservePolicyRadio = isThreeStepForm || false; var canUpdateComment = false; var hasGoneStepTwoOnce = false; window.jarvis = new window.JarvisAnalyticsScheduler({ //now window.jarvis and window.JarvisAnalyticsScheduler token: "46138|hKPVSp7yX5m83JP4qdrNFI82ui91fp4yvJteVf3e", companyId: 3, locationId: window.jarvisConfig.officeId, }); const dataLayerPush = (event) => { window.scrollTo(0, 0); (window.dataLayer || []).push(event); try { parent.postMessage(event, "*"); setTimeout(() => { console.log("Attempting to prefill comment"); const comment = getCommentFromURL(); if ( comment && winShadowRoot ) { console.log("Comment:", comment); const textarea = winShadowRoot.querySelector("textarea"); if (textarea) { textarea.value = `Offer: ${comment}`; } } // Attach var emailInput = document .querySelector("jarvis-scheduler-v2") .shadowRoot.querySelector("#email"); if (emailInput) { console.log("Email input found"); emailInput.addEventListener("blur", function () { document.getElementById("Booking-Email").value = emailInput.value; var event = new Event("change"); document.getElementById("Booking-Email").dispatchEvent(event); }); } }, 1000); } catch (e) { window.console && window.console.log(e); } }; window.jarvis.title = ""; window.jarvis.colors.activeNavItemBackground = "#6AC64B"; window.jarvis.colors.primaryOptionColor = "#6AC64B"; window.jarvis.colors.primaryButtonBackground = "#6AC64B"; window.jarvis.colors.primaryButtonBorderColor = "#6AC64B"; window.jarvis.colors.bodyBackground = "#FBFEFF"; window.jarvis.colors.headerBackground = "#FBFEFF"; window.jarvis.colors.nearestCardSubtitleColor = "#B7BCC2"; window.jarvis.colors.inactiveNavItemBackground = "#B7BCC2"; window.jarvis.colors.samedayCardSubtitlesColor = "#B7BCC2"; window.jarvis.colors.availabilityBackground = "#FBFEFF"; window.jarvis.colors.availabilityPaginationBackground = "#EEF2F6"; window.jarvis.colors.availabilityPaginationColor = "#767F87"; window.jarvis.colors.availabilityColumnHeaderBackground = "#EEF2F6"; window.jarvis.colors.availabilityColumnHeaderColor = "#767F87"; window.jarvis.colors.nearestCardColumnHeaderColor = "#767F87"; //window.jarvis.onSubmittedhandlers.push(function () { //console.log("submitted"); //}); setInterval(() => { try { const shadow = window.$shedulerShadowRoot; if (!shadow) return; // Strategy 2: Scan the DOM for the confirmation text (e.g. "Tuesday, May 14 at 10:00 AM") if (!window.jarvisBookingCache.time) { const allTextNodes = shadow.querySelectorAll("h1, h2, h3, h4, p, span, div"); for (let el of allTextNodes) { if (el.children.length > 0) continue; // Only check leaf nodes to avoid duplicate scanning const text = el.textContent.trim(); const timeMatch = text.match(/at\s+(\d{1,2}:\d{2}\s*(?:AM|PM))/i); if (timeMatch && text.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) { window.jarvisBookingCache.time = timeMatch[1].trim(); const datePart = text.split(/at/i)[0].trim(); // "Tuesday, May 14" const d = new Date(datePart); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); } break; } } } // Strategy 3: Find the checked radio button in the Jarvis date picker (e.g. ) const selectedTimeRadio = shadow.querySelector('input[type="radio"]:checked'); if (selectedTimeRadio && selectedTimeRadio.id && selectedTimeRadio.id.startsWith("time-")) { const ariaLabel = selectedTimeRadio.getAttribute("aria-label"); if (ariaLabel && ariaLabel.includes(" on ")) { const parts = ariaLabel.split(" on "); // Safely update the cache ONLY if it's not already set to avoid overwriting with empties const parsedTime = parts[0].trim().replace(/(am|pm)/i, ' $1').toUpperCase().replace(" ", " "); if (parsedTime) window.jarvisBookingCache.time = parsedTime; const d = new Date(parts[1].trim()); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); } } } // Extract Location Name from the selected card const activeLocEl = shadow.querySelector("[data-location-id]"); if (activeLocEl) { const locTitleEl = activeLocEl.parentElement.querySelector("h3"); if (locTitleEl) window.jarvisBookingCache.location = locTitleEl.textContent.trim(); } } catch(e) {} }, 500); window.jarvis.onBookSuccesshandlers.push(function () { // Hide the Jarvis widget immediately to prevent the native success screen from flashing if (window.$shedulerShadowRoot) { const container = window.$shedulerShadowRoot.querySelector("#app"); if (container) { container.style.opacity = "0"; container.style.pointerEvents = "none"; } } // Extract booking details for the Thank You page try { const shadow = window.$shedulerShadowRoot; // 1. Extract Location Name (from Cache or Fallback) let locName = window.jarvisBookingCache.location; if (!locName) { const h1El = document.querySelector('h1'); if (h1El && h1El.textContent && h1El.textContent.trim() !== "Book an Appointment") { locName = h1El.textContent.trim().replace(/Book an Appointment at:\s*/, "").replace(/Dentist in |Orthodontist in /, ""); } else if (window.jarvisConfig && window.jarvisConfig.officeName) { locName = window.jarvisConfig.officeName; } } // 2. Extract Date, Day & Time (from our interval cache) let dateStr = window.jarvisBookingCache.date; let dayStr = window.jarvisBookingCache.day; let timeStr = window.jarvisBookingCache.time; // 4. Extract Phone (from header link href, NOT textContent) const phoneEl = document.querySelector('a[href^="tel:"]'); let phoneStr = ""; if (phoneEl) { const href = phoneEl.getAttribute("href"); let digits = href.replace(/[^0-9]/g, ""); if (digits.length === 11 && digits.startsWith("1")) digits = digits.slice(1); if (digits.length === 10) { phoneStr = `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`; } else { phoneStr = href.replace("tel:", "").trim(); } } // 5. Extract Location URL (for insurance link) let locationUrl = window.location.pathname; if (locationUrl.includes("/book-appointment") && window.jarvisConfig && window.jarvisConfig.vanityUrl) { locationUrl = window.jarvisConfig.vanityUrl.startsWith("/") ? window.jarvisConfig.vanityUrl : "/" + window.jarvisConfig.vanityUrl; } const bookingDetails = { location: locName, date: dateStr, day: dayStr, time: timeStr, phone: phoneStr, url: locationUrl }; sessionStorage.setItem("jarvis_booking_details", JSON.stringify(bookingDetails)); } catch (e) { console.error("Jarvis: error extracting booking details for thank you page", e); } // Tracking: The webhook below will fire, and then the page will redirect to /thank-you // Seed payload with values captured during form intercept (XHR/fetch proxy) const webhookData = { apikey: "HAuu1jpaWurNhCEHdRG1ZW4ZaDpB4Kr5Qs", location_id: window.jarvisConfig.jarvisId || "", type: "form", first_name: customerFirstName || "", last_name: customerLastName || "", email: customerEmail || "", phone: phoneNumber || "", date: new Date().toISOString().slice(0, 19).replace('T', ' '), // Y-m-d H:i:s }; // Override with DOM values where available — shadow DOM inputs are the most reliable // source since the XHR-intercepted values may be captured mid-typing. try { const firstNameInput = window.$shedulerShadowRoot?.querySelector("input#first_name"); const lastNameInput = window.$shedulerShadowRoot?.querySelector("input#last_name"); const middleNameInput = window.$shedulerShadowRoot?.querySelector("input#middle_name"); const appointmentDateEl = window.$shedulerShadowRoot?.querySelector("[date-picker-date]"); if (firstNameInput?.value) webhookData.first_name = firstNameInput.value; if (lastNameInput?.value) webhookData.last_name = lastNameInput.value; if (middleNameInput?.value) webhookData.middle_name = middleNameInput.value; if (appointmentDateEl) webhookData.appointment_date = appointmentDateEl.getAttribute("date-picker-date"); const comment = getCommentFromURL(); if (comment) webhookData.comments = `Offer: ${comment}`; const urlParams = new URLSearchParams(window.location.search); ['utma', 'utmb', 'utmc', 'utmv', 'utmz', 'utmx'].forEach(param => { const value = urlParams.get(param); if (value) webhookData[param] = value; }); if (document.referrer) webhookData.referrer = document.referrer; } catch (error) { console.error("Jarvis: error reading shadow DOM inputs for webhook payload:", error); } // Warn if required fields are missing — do not block the UI, the appointment // is already confirmed in Jarvis regardless of tracking payload completeness. if (!webhookData.first_name || !webhookData.last_name || !webhookData.email) { console.warn("Jarvis webhook: one or more required fields are empty.", webhookData); } // Snapshot the phone number before clearing — the 100ms success-modal // timeout below needs this value, and the global will already be empty. const confirmedPhone = phoneNumber; // Clear cached tracking variables after confirmation payload is assembled customerFirstName = ""; customerLastName = ""; customerEmail = ""; phoneNumber = ""; let redirected = false; const doRedirect = () => { if (!redirected) { redirected = true; window.location.href = "/thank-you"; } }; // Guarantee delivery by waiting for fetch, with a max timeout fallback setTimeout(() => { window.fetch("https://webhooks.jarvisanalytics.com/api/v2/leads", { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json" }, body: JSON.stringify(webhookData), keepalive: true }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { if (window.dataLayer) { window.dataLayer.push({ event: 'jarvis_webhook_success', webhookResponse: data }); } }) .catch(error => { console.error("Jarvis webhook error:", error); if (window.dataLayer) { window.dataLayer.push({ event: 'jarvis_webhook_error', error: error.message }); } }) .finally(() => { doRedirect(); }); }, 10); // Fallback in case fetch hangs (increased to 5000ms to allow slow webhooks to finish) setTimeout(doRedirect, 5000); }); window.jarvis.onloadhandlers.push(function () { //console.log("loaded"); }); window.jarvis.onNextStep(dataLayerPush); window.jarvis.onNextStephandlers.push(function (e) { console.log("Next step data:", e); window.$shedulerShadowRoot.addEventListener( "click", (e) => { const el = e.target; if (!el) return; if (isBookAppointmentButton(el)) { const phoneInput = window.$shedulerShadowRoot.querySelector("#mobile_phone input"); if (!phoneInput) return; const validation = validateUSPhoneNumber(phoneInput.value); if (!validation.valid) { console.warn("Hard blocking before mutation is created"); e.preventDefault(); e.stopImmediatePropagation(); // this is well suited than stopPropagation showPhoneError(validation.message); return false; } } }, true // capture phase (CRITICAL) ); //canUpdatePhoneNumberListener = true; canObservePolicyRadio = true; canUpdateComment = true; if (!hasGoneStepTwoOnce) { hasGoneStepTwoOnce = true; } }); document .querySelector("#wf-back-button-mobile") ?.setAttribute("onclick", "jarvisGoBack()"); document .querySelector("#back-button-sticky-mobile") ?.setAttribute("onclick", "jarvisGoBack()"); document .querySelector("#back-button-sticky") ?.setAttribute("onclick", "jarvisGoBack()"); document .querySelector("#next-button-sticky") ?.addEventListener("click", () => { winShadowRoot.querySelector(".continue-btn")?.click(); }); window.addEventListener("load", async function () { try { const jarvis_location = document.querySelector("#jarvis-location"); //setTimeout( function () { let $jarvisComponent = document.querySelector( "jarvis-scheduler-v2" ); window.$shedulerShadowRoot = $jarvisComponent.shadowRoot; // Strategy 4: Intercept clicks on the time slot BEFORE Vue destroys the DOM (Capture Phase) window.$shedulerShadowRoot.addEventListener("click", (e) => { const label = e.target.closest("label"); if (label && label.getAttribute("for") && label.getAttribute("for").startsWith("time-")) { const ariaLabel = label.getAttribute("aria-label"); // e.g. "8:00 am on May 15, 2026" if (ariaLabel && ariaLabel.includes(" on ")) { const parts = ariaLabel.split(" on "); const parsedTime = parts[0].trim().replace(/(am|pm)/i, ' $1').toUpperCase().replace(" ", " "); if (parsedTime) window.jarvisBookingCache.time = parsedTime; const d = new Date(parts[1].trim()); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); } } } // Also try to intercept nearest availability card clicks OR mobile list items const card = e.target.closest(".availability-card, [data-operatory]"); if (card) { // Strategy for mobile list items (
  • 08:00am
  • ) if (card.tagName.toLowerCase() === 'li') { const timeText = card.textContent.trim(); if (timeText) { window.jarvisBookingCache.time = timeText.replace(/(am|pm)/i, ' $1').toUpperCase().replace(" ", " "); } // Try to find the date span in the mobile carousel const carouselCell = card.closest('.carousel-cell'); if (carouselCell) { const dateSpan = carouselCell.querySelector('span.table-pagination') || carouselCell.querySelector('.date-header'); if (dateSpan) { let dateText = dateSpan.textContent.trim(); // e.g. "Thu, May 21" -> add year if (!/\d{4}/.test(dateText)) { dateText += ", " + new Date().getFullYear(); } const d = new Date(dateText); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); } } } } else { // Strategy for nearest availability cards const text = card.textContent || ""; const timeMatch = text.match(/(\d{1,2}:\d{2}\s*(?:AM|PM))/i); if (timeMatch && text.match(/monday|tuesday|wednesday|thursday|friday|saturday|sunday/i)) { window.jarvisBookingCache.time = timeMatch[1].trim().toUpperCase(); const dateMatch = text.match(/(?:january|february|march|april|may|june|july|august|september|october|november|december)\s+\d{1,2}/i); if (dateMatch) { const d = new Date(dateMatch[0] + ", " + new Date().getFullYear()); if (!isNaN(d.getTime())) { window.jarvisBookingCache.date = d.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); window.jarvisBookingCache.day = d.toLocaleDateString("en-US", { weekday: "long" }); } } } } } }, true); // use capture phase so we intercept before Vue destroys the element! const shadowApp = window.$shedulerShadowRoot.querySelector("#app"); if (shadowApp) { shadowApp.style.position = "relative"; } const closeButton = window.$shedulerShadowRoot.querySelector("button"); if (closeButton) { closeButton.style.display = "none"; } jarvis_location.insertAdjacentElement("afterend", $jarvisComponent); // the lengthy css was moved to a file but we can still add additional css via the style below; // If we really need to override the file content, we can change, repload on webflow and replace the link, webflow natively support only txt and image files for now that's why the style is in txt format // Create and insert a