1/** 2 * Every Calendly link on the site opens as Calendly's popup widget instead of 3 * a new tab, and a booked SALES call is reported to the dataLayer. 4 * 5 * WHY 6 * A plain link sends the visitor to calendly.com, where they book and usually 7 * never come back. The booking still reaches us â Calendly's webhook tells the 8 * server (wp-job-manager-applications/calendly-webhook.php) â but a server 9 * request has no browser, so Google Ads never hears about it and booked calls 10 * could only be credited by uploading gclids by hand. Inside the popup the 11 * visitor stays on our page, and Calendly posts `calendly.event_scheduled` to 12 * it the moment they book. That message is what this file turns into a 13 * `strategy_call_booked` dataLayer event for GTM. 14 * 15 * WHICH BOOKINGS COUNT 16 * Every calendly.com link opens as a popup â employers' interview links shown 17 * to candidates included â but only the sales-call event types listed by 18 * front_calendly_sales_call_slugs() report a conversion. A candidate booking an 19 * interview is not a sale. 20 * 21 * WHY NOT rjhf.track() 22 * rjhf.track() would also POST the event to the lead endpoint and move the lead 23 * to call_booked from the browser. The webhook already does that, after 24 * checking Calendly's signature and the event type; a browser copy would be a 25 * second writer anyone could call. So this pushes to the dataLayer and the DOM 26 * only, the same way funnel-purchase-events.php reports a sale the server has 27 * already recorded. 28 * 29 * ATTRIBUTION 30 * rjhf-analytics.js rewrites Calendly hrefs in the CAPTURE phase of the click, 31 * adding the stored first-touch UTMs and the gclid (as salesforce_uuid). This 32 * listens in the bubble phase, so it reads the already-decorated href, and the 33 * popup passes the URL's query string through to the webhook unchanged. 34 * 35 * FALLBACK 36 * Modified clicks (Ctrl/Cmd/Shift/middle) keep their normal new-tab behaviour. 37 * If Calendly's widget script cannot load (blocked by an extension, offline), 38 * the link opens the ordinary way, so a booking is never lost to this file â 39 * it just goes untracked in the browser, as every booking did before. 40 */ 41(function (window, document) { 42 'use strict'; 43 44 var CFG = window.RJ_CALENDLY || {}; 45 var WIDGET_JS = 'https://assets.calendly.com/assets/external/widget.js'; 46 var WIDGET_CSS = 'https://assets.calendly.com/assets/external/widget.css'; 47 48 // Calendly's own product pages, which are not booking pages. 49 var RESERVED = ['app', 'login', 'signup', 'pricing', 'blog', 'resources', 'integrations', 'about', 'legal', 'help']; 50 51 var loading = null; 52 var failed = false; 53 54 // The booking link most recently opened: its event-type slug and when. 55 var current = null; 56 // The product the funnel's own CTA tracking attached to the same click. 57 var lastIntent = null; 58 // Invitee URIs already reported, so a repeated message cannot double-count. 59 var reported = {}; 60 61 function bookingUrl(href) { 62 var url; 63 64 try { 65 url = new URL(href, window.location.href); 66 } catch (e) { 67 return null; 68 } 69 70 if (url.hostname !== 'calendly.com' && url.hostname !== 'www.calendly.com') { 71 return null; 72 } 73 74 var parts = url.pathname.split('/').filter(Boolean); 75 76 if (!parts.length || RESERVED.indexOf(parts[0]) !== -1) { 77 return null; 78 } 79 80 return url; 81 } 82 83 function slugFor(url) { 84 var parts = url.pathname.split('/').filter(Boolean); 85 86 return parts.length ? parts[parts.length - 1].toLowerCase() : ''; 87 } 88 89 function widgetReady() { 90 return !!(window.Calendly && typeof window.Calendly.initPopupWidget === 'function'); 91 } 92 93 function loadWidget() { 94 if (widgetReady()) { 95 return Promise.resolve(); 96 } 97 98 if (failed) { 99 return Promise.reject(new Error('Calendly widget unavailable.')); 100 } 101 102 if (loading) { 103 return loading; 104 } 105 106 loading = new Promise(function (resolve, reject) { 107 var css = document.createElement('link'); 108 css.rel = 'stylesheet'; 109 css.href = WIDGET_CSS; 110 document.head.appendChild(css); 111 112 var script = document.createElement('script'); 113 script.src = WIDGET_JS; 114 script.async = true; 115 script.onload = function () { 116 if (widgetReady()) { 117 resolve(); 118 } else { 119 reject(new Error('Calendly widget did not initialise.')); 120 } 121 }; 122 script.onerror = function () { 123 reject(new Error('Calendly widget failed to load.')); 124 }; 125 document.head.appendChild(script); 126 127 // A script stalled by a slow network would otherwise leave the click 128 // doing nothing at all. 129 window.setTimeout(function () { 130 reject(new Error('Calendly widget timed out.')); 131 }, 8000); 132 }); 133 134 loading.catch(function () { 135 failed = true; 136 }); 137 138 return loading; 139 } 140 141 function openNormally(link, url) { 142 // Still within the click's user activation in practice (the script 143 // fails fast when blocked), so the new tab is normally allowed; if the 144 // browser refuses it, navigate this tab instead rather than do nothing. 145 if (link.target === '_blank') { 146 var win = window.open(url, '_blank'); 147 if (win) { 148 win.opener = null; 149 return; 150 } 151 } 152 153 window.location.assign(url); 154 } 155 156 document.addEventListener('click', function (e) { 157 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { 158 return; 159 } 160 161 var link = e.target.closest && e.target.closest('a[href]'); 162 var url = link ? bookingUrl(link.href) : null; 163 164 if (!url) { 165 return; 166 } 167 168 e.preventDefault(); 169 170 var href = url.toString(); 171 current = { slug: slugFor(url), at: Date.now() }; 172 173 loadWidget().then(function () { 174 window.Calendly.initPopupWidget({ url: href }); 175 }, function () { 176 openNormally(link, href); 177 }); 178 }); 179 180 // The wizard's "Talk to a Pro first" and funnel-cta-tracking.js both emit 181 // strategy_call_clicked with the product the call is about â more precise 182 // than the page's own declaration on /hire/, which covers every tier. 183 document.addEventListener('rjhf:strategy_call_clicked', function (e) { 184 var product = e.detail && e.detail.product; 185 if (product) { 186 lastIntent = { product: String(product), at: Date.now() }; 187 } 188 }); 189 190 function productFor(booking) { 191 if (lastIntent && Math.abs(lastIntent.at - booking.at) < 3000) { 192 return lastIntent.product; 193 } 194 195 return (CFG.slugProducts || {})[booking.slug] || CFG.product || ''; 196 } 197 198 window.addEventListener('message', function (e) { 199 if (e.origin !== 'https://calendly.com' || !e.data || e.data.event !== 'calendly.event_scheduled') { 200 return; 201 } 202 203 var payload = e.data.payload || {}; 204 var invitee = (payload.invitee && payload.invitee.uri) || ''; 205 206 if (invitee) { 207 if (reported[invitee]) { 208 return; 209 } 210 reported[invitee] = true; 211 } 212 213 if (!current || (CFG.salesSlugs || []).indexOf(current.slug) === -1) { 214 return; 215 } 216 217 var attribution = (window.rjhf && typeof window.rjhf.attribution === 'function') ? window.rjhf.attribution() : {}; 218 var event = { 219 event: 'strategy_call_booked', 220 funnel_product: productFor(current), 221 funnel_page: CFG.page || '', 222 funnel_landing_page: attribution.landing_page || '', 223 funnel_source: attribution.utm_source || '', 224 calendly_event_type: current.slug 225 }; 226 227 // Staff bookings still open the popup; they just aren't reported, the 228 // same rule as the container itself and the purchase events. 229 if (CFG.report) { 230 window.dataLayer = window.dataLayer || []; 231 window.dataLayer.push(event); 232 } 233 234 document.dispatchEvent(new CustomEvent('rjhf:strategy_call_booked', { detail: event })); 235 }); 236 237 // Fetch the widget ahead of the click wherever the page already has a 238 // booking link, so the popup opens instantly â a mobile tap leaves no time 239 // to load it between touch and click. Links rendered later (Alpine modals) 240 // load it on first hover/focus/touch, or on the click itself. 241 function hasBookingLink() {
242 var links = document.querySelectorAll('a[href*="calendly.com/"]'); 243 for (var i = 0; i < links.length; i++) { 244 if (bookingUrl(links[i].href)) { 245 return true; 246 } 247 } 248 return false; 249 } 250 251 function warm(e) { 252 var link = e.target && e.target.closest ? e.target.closest('a[href*="calendly.com/"]') : null; 253 if (link && bookingUrl(link.href)) { 254 loadWidget().catch(function () {}); 255 } 256 } 257 258 document.addEventListener('pointerover', warm, { passive: true }); 259 document.addEventListener('focusin', warm); 260 document.addEventListener('touchstart', warm, { passive: true }); 261 262 function preload() { 263 if (hasBookingLink()) { 264 loadWidget().catch(function () {}); 265 } 266 } 267 268 if (document.readyState === 'loading') { 269 document.addEventListener('DOMContentLoaded', preload); 270 } else { 271 preload(); 272 } 273})(window, document);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.