1import { reportError } from '../../../lib/reportError.js' 2import createTemplateRoot from '../../utils/createTemplateRoot.js' 3import componentRegistry from '../../stores/componentRegistry.js' 4import addFrameworkCSS from '../../utils/frameworkCSS.js' 5 6const languagePack = { 7 bannerText: { de: 'Exklusiv für Gewerbekunden', en: 'Exclusively for business customers' }, 8 infoAriaLabel: { de: 'Weitere Informationen', en: 'More information' }, 9 popupText: { 10 de: 'Dieses Angebot richtet sich ausschlieÃlich an Handwerk und Industrie zur Nutzung im Rahmen ihrer gewerblichen oder selbstständigen Tätigkeit.', 11 en: 'This offer is intended exclusively for trade and industry customers, for use in connection with their commercial or self-employed activity.', 12 }, 13 closeLabel: { de: 'SchlieÃen', en: 'Close' }, 14} 15 16const templateRoot = await createTemplateRoot() 17 18const templateElement = document.createElement('template') 19templateRoot.append(templateElement) 20templateElement.innerHTML = /*html*/` 21 <style> 22 :host { 23 display: block; 24 position: fixed; 25 top: 0; 26 left: 0; 27 right: 0; 28 z-index: 10000; 29 } 30 31 .b2b-bar { 32 position: relative; 33 display: flex; 34 align-items: center; 35 justify-content: center; 36 flex-wrap: wrap; 37 gap: 0.5rem; 38 min-height: 36px; 39 box-sizing: border-box; 40 background: #000d1a; 41 color: #fff; 42 padding: 0.5rem 1rem; 43 text-align: center; 44 font-size: 0.8rem; 45 font-weight: 700; 46 letter-spacing: 0.03em; 47 text-transform: uppercase; 48 } 49 50 .b2b-bar-text { 51 line-height: 1.3; 52 } 53 54 .b2b-info-btn { 55 display: inline-flex; 56 align-items: center; 57 justify-content: center; 58 width: 20px; 59 height: 20px; 60 flex-shrink: 0; 61 border-radius: 50%; 62 border: none; 63 background: rgba(255, 255, 255, 0.18); 64 color: #fff; 65 cursor: pointer; 66 font-size: 0.7rem; 67 transition: background-color 0.2s ease; 68 } 69 .b2b-info-btn:hover { background: rgba(255, 255, 255, 0.32); } 70 .b2b-info-btn:focus-visible { 71 outline: 2px solid rgb(52, 191, 255); 72 outline-offset: 2px; 73 } 74 75 .b2b-popover { 76 display: none; 77 position: absolute; 78 top: 100%; 79 left: 50%; 80 transform: translateX(-50%); 81 margin-top: 0.6rem; 82 width: min(90vw, 380px); 83 background: #fff; 84 color: #333; 85 border-radius: 10px; 86 box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25); 87 padding: 1rem 1.75rem 1rem 1.1rem; 88 font-size: 0.82rem; 89 font-weight: 400; 90 text-transform: none; 91 letter-spacing: normal; 92 line-height: 1.5; 93 text-align: left; 94 } 95 :host(.b2b-popover-open) .b2b-popover { display: block; } 96 97 .b2b-popover-text { margin: 0; } 98 99 .b2b-popover-close { 100 position: absolute; 101 top: 0.4rem; 102 right: 0.4rem; 103 width: 24px; 104 height: 24px; 105 display: inline-flex; 106 align-items: center; 107 justify-content: center; 108 border-radius: 50%; 109 border: none; 110 background: rgba(0, 47, 93, 0.08); 111 color: #002f5d; 112 cursor: pointer; 113 font-size: 0.75rem; 114 } 115 .b2b-popover-close:hover { background: rgba(0, 47, 93, 0.15); } 116 117 @media (max-width: 480px) { 118 .b2b-bar { font-size: 0.72rem; padding: 0.5rem 0.75rem; } 119 } 120 121 @media (prefers-reduced-motion: reduce) { 122 .b2b-info-btn { transition: none; } 123 } 124 </style> 125 126 <div class="b2b-bar"> 127 <span class="b2b-bar-text" property="bannerText"></span> 128 <button type="button" class="b2b-info-btn" data-action="toggle-info" aria-haspopup="dialog" aria-expanded="false"> 129 <i class="fa-solid fa-circle-info" aria-hidden="true"></i> 130 </button> 131 <div class="b2b-popover" role="dialog" data-role="popover"> 132 <button type="button" class="b2b-popover-close" data-action="close-info"> 133 <i class="fa-solid fa-xmark" aria-hidden="true"></i> 134 </button> 135 <p class="b2b-popover-text" property="popupText"></p> 136 </div> 137 </div> 138` 139 140class B2bBanner extends HTMLElement { 141 constructor() { 142 try { 143 super() 144 this.language = document.getElementById('lang')?.getAttribute('data-lang') || 'de' 145 this._resizeObserver = null 146 this._onDocClick = this._onDocClick.bind(this) 147 this._onKeyDown = this._onKeyDown.bind(this) 148 this._onResize = this._syncOffset.bind(this) 149 150 const shadowRoot = this.attachShadow({ mode: 'open' }) 151 shadowRoot.appendChild(templateElement.content.cloneNode(true)) 152 addFrameworkCSS(shadowRoot) 153 } catch (err) { 154 reportError(err) 155 throw err 156 } 157 } 158 159 connectedCallback() { 160 try { 161 componentRegistry.register(this) 162 this._applyLanguagePack() 163 this._bindEvents() 164 this._syncOffset() 165 166 if (typeof ResizeObserver !== 'undefined') { 167 this._resizeObserver = new ResizeObserver(() => this._syncOffset()) 168 this._resizeObserver.observe(this) 169 } else { 170 window.addEventListener('resize', this._onResize) 171 } 172 } catch (err) { 173 reportError(err) 174 throw err 175 } 176 } 177 178 disconnectedCallback() { 179 try { 180 this._resizeObserver?.disconnect() 181 window.removeEventListener('resize', this._onResize) 182 document.removeEventListener('click', this._onDocClick) 183 document.removeEventListener('keydown', this._onKeyDown) 184 document.documentElement.style.removeProperty('--b2b-banner-height') 185 } catch (err) { 186 reportError(err) 187 throw err 188 } 189 } 190 191 _applyLanguagePack() { 192 try { 193 const fields = this.shadowRoot.querySelectorAll('[property]') 194 for (const field of fields) { 195 const property = field.getAttribute('property') 196 if (languagePack[property]) { 197 field.textContent = languagePack[property][this.language] || languagePack[property].en 198 } 199 } 200 const infoBtn = this.shadowRoot.querySelector('[data-action="toggle-info"]') 201 infoBtn?.setAttribute('aria-label', languagePack.infoAriaLabel[this.language] || languagePack.infoAriaLabel.en) 202 const closeBtn = this.shadowRoot.querySelector('[data-action="close-info"]') 203 closeBtn?.setAttribute('aria-label', languagePack.closeLabel[this.language] || languagePack.closeLabel.en) 204 } catch (err) { 205 reportError(err) 206 throw err 207 } 208 } 209 210 _bindEvents() { 211 try { 212 const infoBtn = this.shadowRoot.querySelector('[data-action="toggle-info"]') 213 const closeBtn = this.shadowRoot.querySelector('[data-action="close-info"]') 214 215 infoBtn?.addEventListener('click', (event) => { 216 event.stopPropagation() 217 this._togglePopover() 218 }) 219 closeBtn?.addEventListener('click', () => this._closePopover()) 220 221 document.addEventListener('click', this._onDocClick) 222 document.addEventListener('keydown', this._onKeyDown) 223 } catch (err) { 224 reportError(err) 225 throw err 226 } 227 } 228 229 _togglePopover() { 230 try { 231 if (this.classList.contains('b2b-popover-open')) { 232 this._closePopover() 233 } else { 234 this._openPopover() 235 } 236 } catch (err) { 237 reportError(err) 238 throw err 239 } 240 } 241 242 _openPopover() { 243 try { 244 this.classList.add('b2b-popover-open') 245 this.shadowRoot.querySelector('[data-action="toggle-info"]')?.setAttribute('aria-expanded', 'true') 246 } catch (err) { 247 reportError(err) 248 throw err 249 } 250 } 251 252 _closePopover() { 253 try { 254 this.classList.remove('b2b-popover-open') 255 this.shadowRoot.querySelector('[data-action="toggle-info"]')?.setAttribute('aria-expanded', 'false') 256 } catch (err) { 257 reportError(err) 258 throw err 259 } 260 } 261 262 _onDocClick(event) { 263 try { 264 if (!this.classList.contains('b2b-popover-open')) return 265 if (event.composedPath().includes(this)) return 266 this._closePopover() 267 } catch (err) { 268 reportError(err) 269 throw err 270 } 271 } 272 273 _onKeyDown(event) { 274 try { 275 if (event.key === 'Escape' && this.classList.contains('b2b-popover-open')) { 276 this._closePopover() 277 } 278 } catch (err) { 279 reportError(err) 280 throw err 281 } 282 } 283 284 _syncOffset() { 285 try { 286 document.documentElement.style.setProperty('--b2b-banner-height', `${this.offsetHeight}px`) 287 } catch (err) { 288 reportError(err) 289 throw err 290 } 291 } 292} 293 294customElements.define('b2b-banner', B2bBanner)
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.