PageSourceSearch

https://zinng.ai/static/js/chatwidget.js

js zinng.ai collected 2026-10-05 13:21:01 UTC 13,596 bytes, 466 lines download raw bytes

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.