/** * Libredesk Chat Widget * Embeddable chat widget for websites */ (function () { 'use strict'; if (window.__libredeskWidgetLoaded) { return; } window.__libredeskWidgetLoaded = true; const DEFAULT_LAUNCHER_LOGO_PATH = '/static/public/launcher-logo.png'; class Libredesk { constructor(config = {}) { if (!config.baseURL) { throw new Error('baseURL is required'); } if (!config.inboxID) { throw new Error('inboxID is required'); } this.IFRAME_BORDER_RADIUS = '20px'; this.IFRAME_BOX_SHADOW = 'rgba(9, 14, 21, 0.9) 0px 5px 40px 0px'; this.LAUNCHER_DROP_SHADOW = 'drop-shadow(rgba(9, 14, 21, 0.54) 0px 1px 6px) drop-shadow(rgba(9, 14, 21, 0.9) 0px 2px 32px)'; this.IFRAME_WIDTH = '400px'; this.IFRAME_HEIGHT = '700px'; this.EXPANDED_WIDTH = '750px'; this.MOBILE_BREAKPOINT = 600; this.LAUNCHER_SIZE = 60; this.LAUNCHER_IFRAME_GAP = 20; this.DEFAULT_LAUNCHER_ICON_SCALE = 100; this.LAUNCHER_HOVER_SCALE = 1.08; this.LAUNCHER_OPEN_SCALE = 0.9; this.EASE_OUT = 'cubic-bezier(0.23, 1, 0.32, 1)'; this.EASE_POP = 'cubic-bezier(0.34, 1.56, 0.64, 1)'; this.OPEN_MS = 200; this.CLOSE_MS = 150; this.reducedMotion = Boolean(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); this.MOBILE_LAUNCHER_SIZE = 60; this.CAMPAIGN_POLL_MIN = 2; this.CAMPAIGN_POLL_MAX = 60; this.config = config; this.iframe = null; this.iframeHideTimer = null; this.toggleButton = null; this.isLauncherHovered = false; this.widgetButtonWrapper = null; this.unreadBadge = null; this.isChatVisible = false; this.widgetSettings = null; this.unreadCount = 0; this.previewData = null; this.previewHost = null; this.campaignData = null; this.campaignActiveSeconds = 0; this.campaignURL = location.href; this.campaignEvent = ""; this.campaignDelay = 0; this.campaignBrowserKey = this.getCookie(this.getCookieName('campaign')) || this.randomKey(); this.setCookie(this.getCookieName('campaign'), this.campaignBrowserKey); this.campaignSessionKey = this.getCookie(this.getCookieName('campaign-session')) || this.randomKey(); this.setCampaignSessionCookie(); this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT; this.isExpanded = false; this.hideLauncher = config.hideLauncher || false; this.language = config.language || ''; this.widgetLoaded = false; this._onShowCallback = null; this._onHideCallback = null; this._onUnreadCountChangeCallback = null; this._boundHandleMessage = (e) => this.handleMessage(e); this._boundHandleResize = () => this.handleResize(); this.init(); } // crypto.randomUUID is unavailable on plain-http host pages; getRandomValues is not. randomKey () { if (crypto.randomUUID) return crypto.randomUUID(); const bytes = crypto.getRandomValues(new Uint8Array(16)); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } dropCampaign (id) { this.campaignData = null; this.postToIframe({ type: 'CAMPAIGN_EVENT', event: 'dropped', id }); } postToIframe (data) { if (this.iframe && this.iframe.contentWindow) { this.iframe.contentWindow.postMessage(data, new URL(this.config.baseURL).origin); } } formatBadgeCount (count) { return count > 99 ? '99+' : count.toString(); } getCookieName (type) { return 'libredesk-' + type + '-' + this.config.inboxID; } getCookieDomain () { if (this.config.cookieDomain) return this.config.cookieDomain; if (this._cookieDomain !== undefined) return this._cookieDomain; var hostname = window.location.hostname; if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname) || hostname === 'localhost') { this._cookieDomain = ''; return ''; } var parts = hostname.split('.'); for (var i = parts.length - 1; i >= 0; i--) { var domain = '.' + parts.slice(i).join('.'); document.cookie = '__ld_test__=1;domain=' + domain + ';path=/'; if (document.cookie.indexOf('__ld_test__') !== -1) { document.cookie = '__ld_test__=;domain=' + domain + ';path=/;max-age=0'; this._cookieDomain = domain; return domain; } } this._cookieDomain = ''; return ''; } setCookie (name, value) { var domain = this.getCookieDomain(); var maxAge = 365 * 24 * 60 * 60; var cookie = name + '=' + encodeURIComponent(value) + ';path=/;max-age=' + maxAge + ';SameSite=Lax'; if (domain) { cookie += ';domain=' + domain; } if (window.location.protocol === 'https:') { cookie += ';Secure'; } document.cookie = cookie; } setCampaignSessionCookie () { const domain = this.getCookieDomain(); document.cookie = this.getCookieName('campaign-session') + '=' + this.campaignSessionKey + ';path=/;SameSite=Lax' + (domain ? ';domain=' + domain : '') + (location.protocol === 'https:' ? ';Secure' : ''); } getCookie (name) { var match = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=([^;]*)')); return match ? decodeURIComponent(match[1]) : null; } deleteCookie (name) { var domain = this.getCookieDomain(); var cookie = name + '=;path=/;max-age=0;SameSite=Lax'; if (domain) { cookie += ';domain=' + domain; } document.cookie = cookie; } async init () { try { await this.fetchWidgetSettings(); if (!document.body) { await new Promise((resolve) => { document.addEventListener('DOMContentLoaded', resolve, { once: true }); }); } this.createElements(); this.setLauncherPosition(); this.watchColorScheme(); this.widgetButtonWrapper.style.display = 'none'; this.iframe.addEventListener('load', () => { this.sendMobileState(); }); this.setupMobileDetection(); this.setupEventListeners(); } catch (error) { console.error('Failed to initialize Libredesk Widget:', error); } } async fetchWidgetSettings () { try { const response = await fetch(`${this.config.baseURL}/api/v1/widget/chat/settings/launcher?inbox_id=${this.config.inboxID}`); if (!response.ok) { throw new Error(`Error fetching widget settings. Status: ${response.status}`); } const result = await response.json(); if (result.status !== 'success') { throw new Error('Failed to fetch widget settings'); } this.widgetSettings = result.data; } catch (error) { console.error('Error fetching widget settings:', error); throw error; } } contrastColor (hex) { try { hex = hex.replace(/^#/, ''); if (hex.length === 3) { hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; } var toLinear = function (channel) { var c = parseInt(channel, 16) / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; var r = toLinear(hex.substring(0, 2)); var g = toLinear(hex.substring(2, 4)); var b = toLinear(hex.substring(4, 6)); // Relative luminance per WCAG, gamma-corrected so the 0.179 threshold // reflects perceived brightness instead of raw sRGB values. var L = 0.2126 * r + 0.7152 * g + 0.0722 * b; return L > 0.179 ? '#000000' : '#ffffff'; } catch (e) { return '#ffffff'; } } createElements () { const branding = this.branding(); this.toggleButton = document.createElement('div'); this.toggleButton.style.cssText = ` position: fixed; cursor: pointer; z-index: 9999; width: ${this.launcherSize()}px; height: ${this.launcherSize()}px; background-color: ${this.launcherColor()}; border-radius: 50%; display: flex; justify-content: center; align-items: center; filter: ${this.LAUNCHER_DROP_SHADOW}; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); `; this.iconContainer = document.createElement('div'); this.iconContainer.style.cssText = ` width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; position: relative; `; const iconTransition = `opacity ${this.OPEN_MS}ms ${this.EASE_OUT}, transform ${this.OPEN_MS}ms ${this.EASE_OUT}`; this.defaultIcon = document.createElement('img'); this.defaultIcon.src = branding.launcher?.logo_url || (this.config.baseURL + DEFAULT_LAUNCHER_LOGO_PATH); this.defaultIcon.style.transition = iconTransition; this.styleLauncherIcon(); this.iconContainer.appendChild(this.defaultIcon); this.arrowIcon = document.createElement('div'); const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('width', '24'); svg.setAttribute('height', '24'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('fill', 'none'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M7 10L12 15L17 10'); path.setAttribute('stroke', this.contrastColor(this.launcherColor())); path.setAttribute('stroke-width', '2'); path.setAttribute('stroke-linecap', 'round'); path.setAttribute('stroke-linejoin', 'round'); svg.appendChild(path); this.arrowIcon.appendChild(svg); this.arrowIcon.style.cssText = ` position: absolute; inset: 0; display: flex; justify-content: center; align-items: center; transition: ${iconTransition}; `; this.iconContainer.appendChild(this.arrowIcon); this.setLauncherIcon(false /** open **/); this.toggleButton.appendChild(this.iconContainer); this.unreadBadge = document.createElement('div'); this.unreadBadge.style.cssText = ` position: absolute; top: -5px; right: -5px; background-color: #ef4444; color: white; border-radius: 50%; width: 20px; height: 20px; display: none; justify-content: center; align-items: center; font-size: 12px; font-weight: bold; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; border: 2px solid white; box-sizing: border-box; z-index: 10000; `; const widgetButtonWrapper = document.createElement('div'); widgetButtonWrapper.style.cssText = ` position: fixed; z-index: 9999; `; widgetButtonWrapper.appendChild(this.toggleButton); widgetButtonWrapper.appendChild(this.unreadBadge); this.toggleButton.style.position = 'relative'; this.widgetButtonWrapper = widgetButtonWrapper; this.iframe = document.createElement('iframe'); this.iframe.src = `${this.config.baseURL}/widget?inbox_id=${encodeURIComponent(this.config.inboxID)}&parent_origin=${encodeURIComponent(window.location.origin)}` + (this.language ? `&lang=${encodeURIComponent(this.language)}` : ''); this.iframeLanguage = this.language; this.iframe.title = 'libredesk'; this.iframe.style.cssText = ` position: fixed; border: none; border-radius: ${this.IFRAME_BORDER_RADIUS}; box-shadow: ${this.IFRAME_BOX_SHADOW}; z-index: 9999; width: ${this.IFRAME_WIDTH}; height: ${this.IFRAME_HEIGHT}; transition: ${this.iframeTransition(this.OPEN_MS)}; display: none; `; document.body.appendChild(this.widgetButtonWrapper); document.body.appendChild(this.iframe); } sendMobileState () { this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT; this.postToIframe({ type: 'SET_MOBILE_STATE', isMobile: this.isMobile }); if (this.toggleButton) { this.toggleButton.style.width = this.launcherSize() + 'px'; this.toggleButton.style.height = this.launcherSize() + 'px'; } } launcherSize () { return this.isMobile ? this.MOBILE_LAUNCHER_SIZE : this.LAUNCHER_SIZE; } launcherIconScale () { return this.widgetSettings?.launcher?.icon_scale || this.DEFAULT_LAUNCHER_ICON_SCALE; } styleLauncherIcon () { if (!this.defaultIcon) return; const scale = this.launcherIconScale(); const full = scale >= 100; const style = this.defaultIcon.style; style.width = `${scale}%`; style.height = `${scale}%`; style.borderRadius = full ? '50%' : '0'; style.objectFit = full ? 'cover' : 'contain'; } prefersDark () { return Boolean(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); } branding () { const theme = this.widgetSettings.theme; const isDark = theme === 'dark' || (theme === 'system' && this.prefersDark()); return this.widgetSettings.branding?.[isDark ? 'dark' : 'light'] || {}; } launcherColor () { const branding = this.branding(); return branding.launcher?.color || branding.colors?.primary || '#000000'; } applyLauncherTheme () { if (!this.toggleButton) return; const branding = this.branding(); this.toggleButton.style.backgroundColor = this.launcherColor(); this.defaultIcon.src = branding.launcher?.logo_url || (this.config.baseURL + DEFAULT_LAUNCHER_LOGO_PATH); this.styleLauncherIcon(); const path = this.arrowIcon.querySelector('path'); if (path) path.setAttribute('stroke', this.contrastColor(this.launcherColor())); } watchColorScheme () { if (!window.matchMedia) return; const query = window.matchMedia('(prefers-color-scheme: dark)'); const onChange = () => { if (this.widgetSettings.theme === 'system') this.applyLauncherTheme(); }; // Safari 13.1 and older only have the deprecated addListener. if (query.addEventListener) query.addEventListener('change', onChange); else if (query.addListener) query.addListener(onChange); } iframeBottomOffset () { return this.widgetSettings.launcher.spacing.bottom + this.launcherSize() + this.LAUNCHER_IFRAME_GAP; } getNormalIframeHeight () { return `min(${this.IFRAME_HEIGHT}, calc(100dvh - ${this.iframeBottomOffset() + this.LAUNCHER_IFRAME_GAP}px))`; } sendPageInfo () { this.postToIframe({ type: 'PAGE_VISIT', url: window.location.href, title: document.title || '' }); } setLauncherPosition () { const spacing = this.widgetSettings.launcher.spacing; const side = this.widgetSettings.launcher.position === 'right' ? 'right' : 'left'; this.widgetButtonWrapper.style.bottom = `${spacing.bottom}px`; this.widgetButtonWrapper.style[side] = `${spacing.side}px`; } applyIframeLayout () { if (!this.iframe) return; const iframe = this.iframe; if (this.isMobile) { iframe.style.transformOrigin = 'bottom center'; iframe.style.top = '0'; iframe.style.left = '0'; iframe.style.right = '0'; iframe.style.bottom = '0'; iframe.style.width = '100vw'; iframe.style.height = '100dvh'; iframe.style.borderRadius = '0'; iframe.style.boxShadow = 'none'; return; } const spacing = this.widgetSettings.launcher.spacing; const side = this.widgetSettings.launcher.position === 'right' ? 'right' : 'left'; iframe.style.top = ''; iframe.style.left = ''; iframe.style.right = ''; iframe.style.borderRadius = this.IFRAME_BORDER_RADIUS; iframe.style.boxShadow = this.IFRAME_BOX_SHADOW; iframe.style.transformOrigin = `bottom ${side}`; iframe.style[side] = `${spacing.side}px`; if (this.isExpanded) { iframe.style.width = this.EXPANDED_WIDTH; iframe.style.height = 'calc(100dvh - 40px)'; iframe.style.bottom = '20px'; } else { iframe.style.width = this.IFRAME_WIDTH; iframe.style.height = this.getNormalIframeHeight(); iframe.style.bottom = `${this.iframeBottomOffset()}px`; } } updateLauncherVisibility () { if (!this.widgetButtonWrapper) return; const shouldShow = this.widgetLoaded && !this.hideLauncher && !(this.isChatVisible && this.isMobile); this.widgetButtonWrapper.style.display = shouldShow ? '' : 'none'; } handleMessage (event) { if (event.source !== this.iframe?.contentWindow || event.origin !== new URL(this.config.baseURL).origin || !event.data) return; switch (event.data.type) { case 'SHOW_CAMPAIGN': if (this.isChatVisible || this.unreadCount > 0 || this.hideLauncher) { this.dropCampaign(event.data.delivery.id); break; } this.campaignData = event.data.delivery; this.renderPreviews(); if (this.previewHost) this.postToIframe({ type: 'CAMPAIGN_EVENT', event: 'displayed', id: this.campaignData.id }); else this.dropCampaign(event.data.delivery.id); break; case 'CLEAR_CAMPAIGN': this.campaignData = null; this.renderPreviews(); break; case 'REPLY_PREVIEWS': if (event.data.previews?.length && this.campaignData) this.dropCampaign(this.campaignData.id); this.previewData = event.data; this.renderPreviews(); break; case 'VUE_APP_READY': this.handleVueAppReady(); break; case 'CLOSE_WIDGET': this.hideChat(); break; case 'UPDATE_UNREAD_COUNT': this.updateUnreadCount(event.data.count); break; case 'WIDGET_LOADED': this.handleWidgetLoaded(); if (event.data.campaigns) { this.campaignDelay = Math.min(Math.max(Number(event.data.campaignDelay) || 0, 0), 86400); this.startCampaignTracking(); } break; case 'EXPAND_WIDGET': this.expandWidget(); break; case 'COLLAPSE_WIDGET': this.collapseWidget(); break; case 'REQUEST_PAGE_INFO': this.sendPageInfo(); break; case 'STORE_SESSION': this.setCookie(this.getCookieName('session'), event.data.token); break; case 'STORE_VISITOR_TOKEN': this.setCookie(this.getCookieName('visitor'), event.data.token); break; case 'CLEAR_VISITOR_TOKEN': this.deleteCookie(this.getCookieName('visitor')); break; case 'CLEAR_SESSION_TOKEN': this.deleteCookie(this.getCookieName('session')); break; } } setupEventListeners () { this.toggleButton.addEventListener('click', () => this.toggle()); this.toggleButton.addEventListener('mouseenter', () => { const canHover = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches; if (!canHover) return; this.isLauncherHovered = true; this.applyLauncherScale(); }); this.toggleButton.addEventListener('mouseleave', () => { this.isLauncherHovered = false; this.applyLauncherScale(); }); window.addEventListener('message', this._boundHandleMessage); } handleResize () { const wasMobile = this.isMobile; this.sendMobileState(); this.renderPreviews(); if (this.isChatVisible && wasMobile !== this.isMobile) { this.applyIframeLayout(); this.updateLauncherVisibility(); } } setupMobileDetection () { window.addEventListener('resize', this._boundHandleResize); window.addEventListener('orientationchange', this._boundHandleResize); } handleVueAppReady () { this.startPageTracking(); this.sendMobileState(); var visitorToken = this.getCookie(this.getCookieName('visitor')); if (this.config.userJWT) { this.postToIframe({ type: 'SET_JWT_TOKEN', jwt: this.config.userJWT, visitorToken: visitorToken || '', campaignBrowserKey: this.campaignBrowserKey, campaignSessionKey: this.campaignSessionKey }); return; } var sessionToken = this.getCookie(this.getCookieName('session')); this.postToIframe({ type: 'SESSION_DATA', sessionToken: sessionToken || '', visitorToken: visitorToken || '', campaignBrowserKey: this.campaignBrowserKey, campaignSessionKey: this.campaignSessionKey }); } handleWidgetLoaded () { this.widgetLoaded = true; this.updateLauncherVisibility(); this.renderPreviews(); // setLanguage() may have been called before the iframe loaded. if (this.language !== this.iframeLanguage) { this.postToIframe({ type: 'SET_LANGUAGE', language: this.language }); } } toggle () { if (this.isChatVisible) { this.hideChat(); } else { this.showChat(); } } showChat () { if (!this.iframe) return; this.isMobile = window.innerWidth <= this.MOBILE_BREAKPOINT; this.isChatVisible = true; this.renderPreviews(); const iframe = this.iframe; clearTimeout(this.iframeHideTimer); if (iframe.style.display === 'none') { iframe.style.opacity = '0'; iframe.style.transform = this.iframeHiddenTransform(); iframe.style.display = 'block'; } this.applyIframeLayout(); iframe.style.transition = this.iframeTransition(this.OPEN_MS); // Read layout so the browser commits the hidden start state, else the open transition is skipped. void iframe.offsetWidth; iframe.style.pointerEvents = ''; iframe.style.opacity = '1'; iframe.style.transform = 'none'; this.updateLauncherVisibility(); this.applyLauncherScale(); this.unreadBadge.style.display = 'none'; this.setLauncherIcon(true /** open **/); this.postToIframe({ type: 'WIDGET_OPENED' }); if (this._onShowCallback) this._onShowCallback(); } hideChat () { if (!this.iframe) return; const iframe = this.iframe; iframe.style.transition = this.iframeTransition(this.CLOSE_MS); iframe.style.pointerEvents = 'none'; iframe.style.opacity = '0'; iframe.style.transform = this.iframeHiddenTransform(); clearTimeout(this.iframeHideTimer); this.iframeHideTimer = setTimeout(() => { if (this.iframe && !this.isChatVisible) this.iframe.style.display = 'none'; }, this.CLOSE_MS); this.isChatVisible = false; this.renderPreviews(); this.applyLauncherScale(); this.updateLauncherVisibility(); this.setLauncherIcon(false /** open **/); if (this.unreadCount > 0) this.showUnreadBadge(this.unreadCount); this.postToIframe({ type: 'WIDGET_CLOSED' }); if (this._onHideCallback) this._onHideCallback(); } applyLauncherScale () { if (!this.toggleButton) return; const base = !this.reducedMotion && this.isChatVisible ? this.LAUNCHER_OPEN_SCALE : 1; const scale = !this.reducedMotion && this.isLauncherHovered ? base * this.LAUNCHER_HOVER_SCALE : base; this.toggleButton.style.transform = `scale(${scale})`; } setLauncherIcon (open) { const hidden = (deg) => this.reducedMotion ? 'none' : `rotate(${deg}deg) scale(0.9)`; if (this.defaultIcon) { this.defaultIcon.style.opacity = open ? '0' : '1'; this.defaultIcon.style.transform = open ? hidden(45) : 'none'; } this.arrowIcon.style.opacity = open ? '1' : '0'; this.arrowIcon.style.transform = open ? 'none' : hidden(-45); } iframeTransition (durationMs) { const fade = `opacity ${durationMs}ms ${this.EASE_OUT}`; if (this.reducedMotion) return fade; return `width 0.18s ease, height 0.18s ease, bottom 0.18s ease, border-radius 0.18s ease, box-shadow 0.18s ease, ${fade}, transform ${durationMs}ms ${this.EASE_OUT}`; } iframeHiddenTransform () { return this.reducedMotion ? 'none' : 'translateY(8px) scale(0.95)'; } showUnreadBadge (count) { const badge = this.unreadBadge; const wasHidden = badge.style.display === 'none'; badge.textContent = this.formatBadgeCount(count); badge.style.display = 'flex'; if (!wasHidden || typeof badge.animate !== 'function') return; const from = this.reducedMotion ? { opacity: 0 } : { opacity: 0, transform: 'scale(0.9)' }; const to = this.reducedMotion ? { opacity: 1 } : { opacity: 1, transform: 'none' }; badge.animate([from, to], { duration: this.OPEN_MS, easing: this.EASE_POP }); } updateUnreadCount (count) { this.unreadCount = count; if (this._onUnreadCountChangeCallback) this._onUnreadCountChangeCallback(count); if (count > 0 && !this.isChatVisible) { this.showUnreadBadge(count); } else { this.unreadBadge.style.display = 'none'; } } expandWidget () { if (!this.iframe || !this.isChatVisible || this.isMobile) return; this.isExpanded = true; this.applyIframeLayout(); this.postToIframe({ type: 'WIDGET_EXPANDED', isExpanded: true }); } collapseWidget () { if (!this.iframe || !this.isChatVisible || this.isMobile) return; this.isExpanded = false; this.applyIframeLayout(); this.postToIframe({ type: 'WIDGET_EXPANDED', isExpanded: false }); } startPageTracking () { this._lastPageURL = ''; this._origPushState = history.pushState; this._origReplaceState = history.replaceState; const self = this; const onPageChange = () => { const url = window.location.href; if (url === self._lastPageURL) return; self._lastPageURL = url; // Defer to let SPA frameworks update document.title after route change. setTimeout(() => { self.sendPageInfo(); }, 100); }; history.pushState = function () { self._origPushState.apply(this, arguments); onPageChange(); }; history.replaceState = function () { self._origReplaceState.apply(this, arguments); onPageChange(); }; this._onPopState = onPageChange; this._onHashChange = onPageChange; window.addEventListener('popstate', this._onPopState); window.addEventListener('hashchange', this._onHashChange); this._pageTrackInterval = setInterval(onPageChange, 7000); onPageChange(); } stopPageTracking () { if (this._origPushState) history.pushState = this._origPushState; if (this._origReplaceState) history.replaceState = this._origReplaceState; if (this._onPopState) window.removeEventListener('popstate', this._onPopState); if (this._onHashChange) window.removeEventListener('hashchange', this._onHashChange); if (this._pageTrackInterval) clearInterval(this._pageTrackInterval); } clearPreviews () { this.previewData = null; this.campaignData = null; this.dismissedPreviews = []; this.previewHost?.remove(); this.previewHost = null; this.previewSignature = ''; } dismissPreview (key) { if (this.campaignData?.id === key) { this.postToIframe({ type: 'CAMPAIGN_EVENT', event: 'dismissed', id: key }); this.campaignData = null; this.renderPreviews(); return; } const storageKey = `libredesk-previews-${this.config.inboxID}-${this.previewData?.identity || 'visitor'}`; let dismissed = this.dismissedPreviews || []; try { dismissed = JSON.parse(localStorage.getItem(storageKey) || '[]'); } catch {} dismissed = [...new Set([...dismissed, key])].slice(-200); this.dismissedPreviews = dismissed; try { localStorage.setItem(storageKey, JSON.stringify(dismissed)); } catch {} this.renderPreviews(); } renderPreviews () { let data = this.previewData; const invitation = this.campaignData; if (invitation && this.unreadCount === 0 && data) { const snapshot = invitation.snapshot; data = { ...data, identity: 'campaign', previews: [{ key: invitation.id, campaign: true, name: snapshot.sender, avatar: snapshot.avatar, text: snapshot.message.slice(0, 240), }] }; } if (!data || !this.widgetLoaded || this.isChatVisible || this.hideLauncher) { this.previewHost?.remove(); this.previewHost = null; this.previewSignature = ''; return; } const storageKey = `libredesk-previews-${this.config.inboxID}-${data.identity || 'visitor'}`; let dismissed = this.dismissedPreviews || []; try { dismissed = JSON.parse(localStorage.getItem(storageKey) || '[]'); } catch {} const previews = data.previews.filter(item => !dismissed.includes(item.key)).slice(0, 3); const signature = JSON.stringify([previews, data.theme, data.labels]); if (signature === this.previewSignature) return; this.previewSignature = signature; this.previewHost?.remove(); this.previewHost = null; if (!previews.length) return; const host = document.createElement('div'); const side = this.widgetSettings.launcher.position === 'left' ? 'left' : 'right'; const spacing = this.widgetSettings.launcher.spacing; Object.assign(host.style, { position: 'fixed', zIndex: '9998', bottom: `${spacing.bottom + this.launcherSize() + 12}px`, [side]: `${spacing.side}px`, width: `min(340px, calc(100vw - ${spacing.side * 2}px))` }); host.style.setProperty('--align', side === 'left' ? 'flex-start' : 'flex-end'); host.style.setProperty('--muted', data.theme.muted || data.theme.foreground); const root = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ':host{font:14px/1.45 system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased}button{font:inherit;color:inherit;cursor:pointer;border:0;background:transparent;padding:0}button:focus-visible{outline:2px solid;outline-offset:2px}.stack{display:flex;flex-direction:column;gap:10px;align-items:var(--align)}.card{position:relative;display:flex;max-width:100%;border-radius:18px;box-shadow:0 1px 3px rgba(9,14,21,.12),0 8px 28px rgba(9,14,21,.16);animation:rise .22s ease-out}.open{display:flex;gap:10px;align-items:flex-start;min-width:0;padding:12px 14px;text-align:left;border-radius:inherit}.body{display:flex;flex-direction:column;min-width:0;gap:2px}.name{font-size:12px;line-height:1.3;color:var(--muted)}.text{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere;white-space:pre-line}.avatar{flex:none;width:32px;height:32px;border-radius:50%;object-fit:cover}.image{margin-top:6px;max-width:160px;max-height:96px;border-radius:10px;object-fit:cover}.close{position:absolute;top:-8px;inset-inline-end:-8px;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;line-height:1;color:var(--muted);box-shadow:0 1px 3px rgba(9,14,21,.18);opacity:0;transition:opacity .15s}.card:hover .close,.card:focus-within .close{opacity:1}@media(hover:none){.close{opacity:1}}.all{font-size:12px;color:var(--muted);padding:4px 6px;border-radius:6px}.all:hover{text-decoration:underline}@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(prefers-reduced-motion:reduce){.card{animation:none}}'; root.append(style); const stack = document.createElement('div'); stack.className = 'stack'; const notice = document.createElement('span'); notice.setAttribute('role', 'status'); notice.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)'; notice.textContent = previews[0].text; stack.append(notice); const safeImage = (src, className) => { if (!src) return null; let url; try { url = new URL(src, this.config.baseURL); } catch { return null; } if (!['https:', 'http:'].includes(url.protocol)) return null; const img = document.createElement('img'); img.src = url.href; img.alt = ''; img.className = className; return img; }; for (const item of previews) { const card = document.createElement('div'); card.className = 'card'; Object.assign(card.style, { background: data.theme.background, color: data.theme.foreground }); const open = document.createElement('button'); open.type = 'button'; open.className = 'open'; open.setAttribute('aria-label', `${data.labels.open}: ${item.name}. ${item.text}`); open.addEventListener('click', () => { if (item.campaign) { this.postToIframe({ type: 'CAMPAIGN_EVENT', event: 'opened', id: item.key }); this.campaignData = null; } else this.postToIframe({ type: 'OPEN_CONVERSATION', uuid: item.conversation }); this.showChat(); }); const avatar = safeImage(item.avatar, 'avatar'); if (avatar) open.append(avatar); const body = document.createElement('span'); body.className = 'body'; const name = document.createElement('span'); name.className = 'name'; name.textContent = item.name; const content = document.createElement('span'); content.className = 'text'; content.textContent = item.text; body.append(name, content); const image = safeImage(item.image, 'image'); if (image) body.append(image); open.append(body); const close = document.createElement('button'); close.type = 'button'; close.className = 'close'; close.textContent = '×'; close.setAttribute('aria-label', data.labels.dismiss); Object.assign(close.style, { background: data.theme.background, border: `1px solid ${data.theme.border}` }); close.addEventListener('click', () => { this.dismissPreview(item.key); this.toggleButton.focus(); }); card.append(open, close); stack.append(card); } if (previews.length > 1) { const all = document.createElement('button'); all.type = 'button'; all.className = 'all'; all.textContent = data.labels.dismissAll; all.addEventListener('click', () => { for (const item of previews) this.dismissPreview(item.key); this.toggleButton.focus(); }); stack.append(all); } root.append(stack); document.body.append(host); this.previewHost = host; } startCampaignTracking () { if (this.campaignInterval) return; let last = performance.now(); let elapsed = 0; let asked = false; const firstGap = () => Math.max(this.CAMPAIGN_POLL_MIN, this.campaignDelay); let gap = firstGap(); this.campaignInterval = setInterval(() => { const now = performance.now(); const delta = Math.min((now - last) / 1000, 2); last = now; if (location.href !== this.campaignURL) { this.campaignURL = location.href; this.campaignActiveSeconds = 0; this.campaignEvent = ''; elapsed = 0; asked = false; gap = firstGap(); if (this.campaignData) this.dropCampaign(this.campaignData.id); this.renderPreviews(); } if (document.hidden || this.isChatVisible || this.hideLauncher) return; this.campaignActiveSeconds += delta; elapsed += delta; if (elapsed < gap || this.unreadCount > 0 || this.campaignData) return; elapsed = 0; gap = asked ? Math.min(gap * 2, this.CAMPAIGN_POLL_MAX) : this.CAMPAIGN_POLL_MIN; asked = true; this.postToIframe({ type: 'CAMPAIGN_CONTEXT', context: { url: location.href, mobile: this.isMobile, active_seconds: Math.floor(this.campaignActiveSeconds), event: this.campaignEvent } }); }, 1000); } trackEvent (name) { if (typeof name === 'string' && name.length <= 128) this.campaignEvent = name; } setUser (jwt) { this.clearPreviews(); this.postToIframe({ type: 'SET_JWT_TOKEN', jwt: jwt }); } setLanguage (language) { this.language = language || ''; if (this.widgetLoaded) { this.postToIframe({ type: 'SET_LANGUAGE', language: this.language }); } } logout () { this.clearPreviews(); this.deleteCookie(this.getCookieName('session')); this.deleteCookie(this.getCookieName('visitor')); this.campaignBrowserKey = this.randomKey(); this.setCookie(this.getCookieName('campaign'), this.campaignBrowserKey); this.postToIframe({ type: 'CLEAR_SESSION', campaignBrowserKey: this.campaignBrowserKey }); } destroy () { this.clearPreviews(); clearInterval(this.campaignInterval); this.stopPageTracking(); window.removeEventListener('message', this._boundHandleMessage); window.removeEventListener('resize', this._boundHandleResize); window.removeEventListener('orientationchange', this._boundHandleResize); if (this.widgetButtonWrapper) { document.body.removeChild(this.widgetButtonWrapper); this.widgetButtonWrapper = null; this.toggleButton = null; this.unreadBadge = null; } if (this.iframe) { document.body.removeChild(this.iframe); this.iframe = null; } this.isChatVisible = false; this._onShowCallback = null; this._onHideCallback = null; this._onUnreadCountChangeCallback = null; } } Libredesk.prototype.show = Libredesk.prototype.showChat; Libredesk.prototype.hide = Libredesk.prototype.hideChat; Libredesk.prototype.isVisible = function () { return this.isChatVisible; }; Libredesk.prototype.onShow = function (fn) { this._onShowCallback = fn; }; Libredesk.prototype.onHide = function (fn) { this._onHideCallback = fn; }; Libredesk.prototype.onUnreadCountChange = function (fn) { this._onUnreadCountChangeCallback = fn; fn(this.unreadCount); }; window.Libredesk = Libredesk; window.initLibredesk = function (config = {}) { if (window.Libredesk && window.Libredesk instanceof Libredesk) { console.warn('Libredesk Widget is already initialized'); return window.Libredesk; } window.Libredesk = new Libredesk(config); return window.Libredesk; }; function autoInit () { if (window.LibredeskSettings) { window.initLibredesk(window.LibredeskSettings); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInit, { once: true }); } else { autoInit(); } })();