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