PageSourceSearch

https://resources.tallysolutions.com/wp-content/themes/tally/assets/js/chat-widget.js

js tallysolutions.com collected 2026-10-01 10:31:26 UTC 16,453 bytes, 510 lines download raw bytes

1/**
2 * Tally Chat Widget — Options-only UI (floating pills, no chat window)
3 *
4 * Usage:
5 *   <link rel="stylesheet" href="chat-widget.css">
6 *   <script src="chat-widget.js" data-auto-init></script>
7 *
8 * Or manual:
9 *   TallyChatWidget.init({
10 *     delay: 4000,
11 *     onVideoChat: function () { },
12 *     onExpertChat: function () { },
13 *     onAiChat: function () { }
14 *   });
15 */
16(function () {
17  'use strict';
18
19  var STORAGE_KEY = 'tally_chat_prompt_dismissed';
20
21  var TALLY_LOGO = 'https://tallysolutions.store/cdn/shop/files/tally-logo-black.svg?v=1780031137&width=120';
22  var WHATSAPP_ICON = 'https://resources.tallysolutions.com/wp-content/themes/tally/assets/images/icons/whats-app-icon.svg';
23
24  var defaultConfig = {
25    delay: 120000,
26    // Empty = show on all pages (no restrictions)
27    showOnPages: [],
28    headerTitle: 'Chat with us',
29    headerSubtitle: 'Online',
30    greeting: 'Need help choosing the right plan? Pick how you\'d like to get assistance.',
31    subtitle: '',
32    logoUrl: TALLY_LOGO,
33    poweredBy: 'Orbitel',
34    options: [
35      {
36        id: 'video',
37        label: 'Video Chat',
38        description: 'Connect with a sales expert for buying guidance.',
39        action: 'video-chat',
40        className: 'tally-chat-prompt__option--video'
41      },
42      {
43        id: 'whatsapp',
44        label: 'WhatsApp Support',
45        description: 'Get help with technical and product support.',
46        action: 'whatsapp',
47        className: 'tally-chat-prompt__option--expert'
48      },
49      {
50        id: 'ai',
51        label: '24×7 Chat',
52        description: 'Instant answers to product and pricing questions.',
53        action: 'ai-chat',
54        className: 'tally-chat-prompt__option--ai'
55      }
56    ],
57    // Per-option click callbacks (pass these in init)
58    onVideoChat: null,
59    onExpertChat: null,
60    onAiChat: null,
61    // Called for any option after the specific handler above
62    onSelect: null,
63    onDismiss: null,
64    // Default: options-only — 3 floating pills, no chat window
65    variant: 'options-only'
66  };
67
68  function createSVGElement(paths, viewBox) {
69    var ns = 'http://www.w3.org/2000/svg';
70    var svg = document.createElementNS(ns, 'svg');
71    svg.setAttribute('viewBox', viewBox || '0 0 24 24');
72    svg.setAttribute('fill', 'none');
73    svg.setAttribute('stroke', 'currentColor');
74    svg.setAttribute('stroke-width', '2');
75    svg.setAttribute('stroke-linecap', 'round');
76    svg.setAttribute('stroke-linejoin', 'round');
77    svg.setAttribute('aria-hidden', 'true');
78    paths.forEach(function (d) {
79      var path = document.createElementNS(ns, 'path');
80      path.setAttribute('d', d);
81      svg.appendChild(path);
82    });
83    return svg;
84  }
85
86  function createImageIcon(src, alt) {
87    var img = document.createElement('img');
88    img.src = src;
89    img.alt = alt || '';
90    img.setAttribute('aria-hidden', 'true');
91    img.setAttribute('style', 'width: 2rem;');
92    return img;
93  }
94
95  var icons = {
96    tally: createSVGElement([
97      'M12 2L2 7l10 5 10-5-10-5z',
98      'M2 17l10 5 10-5',
99      'M2 12l10 5 10-5'
100    ]),
101    close: createSVGElement(['M18 6L6 18', 'M6 6l12 12']),
102    video: createSVGElement([
103      'M23 7l-7 5 7 5V7z',
104      'M14 5H3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z'
105    ]),
106    whatsapp: createImageIcon(WHATSAPP_ICON, 'WhatsApp'),
107    ai: createSVGElement([
108      'M12 2a4 4 0 0 1 4 4c0 1.5-.8 2.8-2 3.5V12h4a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2h-1v2a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-2H9a2 2 0 0 1-2-2v-1a2 2 0 0 1 2-2h4V9.5A4 4 0 0 1 12 2z'
109    ]),
110    chat: createSVGElement([
111      'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'
112    ]),
113    arrow: createSVGElement(['M9 18l6-6-6-6'])
114  };
115
116  function shouldShowOnPage(config) {
117    var pages = config.showOnPages || [];
118    // No list (or empty) → show on every page
119    if (!pages.length) return true;
120    var path = window.location.pathname.toLowerCase();
121    return pages.some(function (segment) {
122      return path.indexOf(String(segment).toLowerCase()) !== -1;
123    });
124  }
125
126  function wasDismissed() {
127    try {
128      return sessionStorage.getItem(STORAGE_KEY) === '1';
129    } catch (e) {
130      return false;
131    }
132  }
133
134  function markDismissed() {
135    try {
136      sessionStorage.setItem(STORAGE_KEY, '1');
137    } catch (e) { /* ignore */ }
138  }
139
140  function handleOptionClick(config, opt) {
141
142    var namedHandlers = {
143      video: config.onVideoChat,
144      expert: config.onExpertChat,
145      ai: config.onAiChat,
146      'video-chat': config.onVideoChat,
147      'whatsapp': config.onExpertChat,
148      'ai-chat': config.onAiChat
149    };
150    var named = namedHandlers[opt.id] || namedHandlers[opt.action];
151    if (typeof named === 'function') {
152      named(opt);
153    }
154
155    if (typeof config.onSelect === 'function') {
156      config.onSelect(opt);
157    } else if (
158      typeof opt.onClick !== 'function' &&
159      typeof named !== 'function'
160    ) {
161      if(opt.action == "video-chat"){
162        Popin('open');
163      }
164      if(opt.action == "ai-chat"){
165        Popin('close');
166        openOrbitelChat();
167      }
168      if(opt.action == "whatsapp"){
169        Popin('close');
170        window.open("https://api.whatsapp.com/send/?phone=919019910043&text=Hi&type=phone_number&app_absent=0", "_blank");
171      }
172    }
173  }
174
175  /**
176   * iOS-friendly tap binding.
177   * Uses pointer events when available; otherwise touchend + click
178   * with a short lock to avoid double-firing. Cancels if the finger moves
179   * (scroll) more than TAP_MOVE_THRESHOLD.
180   */
181  var TAP_MOVE_THRESHOLD = 10;
182
183  function bindTap(el, handler) {
184    if (!el || typeof handler !== 'function') return;
185
186    var locked = false;
187    var touchStartX = 0;
188    var touchStartY = 0;
189    var moved = false;
190
191    function run(e) {
192      if (locked) return;
193      locked = true;
194      if (e && e.cancelable) e.preventDefault();
195      try {
196        handler(e);
197      } finally {
198        setTimeout(function () {
199          locked = false;
200        }, 400);
201      }
202    }
203
204    if (window.PointerEvent) {
205      el.addEventListener('pointerup', function (e) {
206        if (e.pointerType === 'mouse' && e.button !== 0) return;
207        if (moved) {
208          moved = false;
209          return;
210        }
211        run(e);
212      });
213      el.addEventListener('pointerdown', function (e) {
214        moved = false;
215        touchStartX = e.clientX;
216        touchStartY = e.clientY;
217      });
218      el.addEventListener('pointermove', function (e) {
219        if (
220          Math.abs(e.clientX - touchStartX) > TAP_MOVE_THRESHOLD ||
221          Math.abs(e.clientY - touchStartY) > TAP_MOVE_THRESHOLD
222        ) {
223          moved = true;
224        }
225      });
226      // Fallback click for older Safari / keyboard activate
227      el.addEventListener('click', function (e) {
228        if (!locked) run(e);
229      });
230      return;
231    }
232
233    el.addEventListener('touchstart', function (e) {
234      moved = false;
235      var t = e.touches && e.touches[0];
236      if (t) {
237        touchStartX = t.clientX;
238        touchStartY = t.clientY;
239      }
240    }, { passive: true });
241
242    el.addEventListener('touchmove', function (e) {
243      var t = e.touches && e.touches[0];
244      if (!t) return;
245      if (
246        Math.abs(t.clientX - touchStartX) > TAP_MOVE_THRESHOLD ||
247        Math.abs(t.clientY - touchStartY) > TAP_MOVE_THRESHOLD
248      ) {
249        moved = true;
250      }
251    }, { passive: true });
252
253    el.addEventListener('touchend', function (e) {
254      if (moved) {
255        moved = false;
256        return;
257      }
258      run(e);
259    }, { passive: false });
260
261    el.addEventListener('click', function (e) {
262      if (!locked) run(e);
263    });
264  }
265
266  function buildWidget(config) {
267    var widget = document.createElement('div');
268    widget.className = 'tally-chat-widget';
269    if (config.variant) {
270      widget.classList.add('tally-chat-widget--' + config.variant);
271    }
272    widget.setAttribute('role', 'complementary');
273    widget.setAttribute('aria-label', 'Tally support chat');
274
275    /* Prompt card */
276    var prompt = document.createElement('div');
277    prompt.className = 'tally-chat-prompt';
278    prompt.setAttribute('role', 'dialog');
279    prompt.setAttribute('aria-labelledby', 'tally-chat-prompt-title');
280    prompt.setAttribute('aria-describedby', 'tally-chat-prompt-greeting');
281
282    var header = document.createElement('div');
283    header.className = 'tally-chat-prompt__header';
284
285    var avatar = document.createElement('div');
286    avatar.className = 'tally-chat-prompt__avatar';
287    var logoImg = document.createElement('img');
288    logoImg.src = config.logoUrl;
289    logoImg.alt = 'Tally';
290    avatar.appendChild(logoImg);
291
292    var titleWrap = document.createElement('div');
293    titleWrap.className = 'tally-chat-prompt__title-wrap';
294
295    var title = document.createElement('h3');
296    title.className = 'tally-chat-prompt__title';
297    title.id = 'tally-chat-prompt-title';
298    title.textContent = config.headerTitle;
299
300    var subtitle = document.createElement('p');
301    subtitle.className = 'tally-chat-prompt__subtitle';
302    subtitle.id = 'tally-chat-prompt-subtitle';
303    subtitle.textContent = config.headerSubtitle;
304
305    titleWrap.appendChild(title);
306    titleWrap.appendChild(subtitle);
307
308    var closeBtn = document.createElement('button');
309    closeBtn.className = 'tally-chat-prompt__close';
310    closeBtn.setAttribute('aria-label', 'Dismiss');
311    closeBtn.appendChild(icons.close.cloneNode(true));
312
313    header.appendChild(avatar);
314    header.appendChild(titleWrap);
315    header.appendChild(closeBtn);
316
317    var body = document.createElement('div');
318    body.className = 'tally-chat-prompt__body';
319
320    var message = document.createElement('div');
321    message.className = 'tally-chat-prompt__message';
322
323    var msgAvatar = document.createElement('div');
324    msgAvatar.className = 'tally-chat-prompt__msg-avatar';
325    var msgLogo = document.createElement('img');
326    msgLogo.src = config.logoUrl;
327    msgLogo.alt = '';
328    msgAvatar.appendChild(msgLogo);
329
330    var bubble = document.createElement('p');
331    bubble.className = 'tally-chat-prompt__bubble';
332    bubble.id = 'tally-chat-prompt-greeting';
333    bubble.textContent = config.greeting;
334
335    message.appendChild(msgAvatar);
336    message.appendChild(bubble);
337    body.appendChild(message);
338
339    var optionsList = document.createElement('div');
340    optionsList.className = 'tally-chat-prompt__options';
341    optionsList.setAttribute('role', 'list');
342
343    var optionIcons = { video: icons.video, whatsapp: icons.whatsapp, ai: icons.ai };
344
345    config.options.forEach(function (opt) {
346      var btn = document.createElement('button');
347      btn.className = 'tally-chat-prompt__option ' + (opt.className || '');
348      btn.setAttribute('role', 'listitem');
349      btn.setAttribute('data-action', opt.action);
350      btn.setAttribute('type', 'button');
351
352      var iconWrap = document.createElement('span');
353      iconWrap.className = 'tally-chat-prompt__option-icon';
354      var iconKey = opt.id;
355      if (optionIcons[iconKey]) {
356        iconWrap.appendChild(optionIcons[iconKey].cloneNode(true));
357      }
358
359      var textWrap = document.createElement('span');
360      textWrap.className = 'tally-chat-prompt__option-text';
361
362      var label = document.createElement('span');
363      label.className = 'tally-chat-prompt__option-label';
364      label.textContent = opt.label;
365
366      var desc = document.createElement('span');
367      desc.className = 'tally-chat-prompt__option-desc';
368      desc.textContent = opt.description;
369
370      textWrap.appendChild(label);
371      textWrap.appendChild(desc);
372
373      var arrow = document.createElement('span');
374      arrow.className = 'tally-chat-prompt__option-arrow';
375      arrow.appendChild(icons.arrow.cloneNode(true));
376
377      btn.appendChild(iconWrap);
378      btn.appendChild(textWrap);
379      btn.appendChild(arrow);
380
381      // Enable iOS clickability on button children
382      btn.style.cursor = 'pointer';
383      btn.style.webkitTapHighlightColor = 'transparent';
384      btn.style.touchAction = 'manipulation';
385
386      bindTap(btn, function () {
387        hidePrompt(prompt, true, widget);
388        handleOptionClick(config, opt);
389      });
390
391      optionsList.appendChild(btn);
392    });
393
394    var footer = document.createElement('div');
395    footer.className = 'tally-chat-prompt__footer';
396    var footerText = document.createElement('p');
397    footerText.className = 'tally-chat-prompt__footer-text';
398    footerText.innerHTML = 'Powered by <strong>' + config.poweredBy + '</strong>';
399    footer.appendChild(footerText);
400
401    body.appendChild(optionsList);
402    prompt.appendChild(header);
403    prompt.appendChild(body);
404    prompt.appendChild(footer);
405
406    /* Launcher */
407    var launcher = document.createElement('button');
408    launcher.className = 'tally-chat-launcher has-pulse';
409    launcher.setAttribute('aria-label', 'Open Tally support chat');
410    launcher.setAttribute('type', 'button');
411
412    var chatIcon = icons.chat.cloneNode(true);
413    chatIcon.classList.add('tally-chat-launcher__icon-chat');
414    var closeIcon = icons.close.cloneNode(true);
415    closeIcon.classList.add('tally-chat-launcher__icon-close');
416
417    launcher.appendChild(chatIcon);
418    launcher.appendChild(closeIcon);
419
420    widget.appendChild(prompt);
421    widget.appendChild(launcher);
422
423    /* Events — iOS-compatible taps */
424    closeBtn.style.cursor = 'pointer';
425    closeBtn.style.webkitTapHighlightColor = 'transparent';
426    closeBtn.style.touchAction = 'manipulation';
427    bindTap(closeBtn, function () {
428      hidePrompt(prompt, true, widget);
429      if (typeof config.onDismiss === 'function') config.onDismiss();
430    });
431
432    launcher.style.cursor = 'pointer';
433    launcher.style.webkitTapHighlightColor = 'transparent';
434    launcher.style.touchAction = 'manipulation';
435    bindTap(launcher, function () {
436      var isVisible = prompt.classList.contains('is-visible');
437      if (isVisible) {
438        hidePrompt(prompt, false, widget);
439      } else {
440        Popin('close');
441        showPrompt(prompt, launcher, widget);
442      }
443    });
444
445    document.addEventListener('keydown', function (e) {
446      if (e.key === 'Escape' && prompt.classList.contains('is-visible')) {
447        hidePrompt(prompt, true, widget);
448      }
449    });
450
451    return { widget: widget, prompt: prompt, launcher: launcher };
452  }
453
454  function showPrompt(prompt, launcher, widget) {
455    prompt.classList.remove('is-minimized');
456    prompt.classList.add('is-visible');
457    launcher.classList.remove('has-pulse');
458    launcher.setAttribute('aria-expanded', 'true');
459    if (widget) widget.classList.add('is-open');
460  }
461
462  function hidePrompt(prompt, dismiss, widget) {
463    prompt.classList.remove('is-visible');
464    prompt.classList.add('is-minimized');
465    if (widget) widget.classList.remove('is-open');
466    if (dismiss) markDismissed();
467  }
468
469  function init(userConfig) {
470    userConfig = userConfig || {};
471    var config = Object.assign({}, defaultConfig, userConfig);
472    var forceShow = !!userConfig.forceShow;
473
474    if (!shouldShowOnPage(config) && !forceShow) {
475      return null;
476    }
477
478    var existing = document.querySelector('.tally-chat-widget');
479    if (existing) existing.remove();
480
481    var built = buildWidget(config);
482    document.body.appendChild(built.widget);
483
484    if (!wasDismissed() || forceShow) {
485      setTimeout(function () {
486        showPrompt(built.prompt, built.launcher, built.widget);
487      }, config.delay);
488    }
489
490    return built;
491  }
492
493  window.TallyChatWidget = {
494    init: init,
495    defaultConfig: defaultConfig,
496    hidePrompt: function () {
497      var prompt = document.querySelector('.tally-chat-prompt');
498      var widget = document.querySelector('.tally-chat-widget');
499      if (prompt) hidePrompt(prompt, true, widget);
500    }
501  };
502
503  if (document.currentScript && document.currentScript.hasAttribute('data-auto-init')) {
504    if (document.readyState === 'loading') {
505      document.addEventListener('DOMContentLoaded', function () { init(); });
506    } else {
507      init();
508    }
509  }
510})();

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.