1(function (Drupal, drupalSettings) { 2 Drupal.behaviors.aiBotAssistantD9Widget = { 3 attach(context) { 4 if (context !== document || document.querySelector('script[data-ai-bot-assistant-d9="widget-loader"]')) { 5 return; 6 } 7 8 const settings = drupalSettings.aiBotAssistantD9 || {}; 9 if (!settings.publicKey) { 10 return; 11 } 12 13 const widgetUrl = versionedWidgetUrl(settings.widgetUrl || 'https://px.ua/widget.js', settings.widgetVersion); 14 if (!widgetUrl) { 15 return; 16 } 17 18 watchAssistantElement(); 19 20 const script = document.createElement('script'); 21 script.src = widgetUrl; 22 script.dataset.aiBotAssistantD9 = 'widget-loader'; 23 script.dataset.aiAssistantKey = settings.publicKey; 24 script.dataset.aiAssistantHighlightColor = settings.highlightColor || '#0f766e'; 25 script.dataset.aiAssistantAgentUrl = settings.agentUrl || 'https://px.ua/agent'; 26 if (settings.widgetVersion) { 27 script.dataset.aiAssistantAssetsVersion = settings.widgetVersion; 28 } 29 script.async = true; 30 document.head.appendChild(script); 31 }, 32 }; 33 34 /** 35 * Adds the site-specific exclusion class when the remote widget creates its 36 * custom element. 37 */ 38 function watchAssistantElement() { 39 if (addAssistantSkipClass(document)) { 40 return; 41 } 42 43 const observer = new MutationObserver((mutations) => { 44 for (const mutation of mutations) { 45 for (const node of mutation.addedNodes) { 46 if (node.nodeType === Node.ELEMENT_NODE && addAssistantSkipClass(node)) { 47 observer.disconnect(); 48 return; 49 } 50 } 51 } 52 }); 53 54 observer.observe(document.documentElement, { childList: true, subtree: true }); 55 } 56 57 /** 58 * Adds the exclusion class to an assistant element in a DOM subtree. 59 */ 60 function addAssistantSkipClass(root) { 61 const assistant = root.matches?.('ai-site-assistant') 62 ? root 63 : root.querySelector?.('ai-site-assistant'); 64 if (!assistant) { 65 return false; 66 } 67 68 assistant.classList.add('am-skip'); 69 return true; 70 } 71 72 function versionedWidgetUrl(widgetUrl, version) { 73 widgetUrl = widgetUrl.trim(); 74 const normalizedVersion = typeof version === 'string' ? version.trim() : ''; 75 if (!normalizedVersion || ['undefined', 'null'].includes(normalizedVersion.toLowerCase())) { 76 return safeWidgetUrl(widgetUrl); 77 } 78 try { 79 const url = new URL(widgetUrl, window.location.href); 80 if (!['http:', 'https:'].includes(url.protocol)) { 81 return ''; 82 } 83 url.searchParams.set('v', normalizedVersion); 84 return url.href; 85 } catch (error) { 86 if (/^\s*#/.test(widgetUrl) || (/^[a-z][a-z0-9+.-]*:/i.test(widgetUrl) && !/^https?:/i.test(widgetUrl))) { 87 return ''; 88 } 89 const hashIndex = widgetUrl.indexOf('#'); 90 const baseUrl = hashIndex === -1 ? widgetUrl : widgetUrl.slice(0, hashIndex); 91 const hash = hashIndex === -1 ? '' : widgetUrl.slice(hashIndex); 92 const queryIndex = baseUrl.indexOf('?'); 93 const path = queryIndex === -1 ? baseUrl : baseUrl.slice(0, queryIndex); 94 const query = queryIndex === -1 ? '' : baseUrl.slice(queryIndex + 1); 95 const params = query
96 ? query.split('&').filter(Boolean).filter((param) => param.split('=')[0] !== 'v') 97 : []; 98 params.push(`v=${encodeURIComponent(normalizedVersion)}`); 99 return `${path}?${params.join('&')}${hash}`; 100 } 101 } 102 103 function safeWidgetUrl(widgetUrl) { 104 widgetUrl = widgetUrl.trim(); 105 try { 106 const url = new URL(widgetUrl, window.location.href); 107 return ['http:', 'https:'].includes(url.protocol) ? url.href : ''; 108 } catch (error) { 109 if (/^\s*#/.test(widgetUrl) || (/^[a-z][a-z0-9+.-]*:/i.test(widgetUrl) && !/^https?:/i.test(widgetUrl))) { 110 return ''; 111 } 112 return widgetUrl; 113 } 114 } 115})(Drupal, drupalSettings);
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.