1/** 2 * FlowBot Widget Embed Script 3 * 4 * This script creates iframe widgets for FlowBot chat and SuperSearch search. 5 * It should be included with a script tag: 6 * 7 * <script src="https://your-domain.com/embed.js" defer></script> 8 * 9 * Chat widget: <div data-flowbot-id="YOUR_BOT_ID"></div> 10 * SuperSearch search: <div data-flowbot-supersearch="YOUR_BOT_ID" data-flowbot-supersearch-id="INSTANCE_ID"></div> 11 * AI showcase / uutiset: height follows content via supersearch-resize (optional data-flowbot-height sets initial/min for AI showcase). 12 */ 13 14(function() { 15 'use strict'; 16 17 const scriptTag = document.currentScript || document.querySelector('script[src*="embed.js"]'); 18 const baseUrl = scriptTag ? scriptTag.src.replace(/\/embed\.js.*$/, '') : window.location.origin; 19 20 const CONFIG = { 21 baseUrl: baseUrl 22 }; 23 24 function flowbotPostTargetOrigin() { 25 try { 26 return new URL(CONFIG.baseUrl).origin || '*'; 27 } catch (e) { 28 return '*'; 29 } 30 } 31 32 function applyFlowbotIframeIsolation(iframe) { 33 iframe.setAttribute( 34 'sandbox', 35 'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation allow-storage-access-by-user-activation allow-modals' 36 ); 37 } 38 39 window.__flowbotIframes = window.__flowbotIframes || {}; 40 // SuperSearch / direct_embed iframes (separate from chat __flowbotIframes). 41 window.__flowbotSuperSearchIframes = window.__flowbotSuperSearchIframes || {}; 42 43 // Mobile breakpoint matches the widget's `useIsMobile` hook so both sides agree. 44 const MOBILE_BREAKPOINT = 640; 45 // Reserved space at top of mobile viewport so the host page's top bar stays tappable. 46 const MOBILE_TOP_RESERVE = 96; 47 // Side gutter on mobile so the iframe never bleeds to the screen edge. 48 const MOBILE_SIDE_GUTTER = 24; 49 // Desktop safety: keep at least this much breathing room from the top of the viewport. 50 const DESKTOP_TOP_RESERVE = 40; 51 52 function parsePx(value) { 53 if (typeof value !== 'string') return NaN; 54 const n = parseFloat(value); 55 return Number.isFinite(n) ? n : NaN; 56 } 57 58 function getViewport() { 59 const vv = window.visualViewport; 60 return { 61 width: (vv && vv.width) || window.innerWidth || document.documentElement.clientWidth || 0, 62 height: (vv && vv.height) || window.innerHeight || document.documentElement.clientHeight || 0, 63 top: (vv && Number.isFinite(vv.offsetTop)) ? vv.offsetTop : 0, 64 left: (vv && Number.isFinite(vv.offsetLeft)) ? vv.offsetLeft : 0, 65 }; 66 } 67 68 /** Soft keyboard (or browser chrome) has shrunk the visual viewport vs layout. */ 69 function isSoftKeyboardOpen() { 70 const { height } = getViewport(); 71 const layoutH = window.innerHeight || document.documentElement.clientHeight || height; 72 if (!layoutH || !height) return false; 73 return (layoutH - height) > 120; 74 } 75 76 function isLauncherDimensions(width, height) { 77 return ( 78 (width === '64px' && height === '64px') || 79 (width === '44px' && height === '72px') || 80 (width === '36px' && height === '70px') 81 ); 82 } 83 84 // ââ Prevent host scroll chaining through the chat iframe only âââââââââââââ 85 // Host page must still scroll beside the panel; never freeze document.body. 86 87 function setChatIframeScrollContain(iframe, enabled) { 88 if (!iframe) return; 89 if (enabled) { 90 iframe.style.overscrollBehavior = 'contain'; 91 iframe.style.overscrollBehaviorY = 'contain'; 92 iframe.style.overscrollBehaviorX = 'contain'; 93 iframe.dataset.flowbotScrollContain = 'true'; 94 } else { 95 iframe.style.overscrollBehavior = ''; 96 iframe.style.overscrollBehaviorY = ''; 97 iframe.style.overscrollBehaviorX = ''; 98 delete iframe.dataset.flowbotScrollContain; 99 } 100 } 101 102 function syncHostScrollLockForChat(iframe, fullViewport, width, height) { 103 if (!iframe) return; 104 var open = !!fullViewport || (width && height && !isLauncherDimensions(width, height));
105 setChatIframeScrollContain(iframe, open); 106 } 107 108 /** Host page context forwarded to iframe widgets (domain, path, `<html lang>`). */ 109 function getParentPageContext() { 110 var parentHost = ''; 111 var parentPath = ''; 112 var parentLang = ''; 113 try { 114 parentHost = window.location.hostname || ''; 115 parentPath = window.location.pathname || ''; 116 if (document.documentElement) { 117 parentLang = document.documentElement.getAttribute('lang') || document.documentElement.lang || ''; 118 } 119 } catch (e) {} 120 return { parentHost: parentHost, parentPath: parentPath, parentLang: parentLang }; 121 } 122 123 function appendParentContextQuery(url) { 124 var ctx = getParentPageContext(); 125 if (ctx.parentHost) url += '&parentHost=' + encodeURIComponent(ctx.parentHost); 126 if (ctx.parentPath) url += '&parentPath=' + encodeURIComponent(ctx.parentPath); 127 if (ctx.parentLang) url += '&parentLang=' + encodeURIComponent(ctx.parentLang); 128 return url; 129 } 130 131 var FULLVIEWPORT_MS = 400; 132 var FULLVIEWPORT_EASING = 'cubic-bezier(0.34, 1.2, 0.64, 1)'; 133 134 function prefersReducedMotion() { 135 try { 136 return window.matchMedia('(prefers-reduced-motion: reduce)').matches; 137 } catch (e) { 138 return false; 139 } 140 } 141 142 function setIframeBoxTransition(iframe, enabled) { 143 if (!iframe) return; 144 if (enabled && !prefersReducedMotion()) { 145 iframe.style.transition = 146 'width ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' + 147 'height ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' + 148 'bottom ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' + 149 'right ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' + 150 'border-radius ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' + 151 'box-shadow ' + FULLVIEWPORT_MS + 'ms ease'; 152 } else { 153 iframe.style.transition = ''; 154 } 155 } 156 157 function clearIframeBoxTransitionLater(iframe) { 158 if (!iframe) return; 159 var token = String(Date.now()); 160 iframe.dataset.flowbotTransitionToken = token; 161 window.setTimeout(function () { 162 if (iframe.dataset.flowbotTransitionToken === token) { 163 setIframeBoxTransition(iframe, false); 164 delete iframe.dataset.flowbotTransitionToken; 165 } 166 }, FULLVIEWPORT_MS + 60); 167 } 168 169 /** 170 * Expand iframe to the visual viewport so the soft keyboard resizes the 171 * shell instead of covering the composer. Without keyboard: bottom-right 172 * anchor (open morph from launcher). With keyboard: pin to VV top/left so 173 * the visible box stays centered above the keyboard. 174 * @param {HTMLIFrameElement} iframe 175 * @param {{ animate?: boolean }} [options] 176 */ 177 function applyFullViewportIframe(iframe, options) { 178 if (!iframe) return; 179 const animate = !!(options && options.animate); 180 const { width, height, top, left } = getViewport(); 181 const layoutW = window.innerWidth || document.documentElement.clientWidth || width; 182 const layoutH = window.innerHeight || document.documentElement.clientHeight || height; 183 const keyboardOpen = isSoftKeyboardOpen(); 184 const alreadyFull = iframe.dataset.flowbotFullViewport === 'true'; 185 186 iframe.style.position = 'fixed'; 187 188 if (animate && !alreadyFull) { 189 setIframeBoxTransition(iframe, true); 190 // Force reflow so the transition starts from the current launcher box. 191 void iframe.offsetWidth; 192 } else { 193 setIframeBoxTransition(iframe, false); 194 } 195 196 if (keyboardOpen) { 197 // Pin exactly to the visual viewport rect (above the soft keyboard). 198 iframe.style.top = top + 'px'; 199 iframe.style.left = left + 'px'; 200 iframe.style.right = 'auto'; 201 iframe.style.bottom = 'auto'; 202 iframe.dataset.flowbotKeyboardOpen = 'true'; 203 } else { 204 // Bottom-right anchoring so width/height growth expands from the FAB. 205 const bottom = Math.max(0, layoutH - top - height); 206 const right = Math.max(0, layoutW - left - width); 207 iframe.style.top = 'auto'; 208 iframe.style.left = 'auto'; 209 iframe.style.right = right + 'px'; 210 iframe.style.bottom = bottom + 'px'; 211 delete iframe.dataset.flowbotKeyboardOpen; 212 } 213 214 iframe.style.width = width + 'px'; 215 iframe.style.height = height + 'px'; 216 iframe.style.maxWidth = 'none'; 217 iframe.style.maxHeight = 'none'; 218 iframe.style.borderRadius = '0';
219 iframe.style.boxShadow = 'none'; 220 iframe.style.transform = ''; 221 iframe.dataset.flowbotFullViewport = 'true'; 222 223 if (animate && !alreadyFull) { 224 clearIframeBoxTransitionLater(iframe); 225 } 226 227 // Notify widget so it can switch to a centered card above the keyboard. 228 if (iframe.contentWindow) { 229 try { 230 iframe.contentWindow.postMessage({ 231 type: 'flowbot-host-viewport', 232 width: layoutW > 0 ? layoutW : width, 233 height: height, 234 keyboardOpen: keyboardOpen, 235 }, flowbotPostTargetOrigin()); 236 } catch (e) {} 237 } 238 } 239 240 function clearFullViewportIframe(iframe) { 241 if (!iframe || iframe.dataset.flowbotFullViewport !== 'true') return; 242 delete iframe.dataset.flowbotFullViewport; 243 delete iframe.dataset.flowbotKeyboardOpen; 244 // Leave box edges in place; the next applyIframeDimensions call sets 245 // launcher/panel geometry (optionally animated from this full box). 246 iframe.style.maxWidth = ''; 247 iframe.style.maxHeight = ''; 248 } 249 250 /** 251 * Apply launcher/panel size. Box transition is only for leaving mobile 252 * full-viewport (shrink toward launcher). Desktop launcherâpanel always 253 * snaps size and border-radius â no 50%â16px / circularâcard interpolate. 254 */ 255 function applySizedIframeFromFullViewport(iframe, requestedWidth, requestedHeight, animate, fabShape) { 256 if (!iframe) return; 257 const wasFull = iframe.dataset.flowbotFullViewport === 'true'; 258 // Only morph when closing mobile full-bleed; never for desktop open/close. 259 const shouldAnimate = !!(animate && wasFull && !prefersReducedMotion()); 260 if (shouldAnimate) { 261 setIframeBoxTransition(iframe, true); 262 void iframe.offsetWidth; 263 } else { 264 setIframeBoxTransition(iframe, false); 265 } 266 clearFullViewportIframe(iframe); 267 if (requestedWidth && requestedHeight) { 268 iframe.dataset.flowbotRequestedWidth = String(requestedWidth); 269 iframe.dataset.flowbotRequestedHeight = String(requestedHeight); 270 if (fabShape === 'edge-tab' || fabShape === 'round') { 271 iframe.dataset.flowbotFabShape = fabShape; 272 } 273 applyIframeDimensions(iframe, requestedWidth, requestedHeight, fabShape); 274 } 275 if (shouldAnimate) { 276 clearIframeBoxTransitionLater(iframe); 277 } 278 } 279 280 /** 281 * Prefer explicit fabShape, then dimensions, then stored dataset. 282 * Dimensions win over a stale dataset value (e.g. 64Ã64 after leaving edge-tab). 283 */ 284 function resolveLauncherShape(iframe, requestedWidth, requestedHeight, fabShape) { 285 if (fabShape === 'edge-tab' || fabShape === 'round') return fabShape; 286 if (requestedWidth === '44px' && requestedHeight === '72px') return 'edge-tab'; 287 if ( 288 (requestedWidth === '64px' && requestedHeight === '64px') || 289 (requestedWidth === '36px' && requestedHeight === '70px') 290 ) { 291 return 'round'; 292 } 293 const stored = iframe && iframe.dataset ? iframe.dataset.flowbotFabShape : ''; 294 if (stored === 'edge-tab' || stored === 'round') return stored; 295 return 'round'; 296 } 297 298 /** 299 * Apply the requested dimensions to the iframe, clamping to the current 300 * viewport so the chat never overflows on small screens. Launcher sizes 301 * bypass clamping so the FAB stays at the configured shape. 302 */ 303 function applyIframeDimensions(iframe, requestedWidth, requestedHeight, fabShape) { 304 if (!iframe) return; 305 306 const isLauncherSize = 307 (requestedWidth === '64px' && requestedHeight === '64px') || 308 (requestedWidth === '44px' && requestedHeight === '72px') || 309 (requestedWidth === '36px' && requestedHeight === '70px'); 310 311 if (isLauncherSize) { 312 const launcherShape = resolveLauncherShape( 313 iframe, 314 requestedWidth, 315 requestedHeight, 316 fabShape 317 ); 318 iframe.dataset.flowbotFabShape = launcherShape; 319 320 if (launcherShape === 'edge-tab') { 321 iframe.style.width = '44px'; 322 iframe.style.height = '72px'; 323 iframe.style.bottom = 'calc(20px + env(safe-area-inset-bottom, 0px))'; 324 iframe.style.right = '0'; 325 iframe.style.left = 'auto'; 326 iframe.style.top = 'auto'; 327 iframe.style.transform = ''; 328 iframe.style.borderRadius = '12px 0 0 12px'; 329 iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'; 330 return; 331 } 332 333 // Round launcher (default; legacy 36Ã70 maps here). 334 iframe.style.width = '64px'; 335 iframe.style.height = '64px'; 336 iframe.style.bottom = '20px'; 337 iframe.style.right = '20px'; 338 iframe.style.left = 'auto'; 339 iframe.style.top = 'auto'; 340 iframe.style.transform = ''; 341 iframe.style.borderRadius = '50%'; 342 iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'; 343 return; 344 } 345 346 const reqW = parsePx(requestedWidth); 347 const reqH = parsePx(requestedHeight); 348 if (!Number.isFinite(reqW) || !Number.isFinite(reqH)) return; 349 350 const { width: vw, height: vh } = getViewport(); 351 const isMobile = vw > 0 && vw <= MOBILE_BREAKPOINT; 352 353 const finalW = isMobile 354 ? Math.max(240, Math.min(reqW, vw - MOBILE_SIDE_GUTTER)) 355 : Math.min(reqW, Math.max(240, vw - 16)); 356 const finalH = isMobile 357 ? Math.max(320, Math.min(reqH, vh - MOBILE_TOP_RESERVE)) 358 : Math.min(reqH, Math.max(320, vh - DESKTOP_TOP_RESERVE)); 359 360 iframe.style.width = finalW + 'px'; 361 iframe.style.height = finalH + 'px'; 362 iframe.style.bottom = isMobile ? '12px' : '20px'; 363 iframe.style.right = isMobile ? '12px' : '20px'; 364 iframe.style.left = ''; 365 iframe.style.top = ''; 366 iframe.style.transform = ''; 367 iframe.style.borderRadius = '16px'; 368 iframe.style.boxShadow = '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'; 369 } 370 371 function postHostViewportToIframe(iframe, vw, vh, keyboardOpen) { 372 if (!iframe || !iframe.isConnected || !iframe.contentWindow) return; 373 try { 374 iframe.contentWindow.postMessage({ 375 type: 'flowbot-host-viewport', 376 width: vw, 377 height: vh, 378 keyboardOpen: keyboardOpen, 379 }, flowbotPostTargetOrigin()); 380 } catch (e) {} 381 } 382 383 function superSearchIframeKey(botId, instanceId) { 384 return String(botId || '') + '::' + String(instanceId || ''); 385 } 386 387 function registerSuperSearchIframe(botId, instanceId, iframe) { 388 if (!iframe) return; 389 window.__flowbotSuperSearchIframes = window.__flowbotSuperSearchIframes || {}; 390 window.__flowbotSuperSearchIframes[superSearchIframeKey(botId, instanceId)] = iframe; 391 } 392 393 /**
394 * Re-apply cached dimensions to every tracked iframe. Called on resize / 395 * orientationchange / visualViewport resize so rotating the device or the 396 * soft keyboard popping up keeps the widget within bounds. 397 */ 398 function reapplyAllIframeDimensions() { 399 const map = window.__flowbotIframes || {}; 400 const { width: vw, height: vh } = getViewport(); 401 const keyboardOpen = isSoftKeyboardOpen(); 402 Object.keys(map).forEach(function (id) { 403 const iframe = map[id]; 404 if (!iframe || !iframe.isConnected) return; 405 // Keep the embedded widget in sync with the HOST viewport so it can 406 // re-evaluate mobile vs desktop sizing and soft-keyboard layout. 407 postHostViewportToIframe(iframe, vw, vh, keyboardOpen); 408 if (iframe.dataset.flowbotFullViewport === 'true') { 409 // Keyboard / orientation: snap, do not replay open animation. 410 applyFullViewportIframe(iframe, { animate: false }); 411 return; 412 } 413 const w = iframe.dataset.flowbotRequestedWidth; 414 const h = iframe.dataset.flowbotRequestedHeight; 415 if (!w || !h) return; 416 applyIframeDimensions(iframe, w, h); 417 }); 418 419 // SuperSearch iframes are not in __flowbotIframes â still need live 420 // host viewport so useHostIsMobile / DirectEmbed bar width update without reload. 421 const ssMap = window.__flowbotSuperSearchIframes || {}; 422 Object.keys(ssMap).forEach(function (id) { 423 var ssIframe = ssMap[id]; 424 postHostViewportToIframe(ssIframe, vw, vh, keyboardOpen); 425 if ( 426 ssIframe && 427 (ssIframe.hasAttribute('data-flowbot-direct-embed') || 428 ssIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true') 429 ) { 430 applyDirectEmbedIframePosition(ssIframe); 431 } 432 }); 433 } 434 435 function attachViewportListenersOnce() { 436 if (window.__flowbotViewportListenersAttached) return; 437 window.__flowbotViewportListenersAttached = true; 438 439 let rafId = 0; 440 const schedule = function () { 441 if (rafId) return; 442 rafId = window.requestAnimationFrame(function () { 443 rafId = 0; 444 reapplyAllIframeDimensions(); 445 }); 446 }; 447 448 window.addEventListener('resize', schedule); 449 window.addEventListener('orientationchange', schedule); 450 if (window.visualViewport) { 451 window.visualViewport.addEventListener('resize', schedule); 452 } 453 } 454 455 function injectCriticalStyles() { 456 if (document.getElementById('flowbot-critical-styles')) return; 457 const style = document.createElement('style'); 458 style.id = 'flowbot-critical-styles'; 459 style.textContent = ` 460 [data-flowbot-id] { 461 position: relative; 462 display: block; 463 } 464 [data-flowbot-supersearch] { 465 position: relative; 466 display: block; 467 width: 100%; 468 height: 100%; 469 min-width: 120px; 470 min-height: 40px; 471 } 472 [data-flowbot-supersearch][data-flowbot-uutiset="true"] { 473 height: auto; 474 overflow: visible; 475 } 476 iframe[data-flowbot-uutiset="true"] { 477 display: block; 478 width: 100% !important; 479 border: none !important; 480 background: transparent; 481 overflow: hidden; 482 } 483 [data-flowbot-supersearch][data-flowbot-embed-layout="direct_embed"], 484 [data-flowbot-supersearch][data-flowbot-direct-embed="true"] { 485 position: fixed !important; 486 width: 0 !important; 487 height: 0 !important; 488 min-width: 0 !important; 489 min-height: 0 !important; 490 margin: 0 !important; 491 padding: 0 !important; 492 overflow: visible !important; 493 background: transparent !important; 494 border: none !important; 495 pointer-events: none !important; 496 } 497 [data-flowbot-popup] { 498 display: none; 499 } 500 iframe[data-flowbot-direct-embed="true"] { 501 pointer-events: auto !important; 502 border: none !important; 503 background: transparent !important; 504 background-color: transparent !important; 505 color-scheme: normal; 506 overflow: hidden !important; 507 }
508 iframe[data-flowbot-direct-embed-pending="true"] { 509 visibility: hidden !important; 510 opacity: 0 !important; 511 pointer-events: none !important; 512 } 513 /* Open chat: stop scroll chaining into the host; host still scrolls beside the iframe. */ 514 iframe[data-flowbot-chat="true"][data-flowbot-scroll-contain="true"] { 515 overscroll-behavior: contain; 516 } 517 `; 518 document.head.appendChild(style); 519 } 520 521 function applySuperSearch360Dimensions(foundIframe, width, height) { 522 if (!foundIframe) return; 523 var w = typeof width === 'number' ? width + 'px' : width; 524 var h = typeof height === 'number' ? height + 'px' : height; 525 if (!w || !h) return; 526 527 foundIframe.style.width = w; 528 foundIframe.style.height = h; 529 foundIframe.style.display = 'block'; 530 foundIframe.style.minWidth = '0'; 531 foundIframe.style.minHeight = '0'; 532 applySuperSearchIframeTrans
532parency(foundIframe); 533 534 var container = foundIframe.parentElement; 535 if (container && container.hasAttribute('data-flowbot-supersearch')) { 536 container.style.position = 'relative'; 537 container.style.width = w; 538 container.style.height = h; 539 container.style.margin = '0 auto'; 540 container.style.top = ''; 541 container.style.left = ''; 542 container.style.right = ''; 543 container.style.bottom = ''; 544 container.style.transform = 'none'; 545 } 546 } 547 548 var AI_SHOWCASE_INITIAL_HEIGHT_PX = 420; 549 var AI_SHOWCASE_HEIGHT_MIN_PX = 200; 550 var AI_SHOWCASE_HEIGHT_MAX_PX = 6000; 551 552 function parseHostHeightPx(value) { 553 if (value == null || value === '') return 0; 554 if (typeof value === 'number' && isFinite(value)) { 555 return value >= 48 ? Math.round(value) : 0; 556 } 557 var match = String(value).trim().match(/^(\d+(?:\.\d+)?)\s*(px)?$/i); 558 if (!match) return 0; 559 var n = Math.round(parseFloat(match[1])); 560 return n >= 48 ? n : 0; 561 } 562 563 /** Optional explicit initial height; otherwise a compact placeholder until content resize. */ 564 function resolveAiShowcaseInitialHeightPx(container) { 565 if (!container) return AI_SHOWCASE_INITIAL_HEIGHT_PX; 566 567 var explicit = 568 parseHostHeightPx(container.getAttribute('data-flowbot-height')) || 569 parseHostHeightPx(container.getAttribute('data-flowbot-supersearch-height')); 570 571 var parent = container.parentElement; 572 if (!explicit && parent) { 573 explicit = parseHostHeightPx(parent.getAttribute('data-flowbot-height')); 574 } 575 576 if (explicit >= AI_SHOWCASE_HEIGHT_MIN_PX) { 577 return Math.min(AI_SHOWCASE_HEIGHT_MAX_PX, explicit); 578 } 579 return AI_SHOWCASE_INITIAL_HEIGHT_PX; 580 } 581 582 function clampAiShowcaseHeightPx(value) { 583 if (typeof value !== 'number' || !isFinite(value)) return null; 584 return Math.min( 585 AI_SHOWCASE_HEIGHT_MAX_PX, 586 Math.max(AI_SHOWCASE_HEIGHT_MIN_PX, Math.round(value)) 587 ); 588 } 589 590 function applyAiShowcaseHostSize(iframe, container) { 591 if (!iframe) return; 592 var heightPx = resolveAiShowcaseInitialHeightPx(container); 593 var heightCss = heightPx + 'px'; 594 var hostParent = container && container.parentElement; 595 596 iframe.setAttribute('data-flowbot-ai-showcase', 'true'); 597 iframe.style.border = 'none'; 598 iframe.style.borderRadius = '28px'; 599 iframe.style.boxShadow = 'none'; 600 iframe.style.background = 'transparent'; 601 iframe.style.display = 'block'; 602 iframe.style.overflow = 'hidden'; 603 setImportantStyle(iframe, 'width', '100%'); 604 setImportantStyle(iframe, 'height', heightCss); 605 setImportantStyle(iframe, 'min-height', '0'); 606 607 if (!container || !container.hasAttribute('data-flowbot-supersearch')) return; 608 609 container.dataset.flowbotHostHeight = String(heightPx); 610 container.setAttribute('data-flowbot-ai-showcase', 'true'); 611 container.style.position = 'relative'; 612 container.style.top = ''; 613 container.style.left = ''; 614 container.style.right = ''; 615 container.style.bottom = ''; 616 container.style.display = 'block'; 617 container.style.maxWidth = ''; 618 container.style.marginLeft = ''; 619 container.style.marginRight = ''; 620 container.style.overflow = 'visible'; 621 container.style.background = 'transparent'; 622 container.style.border = 'none'; 623 container.style.borderRadius = '28px'; 624 container.style.boxShadow = 'none'; 625 setImportantStyle(container, 'width', '100%'); 626 setImportantStyle(container, 'height', heightCss); 627 setImportantStyle(container, 'min-height', '0'); 628 629 if (hostParent) { 630 hostParent.style.overflow = 'visible'; 631 hostParent.style.maxWidth = ''; 632 } 633 } 634 635 function applyAiShowcaseResize(iframe, height) { 636 if (!iframe) return; 637 var heightPx = clampAiShowcaseHeightPx(height); 638 if (heightPx == null) return; 639 var heightCss = heightPx + 'px'; 640 var container = iframe.parentElement; 641 642 setImportantStyle(iframe, 'width', '100%'); 643 setImportantStyle(iframe, 'height', heightCss); 644 setImportantStyle(iframe, 'min-height', '0'); 645 646 if (!container || !container.hasAttribute('data-flowbot-supersearch')) return; 647 container.dataset.flowbotHostHeight = String(heightPx); 648 container.style.maxWidth = ''; 649 setImportantStyle(container, 'width', '100%'); 650 setImportantStyle(container, 'height', heightCss); 651 setImportantStyle(container, 'min-height', '0'); 652 } 653 654 var UUTISET_INITIAL_HEIGHT_PX = 132; 655 var UUTISET_HEIGHT_MIN_PX = 88; 656 var UUTISET_HEIGHT_MAX_PX = 8000; 657 658 function clampUutisetHeightPx(value) { 659 if (typeof value !== 'number' || !isFinite(value)) return null; 660 return Math.min( 661 UUTISET_HEIGHT_MAX_PX, 662 Math.max(UUTISET_HEIGHT_MIN_PX, Math.round(value)) 663 ); 664 } 665 666 /** 667 * The uutiset iframe has an explicit px height. Release clipping on the 668 * immediate parent so sibling widgets do not collapse that height. 669 */ 670 function releaseUutisetParentOverflow(container) { 671 if (!container) return; 672 var parent = container.parentElement; 673 if (!parent || parent === document.body || parent === document.documentElement) return; 674 parent.style.overflow = 'visible'; 675 parent.style.maxHeight = 'none'; 676 } 677 678 function applyUutisetFrameChrome(iframe) { 679 if (!iframe) return; 680 iframe.setAttribute('data-flowbot-uutiset', 'true'); 681 iframe.setAttribute('scrolling', 'no'); 682 iframe.style.border = 'none'; 683 iframe.style.borderRadius = '0';
684 iframe.style.boxShadow = 'none'; 685 iframe.style.background = 'transparent'; 686 iframe.style.display = 'block'; 687 iframe.style.overflow = 'hidden'; 688 setImportantStyle(iframe, 'width', '100%'); 689 setImportantStyle(iframe, 'min-height', '0'); 690 applySuperSearchIframeTransparency(iframe); 691 } 692 693 function applyUutisetHostSize(iframe, container) { 694 if (!iframe) return; 695 applyUutisetFrameChrome(iframe); 696 setImportantStyle(iframe, 'height', UUTISET_INITIAL_HEIGHT_PX + 'px'); 697 setImportantStyle(iframe, 'min-height', UUTISET_INITIAL_HEIGHT_PX + 'px'); 698 699 if (!container || !container.hasAttribute('data-flowbot-supersearch')) return; 700 container.setAttribute('data-flowbot-uutiset', 'true'); 701 container.dataset.flowbotHostHeight = String(UUTISET_INITIAL_HEIGHT_PX); 702 container.style.position = 'relative'; 703 container.style.top = ''; 704 container.style.left = ''; 705 container.style.right = ''; 706 container.style.bottom = ''; 707 container.style.display = 'block'; 708 container.style.maxWidth = ''; 709 container.style.marginLeft = ''; 710 container.style.marginRight = ''; 711 container.style.overflow = 'visible'; 712 container.style.background = 'transparent'; 713 container.style.border = 'none'; 714 container.style.borderRadius = '0'; 715 container.style.boxShadow = 'none'; 716 setImportantStyle(container, 'width', '100%'); 717 setImportantStyle(container, 'height', UUTISET_INITIAL_HEIGHT_PX + 'px'); 718 setImportantStyle(container, 'min-height', UUTISET_INITIAL_HEIGHT_PX + 'px'); 719 releaseUutisetParentOverflow(container); 720 } 721 722 function applyUutisetResize(iframe, height) { 723 if (!iframe) return; 724 var heightPx = clampUutisetHeightPx(height); 725 if (heightPx == null) return; 726 var heightCss = heightPx + 'px'; 727 var container = iframe.parentElement; 728 729 applyUutisetFrameChrome(iframe); 730 setImportantStyle(iframe, 'height', heightCss); 731 setImportantStyle(iframe, 'min-height', heightCss); 732 733 if (!container || !container.hasAttribute('data-flowbot-supersearch')) return; 734 container.setAttribute('data-flowbot-uutiset', 'true'); 735 container.dataset.flowbotHostHeight = String(heightPx); 736 container.style.maxWidth = ''; 737 setImportantStyle(container, 'width', '100%'); 738 setImportantStyle(container, 'height', heightCss); 739 setImportantStyle(container, 'min-height', heightCss); 740 releaseUutisetParentOverflow(container); 741 } 742 743 function isUutisetSupersearchFrame(iframe) { 744 if (!iframe) return false; 745 if (iframe.hasAttribute('data-flowbot-uutiset')) return true; 746 var parent = iframe.parentElement; 747 return !!(parent && parent.hasAttribute('data-flowbot-uutiset')); 748 } 749 750 var DIRECT_EMBED_ANCHORS = [ 751 'top-left', 'top-center', 'top-right', 752 'bottom-left', 'bottom-center', 'bottom-right' 753 ]; 754 var DIRECT_EMBED_OFFSET_MIN = -500; 755 var DIRECT_EMBED_OFFSET_MAX = 500; 756 // Floor rejects border-only first measures (~2â4px) from a starved iframe. 757 var DIRECT_EMBED_WIDTH_MIN = 48; 758 var DIRECT_EMBED_HEIGHT_MIN = 40; 759 var DIRECT_EMBED_DIM_MAX = 2000; 760 var DIRECT_EMBED_ZINDEX_MIN = 1; 761 var DIRECT_EMBED_ZINDEX_MAX = 2147483646; 762 763 var trustedFlowbotOrigin = ''; 764 try { 765 trustedFlowbotOrigin = new URL(CONFIG.baseUrl).origin; 766 } catch (e) {} 767 768 function isTrustedFlowbotMessageOrigin(event) { 769 if (!event || !event.origin || !trustedFlowbotOrigin) return false; 770 if (event.origin === trustedFlowbotOrigin) return true; 771 // Allow firebase hosting alias pair for same site name 772 try { 773 var a = new URL(trustedFlowbotOrigin); 774 var b = new URL(event.origin); 775 if (a.protocol !== b.protocol) return false; 776 var aHost = a.hostname; 777 var bHost = b.hostname; 778 function projectId(host) { 779 if (host.endsWith('.firebaseapp.com')) return host.slice(0, -'.firebaseapp.com'.length); 780 if (host.endsWith('.web.app')) return host.slice(0, -'.web.app'.length); 781 return null; 782 } 783 var pa = projectId(aHost); 784 var pb = projectId(bHost); 785 return !!(pa && pb && pa === pb); 786 } catch (e) { return false; } 787 } 788 789 function clampDirectEmbedDimension(value, min) { 790 if (typeof value !== 'number' || !isFinite(value)) return null; 791 var floor = typeof min === 'number' && isFinite(min) ? min : DIRECT_EMBED_WIDTH_MIN; 792 return Math.min(DIRECT_EMBED_DIM_MAX, Math.max(floor, Math.round(value))); 793 } 794 795 function clampDirectEmbedOffset(value, fallback) { 796 var n = typeof value === 'number' && isFinite(value) ? value : fallback; 797 return Math.min(DIRECT_EMBED_OFFSET_MAX, Math.max(DIRECT_EMBED_OFFSET_MIN, n)); 798 } 799
800 function normalizeDirectEmbedAnchor(anchor) { 801 return DIRECT_EMBED_ANCHORS.indexOf(anchor) > -1 ? anchor : 'top-center'; 802 } 803 804 function getNormalizedEmbedLayout(container) { 805 if (!container) return ''; 806 var raw = container.getAttribute('data-flowbot-embed-layout'); 807 if (!raw) return ''; 808 return raw.trim().toLowerCase(); 809 } 810 811 function setImportantStyle(el, prop, value) { 812 if (!el) return; 813 el.style.setProperty(prop, value, 'important'); 814 } 815 816 // Keep in sync with computeEffectiveDirectEmbedBarWidth (superSearchService.ts): 817 // never wider than viewport â gutter; may go below 240 when viewport requires it. 818 function getDirectEmbedInitialWidth(layout) { 819 var vw = getViewport().width || 0; 820 var barWidth = 400; 821 var maxWidthVw = 100; 822 var gutter = 32; 823 if (layout && typeof layout === 'object') { 824 if (typeof layout.barWidth === 'number' && isFinite(layout.barWidth)) { 825 barWidth = Math.min(640, Math.max(240, Math.round(layout.barWidth))); 826 } 827 if (typeof layout.maxWidthVw === 'number' && isFinite(layout.maxWidthVw)) { 828 maxWidthVw = Math.min(100, Math.max(40, Math.round(layout.maxWidthVw))); 829 } 830 } 831 if (vw <= 0) return barWidth; 832 var vwCap = Math.floor(vw * maxWidthVw / 100); 833 var available = Math.max(1, vw - gutter); 834 return Math.min(barWidth, vwCap, available); 835 } 836 837 function hideDirectEmbedIframePending(iframe) { 838 if (!iframe) return; 839 iframe.style.visibility = 'hidden'; 840 iframe.style.opacity = '0'; 841 iframe.style.pointerEvents = 'none'; 842 iframe.setAttribute('data-flowbot-direct-embed-pending', 'true'); 843 } 844 845 function revealDirectEmbedIframe(iframe) { 846 if (!iframe || iframe.dataset.flowbotDirectEmbedHidden === '1') return; 847 iframe.style.display = 'block'; 848 iframe.style.visibility = 'visible'; 849 iframe.style.opacity = '1'; 850 iframe.style.pointerEvents = 'auto'; 851 iframe.removeAttribute('data-flowbot-direct-embed-pending'); 852 } 853 854 function markDirectEmbedIframeHidden(iframe) { 855 if (!iframe) return; 856 iframe.dataset.flowbotDirectEmbedHidden = '1'; 857 iframe.removeAttribute('data-flowbot-direct-embed-pending'); 858 iframe.style.display = 'none'; 859 iframe.style.visibility = 'hidden'; 860 iframe.style.opacity = '0'; 861 iframe.style.pointerEvents = 'none'; 862 } 863 864 function findDirectEmbedContainer(iframe) { 865 if (!iframe) return null; 866 var mountId = iframe.dataset.flowbotMountId; 867 if (mountId) { 868 var linked = document.querySelector( 869 '[data-flowbot-supersearch][data-flowbot-mount-id="' + mountId + '"]' 870 ); 871 if (linked) return linked; 872 } 873 var parent = iframe.parentElement; 874 if (parent && parent.hasAttribute('data-flowbot-supersearch')) { 875 return parent; 876 } 877 return null; 878 } 879 880 function ensureDirectEmbedIframeOnBody(iframe, container) { 881 if (!iframe || !container) return; 882 if (!container.dataset.flowbotMountId) { 883 var botId = container.getAttribute('data-flowbot-supersearch') || 'unknown'; 884 var instanceId = container.getAttribute('data-flowbot-supersearch-id') || ''; 885 container.dataset.flowbotMountId = 'flowbot-de-' + botId + (instanceId ? '-' + instanceId : ''); 886 } 887 iframe.dataset.flowbotMountId = container.dataset.flowbotMountId; 888 if (iframe.parentElement !== document.body) { 889 document.body.appendChild(iframe); 890 } 891 } 892 893 function applySuperSearchIframeTransparency(iframe) { 894 if (!iframe) return; 895 iframe.style.background = 'transparent'; 896 iframe.style.backgroundColor = 'transparent'; 897 iframe.style.colorScheme = 'normal'; 898 iframe.setAttribute('allowTransparency', 'true'); 899 } 900 901 function applyDirectEmbedIframePosition(iframe) { 902 if (!iframe) return; 903 var resolved = normalizeDirectEmbedAnchor( 904 iframe.dataset.flowbotDirectEmbedAnchor || 'top-center' 905 ); 906 var offsetX = clampDirectEmbedOffset( 907 parseFloat(iframe.dataset.flowbotDirectEmbedOffsetX), 908 0 909 ); 910 var offsetY = clampDirectEmbedOffset( 911 parseFloat(iframe.dataset.flowbotDirectEmbedOffsetY),
912 16 913 ); 914 var zIndex = iframe.dataset.flowbotDirectEmbedZIndex || '9998'; 915 var vv = getViewport(); 916 var keyboardOpen = isSoftKeyboardOpen(); 917 var iframeWidth = 918 parseFloat(iframe.dataset.flowbotDirectEmbedWidth) || 919 parseFloat(iframe.style.width) || 920 0; 921 922 iframe.style.position = 'fixed'; 923 setImportantStyle(iframe, 'z-index', String(zIndex)); 924 iframe.style.top = ''; 925 iframe.style.bottom = ''; 926 iframe.style.left = ''; 927 iframe.style.right = ''; 928 iframe.style.transform = 'none'; 929 930 if (keyboardOpen && resolved.indexOf('bottom') !== 0) { 931 // Pin into the visual viewport so Chrome does not paint a layout-viewport ghost. 932 iframe.style.top = (vv.top + offsetY) + 'px'; 933 iframe.style.bottom = 'auto'; 934 } else if (keyboardOpen && resolved.indexOf('bottom') === 0) { 935 var layoutH = window.innerHeight || document.documentElement.clientHeight || vv.height; 936 var gapBelow = Math.max(0, layoutH - vv.top - vv.height); 937 iframe.style.bottom = (gapBelow + offsetY) + 'px'; 938 iframe.style.top = 'auto'; 939 } else if (resolved.indexOf('bottom') === 0) { 940 iframe.style.bottom = offsetY + 'px'; 941 iframe.style.top = 'auto'; 942 } else { 943 iframe.style.top = offsetY + 'px'; 944 iframe.style.bottom = 'auto'; 945 } 946 947 if (resolved.indexOf('-left') > -1) { 948 iframe.style.left = (keyboardOpen ? vv.left + offsetX : offsetX) + 'px'; 949 iframe.style.right = 'auto'; 950 } else if (resolved.indexOf('-right') > -1) { 951 iframe.style.right = offsetX + 'px'; 952 iframe.style.left = 'auto'; 953 } else if (iframeWidth > 0) { 954 // Avoid transform: translateX(-50%) â Chrome can leave a compositor ghost layer. 955 if (keyboardOpen) { 956 iframe.style.left = (vv.left + vv.width / 2 - iframeWidth / 2 + offsetX) + 'px'; 957 } else { 958 iframe.style.left = 'calc(50% + ' + offsetX + 'px - ' + (iframeWidth / 2) + 'px)'; 959 } 960 iframe.style.right = 'auto'; 961 } else { 962 iframe.style.left = 'calc(50% + ' + offsetX + 'px)'; 963 iframe.style.right = 'auto'; 964 iframe.style.transform = 'translateX(-50%)'; 965 } 966 } 967 968 function parseDirectEmbedLayout(layout) { 969 var resolved = layout && typeof layout === 'object' ? layout : {}; 970 return { 971 anchor: normalizeDirectEmbedAnchor( 972 typeof resolved.anchor === 'string' ? resolved.anchor : 'top-center' 973 ), 974 offsetX: clampDirectEmbedOffset(resolved.offsetX, 0), 975 offsetY: clampDirectEmbedOffset(resolved.offsetY, 16), 976 zIndex: typeof resolved.zIndex === 'number' && isFinite(resolved.zIndex) 977 ? Math.min(DIRECT_EMBED_ZINDEX_MAX, Math.max(DIRECT_EMBED_ZINDEX_MIN, Math.round(resolved.zIndex))) 978 : 9998 979 }; 980 } 981 982 function applyDirectEmbedLayoutToIframe(iframe, layout) { 983 if (!iframe) return; 984 var parsed = parseDirectEmbedLayout(layout); 985 iframe.setAttribute('data-flowbot-direct-embed', 'true'); 986 iframe.dataset.flowbotDirectEmbedAnchor = parsed.anchor; 987 iframe.dataset.flowbotDirectEmbedOffsetX = String(parsed.offsetX); 988 iframe.dataset.flowbotDirectEmbedOffsetY = String(parsed.offsetY); 989 iframe.dataset.flowbotDirectEmbedZIndex = String(parsed.zIndex); 990 applyDirectEmbedIframePosition(iframe); 991 } 992 993 function applyDirectEmbedContainerMountStyles(container, iframe) { 994 if (!container || !iframe) return; 995 container.setAttribute('data-flowbot-direct-embed', 'true'); 996 ensureDirectEmbedIframeOnBody(iframe, container); 997 998 // Zero-size mount marker â anchor/position live on the iframe (fixed to viewport) 999 setImportantStyle(container, 'position', 'fixed'); 1000 setImportantStyle(container, 'width', '0'); 1001 setImportantStyle(container, 'height', '0'); 1002 setImportantStyle(container, 'display', 'block'); 1003 setImportantStyle(container, 'padding', '0'); 1004 container.style.overflow = 'visible'; 1005 container.style.background = 'transparent'; 1006 container.style.border = 'none'; 1007 container.style.borderRadius = '0';
1008 container.style.boxShadow = 'none'; 1009 setImportantStyle(container, 'margin', '0'); 1010 container.style.pointerEvents = 'none'; 1011 setImportantStyle(container, 'min-width', '0'); 1012 setImportantStyle(container, 'min-height', '0'); 1013 container.style.maxWidth = ''; 1014 container.style.maxHeight = ''; 1015 setImportantStyle(container, 'top', 'auto'); 1016 setImportantStyle(container, 'bottom', 'auto'); 1017 setImportantStyle(container, 'left', 'auto'); 1018 setImportantStyle(container, 'right', 'auto'); 1019 container.style.transform = ''; 1020 container.style.zIndex = ''; 1021 1022 iframe.style.border = 'none'; 1023 iframe.style.borderRadius = '0'; 1024 iframe.style.boxShadow = 'none'; 1025 iframe.style.overflow = 'hidden'; 1026 iframe.setAttribute('scrolling', 'no'); 1027 iframe.style.display = 'block'; 1028 iframe.style.minWidth = '0'; 1029 iframe.style.minHeight = '0'; 1030 applySuperSearchIframeTransparency(iframe); 1031 } 1032 1033 // Taller chrome bar (~title + input row) needs >48px to avoid first-paint clip. 1034 var DIRECT_EMBED_INITIAL_HEIGHT_PX = 64; 1035 // Collapsed pill (expandOnActivate) â keep init reveal close to real pill size. 1036 var DIRECT_EMBED_COLLAPSED_INITIAL_HEIGHT_PX = 48; 1037 1038 function estimateCollapsedPillWidth(layout) { 1039 var label = 'Haku'; 1040 if (layout && typeof layout.collapsedLabel === 'string' && layout.collapsedLabel.trim()) { 1041 label = layout.collapsedLabel.trim(); 1042 } 1043 // Rough text + icon + horizontal padding; clamp so init isn't a full bar. 1044 var approx = Math.ceil(label.length * 8 + 56); 1045 return Math.min(200, Math.max(72, approx)); 1046 } 1047 1048 function getDirectEmbedBootDimensions(layout) { 1049 var collapsed = !!(layout && layout.expandOnActivate); 1050 if (collapsed) { 1051 return { 1052 width: estimateCollapsedPillWidth(layout), 1053 height: DIRECT_EMBED_COLLAPSED_INITIAL_HEIGHT_PX 1054 }; 1055 } 1056 return { 1057 width: getDirectEmbedInitialWidth(layout), 1058 height: DIRECT_EMBED_INITIAL_HEIGHT_PX 1059 }; 1060 } 1061 1062 function ensureDirectEmbedInitialDimensions(iframe, layout) { 1063 if (!iframe || iframe.dataset.flowbotDirectEmbedHeight) return; 1064 var boot = getDirectEmbedBootDimensions(layout); 1065 iframe.style.width = boot.width + 'px'; 1066 iframe.style.height = boot.height + 'px'; 1067 iframe.dataset.flowbotDirectEmbedWidth = String(boot.width); 1068 iframe.dataset.flowbotDirectEmbedHeight = String(boot.height); 1069 } 1070 1071 function handleDirectEmbedSupersearchResize(iframe, width, height) { 1072 if (!iframe) return false; 1073 var isDirectEmbed = 1074 iframe.hasAttribute('data-flowbot-direct-embed') || 1075 iframe.getAttribute('data-flowbot-direct-embed-pending') === 'true'; 1076 if (!isDirectEmbed) return false; 1077 // Ignore content-measure resizes until init reveals with boot dimensions. 1078 // Early posts can be ~border-width (2â4px) while the iframe is still pending. 1079 if (iframe.getAttribute('data-flowbot-direct-embed-pending') === 'true') { 1080 return true; 1081 } 1082 if (!iframe.hasAttribute('data-flowbot-direct-embed')) { 1083 iframe.setAttribute('data-flowbot-direct-embed', 'true'); 1084 } 1085 applyDirectEmbedIframeDimensions(iframe, width, height); 1086 return true; 1087 } 1088 1089 function handleDirectEmbedSupersearchInit(iframe, layout) { 1090 if (!iframe) return; 1091 var directContainer = findDirectEmbedContainer(iframe); 1092 if (directContainer) { 1093 applyDirectEmbedContainerMountStyles(directContainer, iframe); 1094 } 1095 if (layout) { 1096 applyDirectEmbedLayoutToIframe(iframe, layout); 1097 } 1098 // Always re-seed boot size on init while revealing so a stale full-bar 1099 // width cannot flash for expandOnActivate / collapsed pills. 1100 var boot = getDirectEmbedBootDimensions(layout); 1101 iframe.dataset.flowbotDirectEmbedWidth = String(boot.width); 1102 iframe.dataset.flowbotDirectEmbedHeight = String(boot.height); 1103 applyDirectEmbedIframeDimensions(iframe, boot.width, boot.height); 1104 } 1105 1106 function bootstrapDirectEmbedIframe(container, iframe) { 1107 if (!iframe) return; 1108 if (container) { 1109 applyDirectEmbedContainerMountStyles(container, iframe); 1110 } 1111 hideDirectEmbedIframePending(iframe); 1112 iframe.style.position = 'fixed'; 1113 iframe.style.top = '0'; 1114 iframe.style.left = '0'; 1115 applySuperSearchIframeTransparency(iframe); 1116 // Seed a pill-sized box (still visibility:hidden) so first in-iframe layout 1117 // is not max-w-full-starved to ~1â4px before supersearch-init. 1118 var boot = getDirectEmbedBootDimensions({ expandOnActivate: true }); 1119 iframe.style.width = boot.width + 'px'; 1120 iframe.style.height = boot.height + 'px'; 1121 iframe.dataset.flowbotDirectEmbedWidth = String(boot.width); 1122 iframe.dataset.flowbotDirectEmbedHeight = String(boot.height); 1123 setImportantStyle(iframe, 'z-index', '9998'); 1124 } 1125 1126 function applyDirectEmbedContainerStyles(container, iframe, layout) { 1127 if (!iframe) return; 1128 if (container) { 1129 applyDirectEmbedContainerMountStyles(container, iframe); 1130 } 1131 if (layout) { 1132 applyDirectEmbedLayoutToIframe(iframe, layout); 1133 } 1134 ensureDirectEmbedInitialDimensions(iframe, layout); 1135 } 1136 1137 // Host snaps W/H immediately â expand/collapse morph runs inside the iframe. 1138 function applyDirectEmbedIframeDimensions(foundIframe, width, height) { 1139 if (!foundIframe || foundIframe.dataset.flowbotDirectEmbedHidden === '1') return; 1140 var w = clampDirectEmbedDimension(width, DIRECT_EMBED_WIDTH_MIN); 1141 var h = clampDirectEmbedDimension(height, DIRECT_EMBED_HEIGHT_MIN); 1142 if (w == null && h == null) return; 1143 1144 // Always snap (no size transition). Pending-hide first paint stays instant.
1145 foundIframe.style.transition = ''; 1146 1147 if (w != null) { 1148 foundIframe.style.width = w + 'px'; 1149 foundIframe.dataset.flowbotDirectEmbedWidth = String(w); 1150 } 1151 if (h != null) { 1152 foundIframe.style.height = h + 'px'; 1153 foundIframe.dataset.flowbotDirectEmbedHeight = String(h); 1154 } 1155 1156 foundIframe.style.display = 'block'; 1157 foundIframe.style.minWidth = '0'; 1158 foundIframe.style.minHeight = '0'; 1159 foundIframe.style.overflow = 'hidden'; 1160 applySuperSearchIframeTransparency(foundIframe); 1161 1162 applyDirectEmbedIframePosition(foundIframe); 1163 revealDirectEmbedIframe(foundIframe); 1164 } 1165 1166 function findIframeByEventSource(eventSource) { 1167 if (!eventSource) return null; 1168 try { 1169 var iframes = document.querySelectorAll('iframe'); 1170 for (var i = 0; i < iframes.length; i++) { 1171 if (iframes[i].contentWindow === eventSource) { 1172 return iframes[i]; 1173 } 1174 } 1175 } catch (e) {} 1176 return null; 1177 } 1178 1179 function isTrustedSupersearchMessage(event) { 1180 return isTrustedFlowbotMessageOrigin(event) || !!findIframeByEventSource(event.source); 1181 } 1182 1183 function isSafeSupersearchNavigateUrl(url) { 1184 if (typeof url !== 'string') return false; 1185 var trimmed = url.trim(); 1186 if (!trimmed || trimmed === '#' || trimmed.length > 4096) return false; 1187 try { 1188 var parsed = new URL(trimmed, window.location.href); 1189 return parsed.protocol === 'http:' || parsed.protocol === 'https:'; 1190 } catch (e) { 1191 return false; 1192 } 1193 } 1194 1195 /** Open a SuperSearch hit on the host page (iframe target=_top is sandboxed). */ 1196 function handleSupersearchNavigate(event) { 1197 if (!event || !event.data || event.data.type !== 'supersearch-navigate') return false; 1198 if (!isTrustedSupersearchMessage(event)) return false; 1199 if (!isSafeSupersearchNavigateUrl(event.data.url)) return false; 1200 try { 1201 var parsed = new URL(String(event.data.url).trim(), window.location.href); 1202 window.location.assign(parsed.href); 1203 } catch (e) { 1204 return false; 1205 } 1206 return true; 1207 } 1208 1209 function truncInt(n) { 1210 return n < 0 ? Math.ceil(n) : Math.floor(n); 1211 } 1212 1213 function replyFlowbotAddToCartResult(event, tid, success) { 1214 if (!event || !event.source || typeof event.source.postMessage !== 'function') return; 1215 try { 1216 event.source.postMessage({ 1217 type: 'flowbot-add-to-cart-result', 1218 tid: tid, 1219 success: !!success 1220 }, event.origin && event.origin !== 'null' ? event.origin : flowbotPostTargetOrigin()); 1221 } catch (e) {} 1222 } 1223 1224 /** Explicit false / { success: false } is failure; a resolved/non-throwing add is success. */ 1225 function isCartAddSuccess(res) { 1226 if (res === false) return false; 1227 if (res && typeof res === 'object' && res.success === false) return false; 1228 return true; 1229 } 1230 1231 function finishHostCartAdd(site, res, event, tid) { 1232 var ok = isCartAddSuccess(res); 1233 if (ok && site && site.cart && typeof site.cart.update === 'function') { 1234 try { site.cart.update(); } catch (e) {} 1235 } 1236 replyFlowbotAddToCartResult(event, tid, ok); 1237 } 1238 1239 function handleFlowbotAddToCart(event) { 1240 var tidRaw = event.data.tid; 1241 var tid = typeof tidRaw === 'number' ? tidRaw : Number(tidRaw); 1242 if (!isFinite(tid)) { 1243 replyFlowbotAddToCartResult(event, tidRaw, false); 1244 return; 1245 } 1246 tid = truncInt(tid); 1247 if (tid < 1) { 1248 replyFlowbotAddToCartResult(event, tid, false); 1249 return; 1250 } 1251 1252 var amount = 1; 1253 1254 var site = window.site; 1255 if (!site || !site.cart || typeof site.cart.add !== 'function') { 1256 replyFlowbotAddToCartResult(event, tid, false); 1257 return; 1258 } 1259 1260 try { 1261 // Dataflow cart.add(tid, amount) â scalars, not {tid}/{amount} objects. 1262 var pending = site.cart.add(tid, amount); 1263 if (!pending || typeof pending.then !== 'function') { 1264 finishHostCartAdd(site, pending, event, tid); 1265 return; 1266 } 1267 pending.then(function (res) { 1268 finishHostCartAdd(site, res, event, tid); 1269 }, function () { 1270 replyFlowbotAddToCartResult(event, tid, false); 1271 }); 1272 } catch (e) { 1273 replyFlowbotAddToCartResult(event, tid, false); 1274 } 1275 } 1276 1277 function attachFlowbotMessageListenerOnce() { 1278 if (window.__flowbotResizeListenerAttached) return; 1279 window.__flowbotResizeListenerAttached = true; 1280 1281 window.addEventListener('message', function(event) { 1282 if (!event.data || typeof event.data.type !== 'string') return; 1283 if (handleSupersearchNavigate(event)) return; 1284 1285 if (event.data.type === 'flowbot-add-to-cart') { 1286 if (!isTrustedFlowbotMessageOrigin(event)) return; 1287 handleFlowbotAddToCart(event); 1288 return; 1289 } 1290 1291 if (event.data.type === 'flowbot-hide') { 1292 if (!isTrustedFlowbotMessageOrigin(event)) return; 1293 var foundIframe = findIframeByEventSource(event.source); 1294 if (foundIframe) { 1295 if ( 1296 foundIframe.hasAttribute('data-flowbot-direct-embed') || 1297 foundIframe.getAttribute('data-flowbot-direct-embed-
1297pending') === 'true' 1298 ) { 1299 markDirectEmbedIframeHidden(foundIframe); 1300 } else { 1301 foundIframe.style.display = 'none'; 1302 } 1303 var parentContainer = foundIframe.parentElement; 1304 if (parentContainer && ( 1305 parentContainer.hasAttribute('data-flowbot-supersearch') || 1306 parentContainer.hasAttribute('data-flowbot-id') 1307 )) { 1308 parentContainer.style.display = 'none'; 1309 } 1310 } else { 1311 var targetBotId = event.data.botId; 1312 var targetIframe = targetBotId 1313 ? window.__flowbotIframes[targetBotId] 1314 : Object.values(window.__flowbotIframes)[0]; 1315 if (targetIframe) { 1316 if ( 1317 targetIframe.hasAttribute('data-flowbot-direct-embed') || 1318 targetIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true' 1319 ) { 1320 markDirectEmbedIframeHidden(targetIframe); 1321 } else { 1322 targetIframe.style.display = 'none'; 1323 } 1324 } 1325 } 1326 return; 1327 } 1328 if (event.data.type === 'flowbot-resize' && event.data.dimensions) { 1329 if (!isTrustedFlowbotMessageOrigin(event)) return; 1330 var dimensions = event.data.dimensions; 1331 var fullViewport = event.data.fullViewport; 1332 var resizeBotId = event.data.botId; 1333 var resizeIframe = resizeBotId 1334 ? window.__flowbotIframes[resizeBotId] 1335 : Object.values(window.__flowbotIframes)[0]; 1336 if (!resizeIframe) return; 1337 syncHostScrollLockForChat( 1338 resizeIframe, 1339 fullViewport, 1340 dimensions && dimensions.width, 1341 dimensions && dimensions.height 1342 ); 1343 if (fullViewport) { 1344 applyFullViewportIframe(resizeIframe, { animate: true }); 1345 return; 1346 } 1347 if (dimensions.width && dimensions.height) { 1348 applySizedIframeFromFullViewport( 1349 resizeIframe, 1350 dimensions.width, 1351 dimensions.height, 1352 true, 1353 event.data.fabShape 1354 ); 1355 } else { 1356 clearFullViewportIframe(resizeIframe); 1357 } 1358 return; 1359 } 1360 if (event.data.type === 'supersearch-init') { 1361 if (!isTrustedSupersearchMessage(event)) return; 1362 var displayType = event.data.displayType; 1363 var initIframe = findIframeByEventSource(event.source); 1364 if (initIframe && displayType === '360haku') { 1365 initIframe.setAttribute('data-flowbot-360haku', 'true'); 1366 initIframe.style.border = 'none'; 1367 initIframe.style.borderRadius = '0'; 1368 initIframe.style.boxShadow = 'none'; 1369 initIframe.style.minHeight = '0'; 1370 initIframe.style.minWidth = '0'; 1371 initIframe.style.overflow = 'visible'; 1372 applySuperSearchIframeTransparency(initIframe); 1373 1374 var initContainer = initIframe.parentElement; 1375 if (initContainer && initContainer.hasAttribute('data-flowbot-supersearch')) { 1376 initContainer.setAttribute('data-flowbot-360haku', 'true'); 1377 initContainer.style.border = 'none'; 1378 initContainer.style.borderRadius = '0'; 1379 initContainer.style.boxShadow = 'none'; 1380 initContainer.style.background = 'transparent'; 1381 initContainer.style.minHeight = '0'; 1382 initContainer.style.minWidth = '0'; 1383 initContainer.style.overflow = 'visible'; 1384 initContainer.style.position = 'relative'; 1385 initContainer.style.margin = '0 auto'; 1386 initContainer.style.width = 'auto'; 1387 initContainer.style.height = 'auto'; 1388 initContainer.style.top = ''; 1389 initContainer.style.left = ''; 1390 initContainer.style.right = ''; 1391 initContainer.style.bottom = ''; 1392 initContainer.style.transform = 'none'; 1393 1394 var hostParent = initContainer.parentElement; 1395 if (hostParent) { 1396 hostParent.style.overflow = 'visible'; 1397 if (!hostParent.style.position || hostParent.style.position === 'static') { 1398 hostParent.style.position = 'relative'; 1399 } 1400 } 1401 } 1402 } else if (initIframe && displayType === 'direct_embed') { 1403 handleDirectEmbedSupersearchInit(initIframe, event.data.layout); 1404 } else if (initIframe && displayType === 'ai_showcase') { 1405 // Must live here: this listener is attached first and blocks the chat-path duplicate. 1406 applyAiShowcaseHostSize(initIframe, initIframe.parentElement); 1407 } else if (initIframe && displayType === 'uutiset') { 1408 applyUutisetHostSize(initIframe, initIframe.parentElement); 1409 } 1410 return; 1411 } 1412 if (event.data.type === 'supersearch-resize') { 1413 if (!isTrustedSupersearchMessage(event)) return; 1414 var resizeWidth = event.data.width; 1415 var resizeHeight = event.data.height; 1416 var supersearchIframe = findIframeByEventSource(event.source); 1417 if (!supersearchIframe) return; 1418 1419 var supersearchContainer = findDirectEmbedContainer(supersearchIframe); 1420 if (supersearchContainer && supersearchContainer.hasAttribute('data-flowbot-360haku')) { 1421 applySuperSearch360Dimensions(supersearchIframe, resizeWidth, resizeHeight); 1422 return; 1423 } 1424 if (handleDirectEmbedSupersearchResize(supersearchIframe, resizeWidth, resizeHeight)) { 1425 return; 1426 } 1427 if ( 1428 supersearchIframe.hasAttribute('data-flowbot-ai-showcase') || 1429 (supersearchIframe.parentElement && 1430 supersearchIframe.parentElement.hasAttribute('data-flowbot-ai-showcase')) 1431 ) { 1432 applyAiShowcaseResize(supersearchIframe, resizeHeight); 1433 return; 1434 } 1435 if ( 1436 event.data.displayType === 'uutiset' || 1437 isUutisetSupersearchFrame(supersearchIframe) 1438 ) { 1439 applyUutisetResize(supersearchIframe, resizeHeight); 1440 return; 1441 } 1442 1443 supersearchContainer = supersearchIframe.parentElement; 1444 1445 var genericW = clampDirectEmbedDimension(resizeWidth); 1446 var genericH = clampDirectEmbedDimension(resizeHeight);
1447 if (genericW != null) supersearchIframe.style.width = genericW + 'px'; 1448 if (genericH != null) supersearchIframe.style.height = genericH + 'px'; 1449 1450 if (supersearchContainer && supersearchContainer.hasAttribute('data-flowbot-supersearch')) { 1451 if (genericW != null) supersearchContainer.style.width = genericW + 'px'; 1452 if (genericH != null) supersearchContainer.style.height = genericH + 'px'; 1453 } 1454 return; 1455 } 1456 }); 1457 } 1458 1459 function initialize() { 1460 injectCriticalStyles(); 1461 attachFlowbotMessageListenerOnce(); 1462 1463 function createIframes() { 1464 const widgetContainers = document.querySelectorAll('[data-flowbot-id]:not([data-flowbot-initialized])'); 1465 1466 widgetContainers.forEach((container) => { 1467 const botId = container.getAttribute('data-flowbot-id'); 1468 1469 if (!botId) { 1470 console.error('FlowBot: Element has data-flowbot-id attribute but no value'); 1471 return; 1472 } 1473 1474 if (container.hasAttribute('data-flowbot-initialized')) { 1475 return; 1476 } 1477 1478 const existingIframe = window.__flowbotIframes[botId]; 1479 if (existingIframe && existingIframe.isConnected) { 1480 container.setAttribute('data-flowbot-initialized', 'true'); 1481 return; 1482 } 1483 1484 try { 1485 const iframe = document.createElement('iframe'); 1486 const { width: vw } = getViewport(); 1487 // Pass the HOST viewport width to the widget. Inside the iframe the 1488 // widget can only measure its own (always narrow) width, so without 1489 // this it would always think it is "mobile" and ignore size presets. 1490 var iframeSrc = CONFIG.baseUrl + '?mode=public&botId=' + encodeURIComponent(botId) + 1491 '&parentWidth=' + (vw || window.innerWidth || 0); 1492 iframe.src = appendParentContextQuery(iframeSrc); 1493 1494 iframe.style.position = 'fixed'; 1495 iframe.style.bottom = '20px'; 1496 iframe.style.right = '20px'; 1497 iframe.style.left = 'auto'; 1498 iframe.style.top = 'auto'; 1499 iframe.style.width = '64px'; 1500 iframe.style.height = '64px'; 1501 iframe.style.borderRadius = '50%'; 1502 iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'; 1503 iframe.style.border = 'none'; 1504 iframe.style.zIndex = '9999'; 1505 iframe.style.backgroundColor = 'transparent'; 1506 // No default box transition: desktop launcherâpanel snaps. 1507 // Mobile full-viewport morph opts in via setIframeBoxTransition. 1508 iframe.style.transition = ''; 1509 iframe.setAttribute('allow', 'microphone; storage-access'); 1510 applyFlowbotIframeIsolation(iframe); 1511 iframe.setAttribute('title', 'FlowBot Chat'); 1512 iframe.setAttribute('data-flowbot-bot-id', botId); 1513 iframe.setAttribute('data-flowbot-chat', 'true'); 1514 1515 if (!window.__flowbotResizeListenerAttached) { 1516 window.__flowbotResizeListenerAttached = true; 1517 window.addEventListener('message', function(event) { 1518 if (!event.data || typeof event.data.type !== 'string') return; 1519 if (handleSupersearchNavigate(event)) return; 1520 1521 if (event.data.type === 'flowbot-hide') { 1522 if (!isTrustedFlowbotMessageOrigin(event)) return; 1523 let foundIframe = null; 1524 if (event.source) { 1525 try { 1526 const iframes = document.querySelectorAll('iframe'); 1527 for (let i = 0; i < iframes.length; i++) { 1528 if (iframes[i].contentWindow === event.source) { 1529 foundIframe = iframes[i]; 1530 break; 1531 } 1532 } 1533 } catch (e) {} 1534 } 1535 if (foundIframe) { 1536 if ( 1537 foundIframe.hasAttribute('data-flowbot-direct-embed') || 1538 foundIframe.getAttribute('data-flowbot-direct-embed-
1538pending') === 'true' 1539 ) { 1540 markDirectEmbedIframeHidden(foundIframe); 1541 } else { 1542 foundIframe.style.display = 'none'; 1543 } 1544 const parentContainer = foundIframe.parentElement; 1545 if (parentContainer && ( 1546 parentContainer.hasAttribute('data-flowbot-supersearch') || 1547 parentContainer.hasAttribute('data-flowbot-id') 1548 )) { 1549 parentContainer.style.display = 'none'; 1550 } 1551 } else { 1552 const targetBotId = event.data.botId; 1553 const targetIframe = targetBotId 1554 ? window.__flowbotIframes[targetBotId] 1555 : Object.values(window.__flowbotIframes)[0]; 1556 if (targetIframe) { 1557 if ( 1558 targetIframe.hasAttribute('data-flowbot-direct-embed') || 1559 targetIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true' 1560 ) { 1561 markDirectEmbedIframeHidden(targetIframe); 1562 } else { 1563 targetIframe.style.display = 'none'; 1564 } 1565 } 1566 } 1567 return; 1568 } 1569 if (event.data.type === 'flowbot-resize' && event.data.dimensions) { 1570 if (!isTrustedFlowbotMessageOrigin(event)) return; 1571 const { dimensions, fullViewport } = event.data; 1572 const targetBotId = event.data.botId; 1573 const targetIframe = targetBotId 1574 ? window.__flowbotIframes[targetBotId] 1575 : Object.values(window.__flowbotIframes)[0]; 1576 if (!targetIframe) return; 1577 1578 syncHostScrollLockForChat( 1579 targetIframe, 1580 fullViewport, 1581 dimensions && dimensions.width, 1582 dimensions && dimensions.height 1583 ); 1584 1585 if (fullViewport) { 1586 applyFullViewportIframe(targetIframe, { animate: true }); 1587 return; 1588 } 1589 1590 if (dimensions.width && dimensions.height) { 1591 applySizedIframeFromFullViewport( 1592 targetIframe, 1593 dimensions.width, 1594 dimensions.height, 1595 true, 1596 event.data.fabShape 1597 ); 1598 } else { 1599 clearFullViewportIframe(targetIframe); 1600 } 1601 } 1602 if (event.data.type === 'supersearch-init') { 1603 if (!isTrustedSupersearchMessage(event)) return; 1604 const displayType = event.data.displayType; 1605 const foundIframe = findIframeByEventSource(event.source); 1606 if (foundIframe) { 1607 if (displayType === '360haku') {
1608 foundIframe.setAttribute('data-flowbot-360haku', 'true'); 1609 foundIframe.style.border = 'none'; 1610 foundIframe.style.borderRadius = '0'; 1611 foundIframe.style.boxShadow = 'none'; 1612 foundIframe.style.minHeight = '0'; 1613 foundIframe.style.minWidth = '0'; 1614 foundIframe.style.width = '100%'; 1615 foundIframe.style.height = '100%'; 1616 foundIframe.style.overflow = 'visible'; 1617 applySuperSearchIframeTransparency(foundIframe); 1618 1619 const container = foundIframe.parentElement; 1620 if (container && container.hasAttribute('data-flowbot-supersearch')) { 1621 container.setAttribute('data-flowbot-360haku', 'true'); 1622 container.style.border = 'none'; 1623 container.style.borderRadius = '0'; 1624 container.style.boxShadow = 'none'; 1625 container.style.background = 'transparent'; 1626 container.style.minHeight = '0'; 1627 container.style.minWidth = '0'; 1628 container.style.overflow = 'visible'; 1629 1630 // Fill host div â size comes from the host page's container 1631 container.style.position = 'absolute'; 1632 container.style.top = '0'; 1633 container.style.left = '0'; 1634 container.style.right = '0'; 1635 container.style.bottom = '0'; 1636 container.style.width = '100%'; 1637 container.style.height = '100%'; 1638 container.style.transform = 'none'; 1639 1640 const hostParent = container.parentElement; 1641 if (hostParent) { 1642 hostParent.style.overflow = 'visible'; 1643 if (!hostParent.style.position || hostParent.style.position === 'static') { 1644 hostParent.style.position = 'relative'; 1645 } 1646 } 1647 } 1648 } else if (displayType === 'direct_embed') { 1649 handleDirectEmbedSupersearchInit(foundIframe, event.data.layout); 1650 } else if (displayType === 'ai_showcase') { 1651 applyAiShowcaseHostSize( 1652 foundIframe, 1653 foundIframe.parentElement 1654 ); 1655 } else if (displayType === 'uutiset') { 1656 applyUutisetHostSize( 1657 foundIframe, 1658 foundIframe.parentElement 1659 ); 1660 } 1661 } 1662 return; 1663 } 1664 if (event.data.type === 'supersearch-resize') { 1665 if (!isTrustedSupersearchMessage(event)) return; 1666 const { width, height } = event.data; 1667 const foundIframe = findIframeByEventSource(event.source); 1668 if (foundIframe) { 1669 if (handleDirectEmbedSupersearchResize(foundIframe, width, height)) { 1670 return; 1671 } 1672 const container = foundIframe.parentElement; 1673 // Floating search (360haku display type) fills the host div â never apply fixed pixel sizes 1674 if (container && container.hasAttribute('data-flowbot-360haku')) { 1675 return; 1676 } 1677 if ( 1678 foundIframe.hasAttribute('data-flowbot-ai-showcase') || 1679 (container && container.hasAttribute('data-flowbot-ai-showcase')) 1680 ) { 1681 applyAiShowcaseResize(foundIframe, height); 1682 return; 1683 } 1684 if ( 1685 event.data.displayType === 'uutiset' || 1686 isUutisetSupersearchFrame(foundIframe) 1687 ) { 1688 applyUutisetResize(foundIframe, height); 1689 return; 1690 } 1691 var genericW = clampDirectEmbedDimension(width); 1692 var genericH = clampDirectEmbedDimension(height); 1693 if (genericW != null) foundIframe.
1693style.width = genericW + 'px'; 1694 if (genericH != null) foundIframe.style.height = genericH + 'px'; 1695 1696 if (container && container.hasAttribute('data-flowbot-supersearch')) { 1697 if (genericW != null) container.style.width = genericW + 'px'; 1698 if (genericH != null) container.style.height = genericH + 'px'; 1699 } 1700 } 1701 return; 1702 } 1703 }); 1704 } 1705 1706 attachViewportListenersOnce(); 1707 1708 document.body.appendChild(iframe); 1709 window.__flowbotIframes[botId] = iframe; 1710 container.setAttribute('data-flowbot-initialized', 'true'); 1711 } catch (error) { 1712 console.error(`FlowBot: Error creating iframe for botId ${botId}:`, error); 1713 } 1714 }); 1715 1716 // SuperSearch search widgets 1717 const superSearchContainers = document.querySelectorAll('[data-flowbot-supersearch]:not([data-flowbot-initialized])'); 1718 superSearchContainers.forEach((container) => { 1719 const botId = container.getAttribute('data-flowbot-supersearch'); 1720 const instanceId = container.getAttribute('data-flowbot-supersearch-id') || ''; 1721 if (!botId) return; 1722 if (container.hasAttribute('data-flowbot-initialized')) return; 1723 1724 try { 1725 var isDirectEmbedLayout = getNormalizedEmbedLayout(container) === 'direct_embed'; 1726 const parent = container.parentElement; 1727 var parentHostWidth = 0; 1728 var parentHostHeight = 0; 1729 var hasMeaningfulParentHeight = false; 1730 container.style.minHeight = '0'; 1731 // Only trust explicit height attrs/inline â never collapsed offsetHeight. 1732 var explicitHostHeight = 1733 parseHostHeightPx(container.getAttribute('data-flowbot-height')) || 1734 parseHostHeightPx(container.style && container.style.height) || 1735 (parent 1736 ? parseHostHeightPx(parent.getAttribute('data-flowbot-height')) || 1737 parseHostHeightPx(parent.style && parent.style.height) 1738 : 0); 1739 if (parent && !isDirectEmbedLayout) { 1740 parent.style.position = 'relative'; 1741 parent.style.overflow = 'visible'; 1742 parentHostWidth = parent.offsetWidth || 0; 1743 parentHostHeight = explicitHostHeight || 0; 1744 hasMeaningfulParentHeight = parentHostHeight >= 48; 1745 container.style.position = 'relative'; 1746 container.style.display = 'block'; 1747 container.style.margin = '0 auto'; 1748 container.style.width = '100%'; 1749 if (hasMeaningfulParentHeight) { 1750 container.style.height = parentHostHeight + 'px'; 1751 container.dataset.flowbotHostHeight = String(parentHostHeight); 1752 } 1753 } else if (explicitHostHeight && !isDirectEmbedLayout) { 1754 parentHostHeight = explicitHostHeight; 1755 hasMeaningfulParentHeight = true; 1756 container.style.height = explicitHostHeight + 'px'; 1757 container.dataset.flowbotHostHeight = String(explicitHostHeight); 1758 } 1759 1760 const iframe = document.createElement('iframe'); 1761 // parentHost: asiakassivun hostname Callable domain -tarkistusta ja konfiguraatiota varten 1762 // (document.referrer iframessa voi olla tyhjä tiukan Referrer-Policy:n takia). 1763 var parentHost = ''; 1764 var parentPath = ''; 1765 try { 1766 parentHost = window.location.hostname || ''; 1767 parentPath = window.location.pathname || ''; 1768 } catch (e) {} 1769 const { width: vw } = getViewport(); 1770 var embedLayout = ''; 1771 try { 1772 // Explicit direct_embed wins. 360 only when this container 1773 // is the panorama widget itself, not a nested search. 1774 if (isDirectEmbedLayout) { 1775 embedLayout = 'direct_embed'; 1776 } else if ( 1777 container.classList.contains('panorama-superSearch') || 1778 (container.parentElement && 1779 container.parentElement.classList.contains('panorama-superSearch')) 1780 ) { 1781 embedLayout = '360haku'; 1782 } 1783 } catch (e) {} 1784 var iframeSrc = CONFIG.baseUrl + '?mode=superSearch&botId=' + encodeURIComponent(botId) + 1785 (instanceId ? '&instanceId=' + encodeURIComponent(instanceId) : '') + 1786 '&parentWidth=' + (parentHostWidth || vw) + 1787 (hasMeaningfulParentHeight ? '&parentHeight=' + parentHostHeight : '') + 1788 (embedLayout ? '&embedLayout=' + encodeURIComponent(embedLayout) : ''); 1789 iframeSrc = appendParentContextQuery(iframeSrc); 1790 applyFlowbotIframeIsolation(iframe); 1791 iframe.style.display = 'block'; 1792 iframe.style.minHeight = '0'; 1793 iframe.setAttribute('allow', 'microphone; storage-access'); 1794 iframe.setAttribute('title', 'FlowBot SuperSearch'); 1795 applySuperSearchIframeTransparency(iframe); 1796 iframe.src = iframeSrc; 1797 container.innerHTML = ''; 1798 1799 if (isDirectEmbedLayout) { 1800 iframe.setAttribute('scrolling', 'no'); 1801 iframe.style.overflow = 'hidden'; 1802 bootstrapDirectEmbedIframe(container, iframe); 1803 } else { 1804 container.appendChild(iframe); 1805 iframe.style.width = '100%'; 1806 if (hasMeaningfulParentHeight) { 1807 iframe.style.height = '100%'; 1808 } 1809 iframe.style.border = '1px solid rgba(0,0,0,0.12)'; 1810 iframe.style.borderRadius = '8px'; 1811 } 1812 1813 // Initial host viewport (resize listener only fires later on window resize). 1814 iframe.addEventListener('load', function onSuperSearchLoad() { 1815 iframe.removeEventListener('load', onSuperSearchLoad); 1816 var vp = getViewport(); 1817 postHostViewportToIframe( 1818 iframe, 1819 vp.width, 1820 vp.height, 1821 isSoftKeyboardOpen() 1822 ); 1823 }); 1824 1825 attachViewportListenersOnce(); 1826 registerSuperSearchIframe(botId, instanceId, iframe); 1827 container.setAttribute('data-flowbot-initialized', 'true'); 1828 } catch (error) { 1829 console.error('FlowBot: Error creating SuperSearch iframe:', error); 1830 } 1831 }); 1832 1833 // Popup Ad widgets 1834 var popupContainers = document.querySelectorAll('[data-flowbot-popup]:not([data-flowbot-initialized]
1834)'); 1835 console.log('[FlowBot Popup] Found containers:', popupContainers.length); 1836 popupContainers.forEach(function(container) { 1837 var botId = container.getAttribute('data-flowbot-popup'); 1838 if (!botId) { console.warn('[FlowBot Popup] No botId on container'); return; } 1839 if (container.hasAttribute('data-flowbot-initialized')) { console.log('[FlowBot Popup] Already initialized'); return; } 1840 1841 container.setAttribute('data-flowbot-initialized', 'true'); 1842 1843 try { 1844 var iframeSrc = CONFIG.baseUrl + '?mode=popupAd&botId=' + encodeURIComponent(botId); 1845 iframeSrc = appendParentContextQuery(iframeSrc); 1846 console.log('[FlowBot Popup] Creating hidden overlay, iframe src:', iframeSrc); 1847 1848 // Overlay starts invisible so page is usable during load + delay 1849 var overlay = document.createElement('div'); 1850 overlay.id = 'flowbot-popup-overlay-' + botId; 1851 overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;align-items:flex-end;justify-content:center;padding:0;box-sizing:border-box;pointer-events:none;background:rgba(15,23,42,0);transition:background 0.35s ease;'; 1852 1853 var iframe = document.createElement('iframe'); 1854 iframe.src = iframeSrc; 1855 iframe.style.cssText = 'border:none;width:100%;max-width:100%;height:100%;max-height:100%;background:transparent;opacity:0;transition:opacity 0.25s ease;'; 1856 iframe.setAttribute('title', 'FlowBot Popup'); 1857 iframe.setAttribute('allow', 'autoplay; storage-access'); 1858 applyFlowbotIframeIsolation(iframe); 1859 1860 overlay.appendChild(iframe); 1861 document.body.appendChild(overlay); 1862 console.log('[FlowBot Popup] Hidden overlay added to DOM (waiting for POPUP_AD_READY)'); 1863 1864 var currentVariationId = null; 1865 var didShowPopup = false; 1866 var closed = false; 1867 1868 function seenStorageKey(variationId) { 1869 return 'flowbot_popup_seen_' + botId + '_' + variationId; 1870 } 1871 1872 function readSeen(variationId) { 1873 if (!variationId) return false; 1874 try { 1875 return sessionStorage.getItem(seenStorageKey(variationId)) === '1'; 1876 } catch (err) { 1877 return false; 1878 } 1879 } 1880 1881 function writeSeen(variationId) { 1882 if (!variationId) return; 1883 try { 1884 sessionStorage.setItem(seenStorageKey(variationId), '1'); 1885 } catch (err) {} 1886 } 1887 1888 function closePopup(markSeen) { 1889 if (closed) return; 1890 closed = true; 1891 console.log('[FlowBot Popup] Closing popup'); 1892 if (overlay.parentNode) overlay.parentNode.removeChild(overlay); 1893 if (markSeen && currentVariationId) writeSeen(currentVariationId); 1894 window.removeEventListener('message', onPopupMessage); 1895 overlay.removeEventListener('click', onOverlayClick); 1896 } 1897 1898 function showOverlay() { 1899 console.log('[FlowBot Popup] POPUP_AD_READY received â showing overlay and iframe'); 1900 didShowPopup = true; 1901 // Plain dim instead of a full-viewport backdrop-filter blur: blurring the 1902 // entire host page forces the browser to re-composite everything behind the 1903 // overlay every frame, which made the popup feel laggy on the host site. 1904 overlay.style.background = 'rgba(15,23,42,0.58)'; 1905 overlay.style.pointerEvents = 'auto'; 1906 iframe.style.opacity = '1'; 1907 } 1908 1909 function onOverlayClick(e) { 1910 if (e.target === overlay) { console.log('[FlowBot Popup] Overlay clicked, closing'); closePopup(true); } 1911 } 1912 1913 function onPopupMessage(e) { 1914 if (e.source !== iframe.contentWindow) return; 1915 if (e.data && e.data.type === 'POPUP_AD_READY') { 1916 currentVariationId = e.data.variationId || currentVariationId; 1917 if (readSeen(currentVariationId)) { 1918 console.log('[FlowBot Popup] Already seen this campaign, skipping'); 1919 closePopup(false); 1920 return; 1921 } 1922 showOverlay(); 1923 } else if (e.data && e.data.type === 'POPUP_AD_CLOSE') { 1924 console.log('[FlowBot Popup] Received POPUP_AD_CLOSE message'); 1925 if (!currentVariationId && e.data.variationId) currentVariationId = e.data.variationId; 1926 closePopup(didShowPopup); 1927 } 1928 } 1929 1930 overlay.addEventListener('click', onOverlayClick); 1931 window.addEventListener('message', onPopupMessage); 1932 } catch (error) { 1933 console.error('FlowBot: Error creating Popup Ad iframe:', error); 1934 } 1935 }); 1936 } 1937 1938 if (document.readyState === 'loading') { 1939 document.addEventListener('DOMContentLoaded', createIframes); 1940 } else { 1941 setTimeout(createIframes, 0); 1942 } 1943 } 1944 1945 initialize(); 1946})();
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.