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=>({ '&':'&','<':'<','>':'>','"':'"',"'":''' }[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.