1(function () { 2 3 const STORAGE_KEYS = { 4 FIRST_LANDING_URL: "first_landing_url", 5 NOTIFICATION_SHOWN: "chat_widget_shown", 6 }; 7 8 const IDS = { 9 SCRIPT: "chat-widget-loader", 10 BUBBLE: "custom-chat-bubble", 11 IFRAME: "custom-chat-iframe", 12 POPUP: "chat-popup-notification", 13 POPUP_CLOSE: "close-notification-message-popup", 14 }; 15 16 const ROUTES = { 17 WIDGET_CONFIG: "/inbox/api/v1/public/chatwidget/config/", 18 CHAT_WIDGET: "/public/chatwidget/", 19 }; 20 21 const API_GLOBAL = "$zinng"; 22 23 const BUBBLE_CLASS = "customChatWidgetChatBubble"; 24 // Phones only: iPad Mini and other tablets are 768px wide and should use the floating widget. 25 const MOBILE_MAX_WIDTH = 767; 26 const IFRAME_POSITION = "right"; 27 28 const DEFAULT_BUBBLE_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M20 2H4c-1.1 0-2 .9-2 2v14l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>`; 29 const CLOSE_BUBBLE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="24" height="24" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512 298.04" fill="#fff"><path fill-rule="nonzero" d="M12.08 70.78c-16.17-16.24-16.09-42.54.15-58.7 16.25-16.17 42.54-16.09 58.71.15L256 197.76 441.06 12.23c16.17-16.24 42.46-16.32 58.71-.15 16.24 16.16 16.32 42.46.15 58.7L285.27 285.96c-16.24 16.17-42.54 16.09-58.7-.15L12.08 70.78z"/></svg>`; 30 31 const scriptEl = document.getElementById(IDS.SCRIPT); 32 const widgetUuid = scriptEl.dataset.id; 33 const baseUrl = scriptEl.dataset.baseUrl; 34 35 let isMobile = window.innerWidth <= MOBILE_MAX_WIDTH; 36 let isIframeVisible = false; 37 let isIframeLoaded = false; 38 let activeBubbleIcon = DEFAULT_BUBBLE_ICON; 39 let originalTitle = null; 40 let unreadCount = 0; 41 let isWidgetReady = false; 42 let pendingMessage = null; 43 44 function url(path) { 45 return baseUrl + path; 46 } 47 48 function injectStyles(css) { 49 const style = document.createElement("style"); 50 style.textContent = css; 51 document.head.appendChild(style); 52 } 53 54 function storeFirstLandingUrl() { 55 if (!sessionStorage.getItem(STORAGE_KEYS.FIRST_LANDING_URL)) { 56 sessionStorage.setItem( 57 STORAGE_KEYS.FIRST_LANDING_URL, 58 JSON.stringify({ url: window.location.href }) 59 ); 60 } 61 } 62 63 function createBubble() { 64 const bubble = document.createElement("div"); 65 bubble.id = IDS.BUBBLE; 66 bubble.className = BUBBLE_CLASS; 67 bubble.style.display = "flex"; 68 bubble.innerHTML = DEFAULT_BUBBLE_ICON; 69 return bubble; 70 } 71 72 async function fetchConfig() { 73 const res = await fetch(url(`${ROUTES.WIDGET_CONFIG}${widgetUuid}`)); 74 if (!res.ok) throw new Error(`Config fetch failed: ${res.status}`); 75 return res.json(); 76 } 77 78 function applyConfig(config) { 79 if (config.bubble_styling) { 80 injectStyles(config.bubble_styling); 81 } 82 if (config.bubble_icon) { 83 activeBubbleIcon = config.bubble_icon; 84 bubble.innerHTML = config.bubble_icon; 85 } 86 if (config.notification_message) { 87 maybeShowPopup(config.notification_message); 88 } 89 } 90 91 function showBubble() { 92 bubble.style.display = "flex"; 93 bubble.style.opacity = "1"; 94 bubble.style.pointerEvents = "auto"; 95 syncUnreadBadge(); 96 } 97 98 function syncUnreadBadge() { 99 const showBadge = unreadCount > 0 && !isIframeVisible; 100 if (showBadge) { 101 const displayCount = unreadCount > 9 ? "9+" : String(unreadCount); 102 bubble.dataset.unreadCount = displayCount; 103 bubble.setAttribute( 104 "aria-label", 105 `Open chat, ${unreadCount} unread ${unreadCount === 1 ? "message" : "messages"}` 106 ); 107 return; 108 } 109 110 delete bubble.dataset.unreadCount; 111 bubble.setAttribute("aria-label", isIframeVisible ? "Close chat" : "Open chat"); 112 } 113 114 function startBubblePulse() { 115 bubble.classList.add("zinng-bubble--loading"); 116 bubble.style.pointerEvents = "none"; 117 } 118 119 function stopBubblePulse() { 120 bubble.classList.remove("zinng-bubble--loading"); 121 bubble.style.pointerEvents = "auto"; 122 } 123 124 function syncBubbleOnResize() { 125 if (isMobile && isIframeVisible) { 126 bubble.style.display = "none"; 127 } else { 128 bubble.style.display = "flex"; 129 if (!isMobile && isIframeVisible) { 130 bubble.style.transform = "scale(1.2)"; 131 bubble.innerHTML = CLOSE_BUBBLE_ICON; 132 } else { 133 bubble.style.transform = "scale(1)"; 134 bubble.innerHTML = activeBubbleIcon; 135 } 136 } 137 syncUnreadBadge(); 138 } 139 140 function getMobileHeight() {
141 if (window.visualViewport) { 142 return window.visualViewport.height + window.visualViewport.offsetTop; 143 } 144 return window.innerHeight; 145 } 146 147 function applyIframeMobileStyles(iframe) { 148 if (isMobile) { 149 iframe.style.width = "100vw"; 150 iframe.style.height = `${getMobileHeight()}px`; 151 iframe.style.top = "0"; 152 iframe.style.left = "0"; 153 iframe.style.right = "0"; 154 iframe.style.bottom = "0"; 155 iframe.style.borderRadius = "0"; 156 iframe.style.zIndex = "10000"; 157 } else { 158 iframe.style.width = "400px"; 159 iframe.style.height = "min(720px, calc(100dvh - 90px))"; 160 iframe.style.top = "auto"; 161 iframe.style.left = "auto"; 162 iframe.style.bottom = "90px"; 163 iframe.style[IFRAME_POSITION] = "20px"; 164 iframe.style.borderRadius = "8px"; 165 iframe.style.zIndex = "1000"; 166 } 167 } 168 169 function setIframeVisible(show) { 170 const iframe = document.getElementById(IDS.IFRAME); 171 if (!iframe) return; 172 173 isIframeVisible = show; 174 iframe.style.display = show ? "block" : "none"; 175 176 if (isMobile) { 177 bubble.style.display = show ? "none" : "flex"; 178 } else { 179 bubble.style.transform = show ? "scale(1.2)" : "scale(1)"; 180 bubble.innerHTML = show ? CLOSE_BUBBLE_ICON : activeBubbleIcon; 181 } 182 183 if (iframe.contentWindow) { 184 iframe.contentWindow.postMessage({ action: "stateUpdate", isVisible: show }, "*"); 185 } 186 syncUnreadBadge(); 187 } 188 189 function loadIframe() { 190 startBubblePulse(); 191 192 const iframe = document.createElement("iframe"); 193 iframe.id = IDS.IFRAME; 194 iframe.title = "Chat Widget"; 195 iframe.style.cssText = ` 196 position: fixed; bottom: 90px; ${IFRAME_POSITION}: 20px; 197 width: 448px; height: min(640px, calc(100dvh - 110px)); 198 border: none; z-index: 1000; border-radius: 8px; 199 box-shadow: 0 2px 10px rgba(0,0,0,0.3); overflow: hidden; display: none; 200 `; 201 202 iframe.addEventListener("load", function () { 203 stopBubblePulse(); 204 isIframeLoaded = true; 205 applyIframeMobileStyles(iframe); 206 setIframeVisible(true); 207 }, { once: true }); 208 209 iframe.src = url(`${ROUTES.CHAT_WIDGET}?widget_uuid=${widgetUuid}&page_url=${encodeURIComponent(window.location.href)}`); 210 document.body.appendChild(iframe); 211 } 212 213 function openChat() { 214 dismissPopup(); 215 216 if (!isIframeLoaded) { 217 loadIframe(); 218 return; 219 } 220 221 setIframeVisible(true); 222 } 223 224 function hideChat() { 225 if (isIframeLoaded) { 226 setIframeVisible(false); 227 } 228 } 229 230 function flushPendingMessage() { 231 if (pendingMessage === null || !isWidgetReady) return; 232 233 const iframe = document.getElementById(IDS.IFRAME); 234 if (!iframe || !iframe.contentWindow) return; 235 236 iframe.contentWindow.postMessage({ action: "sendMessage", text: pendingMessage }, "*"); 237 pendingMessage = null; 238 } 239 240 function sendChatMessage(text) { 241 const content = typeof text === "string" ? text.trim() : ""; 242 if (!content) return; 243 244 pendingMessage = content; 245 openChat(); 246 flushPendingMessage(); 247 } 248 249 const COMMANDS = { 250 "chat:open": openChat, 251 "chat:show": openChat, 252 "chat:hide": hideChat, 253 "chat:send": sendChatMessage, 254 }; 255 256 function processCommand(command) { 257 if (!Array.isArray(command)) return; 258 259 const [verb, target, payload] = command; 260 if (verb === "do" && typeof COMMANDS[target] === "function") { 261 COMMANDS[target](payload); 262 } 263 } 264 265 function initCommandQueue() { 266 const queue = (window[API_GLOBAL] = Array.isArray(window[API_GLOBAL]) ? window[API_GLOBAL] : []); 267 const queuedCommands = queue.slice(); 268 queue.push = processCommand; 269 queuedCommands.forEach(processCommand); 270 } 271 272 function dismissPopup() { 273 const popup = document.getElementById(IDS.POPUP); 274 if (!popup) return; 275 popup.style.opacity = "0"; 276 popup.style.transform = "translateY(20px)"; 277 setTimeout(() => popup.remove(), 300); 278 } 279 280 function maybeShowPopup(message) { 281 if (sessionStorage.getItem(STORAGE_KEYS.NOTIFICATION_SHOWN)) return; 282 sessionStorage.setItem(STORAGE_KEYS.NOTIFICATION_SHOWN, "true"); 283 284 injectStyles(` 285 @keyframes zinng-fade-in-up { 286 from { opacity: 0; transform: translateY(20px); } 287 50% { opacity: 0.6; transform: translateY(-5px); } 288 to { opacity: 1; transform: translateY(0); } 289 } 290 `); 291 292 const popup = document.createElement("div"); 293 popup.id = IDS.POPUP; 294 popup.style.cssText = `
295 position: fixed; bottom: 100px; right: 20px; 296 background: #007bff; color: #fff; 297 padding: 12px 20px; border-radius: 8px; 298 box-shadow: 0 6px 15px rgba(0,0,0,0.2); 299 font-size: 14px; font-weight: 400; cursor: pointer; 300 display: flex; align-items: center; gap: 10px; 301 max-width: 300px; word-wrap: break-word; 302 transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; 303 animation: zinng-fade-in-up 0.6s ease-out forwards; 304 z-index: 9999; 305 `; 306 307 const text = document.createElement("span"); 308 text.textContent = message; 309 310 const closeBtn = document.createElement("span"); 311 closeBtn.id = IDS.POPUP_CLOSE; 312 closeBtn.textContent = "â"; 313 closeBtn.style.cssText = "margin-left: auto; font-size: 18px; cursor: pointer;"; 314 315 popup.appendChild(text); 316 popup.appendChild(closeBtn); 317 document.body.appendChild(popup); 318 319 closeBtn.addEventListener("click", dismissPopup); 320 setTimeout(dismissPopup, 5000); 321 } 322 323 // -- Initialisation -- 324 325 injectStyles(` 326 @keyframes zinng-pulse { 327 0%, 100% { opacity: 1; } 328 50% { opacity: 0.4; } 329 } 330 .zinng-bubble--loading { 331 animation: zinng-pulse 2s ease-in-out infinite; 332 } 333 #${IDS.BUBBLE}[data-unread-count]::after { 334 content: attr(data-unread-count); 335 position: absolute; 336 top: -6px; 337 right: -6px; 338 min-width: 20px; 339 height: 20px; 340 padding: 0 5px; 341 border: 2px solid #fff; 342 border-radius: 999px; 343 background: #d32f2f; 344 color: #fff; 345 box-sizing: border-box; 346 font: 700 11px/16px Arial, sans-serif; 347 text-align: center; 348 pointer-events: none; 349 } 350 `); 351 352 storeFirstLandingUrl(); 353 354 const bubble = createBubble(); 355 bubble.style.opacity = "0"; 356 bubble.style.pointerEvents = "none"; 357 bubble.style.transition = "opacity 0.3s ease"; 358 359 function mountBubble() { 360 document.body.appendChild(bubble); 361 initCommandQueue(); 362 } 363 364 if (document.readyState === "loading") { 365 document.addEventListener("DOMContentLoaded", mountBubble); 366 } else { 367 mountBubble(); 368 } 369 370 window.addEventListener("load", function () { 371 fetchConfig() 372 .then(applyConfig) 373 .catch(function (err) { 374 console.error("[Zinng] Widget config failed to load:", err); 375 }) 376 .finally(showBubble); 377 }); 378 379 // -- Event Listeners -- 380 381 bubble.addEventListener("click", function (e) { 382 e.stopPropagation(); 383 dismissPopup(); 384 385 if (!isIframeLoaded) { 386 loadIframe(); 387 return; 388 } 389 390 setIframeVisible(!isIframeVisible); 391 }); 392 393 window.addEventListener("click", function (e) { 394 const iframe = document.getElementById(IDS.IFRAME); 395 if ( 396 isIframeVisible && 397 iframe && 398 !iframe.contains(e.target) && 399 !bubble.contains(e.target) 400 ) { 401 setIframeVisible(false); 402 } 403 }, true); 404 405 window.addEventListener("resize", function () { 406 isMobile = window.innerWidth <= MOBILE_MAX_WIDTH; 407 syncBubbleOnResize(); 408 const iframe = document.getElementById(IDS.IFRAME); 409 if (iframe) applyIframeMobileStyles(iframe); 410 }); 411 412 if (window.visualViewport) { 413 window.visualViewport.addEventListener("resize", function () { 414 const iframe = document.getElementById(IDS.IFRAME); 415 if (iframe && isMobile) applyIframeMobileStyles(iframe); 416 }); 417 } 418 419 window.addEventListener("message", function (e) { 420 try { 421 if (!e.data || typeof e.data !== "object") return; 422 423 const iframe = document.getElementById(IDS.IFRAME); 424 425 if (e.data.action === "widgetReady") { 426 isWidgetReady = true; 427 flushPendingMessage(); 428 } 429 430 if (e.data.action === "minimizeChatWidget") { 431 setIframeVisible(!isIframeVisible); 432 } 433 434 if (e.data.action === "adjustIframeHeight" && iframe) { 435 iframe.style.height = e.data.height; 436 } 437 438 if (e.data.action === "resetIframeHeight" && iframe) { 439 iframe.style.height = isMobile ? `${getMobileHeight()}px` : "min(640px, calc(100dvh - 110px))"; 440 } 441 442 if (e.data.action === "updateUnreadTitle") { 443 unreadCount = Math.max(0, Number(e.data.count) || 0); 444 if (originalTitle === null) originalTitle = document.title; 445 document.title = `ð¬ (${unreadCount}) ${originalTitle}`; 446 syncUnreadBadge(); 447 } 448 449 if (e.data.action === "clearUnreadTitle") { 450 unreadCount = 0; 451 if (originalTitle !== null) { 452 document.title = originalTitle; 453 originalTitle = null; 454 } 455 syncUnreadBadge(); 456 } 457 458 if (iframe && isMobile) { 459 applyIframeMobileStyles(iframe); 460 } 461 } catch (err) { 462 console.error("[Zinng] Message handler error:", err); 463 } 464 }); 465 466})();
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.