1// Self-executing function to create and manage the chat widget 2(function () { 3 // --- PRE-EXECUTION CHECKS --- 4 5 // 1. Ensure the script doesn't run twice 6 if (document.getElementById("chat-widget-container")) { 7 console.warn("Chat widget script has already been loaded."); 8 return; 9 } 10 11 // 2. Get configuration from the script tag itself (removes hardcoding) 12 const scriptTag = document.currentScript; 13 const iframeSrc = scriptTag ? scriptTag.dataset.source : null; 14 15 if (!iframeSrc) { 16 console.error( 17 "Chat widget source URL not found. Please add 'data-source' attribute to the script tag." 18 ); 19 return; 20 } 21 22 // 3. Helper to get origin from a URL for security 23 const getOriginFromUrl = (url) => { 24 try { 25 return new URL(url).origin; 26 } catch (e) { 27 console.error("Invalid iframe source URL:", url); 28 return null; 29 } 30 }; 31 32 const iframeOrigin = getOriginFromUrl(iframeSrc); 33 if (!iframeOrigin) return; 34 35 // --- STATE MANAGEMENT --- 36 const widgetState = { 37 isOpen: false, 38 isFullscreen: false, 39 position: "bottom-right", // Default position 40 isMobile: window.innerWidth < 768 || window.innerHeight < 700, 41 // Store normal, expanded dimensions 42 lastNormalDimensions: { 43 width: window.innerWidth < 768 ? "calc(100vw - 40px)" : "500px", 44 height: getAppropriateHeight(), 45 borderRadius: "15px", 46 }, 47 }; 48 49 // Function to determine appropriate height based on device dimensions 50 function getAppropriateHeight() { 51 const width = window.innerWidth; 52 const height = window.innerHeight; 53 54 // iPhone SE and very small screens 55 if (width < 480) { 56 return "min(calc(90vh - 20px), 500px)"; 57 } 58 // Nethub (1024x600) and similar devices 59 else if (width <= 1024 && height <= 650) { 60 return "min(calc(85vh - 20px), 550px)"; 61 } 62 // Other mobile devices 63 else if (width < 768 || height < 700) { 64 return "min(calc(90vh - 40px), 600px)"; 65 } 66 // Desktop 67 else { 68 return "700px"; 69 } 70 } 71 72 // --- DOM CREATION --- 73 const createChatWidget = () => { 74 const container = document.createElement("div"); 75 container.id = "chat-widget-container"; 76 77 const iframe = document.createElement("iframe"); 78 iframe.id = "chat-iframe"; 79 iframe.src = iframeSrc; 80 iframe.style.position = "fixed"; 81 iframe.style.zIndex = "9999"; 82 iframe.style.border = "none"; 83 iframe.style.background = "transparent"; 84 iframe.style.overflow = "hidden"; 85 iframe.style.transition = "all 0.3s ease"; 86 iframe.allow = "microphone; camera"; 87 88 // Start in collapsed state 89 applyStyles(iframe, getCollapsedStyles()); 90 91 container.appendChild(iframe); 92 document.body.appendChild(container); 93 return iframe; 94 }; 95 96 // --- STYLE HELPERS --- 97 const applyStyles = (element, styles) => { 98 for (const property in styles) { 99 element.style[property] = styles[property]; 100 } 101 }; 102 103 const getCollapsedStyles = () => ({ 104 width: "100px", 105 height: "100px", 106 top: "auto", 107 left: widgetState.position === "bottom-left" ? "0" : "auto", 108 bottom: "0", 109 right: widgetState.position === "bottom-right" ? "0" : "auto", 110 borderRadius: "0", 111 }); 112 113 const getExpandedStyles = () => ({ 114 width: widgetState.lastNormalDimensions.width, 115 height: widgetState.lastNormalDimensions.height, 116 top: "auto", 117 left: widgetState.position === "bottom-left" ? 118 (widgetState.isMobile ? "10px" : "20px") : "auto", 119 bottom: widgetState.isMobile ? "10px" : "20px", 120 right: widgetState.position === "bottom-right" ? 121 (widgetState.isMobile ? "10px" : "20px") : "auto", 122 borderRadius: widgetState.lastNormalDimensions.borderRadius, 123 }); 124 125 const getFullscreenStyles = () => ({ 126 width: "100%", 127 height: "100%", 128 top: "0", 129 left: "0", 130 bottom: "auto", 131 right: "auto", 132 borderRadius: "0", 133 }); 134 135 // --- WIDGET ACTIONS --- 136 const expandWidget = () => { 137 if (widgetState.isOpen) return; 138 applyStyles( 139 iframe, 140 widgetState.isFullscreen ? getFullscreenStyles() : getExpandedStyles() 141 ); 142 widgetState.isOpen = true; 143 }; 144 145 const collapseWidget = () => { 146 if (!widgetState.isOpen) return; 147 // Don't reset isFullscreen state when collapsing 148 applyStyles(iframe, getCollapsedStyles()); 149 widgetState.isOpen = false; 150 }; 151 152 const toggleFullscreen = (enable) => { 153 widgetState.isFullscreen = enable; 154 if (widgetState.isOpen) { 155 applyStyles(iframe, enable ? getFullscreenStyles() : getExpandedStyles()); 156 } 157 }; 158 159 // --- EVENT HANDLING --- 160 const handleMessage = (event) => { 161 // Security: only accept messages from the iframe's origin 162 if (event.origin !== iframeOrigin) { 163 return; 164 } 165 166 const { type, value } = event.data; 167 168 switch (type) { 169 case "CHAT_WIDGET_OPEN": 170 expandWidget(); 171 break; 172 case "CHAT_WIDGET_CLOSE": 173 collapseWidget(); 174 break; 175 case "CHAT_WIDGET_FULLSCREEN": 176 toggleFullscreen(value); 177 break; 178 case "EXTERNAL_LINK": 179 if (value && value.url) { 180 window.open(value.url, "_blank", "noopener,noreferrer"); 181 } 182 break; 183 } 184 }; 185 186 // --- INITIALIZATION --- 187 const iframe = createChatWidget(); 188 189 // Listen for messages from the iframe 190 window.addEventListener("message", handleMessage); 191
192 // Handle window resize for responsive adjustments 193 window.addEventListener("resize", () => { 194 const wasMobile = widgetState.isMobile; 195 widgetState.isMobile = window.innerWidth < 768 || window.innerHeight < 700; 196 197 // Always update dimensions on resize to handle orientation changes 198 widgetState.lastNormalDimensions.width = widgetState.isMobile ? "calc(100vw - 40px)" : "500px"; 199 widgetState.lastNormalDimensions.height = getAppropriateHeight(); 200 201 // Re-apply styles if widget is open 202 if (widgetState.isOpen && !widgetState.isFullscreen) { 203 applyStyles(iframe, getExpandedStyles()); 204 } 205 }); 206 207 // More efficient click listener directly on the iframe 208 iframe.addEventListener("click", () => { 209 if (!widgetState.isOpen) { 210 expandWidget(); 211 } 212 }); 213 214 iframe.addEventListener("load", () => 215 console.log("Chat iframe loaded successfully.") 216 ); 217 iframe.addEventListener("error", (e) => 218 console.error("Chat iframe failed to load:", e) 219 ); 220 221 // --- PUBLIC API --- 222 window.ChatWidgetConfig = { 223 setPosition: (position) => { 224 if (position !== "bottom-left" && position !== "bottom-right") { 225 console.warn('Invalid position. Use "bottom-left" or "bottom-right"'); 226 return; 227 } 228 widgetState.position = position; 229 // Re-apply styles based on new position 230 applyStyles( 231 iframe, 232 widgetState.isOpen ? getExpandedStyles() : getCollapsedStyles() 233 ); 234 }, 235 setBrandColor: (color) => { 236 // Post message securely to the specific iframe origin 237 iframe.contentWindow.postMessage( 238 { type: "CHAT_WIDGET_CONFIG", value: { brandColor: color } }, 239 iframeOrigin 240 ); 241 }, 242 }; 243})();
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.