PageSourceSearch

https://chatwidget.closertotruth.com/embed.js

js closertotruth.com collected 2026-09-24 09:02:07 UTC 6,636 bytes, 243 lines download raw bytes

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.