1const POPUP_SESSION_KEY_PREFIX = 'banner-popup-seen-'; 2 3function heapTrack(name) { 4 if (typeof window.heap !== 'undefined' && window.heap.track) { 5 window.heap.track(name); 6 } 7} 8 9function getPopUpPositionStyles(placement) { 10 const px = '24px'; 11 switch (placement) { 12 case 'upper_left': 13 return { top: px, left: px }; 14 case 'upper_right': 15 return { top: px, right: px }; 16 case 'lower_left': 17 return { bottom: px, left: px }; 18 case 'lower_right': 19 return { bottom: px, right: px }; 20 case 'center': 21 default: 22 return { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }; 23 } 24} 25 26class SiteBanner extends HTMLElement { 27 async connectedCallback() { 28 try { 29 const organizationId = this.getOrganizationId(); 30 if (!organizationId) return; 31 32 const banners = await this.fetchBanner(organizationId); 33 if (!banners || banners.length === 0) return; 34 35 const locationId = this.getLocationId(); 36 const visibleBanners = banners.filter((b) => this.isVisibleForLocation(b, locationId)); 37 38 const bannerEntry = visibleBanners.find((b) => b.announcement_type === 'banner'); 39 const popUpEntry = visibleBanners.find((b) => b.announcement_type === 'pop_up'); 40 41 if (bannerEntry?.message_body) { 42 this.hideDefaultBanner(); 43 this.render(bannerEntry); 44 } 45 46 if (popUpEntry?.message_body && !sessionStorage.getItem(POPUP_SESSION_KEY_PREFIX + organizationId)) { 47 this.renderPopUp(popUpEntry, organizationId); 48 } 49 } catch (error) { 50 console.error('Banner: Failed to initialize', error); 51 } 52 } 53 54 getOrganizationId() { 55 return document.body.getAttribute('data-organization-id') || 56 document.querySelector('meta[name="organization-id"]')?.getAttribute('content') || 57 window.organizationId || 58 null; 59 } 60 61 // The chowly-api partner.id of the location subpage being rendered, or null on 62 // the org homepage / listing page where no data-location-id attribute is set. 63 // Rendered into <body data-location-id="..."> by the shared theme layout. 64 getLocationId() { 65 const raw = document.body.getAttribute('data-location-id'); 66 return raw != null && raw !== '' ? raw : null; 67 } 68 69 // A banner is visible on the current page when it is org-wide (location_id is 70 // null/blank -> shown everywhere) or its location_id matches the current 71 // location's partner id. The API emits location_id as an integer, while the 72 // DOM attribute is a string, so both are normalized to strings before 73 // comparison. 74 isVisibleForLocation(banner, locationId) { 75 const bannerLocationId = banner?.location_id; 76 if (bannerLocationId == null || bannerLocationId === '') return true; 77 if (locationId == null) return false; 78 return String(bannerLocationId) === String(locationId); 79 } 80 81 async fetchBanner(organizationId) { 82 const isPreview = window.location.origin === window.siteHost; 83 const url = `${window.chowlyApiHost}/api/v3/public/banners?organization_id=${organizationId}${isPreview ? '&preview=1' : ''}`; 84 85 const response = await fetch(url); 86 if (!response.ok) throw new Error(`API request failed: ${response.status}`); 87 88 const data = await response.json(); 89 const banners = Array.isArray(data) 90 ? data 91 : (Array.isArray(data?.banners) 92 ? data.banners 93 : (data && typeof data === 'object' && data.announcement_type ? [data] : [])); 94 return banners; 95 } 96 97 hideDefaultBanner() { 98 const defaultBanner = document.querySelector('[data-maglev-section-type="site_notification_01"]'); 99 if (defaultBanner) { 100 defaultBanner.style.display = 'none'; 101 } 102 } 103 render(banner) { 104 const { message_body, cta_link_url, cta_text, ticker_tape_style, icon } = banner; 105 if (!message_body) return; 106 107 this.className = 'w-full h-[40px] flex justify-center items-center px-4 col-span-2'; 108 this.style.backgroundColor = 'var(--brand-primary, #000)'; 109 this.setAttribute('role', 'banner'); 110 this.setAttribute('aria-label', 'Site notification banner'); 111 112 if (ticker_tape_style) { 113 this.style.overflow = 'hidden'; 114 } 115 116 const safeIconClass = icon && /^[a-zA-Z0-9\-_\s]+$/.test(icon) ? icon.trim() : ''; 117 const messageContent = `${message_body}${cta_link_url ? ` 118 <a href="${cta_link_url}" 119 class="text-white text-[14px] font-normal leading-[24px] underline hover:no-underline ml-2" 120 target="_blank" 121 rel="noopener noreferrer"> 122 ${cta_text || cta_link_url} 123 </a> 124 ` : ''}`; 125 126 const textClasses = 'text-white text-[14px] font-normal leading-[24px]'; 127 const textCenterClasses = ticker_tape_style ? '' : 'text-center line-clamp-2 overflow-hidden'; 128 const textStyle = `display: inline-block; ${ticker_tape_style ? 'white-space: nowrap;' : ''}`; 129 130 const iconBlock = safeIconClass 131 ? `<div class="w-[30px] h-[30px] overflow-hidden shrink-0 flex items-center justify-center"><span class="text-white ${safeIconClass}" aria-hidden="true"></span></div>` 132 : ''; 133 134 if (ticker_tape_style && safeIconClass) { 135 this.innerHTML = ` 136 <section class="flex items-center max-w-[1440px] w-full justify-start">
137 <span class="message ${textClasses} ${textCenterClasses} inline-flex items-center gap-5 flex-nowrap" style="white-space: nowrap;"> 138 ${iconBlock} 139 <span>${messageContent}</span> 140 </span> 141 </section> 142 `; 143 } else if (safeIconClass) { 144 this.innerHTML = ` 145 <section class="flex items-center gap-[2px] max-w-[1440px] w-full justify-center"> 146 <div class="w-[30px] h-[30px] overflow-hidden shrink-0 flex items-center justify-center"> 147 <span class="text-white ${safeIconClass}" aria-hidden="true"></span> 148 </div> 149 <span class="message ${textClasses} ${textCenterClasses}" style="${textStyle}">${messageContent}</span> 150 </section> 151 `; 152 } else { 153 this.innerHTML = ` 154 <section class="flex items-center space-x-2 max-w-[1440px] w-full ${ticker_tape_style ? 'justify-start' : 'justify-center'}"> 155 <span class="message ${textClasses} ${textCenterClasses}" style="${textStyle}">${messageContent}</span> 156 </section> 157 `; 158 } 159 160 if (ticker_tape_style) { 161 this.setupTickerAnimation(); 162 } 163 } 164 165 renderPopUp(banner, organizationId) { 166 if (!document.getElementById('banner-popup-styles')) { 167 const style = document.createElement('style'); 168 style.id = 'banner-popup-styles'; 169 style.textContent = ` 170 .banner-popup-overlay { 171 position: fixed; 172 inset: 0; 173 background: var(--dim-overlay, rgba(0,0,0,0.4)); 174 z-index: 9999; 175 } 176 .banner-popup-modal { 177 position: absolute; 178 width: 350px; 179 max-width: calc(100vw - 2rem); 180 background: var(--bg-primary, #fff); 181 color: var(--text-primary, #212121); 182 border-radius: 4px; 183 box-shadow: 0 20px 24px -4px rgba(10, 13, 18, 0.10), 0 8px 8px -4px rgba(10, 13, 18, 0.04); 184 overflow: hidden; 185 display: flex; 186 flex-direction: column; 187 max-height: 90vh; 188 } 189 @media (max-width: 640px) { 190 .banner-popup-modal { 191 width: calc(100vw - 2rem) !important; 192 left: 50% !important; 193 top: 50% !important; 194 right: auto !important; 195 bottom: auto !important; 196 transform: translate(-50%, -50%) !important; 197 } 198 } 199 .banner-popup-modal img.banner-popup-image { width: 100%; display: block; max-height: 240px; object-fit: contain; object-position: center; background: var(--bg-secondary, #f2f2f2); } 200 .banner-popup-body { padding: 24px; flex: 1; overflow: auto; } 201 .banner-popup-header { font-size: 18px; font-weight: 600; margin-bottom: 8px; text-align: center; } 202 .banner-popup-message { font-size: 14px; line-height: 24px; text-align: center; } 203 .banner-popup-cta { margin-top: 16px; margin-bottom: 16px; } 204 .banner-popup-cta.banner-popup-cta-standalone { margin-bottom: 0; } 205 .banner-popup-cta .banner-popup-form-button { width: 100%; padding: 10px 16px; background: var(--brand-primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; text-align: center; text-decoration: none; display: block; box-sizing: border-box; } 206 .banner-popup-form { margin-top: 10px; } 207 .banner-popup-form input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid var(--border-secondary); border-radius: 8px; background: var(--bg-form); color: var(--text-primary); font-size: 14px; } 208 .banner-popup-form label { display: block; font-size: 14px; margin-bottom: 4px; color: var(--text-secondary); } 209 .banner-popup-form button, .banner-popup-form .banner-popup-form-button { width: 100%; padding: 10px 16px; background: var(--brand-primary); color: white; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; text-align: center; text-decoration: none; display: block; box-sizing: border-box; } 210 .banner-popup-form .banner-popup-form-button { margin-top: 0; } 211 .banner-popup-close { 212 position: absolute; 213 top: 10px; 214 right: 10px; 215 z-index: 1; 216 padding: 10px; 217 min-width: 44px; 218 min-height: 44px; 219 box-sizing: border-box; 220 border: none; 221 background: transparent; 222 color: var(--text-primary, #212121); 223 cursor: pointer; 224 display: inline-flex; 225 align-items: center; 226 justify-content: center; 227 } 228 .banner-popup-close:hover { opacity: 0.8; } 229 .banner-popup-close-over-image {
230 background: var(--dim-overlay, rgba(0, 0, 0, 0.35)); 231 color: #fff; 232 border-radius: 50%; 233 } 234 .banner-popup-close-over-image:hover { opacity: 0.9; } 235 .banner-popup-close-until-image-loads { 236 opacity: 0; 237 pointer-events: none; 238 transition: opacity 0.2s ease; 239 } 240 .banner-popup-close svg { width: 24px; height: 24px; } 241 `; 242 document.head.appendChild(style); 243 } 244 245 const placement = banner.pop_up_placement || 'center'; 246 const posStyles = getPopUpPositionStyles(placement); 247 248 const overlay = document.createElement('div'); 249 overlay.className = 'banner-popup-overlay'; 250 overlay.setAttribute('role', 'dialog'); 251 overlay.setAttribute('aria-modal', 'true'); 252 253 const modal = document.createElement('div'); 254 modal.className = 'banner-popup-modal'; 255 Object.assign(modal.style, posStyles); 256 257 const closePopup = () => { 258 document.removeEventListener('keydown', escapeHandler); 259 overlay.remove(); 260 }; 261 const escapeHandler = (e) => { 262 if (e.key === 'Escape') { 263 heapTrack('popup_dismiss'); 264 closePopup(); 265 } 266 }; 267 268 const closeBtn = document.createElement('button'); 269 closeBtn.type = 'button'; 270 closeBtn.className = 'banner-popup-close'; 271 if (banner.image_url) { 272 closeBtn.classList.add('banner-popup-close-over-image', 'banner-popup-close-until-image-loads'); 273 } 274 closeBtn.setAttribute('aria-label', 'Close popup'); 275 closeBtn.innerHTML = '<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path></svg>'; 276 closeBtn.addEventListener('click', (e) => { 277 e.stopPropagation(); 278 heapTrack('popup_dismiss'); 279 closePopup(); 280 }); 281 modal.appendChild(closeBtn); 282 283 if (banner.image_url) { 284 const img = document.createElement('img'); 285 img.alt = ''; 286 img.className = 'banner-popup-image'; 287 const showCloseBtn = () => closeBtn.classList.remove('banner-popup-close-until-image-loads'); 288 img.onload = showCloseBtn; 289 img.onerror = function () { 290 console.warn('[Banner pop-up] Image failed to load:', banner.image_url, '- Check DevTools â Network for that request (e.g. 403 = forbidden, 404 = not found).'); 291 showCloseBtn(); 292 }; 293 img.src = banner.image_url; 294 if (img.complete) showCloseBtn(); 295 modal.appendChild(img); 296 } 297 298 const body = document.createElement('div'); 299 body.className = 'banner-popup-body'; 300 301 if (banner.header) { 302 const h = document.createElement('div'); 303 h.className = 'banner-popup-header'; 304 h.textContent = banner.header; 305 body.appendChild(h); 306 } 307 308 const msg = document.createElement('div'); 309 msg.className = 'banner-popup-message'; 310 msg.textContent = banner.message_body; 311 body.appendChild(msg); 312 313 const hasForm = banner.phone_field || banner.email_field; 314 315 if (banner.cta_link_url && !hasForm) { 316 const cta = document.createElement('div'); 317 cta.className = 'banner-popup-cta banner-popup-cta-standalone'; 318 const a = document.createElement('a'); 319 a.href = banner.cta_link_url; 320 a.target = '_blank'; 321 a.rel = 'noopener noreferrer'; 322 a.className = 'banner-popup-form-button'; 323 a.textContent = banner.cta_text || banner.cta_link_url; 324 a.addEventListener('click', (e) => { 325 heapTrack('popup_cta_click'); 326 closePopup(); 327 }); 328 cta.appendChild(a); 329 body.appendChild(cta); 330 } 331 332 if (hasForm) { 333 const form = document.createElement('form'); 334 form.className = 'banner-popup-form'; 335 if (banner.email_field) { 336 const label = document.createElement('label'); 337 label.textContent = 'Email address'; 338 form.appendChild(label); 339 const input = document.createElement('input'); 340 input.type = 'email'; 341 input.placeholder = 'Email address'; 342 input.name = 'email'; 343 form.appendChild(input); 344 } 345 if (banner.phone_field) { 346 const label = document.createElement('label'); 347 label.textContent = 'Phone number'; 348 form.appendChild(label); 349 const input = document.createElement('input'); 350 input.type = 'tel'; 351 input.placeholder = 'Phone number'; 352 input.name = 'phone'; 353 form.appendChild(input); 354 } 355 const buttonLabel = banner.cta_text || 'Submit'; 356 if (banner.cta_link_url) { 357 const linkBtn = document.createElement('a'); 358 linkBtn.href = banner.cta_link_url; 359 linkBtn.target = '_blank'; 360 linkBtn.rel = 'noopener noreferrer'; 361 linkBtn.className = 'banner-popup-form-button'; 362 linkBtn.textContent = buttonLabel; 363 linkBtn.addEventListener('click', () => { 364 heapTrack('popup_cta_click'); 365 closePopup(); 366 }); 367 form.appendChild(linkBtn); 368 } else { 369 const submit = document.createElement('button'); 370 submit.type = 'submit'; 371 submit.textContent = buttonLabel; 372 form.appendChild(submit); 373 } 374 form.addEventListener('submit', (e) => { 375 e.preventDefault(); 376 heapTrack('popup_form_submit'); 377 closePopup(); 378 }); 379 body.appendChild(form); 380 } 381 382 modal.appendChild(body); 383 overlay.appendChild(modal); 384 385 try { 386 sessionStorage.setItem(POPUP_SESSION_KEY_PREFIX + organizationId, '1'); 387 } catch (err) {} 388 389 overlay.addEventListener('click', (e) => { 390 if (e.target === overlay) { 391 heapTrack('popup_dismiss'); 392 closePopup(); 393 } 394 }); 395 modal.addEventListener('click', (e) => e.stopPropagation()); 396 document.addEventListener('keydown', escapeHandler); 397 398 document.body.appendChild(overlay); 399 heapTrack('popup_open'); 400 } 401 402 setupTickerAnimation() { 403 if (!document.getElementById('banner-ticker-animation')) { 404 const style = document.createElement('style'); 405 style.id = 'banner-ticker-animation'; 406 style.textContent = ` 407 @keyframes ticker { 408 from { transform: translateX(0); } 409 to { transform: translateX(-100%); } 410 } 411 412 site-banner .message { 413 padding-left: 100%; 414 animation: ticker 20s linear infinite; 415 } 416 `; 417 document.head.appendChild(style); 418 } 419 } 420} 421 422customElements.define('site-banner', SiteBanner); 423 424if (document.readyState === 'loading') { 425 document.addEventListener('DOMContentLoaded', () => { 426 const skipLink = document.querySelector('a[href="#main-content"]'); 427 const insertionPoint = skipLink?.nextElementSibling || document.body.firstChild;
428 insertionPoint?.insertAdjacentHTML('beforebegin', '<site-banner></site-banner>'); 429 }); 430} else { 431 const skipLink = document.querySelector('a[href="#main-content"]'); 432 const insertionPoint = skipLink?.nextElementSibling || document.body.firstChild; 433 insertionPoint?.insertAdjacentHTML('beforebegin', '<site-banner></site-banner>'); 434};
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.