1/** 2 * Sets up the chat and knowledge base 3 * 4 * @param {boolean} isLitePage - Whether the page is a lite page or not 5 */ 6function initializeFront(isLitePage = false) { 7 const button = document.querySelector('#openSupportChat') 8 button?.addEventListener('click', async (e) => { 9 startLoadingAnimation(button, isLitePage) 10 if (button.dataset.initialized !== 'true') { 11 await initFrontChat(button, true) 12 } else { 13 await showFrontChat() 14 } 15 setHasMessages(false) 16 stopLoadingAnimation(button, isLitePage) 17 setCookie('openFrontChat', true) 18 }) 19 document.querySelectorAll('.chatBotLink, .chatLink').forEach((element) => { 20 element.addEventListener('click', (event) => { 21 event.preventDefault() 22 document.querySelector('.fc-button').click() 23 }) 24 }) 25 document.querySelectorAll('.chatBotLink').forEach((element) => { 26 element.addEventListener('click', (event) => { 27 setTimeout(() => { 28 sendMessageInFrontChat('Starte den Bot') 29 }, 50) 30 }) 31 }) 32 33 if (getCookie('openFrontChat')) { 34 initFrontChat(button, false) 35 } 36} 37 38/** 39 * Initializes the chat window 40 * 41 * @param {HTMLElement} initButton - The button that initializes the chat 42 * @param {boolean} show - Whether to also show the chat window or not 43 */ 44async function initFrontChat(initButton, show = false) { 45 const globals = getGlobals() 46 let chatId = globals.frontChatId 47 let frontIdentity = globals.frontIdentity 48 if (frontIdentity.length === 0) { 49 chatId = globals.frontSalesChatId 50 frontIdentity = {} 51 } 52 frontIdentity.chatId = chatId 53 frontIdentity.useDefaultLauncher = false 54 55 window.FrontChat('init', frontIdentity) 56 57 // Front has no real callback for when the chat is ready, so we have to poll 58 while (!isFrontChatInitialized()) { 59 await sleep(200) 60 } 61 if (show) { 62 await showFrontChat() 63 } 64 initButton.dataset.initialized = 'true' 65 window.FrontChat('onUnreadChange', (data) => { 66 if (data.unread_count && !isFrontChatOpen()) { 67 setHasMessages(true) 68 } 69 }) 70} 71 72/** 73 * Opens the chat window 74 */ 75async function showFrontChat() { 76 while (!isFrontChatOpen()) { 77 window.FrontChat('show') 78 await sleep(200) 79 } 80 const supportUnavailableMessage = getContextData('supportUnavailable') 81 if (supportUnavailableMessage && supportUnavailableMessage.length) { 82 const chatFrame = document.getElementById('front-chat-iframe') 83 if (chatFrame) { 84 const chatDocument = chatFrame.contentWindow.document 85 if (!chatDocument.querySelector('.supportUnavailableMessage')) { 86 const html = `<div class="supportUnavailableMessage" style="margin: 1rem; padding: .5rem; border: 1px solid #23985D; background-color: #E2F8ED; font-size: smaller; border-radius: 1rem;">${supportUnavailableMessage}</div>` 87 chatDocument 88 ?.querySelector('textarea') 89 ?.parentNode?.parentNode?.parentNode?.previousElementSibling?.insertAdjacentHTML( 90 'beforeend', 91 html 92 ) 93 } 94 } 95 } 96} 97 98/** 99 * Checks if the chat window is initialized 100 * 101 * @returns {boolean} Whether the chat window is initialized or not 102 */ 103function isFrontChatInitialized() { 104 const chatFrame = document.getElementById('front-chat-iframe') 105 return ( 106 chatFrame && chatFrame.contentWindow && chatFrame.contentWindow.FrontChatApp 107 ) 108} 109 110/** 111 * Checks if the chat window is open 112 * 113 * @returns {boolean} Whether the chat window is open or not 114 */ 115function isFrontChatOpen() { 116 const chatFrame = document.getElementById('front-chat-iframe') 117 return isFrontChatInitialized() && chatFrame.clientHeight 118} 119 120/** 121 * Toggles the visibility of the unread messages display 122 * Calls the API to remove the unread status 123 * 124 * @param {boolean} hasMessages - whether the user has messages or not 125 */ 126function setHasMessages(hasMessages) { 127 setVisibility('.has-chats-display', hasMessages) 128 setVisibility('.has-no-chats-display', !hasMessages) 129 130 if (!hasMessages && getGlobals().frontIdentity.email !== undefined) { 131 fetch('/webhook/front/read/', { method: 'POST', headers: headers() }) 132 } 133} 134 135/** 136 * Sleeps for a given amount of time 137 * 138 * @param {number} ms - The time to sleep in milliseconds 139 * @returns {Promise<void>} A promise that resolves after the given time 140 */ 141function sleep(ms) { 142 return new Promise((resolve) => setTimeout(resolve, ms)) 143} 144 145/** 146 * Starts the loading animation for the button 147 * 148 * @param {HTMLElement} button - the button for the animation 149 * @param {boolean} isLitePage - whether the page is a lite page or not 150 */ 151function startLoadingAnimation(button, isLitePage) { 152 if (isLitePage) { 153 setLoadingCircle(button.querySelector('.fc-button'), true, false, false) 154 } else { 155 setLoadingCircle(button, false, true) 156 } 157} 158 159/** 160 * Stops the loading animation for the button 161 * 162 * @param {HTMLElement} button - the button for the animation 163 * @param {boolean} isLitePage - whether the page is a lite page or not 164 */ 165function stopLoadingAnimation(button, isLitePage) { 166 button.querySelector('.loading')?.remove() 167 if (isLitePage) { 168 button 169 .querySelector('.fc-button') 170 .insertAdjacentHTML('beforeend', '<i class="far fa-comment-lines"></i>') 171 } 172} 173 174/** 175 * Returns the CSRF token 176 * 177 * @throws If no CSRF Token (`{% csrf_token %}`) is on the page 178 * @returns {string} CSRF middleware token 179 */ 180function getCSRF() { 181 const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]') 182 if (csrfInput === undefined) { 183 throw new Error( 184 "Couldn't find a CSRF-Token input on the page. Please fix the markup." 185 ) 186 } 187 return csrfInput.value 188} 189
190/** 191 * Gives correct headers for API requests 192 * 193 * @returns {object} request header object 194 * @throws if no CSRF-Token has been included in the HTMl template 195 */ 196function headers() { 197 const csrf = getCSRF() 198 return { 199 Accept: 'application/json, text/plain, */*', 200 'Content-Type': 'application/json', 201 'X-CSRFToken': csrf, 202 } 203} 204 205/** 206 * Set the loading circle spinner to the given element if it is not already set 207 * 208 * @param {Element} element - A html element to show the spinner circle inside 209 * @param {boolean} centered - (optional) Set the spinner in the center of the element 210 * @param {boolean} keepContent - (optional) Don't reset the content of the element 211 * @param {boolean} spinnerSmall - (optional) size of the spinner 212 */ 213function setLoadingCircle( 214 element, 215 centered = true, 216 keepContent = false, 217 spinnerSmall = true 218) { 219 setLoadingCircleText(element, undefined, centered, keepContent, spinnerSmall) 220} 221 222/** 223 * Set the loading circle spinner to the given element if it is not already set 224 * 225 * @param {Element} element - A html element to show the spinner circle inside 226 * @param {string} text - Text to show below the spinner. If falsy don't show the text. 227 * @param {boolean} centered - (optional) Set the spinner in the center of the element 228 * @param {boolean} keepContent - (optional) Dont reset the content of the element 229 * @param {boolean} spinnerSmall - (optional) size of the spinner 230 */ 231function setLoadingCircleText( 232 element, 233 text, 234 centered = true, 235 keepContent = false, 236 spinnerSmall = true 237) { 238 if (!element.innerHTML.includes('spinner-border')) { 239 let textSpan = '<span class="visually-hidden">Ladenâ¦</span>' 240 if (text) { 241 textSpan = `<span>${text}</span>` 242 } 243 const loader = ` 244 <span class="loading loading-circle ${ 245 centered 246 ? 'd-flex flex-column align-items-center justify-content-center' 247 : '' 248 }"> 249 <span class="spinner-border ${ 250 spinnerSmall ? 'spinner-border-sm' : '' 251 }" role="status" aria-hidden="true"></span> 252 ${textSpan} 253 </span> 254 ` 255 element.innerHTML = keepContent ? element.innerHTML + ` ${loader}` : loader 256 } 257} 258 259/** 260 * Stores a key value pair as a cookie with optional time to live. 261 * 262 * @param {string} name - name of the cookie 263 * @param {string} value - cookie value 264 * @param {number} [daysTillExpired] - days until cookie expires, can be `null` 265 */ 266function setCookie(name, value, daysTillExpired = null) { 267 if (daysTillExpired !== null) { 268 const d = new Date() 269 d.setTime(d.getTime() + daysTillExpired * 24 * 60 * 60 * 1000) 270 const expires = 'expires=' + d.toUTCString() 271 document.cookie = 272 name + '=' + value + ';' + expires + ';path=/;SameSite=Lax' 273 } else { 274 document.cookie = name + '=' + value + ';path=/;SameSite=Lax' 275 } 276} 277 278/** 279 * Retrieves a cookie 280 * 281 * @param {string} name - name of the cookie to retrieve 282 * @returns {string} retrieved cookie or `''` 283 */ 284function getCookie(name) { 285 name = name + '=' 286 287 // this is needed cause "usedEmails" cookie could contain "%", but "%" crashes in decodeURIComponent, see #13279 288 let decodedCookie = '' 289 try { 290 decodedCookie = decodeURIComponent(document.cookie) 291 } catch (error) { 292 decodedCookie = document.cookie 293 console.error(error) 294 } 295 296 const ca = decodedCookie.split(';') 297 for (let i = 0; i < ca.length; i++) { 298 let c = ca[i] 299 while (c.charAt(0) === ' ') { 300 c = c.substring(1) 301 } 302 if (c.indexOf(name) === 0) { 303 return c.substring(name.length, c.length) 304 } 305 } 306 return '' 307} 308 309/** 310 * Show a HTML element by removing the .hidden class 311 * 312 * @param {object} element - HTML node 313 */ 314function showElement(element) { 315 element.classList.remove('hidden') 316} 317 318/** 319 * Hide a HTML element by adding the .hidden class 320 * 321 * @param {object} element - HTML node 322 */ 323function hideElement(element) { 324 element.classList.add('hidden') 325} 326 327/** 328 * Sets the visibility of the element based on `visible`. 329 * 330 * @param {HTMLElement} element - HTML element 331 * @param {boolean} visible - if true shows the element, else hides it 332 */ 333function setElementVisibility(element, visible) { 334 if (visible) { 335 showElement(element) 336 } else { 337 hideElement(element) 338 } 339} 340 341/** 342 * Set the visibility of all HTML elements matching selector. 343 * 344 * @param {string} selector - string of the selector; what you'd add in querySelector 345 * @param {boolean} visible - if true shows the element, else hides it 346 */ 347function setVisibility(selector, visible) { 348 document.querySelectorAll(selector).forEach((element) => { 349 setElementVisibility(element, visible) 350 }) 351} 352 353/** 354 * Retrieve and parse data from a Django json_script template tag 355 * 356 * @param {string} id - name of the json_script (`json_script:"id"`) 357 * @returns {Array | object} parsed JSON object, or empty array 358 */ 359function getContextData(id) { 360 if (!id) { 361 return [] 362 } 363 const node = document.querySelector(`#${id}`) 364 if (node !== undefined) { 365 try { 366 return JSON.parse(node.innerText) 367 } catch (error) { 368 return [] 369 } 370 } 371 return [] 372} 373 374/** 375 * Returns global Django template constants. 376 * 377 * @returns {object} object of template constants 378 */ 379function getGlobals() { 380 return { 381 languageCode: getContextData('language_code'), 382 userID: getContextData('user_id'), 383 isDebug: getContextData('is_debug'), 384 frontChatId: getContextData('frontChatId'), 385 frontSalesChatId: getContextData('frontSalesChatId'), 386 frontIdentity: getContextData('frontIdentity'), 387 } 388} 389 390document.addEventListener('DOMContentLoaded', () => { 391 const supportUnavailableMessage = getContextData('supportUnavailable') 392 if (!supportUnavailableMessage.length || !supportUnavailableMessage) { 393 initializeFront(true) 394 } 395}) 396 397/** 398 * Sends a message in the FrontChat 399 * 400 * @param {string} message - The message to send 401 */ 402async function sendMessageInFrontChat(message) { 403 if (!isFrontChatInitialized()) { 404 console.error('FrontChat is not initialized.') 405 return 406 } 407 408 const chatFrame = document.getElementById('front-chat-iframe') 409 const chatDocument = chatFrame.contentWindow.document 410 const textArea = chatDocument.querySelector('textarea') 411 let botButton 412 let sendButton 413 chatDocument.querySelectorAll('button').forEach((button) => { 414 if (button.innerText === 'Ich möchte mit einem KI Bot sprechen') { 415 botButton = button 416 } 417 if (button.parentNode.dataset.testid === 'message-input-submit-button') { 418 sendButton = button 419 } 420 }) 421 422 if (textArea) { 423 textArea.value = message 424 const event = new Event('input', { bubbles: true }) 425 textArea.dispatchEvent(event) 426 427 if (sendButton) { 428 await sleep(100) 429 sendButton.click() 430 } else {
431 console.error('Send button not found.') 432 } 433 } else { 434 botButton.click() 435 } 436}
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.