PageSourceSearch

https://www.klimahero.de/plugins/tt_energy_label/frontend/js/energy_label.js?v=1.1.53

js klimahero.de collected 2026-10-09 02:53:48 UTC 53,179 bytes, 1,295 lines download raw bytes

1
2
3(function () {
4    'use strict';
5
6    
7    
8    try { console.log('[TT-EnergyLabel] energy_label.js geladen - v1.1.48'); } catch (e) {}
9
10    if (typeof window.TT_ENERGY_LABEL_CONFIG === 'undefined') {
11        try { console.warn('[TT-EnergyLabel] ABBRUCH: window.TT_ENERGY_LABEL_CONFIG ist undefined'); } catch (e) {}
12        return;
13    }
14
15    const config  = window.TT_ENERGY_LABEL_CONFIG;
16    const cache   = new Map();
17    const pending = new Map();
18    const handled = new WeakSet();
19    const handledTab = new Set();
20
21    const log = (...args) => {
22        if (config.debug) {
23            console.log('[TT-EnergyLabel]', ...args);
24        }
25    };
26
27    const warn = (...args) => {
28        console.warn('[TT-EnergyLabel]', ...args);
29    };
30
31    function detectContext() {
32        const body = document.body;
33        if (!body) return 'all';
34
35        if (body.classList.contains('product-detail') ||
36            body.classList.contains('page-product') ||
37            document.querySelector('.product-buy-box')) {
38            return 'detail';
39        }
40        if (body.classList.contains('page-category') ||
41            body.classList.contains('category-listing')) {
42            return 'category';
43        }
44        if (body.classList.contains('page-cart'))         return 'cart';
45        if (body.classList.contains('page-wishlist'))     return 'wishlist';
46        if (body.classList.contains('page-comparelist'))  return 'comparelist';
47        return 'all';
48    }
49
50    function findArticleId(element) {
51        let current = element;
52        for (let i = 0; i < 6 && current; i++) {
53            const id = current.dataset && (
54                current.dataset.productId ||
55                current.dataset.kartikel ||
56                current.dataset.articleId
57            );
58            if (id) return parseInt(id, 10);
59
60            const hidden = current.querySelector?.(
61                'input[name="a"], input[name="kArtikel"], input[name="article_id"]'
62            );
63            if (hidden && hidden.value) {
64                return parseInt(hidden.value, 10);
65            }
66            current = current.parentElement;
67        }
68
69        const urlMatch = window.location.search.match(/[?&]a=(\d+)/);
70        if (urlMatch) return parseInt(urlMatch[1], 10);
71
72        return null;
73    }
74
75    function fetchLabelData(kArtikel) {
76        if (cache.has(kArtikel))   return Promise.resolve(cache.get(kArtikel));
77        if (pending.has(kArtikel)) return pending.get(kArtikel);
78
79        const url = config.ajaxUrl + '?action=getLabel&kArtikel=' + kArtikel
80            + '&lang=' + encodeURIComponent(config.langIso || '');
81
82        const promise = fetch(url, {
83            credentials: 'same-origin',
84            headers: { 'X-Requested-With': 'XMLHttpRequest' }
85        })
86            .then(r => r.ok ? r.json() : null)
87            .then(data => {
88                cache.set(kArtikel, data);
89                pending.delete(kArtikel);
90                return data;
91            })
92            .catch(err => {
93                log('Fetch error for', kArtikel, err);
94                pending.delete(kArtikel);
95                cache.set(kArtikel, null);
96                return null;
97            });
98
99        pending.set(kArtikel, promise);
100        return promise;
101    }
102
103    function renderArrow(arrow, labelData) {
104        const sizeClass = 'tt-size-' + (config.arrowSize || 'medium');
105        const dirClass  = arrow.richtung === 'rechts' ? 'tt-dir-right' : 'tt-dir-left';
106
107        const wrapper = document.createElement('span');
108        wrapper.className = 'tt-energy-arrow-group';
109
110        if (arrow.titel && arrow.titel !== '') {
111            const title = document.createElement('span');
112            title.className = 'tt-arrow-title';
113            title.textContent = arrow.titel;
114            wrapper.appendChild(title);
115        }
116
117        const arrowEl = document.createElement('button');
118        arrowEl.type = 'button';
119        arrowEl.className = 'tt-energy-arrow ' + sizeClass + ' ' + dirClass;
120        arrowEl.dataset.pos    = arrow.position;
121        arrowEl.dataset.klasse = arrow.klasse;
122        arrowEl.dataset.skala  = arrow.skala;
123        arrowEl.setAttribute('aria-label', labelData.accessibility.arrowAriaLabel);
124        arrowEl.setAttribute('title',      labelData.accessibility.arrowAriaLabel);
125
126        const klasseEl = document.createElement('span');
127        klasseEl.className = 'tt-arrow-class';
128        klasseEl.textContent = arrow.klasse;
129        arrowEl.appendChild(klasseEl);
130
131        arrowEl.addEventListener('click', () => openLabelModal(labelData));
132
133        wrapper.appendChild(arrowEl);
134
135        if (labelData.showScaleRange) {
136            const range = document.createElement('span');
137            range.className = 'tt-scale-range';
138            range.textContent = arrow.skalaVon + ' \u2013 ' + arrow.skalaBis;
139            range.setAttribute('aria-hidden', 'true'); 
140            wrapper.appendChild(range);
141        }
142
143        return wrapper;
144    }
145
146    function renderDatasheetLink(labelData) {
147        if (!labelData.hasDatasheet) return null;
148
149        const link = document.createElement('button');
150        link.type = 'button';
151        link.className = 'tt-energy-datasheet-link';
152        link.textContent = labelData.strings.datasheet_link;
153        link.setAttribute('aria-label', labelData.accessibility.datasheetAria);
154        link.setAttribute('title',      labelData.accessibility.datasheetAria);
155        link.addEventListener('click', () => openDatasheetModal(labelData));
156        return link;
157    }
158
159    let lastFocusedElement = null;
160
161    function getFocusableElements(modal) {
162        return modal.querySelectorAll(
163            'a[href], button:not([disabled]), input:not([disabled]), ' +
164            'select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
165        );
166    }
167
168    function trapFocus(e, modal) {
169        if (e.key !== 'Tab') return;
170        const focusable = getFocusableElements(modal);
171        if (focusable.length === 0) return;
172
173        const first = focusable[0];
174        const last  = focusable[focusable.length - 1];
175
176        if (e.shiftKey && document.activeElement === first) {
177            e.preventDefault();
178            last.focus();
179        } else if (!e.shiftKey && document.activeElement === last) {
180            e.preventDefault();
181            first.focus();
182        }
183    }
184
185    function buildModalShell(titleText, contentHtml, opts) {
186        opts = opts || {};
187        const id = opts.id || 'tt-energy-modal-' + Date.now();
188        const titleId = id + '-title';
189
190        let modal = document.getElementById(opts.singletonId || '');
191        if (!modal) {
192            modal = document.createElement('div');
193            modal.id = opts.singletonId || id;
194            modal.className = 'tt-energy-modal';
195            modal.setAttribute('role', 'dialog');
196            modal.setAttribute('aria-modal', 'true');
197            modal.setAttribute('aria-labelledby', titleId);
198            modal.innerHTML = '<div class="tt-energy-modal-backdrop" data-close="1"></div>'
199                + '<div class="tt-energy-modal-content">'
200                +   '<div class="tt-energy-modal-header">'
201                +     '<h2 class="tt-energy-modal-title" id="' + titleId + '"></h2>'
202                +     '<button type="button" class="tt-energy-modal-close" aria-label="' + escapeAttr(opts.closeLabel || 'Schliessen') + '">'
203                +       '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>'
204                +     '</button>'
205                +   '</div>'
206                +   '<div class="tt-energy-modal-body"></div>'
207                + '</div>';
208            document.body.appendChild(modal);
209        }
210
211        modal.querySelector('.tt-energy-modal-title').textContent = titleText;
212        modal.querySelector('.tt-energy-modal-close').setAttribute('aria-label', opts.closeLabel || 'Schliessen');
213        modal.querySelector('.tt-energy-modal-body').innerHTML = contentHtml;
214
215        const closeFn = () => closeModal(modal);
216        modal.querySelector('.tt-energy-modal-close').onclick = closeFn;
217        modal.querySelector('.tt-energy-modal-backdrop').onclick = closeFn;
218
219        modal._keydownHandler = (e) => {
220            if (e.key === 'Escape') {
221                closeModal(modal);
222            } else {
223                trapFocus(e, modal);
224            }
225        };
226
227        return modal;
228    }
229
230    function openModal(modal) {
231        lastFocusedElement = document.activeElement;
232        modal.classList.add('tt-open');
233        document.addEventListener('keydown', modal._keydownHandler);
234
235        setTimeout(() => {
236            const closeBtn = modal.querySelector('.tt-energy-modal-close');
237            if (closeBtn) closeBtn.focus();
238        }, 50);
239    }
240
241    function closeModal(modal) {
242        modal.classList.remove('tt-open');
243        if (modal._keydownHandler) {
244            document.removeEventListener('keydown', modal._keydownHandler);
245        }
246        
247        if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
248            lastFocusedElement.focus();
249        }
250    }
251
252    function openLabelModal(labelData) {
253        if (!labelData.hasLabel) return;
254
255        let html = '';
256
257        if (labelData.labelImage) {
258            html += '<picture>';
259            if (labelData.labelImage.urlWebp) {
260                html += '<source srcset="' + escapeAttr(labelData.labelImage.urlWebp) + '" type="image/webp">';
261            }
262            html += '<img class="tt-energy-modal-image" src="' + escapeAttr(labelData.labelImage.url)
263                 +  '" alt="' + escapeAttr(labelData.accessibility.imageAlt) + '">';
264            html += '</picture>';
265        }
266
267        if (labelData.eprel && labelData.eprelUrl) {
268            html += '<div class="tt-energy-modal-eprel">';
269            if (labelData.showEprelQr) {
270                html += '<img class="tt-eprel-qr" alt="" src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data='
271                     + encodeURIComponent(labelData.eprelUrl) + '">';
272            }
273            html += '<div>'
274                 +   '<div style="font-size:13px;color:#666;margin-bottom:4px;">EPREL-Registriernummer</div>'
275                 +   '<div style="font-size:18px;font-weight:600;margin-bottom:6px;">' + escapeHtml(labelData.eprel) + '</div>'
276                 +   '<a class="tt-energy-eprel-link" href="' + escapeAttr(labelData.eprelUrl) + '" target="_blank" rel="noopener">'
277                 +     escapeHtml(labelData.strings.eprel_link) + ' &#x2197;'
278                 +   '</a>'
279                 + '</div>';
280            html += '</div>';
281        }
282
283        const modal = buildModalShell(
284            labelData.strings.label_modal,
285            html,
286            { singletonId: 'tt-energy-label-modal', closeLabel: labelData.strings.datasheet_close }
287        );
288        openModal(modal);
289    }
290
291    
292
293    
294
295    
296    function ttFrontendLog(level, message, context) {
297        try {
298            if (level === 'warn' || level === 'error') {
299                console.warn('[TT-PDF]', message, context || '');
300            } else {
301                console.log('[TT-PDF]', message, context || '');
302            }
303        } catch (e) {}
304        if (!config.frontendDebugLog) return;
305        try {
306            var logUrl = (config.pluginUrl || '') + 'frontend/tt_energylabel.php?action=log';
307            fetch(logUrl, {
308                method: 'POST',
309                headers: { 'Content-Type': 'application/json' },
310                body: JSON.stringify({
311                    level:   level,
312                    message: String(message).substring(0, 200),
313                    context: Object.assign({
314                        ua:   navigator.userAgent.substring(0, 100),
315                        url:  location.pathname,
316                        time: new Date().toISOString()
317                    }, context || {})
318                }),
319                credentials: 'same-origin',
320                keepalive:   true
321            }).catch(function () {  });
322        } catch (e) {  }
323    }
324
325    
326    
327    if (config.frontendDebugLog && !window._ttErrorHandlerInstalled) {
328        window._ttErrorHandlerInstalled = true;
329        window.addEventListener('error', function (e) {
330            ttFrontendLog('error', 'JS-Fehler: ' + (e.message || 'unknown'), {
331                file: (e.filename || '').split('/').slice(-2).join('/'),
332                line: e.lineno,
333                col:  e.colno
334            });
335        });
336        window.addEventListener('unhandledrejection', function (e) {
337            ttFrontendLog('error', 'Unhandled Promise-Rejection: ' + (e.reason && e.reason.message ? e.reason.message : String(e.reason)));
338        });
339    }
340
341    var pdfjsLoadPromise = null;
342    function loadPdfJsLib() {
343        if (pdfjsLoadPromise !== null) return pdfjsLoadPromise;
344
345        var base = (config.pluginUrl || '') + 'frontend/vendor/pdfjs/';
346        var pdfJsUrl  = base + 'pdf.min.js';
347        var workerUrl = base + 'pdf.worker.min.js';
348
349        ttFrontendLog('info', 'PDF.js wird geladen', { url: pdfJsUrl });
350
351        pdfjsLoadPromise = new Promise(function (resolve, reject) {
352
353            function findLib() {
354                return window.pdfjsLib
355                    || window['pdfjs-dist/build/pdf']
356                    || (window.pdfjs && window.pdfjs.getDocument ? window.pdfjs : null);
357            }
358
359            var existing = findLib();
360            if (existing) {
361                ttFrontendLog('info', 'PDF.js bereits geladen, Worker konfigurieren');
362                if (existing.GlobalWorkerOptions) {
363                    existing.GlobalWorkerOptions.workerSrc = workerUrl;
364                }
365                resolve(existing);
366                return;
367            }
368
369            var s = document.createElement('script');
370            s.src   = pdfJsUrl;
371            s.async = false;  
372            s.onload = function () {
373                var lib = findLib();
374                ttFrontendLog('info', 'PDF.js Script geladen', {
375                    libFound:    !!lib,
376                    hasGetDoc:   !!(lib && lib.getDocument),
377                    version:     lib && lib.version ? lib.version : 'unknown'
378                });
379                if (!lib || typeof lib.getDocument !== 'function') {
380                    reject(new Error('PDF.js geladen, aber getDocument fehlt'));
381                    return;
382                }
383                if (lib.GlobalWorkerOptions) {
384                    lib.GlobalWorkerOptions.workerSrc = workerUrl;
385                }
386                resolve(lib);
387            };
388            s.onerror = function () {
389                ttFrontendLog('error', 'PDF.js Script konnte nicht geladen werden', { url: pdfJsUrl });
390                reject(new Error('PDF.js-Skript nicht erreichbar: ' + pdfJsUrl));
391            };
392            document.head.appendChild(s);
393        });
394        return pdfjsLoadPromise;
395    }
396
397    var pdfBufferCache = new Map();
398    function fetchPdfBuffer(pdfUrl) {
399        if (pdfBufferCache.has(pdfUrl)) return pdfBufferCache.get(pdfUrl);
400        ttFrontendLog('info', 'Fetche PDF-Bytes', { url: pdfUrl.substring(0, 200) });
401        var p = fetch(pdfUrl, { credentials: 'same-origin' })
402            .then(function (resp) {
403                ttFrontendLog('info', 'PDF-Response erhalten', {
404                    status: resp.status,
405                    ok:     resp.ok,
406                    type:   resp.headers.get('Content-Type')
407                });
408                if (!resp.ok) throw new Error('HTTP ' + resp.status);
409                return resp.arrayBuffer();
410            })
411            .then(function (buf) {
412                ttFrontendLog('info', 'PDF-Bytes empfangen', { bytes: buf.byteLength });
413                return buf;
414            });
415        pdfBufferCache.set(pdfUrl, p);
416        return p;
417    }
418
419    var pdfViewerStates = new Map();
420    function initPdfState(pdfUrl) {
421        var existing = pdfViewerStates.get(pdfUrl);
422        if (existing && existing.pdf) return Promise.resolve(existing);
423        return loadPdfJsLib().then(function (lib) {
424            return fetchPdfBuffer(pdfUrl).then(function (buf) {
425                ttFrontendLog('info', 'PDF.js getDocument startet');
426                return lib.getDocument({
427                    data: buf.slice(0),
428                    disableAutoFetch: true,
429                    disableStream:    true,
430                    isEvalSupported:  false
431                }).promise;
432            });
433        }).then(function (pdf) {
434            ttFrontendLog('info', 'PDF geladen', { pages: pdf.numPages });
435            var state = {
436                pdf: pdf,
437                numPages: pdf.numPages,
438                pageCache: new Map(),
439                currentPage: 1
440            };
441            pdfViewerStates.set(pdfUrl, state);
442            return state;
443        });
444    }
445
446    function renderPdfPage(state, pageNum, target, scale) {
447        scale = scale || 1.5;
448        pageNum = Math.max(1, Math.min(state.numPages, pageNum));
449        var key = pageNum + '@' + scale;
450        if (state.pageCache.has(key)) {
451            var cachedImg = document.createElement('img');
452            cachedImg.src = state.pageCache.get(key);
453            cachedImg.alt = 'Datenblatt Seite ' + pageNum;
454            cachedImg.className = 'tt-energy-modal-pdf-img';
455            target.innerHTML = '';
456            target.appendChild(cachedImg);
457            return Promise.resolve();
458        }
459        return state.pdf.getPage(pageNum).then(function (page) {
460            var viewport = page.getViewport({ scale: scale });
461            var canvas = document.createElement('canvas');
462            var ctx = canvas.getContext('2d');
463            canvas.width  = viewport.width;
464            canvas.height = viewport.height;
465            return page.render({ canvasContext: ctx, viewport: viewport }).promise
466                .then(function () {
467                    try {
468                        var dataUrl = canvas.toDataURL('image/jpeg', 0.85);
469                        state.pageCache.set(key, dataUrl);
470                        var img = document.createElement('img');
471                        img.src = dataUrl;
472                        img.alt = 'Datenblatt Seite ' + pageNum;
473                        img.className = 'tt-energy-modal-pdf-img';
474                        target.innerHTML = '';
475                        target.appendChild(img);
476                    } catch (e) {
477                        canvas.className = 'tt-energy-modal-pdf-img';
478                        target.innerHTML = '';
479                        target.appendChild(canvas);
480                    }
481                });
482        });
483    }
484
485    function updateViewerControls(modal, state) {
486        var pageInfo = modal.querySelector('.tt-energy-modal-pdf-pageinfo');
487        var prevBtn  = modal.querySelector('.tt-energy-modal-pdf-prev');
488        var nextBtn  = modal.querySelector('.tt-energy-modal-pdf-next');
489        if (pageInfo) pageInfo.textContent = 'Seite ' + state.currentPage + ' / ' + state.numPages;
490        if (prevBtn)  prevBtn.disabled = (state.currentPage <= 1);
491        if (nextBtn)  nextBtn.disabled = (state.currentPage >= state.numPages);
492
493        
494        var nav = modal.querySelector('.tt-energy-modal-pdf-nav');
495        if (nav) nav.style.display = (state.numPages > 1) ? '' : 'none';
496    }
497
498    function showPdfError(target, message) {
499        target.innerHTML = ''
500            + '<div class="tt-energy-modal-pdf-error">'
501            +   '<svg viewBox="0 0 64 80" xmlns="http://www.w3.org/2000/svg" style="width:48px;height:60px;" aria-hidden="true">'
502            +     '<rect x="2" y="2" width="60" height="76" rx="4" fill="#fff" stroke="#dc2626" stroke-width="2"/>'
503            +     '<rect x="2" y="2" width="60" height="18" rx="4" fill="#dc2626"/>'
504            +     '<text x="32" y="14" text-anchor="middle" font-family="Arial,sans-serif" font-size="10" font-weight="bold" fill="#fff">PDF</text>'
505            +   '</svg>'
506            +   '<p>Vorschau nicht verfügbar.<br><small style="opacity:0.7;">' + escapeHtml(message) + '</small><br>Datenblatt unten herunterladen.</p>'
507            + '</div>';
508    }
509
510    function setPdfStatus(target, message) {
511
512        var loadingDiv = target.querySelector('.tt-energy-modal-pdf-loading >
512 div:last-child');
513        if (loadingDiv) loadingDiv.textContent = message;
514    }
515
516    function openDatasheetModal(labelData) {
517        ttFrontendLog('info', 'openDatasheetModal aufgerufen', {
518            hasDatasheet: !!labelData.hasDatasheet,
519            url: labelData.datasheet ? labelData.datasheet.url : null
520        });
521        if (!labelData.hasDatasheet) return;
522
523        var url = labelData.datasheet.url;
524        var html = ''
525            + '<div class="tt-energy-modal-pdf-wrap">'
526            +   '<div class="tt-energy-modal-pdf-toolbar">'
527            +     '<div class="tt-energy-modal-pdf-nav">'
528            +       '<button type="button" class="tt-energy-modal-pdf-prev" disabled>'
529            +         '<span aria-hidden="true">&#x2039;</span> Zurück'
530            +       '</button>'
531            +       '<span class="tt-energy-modal-pdf-pageinfo">Seite 1</span>'
532            +       '<button type="button" class="tt-energy-modal-pdf-next" disabled>'
533            +         'Weiter <span aria-hidden="true">&#x203A;</span>'
534            +       '</button>'
535            +     '</div>'
536            +     '<a class="tt-energy-modal-pdf-dlbtn" href="' + escapeAttr(url) + '" target="_blank" rel="noopener" download>'
537            +       '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width:14px;height:14px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>'
538            +       '<span>' + escapeHtml(labelData.strings.datasheet_dl) + '</span>'
539            +     '</a>'
540            +   '</div>'
541            +   '<div class="tt-energy-modal-pdf-page">'
542            +     '<div class="tt-energy-modal-pdf-loading">'
543            +       '<div class="tt-energy-modal-pdf-spinner" aria-hidden="true"></div>'
544            +       '<div>Initialisiere…</div>'
545            +     '</div>'
546            +   '</div>'
547            + '</div>';
548
549        var modal = buildModalShell(
550            labelData.strings.datasheet_modal,
551            html,
552            { singletonId: 'tt-energy-datasheet-modal', closeLabel: labelData.strings.datasheet_close }
553        );
554        openModal(modal);
555
556        var pageContainer = modal.querySelector('.tt-energy-modal-pdf-page');
557        if (!pageContainer) {
558            ttFrontendLog('warn', 'pageContainer nicht gefunden');
559            return;
560        }
561
562        setPdfStatus(pageContainer, 'Lade PDF.js-Bibliothek…');
563        ttFrontendLog('info', 'Step 1/4: Lade PDF.js');
564
565        loadPdfJsLib().then(function (lib) {
566            setPdfStatus(pageContainer, 'PDF.js geladen, lade PDF-Datei…');
567            ttFrontendLog('info', 'Step 2/4: PDF.js OK, fetche PDF', { version: lib.version || '?' });
568            return fetchPdfBuffer(url).then(function (buf) {
569                setPdfStatus(pageContainer, 'PDF geladen (' + Math.round(buf.byteLength/1024) + ' KB), öffne…');
570                ttFrontendLog('info', 'Step 3/4: PDF gefetched, öffne via getDocument');
571                return lib.getDocument({
572                    data: buf.slice(0),
573                    disableAutoFetch: true,
574                    disableStream:    true,
575                    isEvalSupported:  false
576                }).promise;
577            });
578        }).then(function (pdf) {
579            setPdfStatus(pageContainer, 'Rendere Seite 1 von ' + pdf.numPages + '…');
580            ttFrontendLog('info', 'Step 4/4: PDF geöffnet, rendere Seite 1', { pages: pdf.numPages });
581            var state = {
582                pdf: pdf,
583                numPages: pdf.numPages,
584                pageCache: new Map(),
585                currentPage: 1
586            };
587            pdfViewerStates.set(url, state);
588            updateViewerControls(modal, state);
589            return renderPdfPage(state, 1, pageContainer, 1.5).then(function () {
590                ttFrontendLog('info', 'Seite 1 erfolgreich gerendert');
591                var prevBtn = modal.querySelector('.tt-energy-modal-pdf-prev');
592                var nextBtn = modal.querySelector('.tt-energy-modal-pdf-next');
593                if (prevBtn) prevBtn.addEventListener('click', function () {
594                    if (state.currentPage > 1) {
595                        state.currentPage--;
596                        updateViewerControls(modal, state);
597                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
598                    }
599                });
600                if (nextBtn) nextBtn.addEventListener('click', function () {
601                    if (state.currentPage < state.numPages) {
602                        state.currentPage++;
603                        updateViewerControls(modal, state);
604                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
605                    }
606                });
607            });
608        }).catch(function (err) {
609            var errMsg = err && err.message ? err.message : String(err);
610            ttFrontendLog('error', 'PDF-Viewer-Fehler: ' + errMsg);
611            showPdfError(pageContainer, errMsg);
612        });
613    }
614
615    function tryInsertTab(labelData) {
616        if (!labelData.displayModes.includes('tab')) return;
617        if (!labelData.tabContent || labelData.tabContent.trim() === '') return;
618        if (handledTab.has(labelData.kArtikel)) return;
619
620        const tabList = document.querySelector(
621            '#product-tabs ul.nav-tabs, .product-detail-tabs .nav-tabs, ' +
622            '.product-detail .nav-tabs, ul.nav-tabs[role="tablist"]'
623        );
624        const panelContainer = document.querySelector(
625            '#product-tabs .tab-content, .product-detail-tabs .tab-content, ' +
626            '.product-detail .tab-content'
627        );
628
629        if (!tabList || !panelContainer) {
630            log('Keine Tab-Leiste auf der Seite gefunden, Tab-Modus uebersprungen');
631            return;
632        }
633
634        const tabId   = 'tt-energy-tab-' + labelData.kArtikel;
635        const panelId = 'tt-energy-tab-panel-' + labelData.kArtikel;
636
637        if (document.getElementById(tabId)) {
638            handledTab.add(labelData.kArtikel);
639            return;
640        }
641
642        const li = document.createElement('li');
643        li.className = 'nav-item';
644        li.setAttribute('role', 'presentation');
645        li.innerHTML = '<a class="nav-link" id="' + tabId + '" href="#' + panelId + '"'
646            + ' role="tab" aria-controls="' + panelId + '" aria-selected="false" data-toggle="tab">'
647            + escapeHtml(labelData.strings.datasheet_tab)
648            + '</a>';
649
650        const items = tabList.querySelectorAll('li');
651        const pos = labelData.tabPosition;
652
653        if (pos === 'last' || items.length === 0) {
654            tabList.appendChild(li);
655        } else {
656            const targetIdx = parseInt(pos, 10);
657            if (!isNaN(targetIdx) && targetIdx >= 1 && targetIdx <= items.length) {
658                tabList.insertBefore(li, items[targetIdx - 1]);
659            } else {
660                tabList.appendChild(li);
661            }
662        }
663
664        const panel = document.createElement('div');
665        panel.className = 'tab-pane fade';
666        panel.id = panelId;
667        panel.setAttribute('role', 'tabpanel');
668        panel.setAttribute('aria-labelledby', tabId);
669
670        
671
672        
673        var datasheetHtml = '<div class="tt-energy-tab-content">' + labelData.tabContent + '</div>';
674        if (labelData.hasDatasheet && labelData.datasheet && labelData.datasheet.url) {
675            var dlLabel = labelData.strings && labelData.strings.datasheet_dl
676                ? labelData.strings.datasheet_dl : 'Datenblatt herunterladen';
677            datasheetHtml +=
678                '<div class="tt-energy-tab-pdfviewer" data-pdf-url="' + escapeAttr(labelData.datasheet.url) + '" data-rendered="0">' +
679                '  <div class="tt-energy-modal-pdf-toolbar">' +
680                '    <div class="tt-energy-modal-pdf-nav">' +
681                '      <button type="button" class="tt-energy-modal-pdf-prev" disabled>' +
682                '        <span aria-hidden="true">&#x2039;</span> Zurück' +
683                '      </button>' +
684                '      <span class="tt-energy-modal-pdf-pageinfo">Seite 1</span>' +
685                '      <button type="button" class="tt-energy-modal-pdf-next" disabled>' +
686                '        Weiter <span aria-hidden="true">&#x203A;</span>' +
687                '      </button>' +
688                '    </div>' +
689                '    <a class="tt-energy-modal-pdf-dlbtn" href="' + escapeAttr(labelData.datasheet.url) + '" target="_blank" rel="noopener" download>' +
690                '      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="width:14px;height:14px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>' +
691                '      <span>' + escapeHtml(dlLabel) + '</span>' +
692                '    </a>' +
693                '  </div>' +
694                '  <div class="tt-energy-modal-pdf-page">' +
695                '    <div class="tt-energy-modal-pdf-loading">' +
696                '      <div class="tt-energy-modal-pdf-spinner" aria-hidden="true"></div>' +
697                '      <div>Initialisiere…</div>' +
698                '    </div>' +
699                '  </div>' +
700                '</div>';
701        }
702        panel.innerHTML = datasheetHtml;
703        panelContainer.appendChild(panel);
704
705        const link = document.getElementById(tabId);
706        link.addEventListener('click', (e) => {
707            e.preventDefault();
708            
709            tabList.querySelectorAll('.nav-link').forEach(t => {
710                t.classList.remove('active');
711                t.setAttribute('aria-selected', 'false');
712            });
713            panelContainer.querySelectorAll('.tab-pane').forEach(p => {
714                p.classList.remove('active', 'show', 'in');
715            });
716            
717            link.classList.add('active');
718            link.setAttribute('aria-selected', 'true');
719            panel.classList.add('active', 'show', 'in');
720            
721            wireDatasheetTriggers(panel, labelData);
722            
723            triggerTabPdfViewer(panel, labelData);
724        });
725
726        wireDatasheetTriggers(panel, labelData);
727
728        handledTab.add(labelData.kArtikel);
729        log('Tab eingefuegt für Artikel', labelData.kArtikel);
730    }
731
732    function triggerTabPdfViewer(panel, labelData) {
733        var viewer = panel.querySelector('.tt-energy-tab-pdfviewer[data-
733pdf-url]');
734        if (!viewer) return;
735        if (viewer.dataset.rendered === '1') return;
736        viewer.dataset.rendered = '1';
737
738        var url = viewer.dataset.pdfUrl;
739        var pageContainer = viewer.querySelector('.tt-energy-modal-pdf-page');
740        if (!pageContainer) return;
741
742        ttFrontendLog('info', 'Tab-PDF-Viewer startet', { url: url });
743        setPdfStatus(pageContainer, 'Lade PDF.js-Bibliothek…');
744
745        loadPdfJsLib().then(function (lib) {
746            setPdfStatus(pageContainer, 'PDF.js geladen, lade PDF-Datei…');
747            ttFrontendLog('info', 'Tab-PDF Step 2: fetche PDF');
748            return fetchPdfBuffer(url).then(function (buf) {
749                setPdfStatus(pageContainer, 'PDF geladen (' + Math.round(buf.byteLength/1024) + ' KB), öffne…');
750                return lib.getDocument({
751                    data: buf.slice(0),
752                    disableAutoFetch: true,
753                    disableStream:    true,
754                    isEvalSupported:  false
755                }).promise;
756            });
757        }).then(function (pdf) {
758            setPdfStatus(pageContainer, 'Rendere Seite 1 von ' + pdf.numPages + '…');
759            ttFrontendLog('info', 'Tab-PDF Step 4: rendere Seite 1', { pages: pdf.numPages });
760            var state = {
761                pdf: pdf,
762                numPages: pdf.numPages,
763                pageCache: new Map(),
764                currentPage: 1
765            };
766            pdfViewerStates.set(url, state);
767            updateViewerControls(viewer, state);
768            return renderPdfPage(state, 1, pageContainer, 1.5).then(function () {
769                ttFrontendLog('info', 'Tab-PDF: Seite 1 erfolgreich gerendert');
770                var prevBtn = viewer.querySelector('.tt-energy-modal-pdf-prev');
771                var nextBtn = viewer.querySelector('.tt-energy-modal-pdf-next');
772                if (prevBtn) prevBtn.addEventListener('click', function () {
773                    if (state.currentPage > 1) {
774                        state.currentPage--;
775                        updateViewerControls(viewer, state);
776                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
777                    }
778                });
779                if (nextBtn) nextBtn.addEventListener('click', function () {
780                    if (state.currentPage < state.numPages) {
781                        state.currentPage++;
782                        updateViewerControls(viewer, state);
783                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
784                    }
785                });
786            });
787        }).catch(function (err) {
788            var errMsg = err && err.message ? err.message : String(err);
789            ttFrontendLog('error', 'Tab-PDF-Viewer-Fehler: ' + errMsg);
790            showPdfError(pageContainer, errMsg);
791        });
792    }
793
794    function wireDatasheetTriggers(container, labelData) {
795        const triggers = container.querySelectorAll('.tt-energy-datasheet-link, .tt-btn-datasheet');
796        triggers.forEach(t => {
797            if (t.dataset.ttBound) return;
798            t.dataset.ttBound = '1';
799            t.addEventListener('click', (e) => {
800                
801                if (t.tagName === 'BUTTON') {
802                    e.preventDefault();
803                    openDatasheetModal(labelData);
804                } else if (t.tagName === 'A') {
805                    e.preventDefault();
806                    openDatasheetModal(labelData);
807                }
808            });
809        });
810    }
811
812    function processSelectors(rootElement) {
813        const context = detectContext();
814        const contextSelectors = (config.selectors[context] || []);
815        const globalSelectors  = (config.selectors['all']    || []);
816        const selectors = [...contextSelectors, ...globalSelectors];
817
818        if (selectors.length === 0) {
819            log('Keine Selektoren für Kontext', context);
820            return;
821        }
822
823        log('Verarbeite', selectors.length, 'Selektoren in Kontext', context);
824
825        selectors.forEach(sel => {
826            let elements;
827            try {
828                elements = rootElement.querySelectorAll(sel.selector);
829            } catch (err) {
830                log('Ungueltiger Selektor:', sel.selector, err);
831                return;
832            }
833
834            elements.forEach(el => {
835                if (handled.has(el)) return;
836
837                const kArtikel = findArticleId(el);
838                if (!kArtikel) {
839                    log('Keine Artikel-ID für', sel.selector, el);
840                    return;
841                }
842
843                handled.add(el);
844
845                fetchLabelData(kArtikel).then(data => {
846                    if (!data || !data.arrows || data.arrows.length === 0) return;
847
848                    if (sel.type === 'arrow') {
849                        const wrapper = document.createElement('span');
850                        wrapper.className = 'tt-energy-label-wrapper';
851                        data.arrows.forEach(arrow => {
852                            wrapper.appendChild(renderArrow(arrow, data));
853                        });
854                        insertElement(el, wrapper, sel.method);
855                    } else if (sel.type === 'datasheet') {
856                        
857                        if (data.displayModes && data.displayModes.includes('inline')) {
858                            const link = renderDatasheetLink(data);
859                            if (link) insertElement(el, link, sel.method);
860                        }
861                    }
862
863                    if (context === 'detail' && data.displayModes && data.displayModes.includes('tab')) {
864                        tryInsertTab(data);
865                    }
866                });
867            });
868        });
869    }
870
871    function insertElement(target, newElement, method) {
872        switch (method) {
873            case 'before':  target.parentNode?.insertBefore(newElement, target); break;
874            case 'after':   target.parentNode?.insertBefore(newElement, target.nextSibling); break;
875            case 'prepend': target.insertBefore(newElement, target.firstChild); break;
876            case 'append':  target.appendChild(newElement); break;
877            case 'replace': target.innerHTML = ''; target.appendChild(newElement); break;
878            default:        target.parentNode?.insertBefore(newElement, target.nextSibling);
879        }
880    }
881
882    function escapeHtml(s) {
883        return String(s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
884    }
885    function escapeAttr(s) {
886        return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
887    }
888
889    
890
891    function getDetailArticleId() {
892        
893        const urlMatch = window.location.search.match(/[?&]a=(\d+)/);
894        if (urlMatch) {
895            return parseInt(urlMatch[1], 10);
896        }
897
898        const buyInput = document.querySelector(
899            'form[id^="buy_form"] input[name="a"], form#buy_form input[name="a"], input[name="a"][type="hidden"]'
900        );
901        if (buyInput && buyInput.value) {
902            return parseInt(buyInput.value, 10);
903        }
904
905        const dataNode = document.querySelector(
906            '[data-product-id], [data-kartikel], [data-article-id]'
907        );
908        if (dataNode) {
909            const id = dataNode.dataset.productId || dataNode.dataset.kartikel || dataNode.dataset.articleId;
910            if (id) return parseInt(id, 10);
911        }
912
913        return null;
914    }
915
916    function isDetailPage() {
917        const body = document.body;
918        if (!body) return false;
919        if (body.classList.contains('product-detail') ||
920            body.classList.contains('page-product')) {
921            return true;
922        }
923        
924        if (document.querySelector('form[id^="buy_form"]')) return true;
925        
926        if (/[?&]a=\d+/.test(window.location.search)) return true;
927        return false;
928    }
929
930    function autoInjectDetail() {
931        if (!isDetailPage()) {
932            log('Auto-Detail: keine Detailseite');
933            return;
934        }
935
936        const kArtikel = getDetailArticleId();
937        if (!kArtikel) {
938            log('Auto-Detail: Keine Artikel-ID gefunden');
939            return;
940        }
941        log('Auto-Detail: kArtikel=' + kArtikel);
942
943        
944        var buyForm = document.querySelector('form[id^="buy_form"]');
945
946        const arrowCandidates = [
947            '.product-detail-price',
948            '.product-detail .price-tile',
949            '.product-detail .product-price',
950            '.price-tile.price-detail',
951            '.price-tile-price',
952            '.price-tile',
953            '.product-buy-box .price',
954            '.product-buy-box .price-wrapper',
955            '.price-wrapper .price',
956            '.price-wrapper',
957            '.product-price',
958            '.h-price',
959            'span.price',
960            '.detail-price-tile',
961            'form[id^="buy_form"] .price',
962            '#main-content .price'
963        ];
964        
965        const datasheetCandidates = [
966            '.product-attribute-list',
967            '.product-attributes',
968            '.product-details-list',
969            '.product-info-box',
970            '.product-buy-box',
971            '.product-detail .product-attributes',
972            '.product-detail h1, h1.h-product-title',
973            'h1[itemprop="name"]'
974        ];
975
976        function findFirstMatch(candidates, label) {
977            for (const sel of candidates) {
978                try {
979                    const el = document.querySelector(sel);
980                    if (el) {
981                        log('Auto-Detail: ' + label + '-Anker gefunden: ' + sel);
982                        return el;
983                    }
984                } catch (e) {}
985            }
986            log('Auto-Detail: ' + label + '-Anker NICHT gefunden, getestet:', candidates);
987            return null;
988        }
989
990        let arrowAnchor = findFirstMatch(arrowCandidates, 'Pfeil');
991        let dsAnchor    = findFirstMatch(datasheetCandidates, 'Datenblatt');
992
993        if (!arrowAnchor) {
994            arrowAnchor = document.querySelector('h1[itemprop="name"], .product-detail h1, main h1, article h1');
995            if (arrowAnchor) {
996                log('Auto-Detail: Fallback auf H1', arrowAnchor);
997            }
998        }
999
1000        if (!arrowAnchor && buyForm) {
1001            arrowAnchor = buyForm;
1002            log('Auto-Detail: Fallback auf buy_form', buyForm);
1003        }
1004
1005        if (!arrowAnchor) {
1006            arrowAnchor = document.querySelector('h1, h2');
1007            if (arrowAnchor) {
1008                log('Auto-Detail: letzter Fallback auf erstes Heading', arrowAnchor);
1009            }
1010        }
1011
1012        if (!arrowAnchor && !dsAnchor) {
1013            warn('Auto-Detail: kein Anker auf der Detailseite gefunden. Selektoren-Tab pruefen.');
1014            return;
1015        }
1016
1017        fetchLabelData(kArtikel).then(data => {
1018            if (!data) {
1019                warn('Auto-Detail: AJAX gab null zurueck für kArtikel=' + kArtikel + '. Mglw. ist Plugin-Frontend-Endpoint nicht erreichbar oder Artikel hat keine Energielabel-Daten/Demo-Konfiguration.');
1020                return;
1021            }
1022            if (!data.arrows || data.arrows.length === 0) {
1023                warn('Auto-Detail: keine Pfeil-Daten für kArtikel=' + kArtikel + '. Demo-Eintrag in xplugin_tt_energy_label_demo pruefen.');
1024                return;
1025            }
1026            log('Auto-Detail: Daten erhalten, Pfeile=' + data.arrows.length, data);
1027
1028            if (arrowAnchor && !handled.has(arrowAnchor) &&
1029                !arrowAnchor.parentNode.querySelector('.tt-energy-label-wrapper')) {
1030                handled.add(arrowAnchor);
1031                const wrapper = document.createElement('span');
1032                wrapper.className = 'tt-energy-label-wrapper tt-energy-auto';
1033                data.arrows.forEach(arrow => {
1034                    wrapper.appendChild(renderArrow(arrow, data));
1035                });
1036                arrowAnchor.parentNode.insertBefore(wrapper, arrowAnchor.nextSibling);
1037                log('Auto-Detail: Pfeil eingefuegt nach', arrowAnchor);
1038            }
1039
1040            if (dsAnchor && data.displayModes && data.displayModes.includes('inline') &&
1041                !handled.has(dsAnchor) &&
1042                !dsAnchor.parentNode.querySelector('.tt-energy-datasheet-link.tt-energy-auto')) {
1043                handled.add(dsAnchor);
1044                const link = renderDatasheetLink(data);
1045                if (link) {
1046                    link.classList.add('tt-energy-auto');
1047                    dsAnchor.parentNode.insertBefore(link, dsAnchor.nextSibling);
1048                    log('Auto-Detail: Datenblatt-Link eingefuegt');
1049                }
1050            }
1051
1052            if (data.displayModes && data.displayModes.includes('tab')) {
1053                tryInsertTab(data);
1054            }
1055        });
1056    }
1057
1058    function init() {
1059
1060        
1061        
1062        log('Init - JS-Modus: nur Klick-Handler, keine DOM-Manipulation');
1063
1064        initModalTabSwitcher();
1065        initBackendRenderedPdfViewers();
1066    }
1067
1068    function initBackendRenderedPdfViewers() {
1069
1070        ttFrontendLog('info', 'energy_label.js Init - suche Backend-PDF-Viewer im DOM');
1071
1072        function findReadyViewers() {
1073            var all = document.querySelectorAll('.tt-energy-tab-pdfviewer[data-pdf-url][data-rendered="0"]');
1074            var ready = [];
1075            for (var i = 0; i < all.length; i++) {
1076                if (all[i].offsetParent !== null) {  
1077                    ready.push(all[i]);
1078                }
1079            }
1080            return ready;
1081        }
1082
1083        document.addEventListener('click', function (e) {
1084            var target = e.target;
1085            while (target && target !== document) {
1086                if (target.classList && target.classList.contains('nav-link')) {
1087                    
1088                    setTimeout(function () {
1089                        var ready = findReadyViewers();
1090                        ttFrontendLog('info', 'Tab-Click: ' + ready.length + ' bereite Viewer gefunden');
1091                        for (var i = 0; i < ready.length; i++) triggerBackendPdfViewer(ready[i]);
1092                    }, 100);
1093                    return;
1094                }
1095                target = target.parentNode;
1096            }
1097        }, true);
1098
1099        document.addEventListener('shown.bs.modal', function () {
1100            setTimeout(function () {
1101                var ready = findReadyViewers();
1102                ttFrontendLog('info', 'Modal-Shown: ' + ready.length + ' bereite Viewer gefunden');
1103                for (var i = 0; i < ready.length; i++) triggerBackendPdfViewer(ready[i]);
1104            }, 100);
1105        }, true);
1106
1107        var initial = findReadyViewers();
1108        ttFrontendLog('info', 'Initial-Scan: ' + initial.length + ' bereite Viewer gefunden');
1109        for (var i = 0; i < initial.length; i++) triggerBackendPdfViewer(initial[i]);
1110
1111        
1112
1113        
1114        var pollCount = 0;
1115        var pollInterval = setInterval(function () {
1116            pollCount++;
1117            var anyViewer = document.querySelector('.tt-energy-tab-pdfviewer[data-pdf-url][data-rendered="0"]');
1118            if (!anyViewer) {
1119                
1120                clearInterval(pollInterval);
1121                return;
1122            }
1123            var ready = findReadyViewers();
1124            if (ready.length > 0) {
1125                
1126                ttFrontendLog('info', 'Polling #' + pollCount + ': ' + ready.length + ' bereite Viewer gefunden');
1127                for (var i = 0; i < ready.length; i++) triggerBackendPdfViewer(ready[i]);
1128            }
1129            
1130            if (pollCount >= 20) clearInterval(pollInterval);
1131        }, 500);
1132    }
1133
1134    function triggerBackendPdfViewer(viewer) {
1135        if (!viewer || viewer.dataset.rendered === '1') return;
1136        viewer.dataset.rendered = '1';
1137
1138        var url = viewer.dataset.pdfUrl;
1139        var pageContainer = viewer.querySelector('.tt-energy-modal-pdf-page');
1140        if (!pageContainer) {
1141            ttFrontendLog('warn', 'Backend-PDF-Viewer ohne page-Container');
1142            return;
1143        }
1144
1145        ttFrontendLog('info', 'Backend-PDF-Viewer startet', { url: url });
1146        setPdfStatus(pageContainer, 'Lade PDF.js-Bibliothek…');
1147
1148        loadPdfJsLib().then(function (lib) {
1149            setPdfStatus(pageContainer, 'PDF.js geladen, lade PDF-Datei…');
1150            return fetchPdfBuffer(url).then(function (buf) {
1151                setPdfStatus(pageContainer, 'PDF geladen (' + Math.round(buf.byteLength/1024) + ' KB), öffne…');
1152                return lib.getDocument({
1153                    data: buf.slice(0),
1154                    disableAutoFetch: true,
1155                    disableStream:    true,
1156                    isEvalSupported:  false
1157                }).promise;
1158            });
1159        }).then(function (pdf) {
1160            setPdfStatus(pageContainer, 'Rendere Seite 1 von ' + pdf.numPages + '…');
1161            ttFrontendLog('info', 'Backend-PDF: rendere Seite 1', { pages: pdf.numPages });
1162            var state = {
1163                pdf: pdf,
1164                numPages: pdf.numPages,
1165                pageCache: new Map(),
1166                currentPage: 1
1167            };
1168            pdfViewerStates.set(url, state);
1169            updateViewerControls(viewer, state);
1170            return renderPdfPage(state, 1, pageContainer, 1.5).then(function () {
1171                ttFrontendLog('info', 'Backend-PDF: Seite 1 erfolgreich gerendert');
1172                var prevBtn = viewer.querySelector('.tt-energy-modal-pdf-prev');
1173                var nextBtn = viewer.querySelector('.tt-energy-modal-pdf-next');
1174                if (prevBtn) prevBtn.addEventListener('click', function () {
1175                    if (state.currentPage > 1) {
1176                        state.currentPage--;
1177                        updateViewerControls(viewer, state);
1178                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
1179                    }
1180                });
1181                if (nextBtn) nextBtn.addEventListener('click', function () {
1182                    if (state.currentPage < state.numPages) {
1183                        state.currentPage++;
1184                        updateViewerControls(viewer, state);
1185                        renderPdfPage(state, state.currentPage, pageContainer, 1.5);
1186                    }
1187                });
1188            });
1189        }).catch(function (err) {
1190            var errMsg = err && err.message ? err.message : String(err);
1191            ttFrontendLog('error', 'Backend-PDF-Viewer-Fehler: ' + errMsg);
1192            showPdfError(pageContainer, errMsg);
1193        });
1194    }
1195
1196    function initModalTabSwitcher() {
1197        document.addEventListener('click', function(e) {
1198            
1199            var arrow    = e.target.closest('.tt-energy-arrow');
1200            var caption  = e.target.closest('.tt-energy-arrow-caption');
1201            var dsLink   = e.target.closest('.tt-energy-datasheet-link');
1202            if (!arrow && !caption && !dsLink) return;
1203
1204            var trigger = dsLink || caption || arrow;
1205
1206            var modalId = trigger.getAttribute('data-tt-modal-id') || '';
1207            if (!modalId) {
1208                var href = trigger.getAttribute('data-target')
1209                        || trigger.getAttribute('data-bs-target')
1210                        || trigger.getAttribute('href') || '';
1211                if (href.charAt(0) === '#') modalId = href.substring(1);
1212            }
1213            if (!modalId) return;
1214
1215            var modal = document.getElementById(modalId);
1216
1217            var targetTab = arrow && !caption && !dsLink ? 'label' : 'sheet';
1218
1219            if (!modal) {
1220
1221                if (dsLink) return;  
1222                e.preventDefault();
1223                return;
1224            }
1225
1226            
1227            if (dsLink) {
1228                e.preventDefault();
1229
1230                
1231                openModal(modalId);
1232            }
1233
1234            
1235            setTimeout(function() {
1236                activateModalTab(modalId, targetTab);
1237            }, 30);
1238        });
1239    }
1240
1241    function openModal(modalId) {
1242        var modal = document.getElementById(modalId);
1243        if (!modal) return;
1244        
1245        if (window.bootstrap && typeof window.bootstrap.Modal === 'function') {
1246            try { window.bootstrap.Modal.getOrCreateInstance(modal).show(); return; } catch (e) {}
1247        }
1248        
1249        if (window.jQuery && typeof window.jQuery.fn.modal === 'function') {
1250            try { window.jQuery(modal).modal('show'); return; } catch (e) {}
1251        }
1252        
1253        modal.classList.add('show');
1254        modal.style.display = 'block';
1255        modal.removeAttribute('aria-hidden');
1256        modal.setAttribute('aria-modal', 'true');
1257        document.body.classList.add('modal-open');
1258    }
1259
1260    function activateModalTab(modalId, which) {
1261        
1262        var tabBtn  = document.getElementById(modalId + '-tab-' + which);
1263        var tabPane = document.getElementById(modalId + '-pane-' + which);
1264        if (!tabBtn || !tabPane) return;  
1265
1266        if (window.bootstrap && typeof window.bootstrap.Tab === 'function') {
1267            try { new window.bootstrap.Tab(tabBtn).show(); return; } catch (e) {}
1268        }
1269        
1270        if (window.jQuery && typeof window.jQuery.fn.tab === 'function') {
1271            try { window.jQuery(tabBtn).tab('show'); return; } catch (e) {}
1272        }
1273        
1274        var modal = document.getElementById(modalId);
1275        if (!modal) return;
1276        var tabs  = modal.querySelectorAll('.nav-link');
1277        var panes = modal.querySelectorAll('.tab-pane');
1278        for (var i = 0; i < tabs.length; i++) {
1279            tabs[i].classList.remove('active');
1280            tabs[i].setAttribute('aria-selected', 'false');
1281        }
1282        for (var j = 0; j < panes.length; j++) {
1283            panes[j].classList.remove('active', 'show');
1284        }
1285        tabBtn.classList.add('active');
1286        tabBtn.setAttribute('aria-selected', 'true');
1287        tabPane.classList.add('active', 'show');
1288    }
1289
1290    if (document.readyState === 'loading') {
1291        document.addEventListener('DOMContentLoaded', init);
1292    } else {
1293        init();
1294    }
1295})();

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.