PageSourceSearch

https://www.eventeny.com/js/Components/GlobalBanner.js?v=1790189618

js eventeny.com collected 2026-09-24 08:55:06 UTC 6,536 bytes, 205 lines download raw bytes

1/**
2 * @file BannerElement.js
3 * @description A reusable banner component with expiration, cookie-based dismissal,
4 * plan gating, and customizable content.
5 *
6 * @example
7 * <global-banner
8 *   id="webinar-banner"
9 *   image="/images/evee/evee-ticket-banner.png"
10 *   text="<b>Feeling stuck?</b> Your event questions, answered live – Oct 29."
11 *   link-href="https://example.com"
12 *   link-text="Register Now!"
13 *   cookie="webinarBanner-10292025"
14 *   expires="2025-10-29T14:01:00-04:00"
15 *   show-if-plan="free"
16 *   dismiss-days="7"
17 * ></global-banner>
18 *
19 * // Optionally set at runtime:
20 * el.userPlanLevel = 'free';
21 *
22 * @attributes
23 * - image        : URL for left image (optional)
24 * - text         : HTML text content
25 * - link-href    : CTA URL (optional)
26 * - link-text    : CTA label (default: "Learn More")
27 * - cookie       : cookie key used to remember dismissal (default: "<id>_cookie")
28 * - expires      : ISO datetime string; hides after this time (optional)
29 * - show-if-plan : only show if user plan matches (optional)
30 * - dismiss-days : cookie days after dismiss (default: 7)
31 *
32 * @events
33 * - banner:shown
34 * - banner:dismissed
35 */
36class GlobalBannerElement extends HTMLElement {
37  static get observedAttributes() {
38    return ['image','text','link-href','link-text','cookie','expires','show-if-plan','dismiss-days','alt'];
39  }
40
41  constructor() {
42    super();
43    this.attachShadow({ mode: 'open' });
44    this.userPlanLevel = null;
45    this._render();
46  }
47
48  connectedCallback(){ this._update(); }
49  attributeChangedCallback(){ this._update(); }
50  dismiss(){ this._dismiss(); }
51
52  _render(){
53    const style = `
54      :host { display:block; }
55      .banner {
56        background: #0BD2CD33;
57        border-radius: 6px 6px 0 0;
58        color: #222;
59        position: relative;
60        z-index: 99;
61        box-shadow: 0 2px 8px rgba(0,0,0,.2);
62        display: flex;
63        justify-content: center;
64        align-items: center;
65        min-height: 48px;
66        height: auto;
67        padding: 0px 48px 0px 0px; /* room for close button */
68      }
69      .content { 
70        display: flex; 
71        align-items: center; 
72        gap: 12px; 
73        max-width: 100%; 
74      }
75      .img { 
76      height: 44px; 
77      flex: 0 0 auto; 
78      display: block; 
79      position: relative; 
80      top: 2px;
81      }
82      .txt {
83        white-space: normal;
84        display: block;
85        margin: 0;
86        font-size: 16px;
87        line-height: 1.35;
88        overflow-wrap: anywhere;
89      }
90      .txt a { 
91        text-decoration: underline; 
92        font-weight: 700; 
93        color: #222; 
94      }
95
96      .close {
97        position: absolute;
98        top: 50%;
99        right: 16px;
100        transform: translateY(-50%);
101        cursor: pointer;
102        font-size: 16px;
103        line-height: 1;
104        border: 0;
105        background: transparent;
106        color: #444;
107        padding: 4px;
108        border-radius: 4px;
109      }
110      .close:hover { background: rgba(0,0,0,.05); }
111
112      .hide { display:none !important; }
113
114      @media (max-width: 640px){
115        .img { display:none; }
116        .txt { font-size:16px; text-align: center; }
117        .banner { padding: 8px 44px 8px 12px; }
118
119        /* put the link on its own line on mobile */
120        .txt a {
121          display: block;
122          width: 100%;
123          margin-top: 6px;   /* a little spacing from the text */
124        }
125      }
126    `;
127
128    const html = `
129      <div class="banner" part="container" aria-live="polite">
130        <div class="content">
131          <img class="img" part="image" />
132          <p class="txt" part="text"></p>
133        </div>
134        <button class="close" part="close" aria-label="Close banner" title="Close">
135          <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
136            <path d="M18.3 5.71a1 1 0 0 0-1.41 0L12 10.59 7.11 5.7A1 1 0 0 0 5.7 7.11L10.59 12l-4.9 4.89a1 1 0 1 0 1.41 1.41L12 13.41l4.89 4.9a1 1 0 0 0 1.41-1.41L13.41 12l4.9-4.89a1 1 0 0 0-.01-1.4z" fill="currentColor"/>
137          </svg>
138        </button>
139      </div>
140    `;
141
142    this.shadowRoot.innerHTML = `<style>${style}</style>${html}`;
143    this._els = {
144      root: this.shadowRoot.querySelector('.banner'),
145      img: this.shadowRoot.querySelector('.img'),
146      txt: this.shadowRoot.querySelector('.txt'),
147      close: this.shadowRoot.querySelector('.close'),
148    };
149    this._els.close.addEventListener('click', () => this._dismiss());
150  }
151
152  _update(){
153    const cfg = this._cfg();
154
155    if (this._isDismissed(cfg.cookie) || this._isExpired(cfg.expires) || !this._passesPlan(cfg.showIfPlan)) {
156      this._els.root.classList.add('hide'); return;
157    }
158
159    if (cfg.image) {
160      this._els.img.src = cfg.image;
161      this._els.img.alt = cfg.alt || 'Banner';
162      this._els.img.classList.remove('hide');
163    } else {
164      this._els.img.classList.add('hide');
165    }
166
167    const link = cfg.linkHref
168      ? ` <a href="${this._esc(cfg.linkHref)}" target="_blank" rel="noopener">${this._esc(cfg.linkText || 'Learn More')}</a>`
169      : '';
170    this._els.txt.innerHTML = (cfg.text || '') + link;
171
172    this._els.root.classList.remove('hide');
173  }
174
175  _cfg(){
176    const attr = (k)=>this.getAttribute(k);
177    return {
178      image: attr('image'),
179      alt: attr('alt') || '',
180      text: attr('text') || '',
181      linkHref: attr('link-href'),
182      linkText: attr('link-text') || 'Learn More',
183      cookie: attr('cookie') || (this.id ? `${this.id}_cookie` : 'banner_cookie'),
184      expires: attr('expires') || null,
185      showIfPlan: attr('show-if-plan') || null,
186      dismissDays: parseInt(attr('dismiss-days') || '7', 10),
187    };
188  }
189
190  _dismiss(){
191    const { cookie, dismissDays } = this._cfg();
192    const d = new Date(); d.setTime(d.getTime() + dismissDays*24*60*60*1000);
193    document.cookie = `${cookie}=hidden;expires=${d.toUTCString()};path=/`;
194    this._els.root.classList.add('hide');
195    this.dispatchEvent(new CustomEvent('banner:dismissed', { bubbles:true }));
196  }
197
198  _passesPlan(required){ if(!required) return true; return (this.userPlanLevel || null) === required; }
199  _isExpired(expires){ if(!expires) return false; const t = +new Date(expires); return Number.isFinite(t) && Date.now() > t; }
200  _isDismissed(name){ return document.cookie.split('; ').some(c => c.startsWith(name+'=hidden')); }
201  _esc(s){ return String(s).replace(/[&<>"']/g, m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' }[m])); }
202}
203
204if(!customElements.get('global-banner')) customElements.define('global-banner', GlobalBannerElement);
205

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.