PageSourceSearch

https://neumann-promotion.de/V1/js/common/components/special/B2bBanner.js

js neumann-promotion.de collected 2026-10-10 00:01:02 UTC 8,379 bytes, 294 lines download raw bytes

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.