class SWChatEmbedder { constructor(baseUrl, chatbotId,iframeContainerId='iframe-sw-chat', initialWidth = '400px', initialHeight = '600px',debug=false) { this.baseUrl = baseUrl; this.version="?v=1.2.4.2.1"; this.iframeContainerId = iframeContainerId; this.chatIframe = null; this.iconIframe = null; this.iframeContainer = null; this.debug = debug; this.isFullscreen = false; this.initialWidth = initialWidth; this.initialHeight = initialHeight; this.iconWidth = '75px'; this.iconHeight = '75px'; this.iconFontSize = null; this.iconSizeBreakpoints = []; this.chatBotId=chatbotId; this.hasIconFrame=false; this.hasUI=true; this.startMessage = "Waarmee kan ik je helpen?"; this.suggestions = ["Schrijf een bericht..."]; this.additionalFormdata = {}; this.internalsOverride={}; } setVersion(version) { this.version="?"+version; } /** * Merges the provided data object into the existing additionalFormdata. * Logs warnings or updated formdata if debug mode is enabled. * * This is used for when the application that uses the chatclients wants to sent extra information to the backend initized from the main application * * @param {Object} data - The data object to be added to the existing additionalFormdata. * Must be a non-null object. * @return {void} */ addFormdata(data) { if (typeof data !== 'object' || data === null) { if (this.debug) console.warn("addFormdata: expected an object"); return; } // Merge new data into existing additionalFormdata this.additionalFormdata = { ...this.additionalFormdata, ...data }; if (this.debug) console.log("Updated formdata:", this.additionalFormdata); } addInternalsOverride(data) { if (typeof data !== 'object' || data === null) { if (this.debug) console.warn("addInternalsOverride: expected an object"); return; } // Merge new data into existing additionalFormdata this.internalsOverride = { ...this.internalsOverride, ...data }; console.log(this.internalsOverride); if (this.debug) console.log("Updated internalsOverride:", this.internalsOverride); } uiIsAvailable() { return false; } /** * activate debug on demand */ setDebug () { this.debug=true; } setStartMessage(message) { this.startMessage=message; } /** * Registers a breakpoint-driven icon iframe size. * Call before init() to affect initial render. Multiple calls are allowed. * * @param {number} breakpointPx - Max viewport width in px where this size applies. * @param {string|number} width - Width in px or any valid CSS size. * @param {string|number} height - Height in px or any valid CSS size. * @param {string|number|null} fontSize - Optional font size for the icon. * @return {void} */ setIconFrameSize(breakpointPx, width, height, fontSize = null) { const normalizedBreakpoint = this.normalizeBreakpoint(breakpointPx); if (normalizedBreakpoint === null) { if (this.debug) console.warn("setIconFrameSize: expected breakpoint in px (number or 'NNNpx')"); return; } this.iconSizeBreakpoints.push({ breakpoint: normalizedBreakpoint, width: this.normalizeSize(width, '75px'), height: this.normalizeSize(height, '75px'), fontSize: fontSize === null ? null : this.normalizeSize(fontSize, '') }); this.iconSizeBreakpoints.sort((a, b) => a.breakpoint - b.breakpoint); this.applyIconFrameSize(); } /** * Normalizes size inputs to valid CSS strings. * * @param {string|number} value - A number (px) or CSS size string. * @param {string} fallback - Fallback size if value is invalid. * @return {string} */ normalizeSize(value, fallback) { if (typeof value === 'number' && !Number.isNaN(value)) { return `${value}px`; } if (typeof value === 'string') { const trimmed = value.trim(); if (trimmed === '') { return fallback; } if (/^\d+(\.\d+)?$/.test(trimmed)) { return `${trimmed}px`; } return trimmed; } return fallback; } /** * Normalizes breakpoint inputs to a number of pixels. * * @param {string|number} value - A number or a 'NNNpx' string. * @return {number|null} */ normalizeBreakpoint(value) { if (typeof value === 'number' && !Number.isNaN(value)) { return value; } if (typeof value === 'string') { const trimmed = value.trim(); if (/^\d+(\.\d+)?px$/.test(trimmed)) { return parseFloat(trimmed.replace('px', '')); } if (/^\d+(\.\d+)?$/.test(trimmed)) { return parseFloat(trimmed); } } return null; } /** * Applies the icon size based on current viewport width and breakpoints. * * @return {void} */ applyIconFrameSize() { let width = '75px'; let height = '75px'; let fontSize = null; if (this.iconSizeBreakpoints.length > 0) { const viewportWidth = window.innerWidth || 0; const matches = this.iconSizeBreakpoints.filter((item) => viewportWidth >= item.breakpoint); if (matches.length > 0) { const match = matches[matches.length - 1]; width = match.width; height = match.height; fontSize = match.fontSize; } } this.iconWidth = width; this.iconHeight = height; this.iconFontSize = fontSize; if (this.iconIframe) { this.iconIframe.style.width = width; this.iconIframe.style.height = height; if (fontSize) { this.applyIconFontSize(fontSize); } } } /** * Applies the icon font size inside the icon iframe. * * @param {string} fontSize - CSS font-size value. * @return {void} */ applyIconFontSize(fontSize) { try { if (!this.iconIframe || !this.iconIframe.contentDocument) { return; } const icon = this.iconIframe.contentDocument.querySelector('.sw-chat-icon'); if (icon) { icon.style.fontSize = fontSize; } } catch (e) { if (this.debug) console.warn("applyIconFontSize: unable to set font size"); } } /** * Initializes the iframe container and the iframes for the icon and chat. */ init(show_icon=true) { this.hasUI=this.uiIsAvailable(); if (this.debug) { console.log(`Initializing iframe container and iframes. `); } this.createIframeContainer(); if (show_icon) this.createIconIframe(); this.hasIconFrame=show_icon; this.createChatIframe(); this.addMessageListener(); this.adjustHeightOnResize(); } /** * Creates the container element where the iframes will be embedded. * If the container with the specified ID already exists, it will not create a new one. */ createIframeContainer() { // Check if the container already exists let container = document.getElementById(this.iframeContainerId); if (!container) { if (this.debug) { console.log(`Creating new iframe container.`); } // Create a new container element container = document.createElement('div'); container.id = this.iframeContainerId; container.classList.add("state-closed"); // Add the container to the body or another specified element document.body.appendChild(container); // Dynamically create a stylesheet const style = document.createElement('style'); style.type = 'text/css'; // Add the CSS rules for the container const css = ` :root { --sw-chat-max-width-${this.iframeContainerId}: ${this.initialWidth}; } #${this.iframeContainerId} { position: fixed; bottom: 10px; right: 20px; z-index: 99999999; } #${this.iframeContainerId}.state-open { width:100%; max-width: min(calc(100vw - 60px), var(--sw-chat-max-width-${this.iframeContainerId})); } #${this.iframeContainerId}.state-closed { width:auto; } @media (min-width: 1920px) { #${this.iframeContainerId} { right: calc((100vw - 1900px) / 2); } } /* Scroll lock on parent when chat is fullscreen */ html.sw-chat-scroll-lock, body.sw-chat-scroll-lock { overflow: hidden !important; height: 100% !important; overscroll-behavior: none !important; } /* iOS fix: body fixed, bewaar positie via inline top */ body.sw-chat-scroll-lock { position: fixed !important; width: 100% !important; } @keyframes sw-chat-slide-open { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes sw-chat-slide-close { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(10px); } } .sw-chat-frame.sw-opening { animation: sw-chat-slide-open 0.3s ease-out forwards; } .sw-chat-frame.sw-closing { animation: sw-chat-slide-close 0.25s ease-in forwards; } `; style.appendChild(document.createTextNode(css)); // Append the stylesheet to the document head document.head.appendChild(style); } else if (this.debug) { console.log(`Iframe container already exists.`); } this.iframeContainer = container; } /** * Creates and configures the iframe for the chat icon. */ createIconIframe() { if (this.debug) { console.log(`Creating icon iframe.`); } // Create the icon iframe element this.iconIframe = document.createElement('iframe'); this.iconIframe.className = `sw-chat-icon-master sw-chat-icon-${this.chatBotId}`; // Set other iframe properties as needed (e.g., size, src, etc.) this.iconIframe.style.border = 'none'; this.applyIconFrameSize(); // Add a random number query parameter to URLs if debug is enabled const randomQuery = this.debug ? `?rand=${Math.floor(Math.random() * 100000)}` : ''; // Set the HTML content of the icon iframe using Font Awesome this.iconIframe.srcdoc = `