(function() { if (window.zipchatWidgetLoaded) { return; // If already loaded, do not load again } // Track the current URL for SPA navigation detection let lastUrl = window.location.href; // Function to handle URL changes in SPAs function handleUrlChange() { const currentUrl = window.location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; // Clean up existing widget elements cleanupWidget(); // Reload widget with the new URL loadWidget(); // Re-setup URL change detection setupUrlChangeDetection(); } } // Function to clean up the widget elements function cleanupWidget() { // Remove the iframe if it exists const iframe = document.getElementById('zipchat-iframe'); if (iframe) { iframe.remove(); } // Remove the chat container if it exists const container = document.getElementById('widget-chat-container'); if (container) { container.remove(); } // Clear the URL check interval if it exists if (window.zipchatUrlCheckInterval) { clearInterval(window.zipchatUrlCheckInterval); window.zipchatUrlCheckInterval = null; } // Reset the widget loaded flag window.zipchatWidgetLoaded = false; } // Set up URL change detection using non-invasive polling function setupUrlChangeDetection() { // Method 1: Listen for popstate event (back/forward navigation) window.addEventListener('popstate', handleUrlChange); // Method 2: Simple polling to check for URL changes (1000ms interval is responsive without being resource-intensive) const urlCheckInterval = setInterval(handleUrlChange, 1000); // Store the interval ID for cleanup window.zipchatUrlCheckInterval = urlCheckInterval; } function toggleBodyScroll(enable) { if (enable) { document.body.style.overflow = ''; } else { document.body.style.overflow = 'hidden'; } } function handleIframe(iframe, chatBubbleContainer, chatTrigger, svg, crossSvg) { if (chatBubbleContainer.querySelector("#close-preview-message")) { chatBubbleContainer.querySelector('#close-preview-message').click() if (!!chatTrigger?.conversation_message){ iframe.contentWindow.postMessage( { action: "getConversationMessage", origin: window.location.origin, message: chatTrigger.conversation_message}, "*" ); } } if (iframe.style.opacity == 1) { iframe.style.transform = "scale(0)"; iframe.style.opacity = "0"; iframe.style.pointerEvents = 'none'; if (window.innerWidth <= 768) { toggleBodyScroll(true); } // Swap SVGs svg.style.display = "block"; crossSvg.style.display = "none"; } else { iframe.style.transform = "scale(1)"; iframe.style.opacity = "1"; iframe.style.pointerEvents = 'all'; iframe.contentWindow.postMessage( { action: "scrollToBottom", origin: window.location.origin }, "*" ); if (window.innerWidth <= 768) { toggleBodyScroll(false); document.body.style.position = "static"; document.body.style.inset = "0px"; document.body.style.margin = "0px"; } // Swap SVGs svg.style.display = "none"; crossSvg.style.display = "block"; } } function createLoadingSpinner(chatBubbleIcon) { // Create loading spinner var loaderSpan = document.createElement('span'); loaderSpan.className = 'widget-chat-loader'; // Append loading spinner chatBubbleIcon.appendChild(loaderSpan); // Create style element var style = document.createElement('style'); style.innerHTML = ` .widget-chat-loader { width: 24px; height: 24px; border: 3px solid #FFF; border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; // Append style to the document's head document.head.appendChild(style); return loaderSpan; } // Create chatbot iframe function createChatbotIframe( url, widgetHorizontal, widgetVertical, widgetCorner, widgetZIndex, chatBubbleSizeDifference, chatBubbleContainer, chatTrigger, chatBubbleIcon, svg, crossSvg ) { let chatbotIframe = document.getElementById('zipchat-iframe'); if (!chatbotIframe) { svg.style.display = "none"; // Show loading spinner const loaderSpan = createLoadingSpinner(chatBubbleIcon); chatbotIframe = document.createElement('iframe'); chatbotIframe.setAttribute('id', 'zipchat-iframe'); chatbotIframe.classList.add("zipchat-iframe"); chatbotIframe.src = url; chatbotIframe.style.maxheight = '700px'; chatbotIframe.style.border = 'none'; chatbotIframe.style.opacity = "0" chatbotIframe.style.transform = "scale(0)"; chatbotIframe.style.height = `min(704px, 100% - ${84 + widgetVertical}px - ${chatBubbleSizeDifference}px)`; chatbotIframe.style.position = 'fixed'; chatbotIframe.style.pointerEvents = 'none'; chatbotIframe.style.fontSize = '16px'; chatbotIframe.style.boxShadow = "0 6px 6px 0 rgba(0,0,0,.02),0 8px 24px 0 rgba(0,0,0,.12)"; chatbotIframe.style.transformOrigin = widgetCorner.replace('-',' '); chatbotIframe.style.transition = "transform 0.3s cubic-bezier(0, 1.2, 1, 1), opacity 0.2s ease-out"; if (window.innerWidth <= 768) { chatbotIframe.style.top = '0'; chatbotIframe.style.bottom = '0'; chatbotIframe.style.width = '100%'; chatbotIframe.style.height = '100%'; chatbotIframe.style.zIndex = widgetZIndex; } else { const horizontal = `${widgetHorizontal}px`; if (widgetCorner.includes('right')) { chatbotIframe.style.right = horizontal; } else { chatbotIframe.style.left = horizontal; } const vertical = `${widgetVertical + 60 + chatBubbleSizeDifference}px`; if (widgetCorner.includes('bottom')) { chatbotIframe.style.bottom = vertical; } else { chatbotIframe.style.top = vertical; } chatbotIframe.style.width = '400px'; chatbotIframe.style.zIndex = widgetZIndex; chatbotIframe.style.borderRadius = "0.75rem" } // Hide the spinner when the iframe has finished loading document.body.appendChild(chatbotIframe); chatbotIframe.onload = function() { // Hide loading spinner chatBubbleIcon.removeChild(loaderSpan); handleIframe(chatbotIframe, chatBubbleContainer, chatTrigger, svg, crossSvg) }; } else { handleIframe(chatbotIframe, chatBubbleContainer, chatTrigger, svg, crossSvg) } } // Create modern chat icon svg function modernChatIcon(newSize) { let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg.setAttribute('fill', 'none'); svg.setAttribute('viewBox', '0 0 21 25'); svg.setAttribute('width', `${newSize}px`); svg.setAttribute('height', `${newSize}px`); var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('fill-rule', 'evenodd'); path.setAttribute('clip-rule', 'evenodd'); path.setAttribute('d', 'M3.60412 0H10.466C10.466 5.74692 5.77465 10.4158 0 10.4158V3.58681C0 1.60705 1.61481 0 3.60412 0ZM10.47 0H15.697C18.5843 0 20.932 2.33243 20.932 5.20589C20.932 8.07935 18.5884 10.4118 15.701 10.4118H10.47V0ZM10.466 10.4216V20.726C10.5239 15.0284 15.1888 10.4175 20.9315 10.4175V20.8172H20.9322V25.0012C18.7149 22.0639 17.1702 20.975 11.7682 20.8333H10.466V20.8334H5.235C2.34767 20.8334 0.00398982 18.501 0.00398982 15.6275C0.00398982 12.754 2.34767 10.4216 5.235 10.4216H10.466Z'); path.setAttribute('fill', 'white'); svg.appendChild(path); return svg; } // Create classic chat icon svg function classicChatIcon(newSize) { let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); svg.setAttribute('fill', 'none'); svg.setAttribute('viewBox', '0 0 24 24'); svg.setAttribute('stroke-width', '1.5'); svg.setAttribute('stroke', '#ffffff'); svg.setAttribute('width', `${newSize}px`); svg.setAttribute('height', `${newSize}px`); svg.setAttribute('style', `width:${newSize}px;height:${newSize}px;fill:none;`); var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('stroke-linecap', 'round'); path.setAttribute('stroke-linejoin', 'round'); path.setAttribute('d', 'M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 01-.923 1.785A5.969 5.969 0 006 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337'); svg.setAttribute('fill', 'none'); svg.appendChild(path); return svg; } // Create custom chat icon function customChatIcon(bubbleIconUrl, newImageSize) { let img = document.createElement('img'); img.setAttribute('src', bubbleIconUrl); img.setAttribute('alt', 'Custom chat icon'); img.setAttribute('width', `${newImageSize}px`); img.setAttribute('height', `${newImageSize}px`); img.setAttribute('border-radius', '50%'); return img; } // Create chat icon svg function createChatIcon(bubbleIconType, newSize, bubbleIconUrl, newImageSize) { if (bubbleIconType === 'modern') { return modernChatIcon(newSize); } else if (bubbleIconType === 'classic') { return classicChatIcon(newSize); } else if (bubbleIconType === 'custom') { return customChatIcon(bubbleIconUrl, newImageSize); } } function loadWidget() { var scriptTag = document.querySelector('script[src*="zipchat.js"]'); var widgetToken = new URL(scriptTag.src).searchParams.get('id'); let widgetConfigurationUrl, chatbotUrl, baseUrl; const currentUrl = window.location.href; // if current url include params zipchat-preview=true remove the chatPreviewMessageHidden flag if (currentUrl.includes('zipchat-preview=true')) { console.log('zipchat-preview=true'); localStorage.removeItem('chatPreviewMessageHidden'); } // Not running as Shopify theme app extension if (widgetToken) { let baseUrl = scriptTag.src.replace(`/widget/zipchat.js?id=${widgetToken}`, '/widget_data'); // Case when shopify is loading the script tag, we need to remove the &shop parameter const shopParamIndex = baseUrl.indexOf('&shop='); if (shopParamIndex !== -1) { baseUrl = baseUrl.substring(0, shopParamIndex); } widgetConfigurationUrl = new URL(baseUrl); widgetConfigurationUrl.searchParams.append('widget_token', widgetToken); } else { widgetConfigurationUrl = new URL(window.location.origin + '/apps/zipchat/widget/configuration'); } widgetConfigurationUrl.searchParams.append('current_url', currentUrl); fetch(widgetConfigurationUrl.toString(), { method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }) .then(response => response.json()) .then(data => { var widgetData = data; var widgetCorner = widgetData.widget_position_corner; var widgetHorizontal = widgetData.widget_horizontal; var widgetVertical = widgetData.widget_vertical; var widgetSize = widgetData.widget_size; const chatTrigger = widgetData.chat_trigger const widgetZIndex = widgetData.z_index; if (widgetData.widget == false) { return } const sizeMap = { 'small': 0, 'medium': 10, 'large': 20 }; const chatBubbleSizeDifference = sizeMap[widgetSize] || 0; var chatPreviewMessage = document.createElement('div'); chatPreviewMessage.setAttribute('id', 'chat-preview-message'); chatPreviewMessage.style.paddingTop = '10px'; chatPreviewMessage.style.paddingBottom = '10px'; chatPreviewMessage.style.fontFamily = 'sans-serif;' chatPreviewMessage.style.color = 'black' chatPreviewMessage.style.paddingLeft = '16px'; chatPreviewMessage.style.paddingRight = '16px'; chatPreviewMessage.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 4px 8px 0px'; const horizontalPreview = `${widgetHorizontal}px`; if (widgetCorner.includes('right')) { chatPreviewMessage.style.right = horizontalPreview; } else { chatPreviewMessage.style.left = horizontalPreview; } const verticalPreview = `${widgetVertical + 55 + chatBubbleSizeDifference}px`; if (widgetCorner.includes('bottom')) { chatPreviewMessage.style.bottom = verticalPreview; } else { chatPreviewMessage.style.top = verticalPreview; } chatPreviewMessage.style.width = '300px'; chatPreviewMessage.style.maxWidth = '300px'; chatPreviewMessage.style.backgroundColor = 'white'; chatPreviewMessage.style.height = 'auto'; chatPreviewMessage.style.position = 'fixed'; chatPreviewMessage.style.zIndex = widgetZIndex; chatPreviewMessage.style.display = "none" chatPreviewMessage.style.borderRadius = "12px" chatPreviewMessage.style.transition = "transform 0.3s cubic-bezier(0, 1.2, 1, 1), opacity 0.2s ease-out"; function linkifyMessage(text) { const urlRegex = /https:\/\/[^\s]+/g; return text.replace(urlRegex, function(url) { return '' + url + ''; }); } const imageElement = widgetData.photo_url ? `
Photo preview
` : ''; const gapStyle = widgetData.photo_url ? 'gap: 12px;' : ''; chatPreviewMessage.innerHTML = `
${imageElement}

${widgetData.brand_name}

Active

${chatTrigger?.popup_message ? linkifyMessage(chatTrigger.popup_message) : ''}
` // Not running as Shopify theme app extension if (widgetToken) { baseUrl = scriptTag.src.replace(`/widget/zipchat.js?id=${widgetToken}`, '/widget'); // Case when shopify is loading the script tag, we need to remove the &shop parameter const shopParamIndex = baseUrl.indexOf('&shop='); if (shopParamIndex !== -1) { baseUrl = baseUrl.substring(0, shopParamIndex); } chatbotUrl = new URL(baseUrl); } else { chatbotUrl = new URL(`${data.api_base}/widget`); } chatbotUrl.searchParams.append('current_url', currentUrl); // Append the token to the chatbot URL function generateRandomString(length) { var result = ''; var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (var i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * characters.length)); } return result; } var visitor_token = localStorage.getItem('visitor_token'); if (!visitor_token) { visitor_token = generateRandomString(20); localStorage.setItem('visitor_token', visitor_token); } chatbotUrl.searchParams.append('visitor_token', visitor_token); chatbotUrl.searchParams.append('widget_token', data.widget_token); // Create the chat bubble container var chatBubbleContainer = document.createElement('div'); chatBubbleContainer.setAttribute('id', 'widget-chat-container'); // chatBubbleContainer.style.opacity = "0" // Create the chat bubble button var chatBubbleButton = document.createElement('div'); chatBubbleButton.setAttribute('id', 'widget-chat-button'); chatBubbleButton.style.position = 'fixed'; chatBubbleButton.style.width = `${50 + chatBubbleSizeDifference}px`; chatBubbleButton.style.height = `${50 + chatBubbleSizeDifference}px`; chatBubbleButton.style.borderRadius = '50%'; chatBubbleButton.style.backgroundColor = widgetData.color; chatBubbleButton.style.boxShadow = 'rgba(0, 0, 0, 0.2) 0px 4px 8px 0px'; chatBubbleButton.style.cursor = 'pointer'; chatBubbleButton.style.zIndex = widgetZIndex; chatBubbleButton.style.transition = 'all 0.2s ease-in-out 0s'; let horizontalPosition = widgetCorner.includes("right") ? "right" : "left"; let verticalPosition = widgetCorner.includes("bottom") ? "bottom" : "top"; chatBubbleButton.style[horizontalPosition] = `${widgetHorizontal}px`; chatBubbleButton.style[verticalPosition] = `${widgetVertical}px`; var chatBubbleIcon = document.createElement('div'); chatBubbleIcon.style.display = 'flex'; chatBubbleIcon.style.alignItems = 'center'; chatBubbleIcon.style.justifyContent = 'center'; chatBubbleIcon.style.width = '100%'; chatBubbleIcon.style.height = '100%'; chatBubbleIcon.style.zIndex = widgetZIndex; // dynamically change the svgs size based on the widget size const svgSizeMap = { 'small': 0, 'medium': 6, 'large': 12 }; const svgSizeDifference = svgSizeMap[widgetSize] || 0; const baseSize = 24; const newSize = baseSize + svgSizeDifference; const newImageSize = 52 + chatBubbleSizeDifference; // chat svg icon var svg = createChatIcon(widgetData.bubble_icon_type || "modern", newSize, widgetData.bubble_icon_url, newImageSize); // close svg icon var crossSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); crossSvg.setAttribute("xmlns", "http://www.w3.org/2000/svg"); crossSvg.setAttribute("fill", "none"); crossSvg.setAttribute("viewBox", "0 0 24 24"); crossSvg.setAttribute("stroke-width", "1.5"); crossSvg.setAttribute("stroke", "#ffffff"); crossSvg.setAttribute("width", `${newSize}px`); crossSvg.setAttribute("height", `${newSize}px`); crossSvg.setAttribute('style', `width:${newSize}px;height:${newSize}px;fill:none;`); var crossPath1 = document.createElementNS("http://www.w3.org/2000/svg", "path"); crossPath1.setAttribute("stroke-linecap", "round"); crossPath1.setAttribute("stroke-linejoin", "round"); crossPath1.setAttribute("d", "M6 18L18 6M6 6l12 12"); crossSvg.appendChild(crossPath1); // Append the SVG to the chat bubble icon chatBubbleIcon.appendChild(svg); // Appen the cross svg that we will hide and show crossSvg.style.display = "none"; chatBubbleIcon.appendChild(crossSvg) // Append the chat bubble icon to the chat bubble button chatBubbleButton.appendChild(chatBubbleIcon); // Append the chat bubble button to the user's website document.body.appendChild(chatBubbleContainer); chatBubbleContainer.appendChild(chatBubbleButton); document.querySelector("#widget-chat-button").addEventListener("mousedown", function (event) { event.stopPropagation(); document.getElementById("widget-chat-button").style.transform = "scale(0.7)"; }); document.querySelector("#widget-chat-button").addEventListener("mouseup", function (event) { event.stopPropagation(); document.getElementById("widget-chat-button").style.transform = "scale(1)"; }); if (!localStorage.getItem('chatPreviewMessageHidden') && !!chatTrigger?.popup_message) { chatBubbleContainer.appendChild(chatPreviewMessage); chatBubbleContainer.querySelector('#chat-preview-message').addEventListener("click", function (event) { if (event.target.nodeName === 'A') { return; } event.stopPropagation(); var chatPreviewMessage = document.querySelector('#chat-preview-message'); chatPreviewMessage.style.transform = "scale(0)"; chatPreviewMessage.style.display = 'none' document.querySelector("#widget-chat-button").click(); }); function showChatPreviewMessage() { chatPreviewMessage.style.transform = "scale(1)"; chatPreviewMessage.style.opacity = "1"; chatPreviewMessage.style.display = ""; } function hideChatPreviewMessage() { chatPreviewMessage.style.transform = "scale(0)"; chatPreviewMessage.style.display = 'none' chatPreviewMessage.remove() } function showPreviewMessageByTime(delay, callback){ setTimeout(() => { callback(); }, delay); } function showPreviewMessageByScrollPercentage(thresholdPercentage, callback){ window.addEventListener('scroll', function() { const scrollTop = window.scrollY || document.documentElement.scrollTop; const windowHeight = window.innerHeight || document.documentElement.clientHeight; const documentHeight = document.documentElement.scrollHeight; const scrollPercentage = (scrollTop + windowHeight) / documentHeight * 100; if (scrollPercentage >= thresholdPercentage) { callback(); window.removeEventListener('scroll', arguments.callee); } }); } chatBubbleContainer.querySelector('#close-preview-message').addEventListener("click", function (event) { event.stopPropagation(); hideChatPreviewMessage(); // Set the localStorage flag to prevent showing the chat preview message again localStorage.setItem('chatPreviewMessageHidden', 'true'); }); if (!!chatTrigger && chatTrigger.setup_type == 'advanced'){ if (chatTrigger.all_rules_met && (!!chatTrigger.show_after_scroll_percentage && !!chatTrigger.show_after_seconds)){ showPreviewMessageByScrollPercentage(chatTrigger.show_after_scroll_percentage, () => showPreviewMessageByTime(chatTrigger.show_after_seconds * 1000, showChatPreviewMessage)) } else { if (!!chatTrigger.show_after_scroll_percentage){ showPreviewMessageByScrollPercentage(chatTrigger.show_after_scroll_percentage, showChatPreviewMessage) } if (!!chatTrigger.show_after_seconds){ showPreviewMessageByTime(chatTrigger.show_after_seconds * 1000, showChatPreviewMessage) } } } else { showPreviewMessageByTime(2000, showChatPreviewMessage) } } // Opening closing the chatbot when cliking on the widget button document.querySelector("#widget-chat-button").addEventListener("click", function (event) { event.stopPropagation(); createChatbotIframe( chatbotUrl.toString(), widgetHorizontal, widgetVertical, widgetCorner, widgetZIndex, chatBubbleSizeDifference, chatBubbleContainer, chatTrigger, chatBubbleIcon, svg, crossSvg ); }); // On mobile, I added a 'x' to close the chatbot, because it's in the iframe, I need to do a post message window.addEventListener('message', function(event) { if (event.origin !== event.data.origin) { return; } var iframe = document.getElementById("zipchat-iframe"); if (event.data.action === 'close-iframe') { iframe.style.transform = "scale(0)"; iframe.style.opacity = "0"; iframe.style.pointerEvents = 'none'; svg.style.display = "block"; crossSvg.style.display = "none"; toggleBodyScroll(true) document.body.style.position = ""; document.body.style.inset = ""; document.body.style.margin = ""; } if (event.data.action === 'remove-iframe') { iframe.remove() document.getElementById("widget-chat-button").remove() } }); // If the clicked element is neither the chat button nor the iframe itself, close the iframe. window.addEventListener("click", function(event) { var iframe = document.getElementById("zipchat-iframe"); var chatButton = document.getElementById("widget-chat-button"); if (event.target !== chatButton && event.target !== iframe && iframe) { iframe.style.transform = "scale(0)"; iframe.style.opacity = "0"; iframe.style.pointerEvents = 'none'; svg.style.display = "block"; crossSvg.style.display = "none"; toggleBodyScroll(true) } }); window.zipchatWidgetLoaded = true; }) .catch(error => { console.error('Error fetching data:', error); }); } if (document.readyState === 'loading') { // If the document is still loading, wait for DOMContentLoaded document.addEventListener('DOMContentLoaded', function() { loadWidget(); setupUrlChangeDetection(); }); } else { // If the document has already finished loading, we can run the function now loadWidget(); setupUrlChangeDetection(); } })();