PageSourceSearch

https://signalsloop.com/embed/sk_89efe32edf48184f641432ff26c6c5df.js

js signalsloop.com collected 2026-10-02 01:43:37 UTC 21,830 bytes, 643 lines download raw bytes

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.