1 2(function() { 3 'use strict'; 4 5 const BLOCKED_HOST_PATTERNS = ['stripe.com']; 6 const hostname = window.location.hostname || ''; 7 if (BLOCKED_HOST_PATTERNS.some(pattern => hostname === pattern || hostname.endsWith('.' + pattern))) { 8 if (window.console && typeof window.console.debug === 'function') { 9 console.debug('SignalsLoop widget disabled on host', hostname); 10 } 11 return; 12 } 13 14 // Prevent multiple widget loads 15 if (window.SignalsLoopWidget) { 16 if (window.console && typeof window.console.debug === 'function') { 17 console.debug('SignalsLoop widget already loaded'); 18 } 19 return; 20 } 21 22 // Widget configuration 23 const CONFIG = {"apiKey":"sk_89efe32edf48184f641432ff26c6c5df","projectSlug":"signalsloop","projectName":"Signalsloop","position":"bottom-right","color":"#3B82F6","text":"Feedback","size":"medium","theme":"light","isPro":true}; 24 const WIDGET_ID = 'signalsloop-widget-' + Math.random().toString(36).substr(2, 9); 25 const IFRAME_URL = 'https://www.signalsloop.com/embed/' + CONFIG.apiKey + '/frame?theme=' + CONFIG.theme + '&color=' + encodeURIComponent(CONFIG.color) + (CONFIG.isPro ? '&hide_branding=true' : ''); 26 27 // Position configurations 28 const POSITIONS = { 29 'bottom-right': { bottom: '20px', right: '20px', top: 'auto', left: 'auto' }, 30 'bottom-left': { bottom: '20px', left: '20px', top: 'auto', right: 'auto' }, 31 'top-right': { top: '20px', right: '20px', bottom: 'auto', left: 'auto' }, 32 'top-left': { top: '20px', left: '20px', bottom: 'auto', left: 'auto' }, 33 'center-right': { top: '50%', right: '20px', bottom: 'auto', left: 'auto', transform: 'translateY(-50%)' } 34 }; 35 36 // Size configurations 37 const SIZES = { 38 'small': { padding: '8px 16px', fontSize: '14px', width: '80px', height: '36px' }, 39 'medium': { padding: '12px 20px', fontSize: '16px', width: '100px', height: '44px' }, 40 'large': { padding: '16px 24px', fontSize: '18px', width: '120px', height: '52px' } 41 }; 42 43 // Create widget button in an iframe for complete isolation 44 function createWidgetButton() { 45 const position = POSITIONS[CONFIG.position] || POSITIONS['bottom-right']; 46 47 // Create an iframe to isolate the button from page styles 48 const iframe = document.createElement('iframe'); 49 iframe.id = WIDGET_ID + '-frame'; 50 51 // Set styles using setProperty with !important 52 iframe.style.setProperty('position', 'fixed', 'important'); 53 iframe.style.setProperty('width', '120px', 'important'); 54 iframe.style.setProperty('height', '50px', 'important'); 55 iframe.style.setProperty('border', 'none', 'important'); 56 iframe.style.setProperty('background', 'transparent', 'important'); 57 iframe.style.setProperty('z-index', '2147483647', 'important'); 58 iframe.style.setProperty('pointer-events', 'auto', 'important'); 59 60 // Set position values - ONLY set the ones that aren't 'auto' 61 if (position.top && position.top !== 'auto') { 62 iframe.style.setProperty('top', position.top, 'important'); 63 } 64 if (position.bottom && position.bottom !== 'auto') { 65 iframe.style.setProperty('bottom', position.bottom, 'important'); 66 } 67 if (position.left && position.left !== 'auto') { 68 iframe.style.setProperty('left', position.left, 'important'); 69 } 70 if (position.right && position.right !== 'auto') { 71 iframe.style.setProperty('right', position.right, 'important'); 72 } 73 if (position.transform) { 74 iframe.style.setProperty('transform', position.transform, 'important'); 75 } 76 77 if (window.console && typeof window.console.debug === 'function') { 78 console.debug('Widget iframe position:', { 79 top: iframe.style.top, 80 bottom: iframe.style.bottom,
81 left: iframe.style.left, 82 right: iframe.style.right, 83 position: CONFIG.position 84 }); 85 } 86 87 // Append iframe to body 88 document.body.appendChild(iframe); 89 90 // Use absolute positioning relative to body, update on scroll 91 function enforceIframePosition() { 92 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 93 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 94 const windowHeight = window.innerHeight; 95 const windowWidth = window.innerWidth; 96 97 // Calculate position to keep it in viewport 98 if (CONFIG.position === 'bottom-right' || !CONFIG.position) { 99 const top = scrollTop + windowHeight - 70; // 50px height + 20px margin 100 const left = scrollLeft + windowWidth - 140; // 120px width + 20px margin 101 iframe.style.setProperty('position', 'absolute', 'important'); 102 iframe.style.setProperty('top', top + 'px', 'important'); 103 iframe.style.setProperty('left', left + 'px', 'important'); 104 iframe.style.removeProperty('bottom'); 105 iframe.style.removeProperty('right'); 106 } else if (CONFIG.position === 'bottom-left') { 107 const top = scrollTop + windowHeight - 70; 108 const left = scrollLeft + 20; 109 iframe.style.setProperty('position', 'absolute', 'important'); 110 iframe.style.setProperty('top', top + 'px', 'important'); 111 iframe.style.setProperty('left', left + 'px', 'important'); 112 iframe.style.removeProperty('bottom'); 113 iframe.style.removeProperty('right'); 114 } else if (CONFIG.position === 'top-right') { 115 const top = scrollTop + 20; 116 const left = scrollLeft + windowWidth - 140; 117 iframe.style.setProperty('position', 'absolute', 'important'); 118 iframe.style.setProperty('top', top + 'px', 'important'); 119 iframe.style.setProperty('left', left + 'px', 'important'); 120 iframe.style.removeProperty('bottom'); 121 iframe.style.removeProperty('right'); 122 } else if (CONFIG.position === 'top-left') { 123 const top = scrollTop + 20; 124 const left = scrollLeft + 20; 125 iframe.style.setProperty('position', 'absolute', 'important'); 126 iframe.style.setProperty('top', top + 'px', 'important'); 127 iframe.style.setProperty('left', left + 'px', 'important'); 128 iframe.style.removeProperty('bottom'); 129 iframe.style.removeProperty('right'); 130 } 131 } 132 133 // Enforce immediately 134 enforceIframePosition(); 135 136 // Re-enforce on scroll, resize, and periodically 137 window.addEventListener('scroll', enforceIframePosition, { passive: true }); 138 window.addEventListener('resize', enforceIframePosition); 139 140 // Enforce every 50ms for smooth scrolling 141 setInterval(enforceIframePosition, 50); 142 143 // Write button HTML into iframe 144 const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 145 iframeDoc.open(); 146 iframeDoc.write( 147 '<!DOCTYPE html>' + 148 '<html>' + 149 '<head>' + 150 '<style>' + 151 '* { margin: 0; padding: 0; box-sizing: border-box; }' + 152 'body {' + 153 'overflow: hidden;' + 154 'background: transparent;' + 155 'width: 120px;' + 156 'height: 50px;' + 157 '}' + 158 '#btn {' + 159 'position: absolute;' + 160 'bottom: 0;' + 161 'right: 0;' + 162 'background-color: ' + CONFIG.color + ';' + 163 'color: white;' + 164 'border: none;' + 165 'border-radius: 25px;' + 166 'padding: 12px 20px;' + 167 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;' + 168 'font-size: 16px;' + 169 'font-weight: 600;' + 170 'cursor: pointer;' + 171 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);' + 172 'pointer-events: auto;' + 173 'transition: all 0.2s ease;' + 174 '}' + 175 '#btn:hover {' + 176 'transform: scale(1.05);' + 177 'box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);' + 178 '}' + 179 '</style>' + 180 '</head>' + 181 '<body>' + 182 '<button id="btn">' + CONFIG.text + '</button>' + 183 '</body>' + 184 '</html>' 185 ); 186 iframeDoc.close(); 187 188 // Get button from iframe and add click handler 189 const button = iframeDoc.getElementById('btn'); 190 button.addEventListener('click', openWidget); 191 192 // Make iframe pointer-events work for button 193 iframe.style.pointerEvents = 'auto'; 194 195 return iframe; 196 } 197 198 // Create modal overlay 199 function createModal() {
200 const overlay = document.createElement('div'); 201 overlay.id = WIDGET_ID + '-overlay'; 202 203 // Detect mobile devices 204 const isMobile = window.innerWidth <= 1024 || /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); 205 const desktopPadding = 32; 206 const desktopMinHeight = 360; 207 const desktopMaxHeight = Math.min(window.innerHeight - 40, 640); 208 209 Object.assign(overlay.style, { 210 position: 'fixed', 211 top: '0', 212 left: '0', 213 width: '100%', 214 height: '100%', 215 backgroundColor: isMobile ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.5)', 216 backdropFilter: 'blur(4px)', 217 zIndex: '1000000', 218 display: 'none', 219 opacity: '0', 220 transition: 'opacity 0.3s ease' 221 }); 222 223 // Create iframe container 224 const container = document.createElement('div'); 225 226 // Mobile-first styles 227 if (isMobile) { 228 Object.assign(container.style, { 229 position: 'absolute', 230 top: '0', 231 left: '0', 232 right: '0', 233 bottom: '0', 234 width: '100%', 235 height: '100vh', 236 transform: 'translateY(100%)', 237 backgroundColor: 'white', 238 borderRadius: '0', 239 boxShadow: '0 -4px 20px rgba(0, 0, 0, 0.25)', 240 transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)', 241 overflow: 'hidden', 242 paddingTop: 'env(safe-area-inset-top)', 243 paddingBottom: 'env(safe-area-inset-bottom)' 244 }); 245 } else { 246 Object.assign(container.style, { 247 position: 'absolute', 248 top: '20px', 249 left: '50%', 250 transform: 'translateX(-50%) scale(0.9)', 251 width: '90%', 252 maxWidth: '500px', 253 maxHeight: desktopMaxHeight + 'px', 254 backgroundColor: 'white', 255 borderRadius: '12px', 256 boxShadow: '0 25px 50px rgba(0, 0, 0, 0.25)', 257 transition: 'transform 0.3s ease', 258 overflow: 'hidden' 259 }); 260 } 261 262 // Create scroll wrapper for iOS fix 263 let scrollWrapper; 264 if (isMobile) { 265 scrollWrapper = document.createElement('div'); 266 Object.assign(scrollWrapper.style, { 267 width: '100%', 268 height: '100%', 269 overflowY: 'scroll', 270 overflowX: 'hidden', 271 WebkitOverflowScrolling: 'touch', 272 position: 'relative' 273 }); 274 scrollWrapper.style.height = 'calc(100vh - env(safe-area-inset-top))'; 275 scrollWrapper.style.maxHeight = 'calc(100vh - env(safe-area-inset-top))'; 276 scrollWrapper.style.paddingBottom = 'calc(env(safe-area-inset-bottom) + 80px)'; 277 } 278 279 // Create iframe 280 const iframe = document.createElement('iframe'); 281 iframe.src = IFRAME_URL; 282 iframe.setAttribute('allow', 'clipboard-write'); 283 if (isMobile) { 284 iframe.setAttribute('scrolling', 'yes'); 285 } 286 // Don't set scrolling attribute - let it be auto 287 Object.assign(iframe.style, { 288 width: '100%', 289 height: '100%', 290 border: 'none', 291 borderRadius: isMobile ? '0' : '12px' 292 }); 293 294 function updateDesktopSizing(contentHeight) { 295 if (isMobile) return; 296 const safeHeight = Math.max( 297 desktopMinHeight, 298 Math.min(contentHeight + desktopPadding, desktopMaxHeight) 299 ); 300 iframe.style.height = safeHeight + 'px'; 301 container.style.height = safeHeight + 'px'; 302 container.style.overflow = contentHeight + desktopPadding > desktopMaxHeight ? 'auto' : 'hidden'; 303 } 304 305 if (isMobile) { 306 iframe.style.height = '100%'; 307 } else { 308 updateDesktopSizing(desktopMinHeight); 309 } 310 311 let sizingListenerAttached = false; 312 const handleResizeMessage = function(e) { 313 if (!e.data || e.data.type !== 'signalsloop-resize' || !e.data.height) return; 314 updateDesktopSizing(e.data.height); 315 }; 316 const attachSizingListener = () => { 317 if (sizingListenerAttached) return; 318 window.addEventListener('message', handleResizeMessage); 319 sizingListenerAttached = true; 320 }; 321 const detachSizingListener = () => { 322 if (!sizingListenerAttached) return; 323 window.removeEventListener('message', handleResizeMessage); 324 sizingListenerAttached = false; 325 }; 326 attachSizingListener(); 327 328 // Create close button 329 const closeButton = document.createElement('button'); 330 closeButton.innerHTML = 'Ã'; 331 closeButton.setAttribute('aria-label', 'Close feedback widget'); 332 333 // Mobile-optimized close button 334 if (isMobile) { 335 Object.assign(closeButton.style, { 336 position: 'fixed', 337 top: 'calc(env(safe-area-inset-top) + 12px)', 338 right: '12px', 339 width: '44px', 340 height: '44px', 341 borderRadius: '50%', 342 border: 'none', 343 backgroundColor: 'rgba(0, 0, 0, 0.6)', 344 color: 'white', 345 fontSize: '24px', 346 fontWeight: 'bold', 347 cursor: 'pointer', 348 zIndex: '10', 349 display: 'flex', 350 alignItems: 'center', 351 justifyContent: 'center', 352 transition: 'all 0.2s ease', 353 touchAction: 'manipulation', 354 WebkitTapHighlightColor: 'transparent' 355 }); 356 } else { 357 Object.assign(closeButton.style, { 358 position: 'absolute', 359 top: '10px', 360 right: '10px', 361 width: '30px', 362 height: '30px', 363 borderRadius: '50%', 364 border: 'none', 365 backgroundColor: 'rgba(255, 255, 255, 0.9)', 366 color: '#666', 367 fontSize: '18px', 368 fontWeight: 'bold', 369 cursor: 'pointer', 370 zIndex: '10', 371 display: 'flex', 372 alignItems: 'center', 373 justifyContent: 'center', 374 transition: 'all 0.2s ease' 375 }); 376 } 377 378 closeButton.addEventListener('click', closeWidget); 379 380 // Touch feedback for mobile 381 if (isMobile) { 382 closeButton.addEventListener('touchstart', function() { 383 closeButton.style.transform = 'scale(0.9)'; 384 closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; 385 }); 386 closeButton.addEventListener('touchend', function() { 387 closeButton.style.transform = 'scale(1)'; 388 closeButton.style.backgroundColor = 'rgba(0, 0, 0, 0.6)'; 389 }); 390 } else { 391 closeButton.addEventListener('mouseenter', function() { 392 closeButton.style.backgroundColor = 'rgba(255, 255, 255, 1)'; 393 closeButton.style.color = '#333'; 394 }); 395 closeButton.addEventListener('mouseleave', function() { 396 closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; 397 closeButton.style.color = '#666'; 398 }); 399 } 400 401 let mobileBoardLinkWrapper = null; 402 let mobileBoardLinkHeight = 0; 403 404 if (isMobile) { 405 scrollWrapper.appendChild(iframe); 406 container.appendChild(scrollWrapper); 407 container.appendChild(closeButton); 408 409 if (CONFIG.boardUrl) { 410 mobileBoardLinkWrapper = document.createElement('div'); 411 Object.assign(mobileBoardLinkWrapper.style, { 412 position: 'absolute', 413 left: '16px', 414 right: '16px', 415 bottom: 'calc(env(safe-area-inset-bottom) + 12px)', 416 display: 'flex', 417 justifyContent: 'center', 418 pointerEvents: 'auto' 419 }); 420 421 const mobileBoardLink = document.createElement('a'); 422 mobileBoardLink.href = CONFIG.boardUrl; 423 mobileBoardLink.target = '_blank'; 424 mobileBoardLink.rel = 'noopener noreferrer'; 425 Object.assign(mobileBoardLink.style, { 426 display: 'inline-flex', 427 alignItems: 'center', 428 gap: '6px', 429 padding: '12px 16px', 430 borderRadius: '9999px', 431 backgroundColor: '#ffffff', 432 color: '#6366f1', 433 fontWeight: '600', 434 fontSize: '14px', 435 textDecoration: 'none', 436 boxShadow: '0 8px 20px rgba(99, 102, 241, 0.25)' 437 }); 438 mobileBoardLink.textContent = 'View all feedback & vote'; 439 440 const mobileBoardLinkIcon = document.createElement('span'); 441 mobileBoardLinkIcon.textContent = 'â'; 442 Object.assign(mobileBoardLinkIcon.style, { 443 fontSize: '16px', 444 lineHeight: '1', 445 marginLeft: '4px' 446 }); 447 448 mobileBoardLink.appendChild(mobileBoardLinkIcon); 449 mobileBoardLinkWrapper.appendChild(mobileBoardLink); 450 container.appendChild(mobileBoardLinkWrapper); 451 452 mobileBoardLinkHeight = mobileBoardLinkWrapper.getBoundingClientRect().height + 24; 453 scrollWrapper.style.paddingBottom = 'calc(env(safe-area-inset-bottom) + ' + mobileBoardLinkHeight + 'px)'; 454 } 455 } else { 456 container.appendChild(iframe); 457 container.appendChild(closeButton); 458 } 459 overlay.appendChild(container); 460 461 // Close on overlay click (desktop only) 462 if (!isMobile) { 463 overlay.addEventListener('click', function(e) { 464 if (e.target === overlay) { 465 closeWidget(); 466 } 467 }); 468 } 469 470 // Close on escape key 471 document.addEventListener('keydown', function(e) { 472 if (e.key === 'Escape' && overlay.style.display === 'block') { 473 closeWidget(); 474 } 475 }); 476 477 // Store mobile state for later use 478 overlay.dataset.isMobile = isMobile.toString(); 479
480 return { overlay, container, iframe, isMobile, attachSizingListener, detachSizingListener, mobileBoardLinkWrapper }; 481 } 482 483 // Widget state 484 let isOpen = false; 485 let modal = null; 486 487 // Open widget 488 function openWidget() { 489 if (isOpen) return; 490 491 if (!modal) { 492 modal = createModal(); 493 document.body.appendChild(modal.overlay); 494 } 495 496 if (typeof modal.attachSizingListener === 'function') { 497 modal.attachSizingListener(); 498 } 499 500 isOpen = true; 501 modal.overlay.style.display = 'block'; 502 if (modal.mobileBoardLinkWrapper) { 503 modal.mobileBoardLinkWrapper.style.pointerEvents = 'auto'; 504 } 505 506 // Animate in with mobile-specific transforms 507 requestAnimationFrame(() => { 508 modal.overlay.style.opacity = '1'; 509 if (modal.isMobile) { 510 modal.container.style.transform = 'translateY(0)'; 511 } else { 512 modal.container.style.transform = 'translateX(-50%) scale(1)'; 513 } 514 }); 515 516 // Track widget open 517 trackEvent('widget_opened'); 518 519 // Disable body scroll and prevent iOS bounce 520 document.body.style.overflow = 'hidden'; 521 document.body.style.position = 'fixed'; 522 document.body.style.width = '100%'; 523 document.body.style.top = '-' + window.scrollY + 'px'; 524 } 525 526 // Close widget 527 function closeWidget() { 528 if (!isOpen || !modal) return; 529 530 isOpen = false; 531 532 // Store scroll position before closing 533 const scrollY = parseInt(document.body.style.top || '0') * -1; 534 535 // Animate out with mobile-specific transforms 536 modal.overlay.style.opacity = '0'; 537 if (modal.isMobile) { 538 modal.container.style.transform = 'translateY(100%)'; 539 } else { 540 modal.container.style.transform = 'translateX(-50%) scale(0.9)'; 541 } 542 543 if (typeof modal.detachSizingListener === 'function') { 544 modal.detachSizingListener(); 545 } 546 547 if (modal.mobileBoardLinkWrapper) { 548 modal.mobileBoardLinkWrapper.style.pointerEvents = 'none'; 549 } 550 551 setTimeout(() => { 552 modal.overlay.style.display = 'none'; 553 554 // Restore body scroll 555 document.body.style.overflow = ''; 556 document.body.style.position = ''; 557 document.body.style.width = ''; 558 document.body.style.top = ''; 559 560 // Restore scroll position 561 if (scrollY > 0) { 562 window.scrollTo(0, scrollY); 563 } 564 565 }, 300); 566 567 // Track widget close 568 trackEvent('widget_closed'); 569 } 570 571 // Simple event tracking 572 function trackEvent(eventName) { 573 try { 574 // Send to SignalsLoop analytics 575 fetch('https://signalsloop.com/api/analytics/events', { 576 method: 'POST', 577 headers: { 578 'Content-Type': 'application/json', 579 }, 580 body: JSON.stringify({ 581 event: eventName, 582 apiKey: CONFIG.apiKey, 583 url: window.location.href, 584 timestamp: new Date().toISOString() 585 }) 586 }).catch(() => {}); // Fail silently 587 } catch (e) { 588 // Fail silently 589 } 590 } 591 592 // Initialize widget 593 function initWidget() { 594 // Don't load on admin pages or if already loaded 595 if (window.location.href.includes('/admin') || 596 window.location.href.includes('/settings') || 597 document.getElementById(WIDGET_ID)) { 598 return; 599 } 600 601 const iframe = createWidgetButton(); 602 // iframe is already appended to body in createWidgetButton() 603 604 // Track widget load 605 trackEvent('widget_loaded'); 606 607 // Add to global scope for API access 608 window.SignalsLoopWidget = { 609 open: openWidget, 610 close: closeWidget, 611 config: CONFIG, 612 version: '1.0.0' 613 }; 614 615 if (window.console && typeof window.console.debug === 'function') { 616 console.debug('SignalsLoop widget loaded for', CONFIG.projectName); 617 } 618 } 619 620 // Wait for DOM to be ready 621 if (document.readyState === 'loading') { 622 document.addEventListener('DOMContentLoaded', initWidget); 623 } else { 624 initWidget(); 625 } 626 627 // Listen for messages from iframe 628 window.addEventListener('message', function(event) { 629 if (event.origin !== 'https://signalsloop.com') { 630 return; 631 } 632 633 if (event.data.type === 'signalsloop_close') { 634 closeWidget(); 635 } else if (event.data.type === 'signalsloop_submitted') { 636 // Show success message or perform action 637 trackEvent('feedback_submitted'); 638 // Optional: Keep modal open or close it
639 setTimeout(closeWidget, 2000); // Close after 2 seconds 640 } 641 }); 642 643})();
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.