PageSourceSearch

https://s-mobile-versicherungsmakler.de/web-app/js/min/classes.min…6b1028971ca2476c0b92d195922a1a32a4401781.js

js s-mobile-versicherungsmakler.de collected 2026-10-06 09:31:00 UTC 312,621 bytes, 9,646 lines download raw bytes

1class Utilities {
2    constructor() {}
3
4    closestAll(el, selector) {
5        const results = [];
6        let node = el?.parentElement || null;
7        while (node) {
8            if (node.matches(selector)) results.push(node);
9            node = node.parentElement;
10        }
11        return results; // [] wenn nichts gefunden
12    }
13
14    appendParamsToUrl(url, params) {
15        const urlObj = new URL(url, window.location.origin);
16        const searchParams = urlObj.searchParams;
17
18        Object.entries(params).forEach(([key, value]) => {
19            searchParams.set(key, value);
20        });
21
22        urlObj.search = searchParams.toString();
23
24        if (!/^(?:[a-z+]+:)?\/\//i.test(url)) {
25            return urlObj.pathname + (urlObj.search ? "?" + urlObj.search : "");
26        }
27
28        return urlObj.toString();
29    }
30
31    prev(el, selector) {
32        const p = el.previousElementSibling;
33        return (p && (!selector || p.matches(selector))) ? p : null;
34    }
35
36    escapeHtmlAttr(str) {
37        if (str == null) return "";
38        return String(str)
39            .replace(/&/g, "&")
40            .replace(/"/g, """)
41            .replace(/'/g, "'")
42            .replace(/</g, "&lt;")
43            .replace(/>/g, "&gt;");
44    }
45
46    escapeStr(string = '') {
47        const newDiv = document?.createElement("div");
48        const newContent = document?.createTextNode(string);
49        newDiv?.appendChild(newContent);
50
51        return newDiv?.innerHTML;
52    }
53
54    /**
55     *
56     * @param objectNames
57     * @return {*}
58     */
59    getTextFromWindowVars(objectNames = '') {
60        let text = '';
61
62        try {
63            if (objectNames.length) {
64                const properties = objectNames.split('.');
65                if (Array.isArray(properties) && properties.length > 0) {
66                    let result = window;
67                    for (const prop of properties) {
68                        if (result.hasOwnProperty(prop)) {
69                            result = result[prop];
70                        } else {
71                            throw new Error(`Property "${prop}" not found`);
72                        }
73                    }
74                    text = result;
75                } else {
76                    if (window.hasOwnProperty(objectNames)) {
77                        text = window[objectNames];
78                    } else {
79                        throw new Error(`Property "${objectNames}" not found`);
80                    }
81                }
82            }
83        } catch (error) {
84            throw new Error(`Error in getTextFromWindowVars: ${error.message}`);
85        }
86        return this.escapeStr(text);
87    }
88}
89
90class Validate
91{
92    constructor(form = null, options = {}) {
93        // Singleton-Instanz
94        if (Validate.instance) {
95            return Validate.instance;
96        }
97
98        this.defaultOptions = {
99            dateFormat          : 'DD.MM.YYYY',
100            dateTimeFormat      : 'DD.MM.YYYY HH:mm',
101            exclude             : ['customError', 'valid'],
102            customErrors        : ['equalto', 'dateTimeFormat', 'dateFormat', 'dateNoFuture', 'ibanTest'],
103            requiredElements    : '[required], [data-ibantest]',
104            toggleElements      : 'input[type="radio"], input[type="checkbox"], select',
105            requiredClass       : 'js-initial-required',
106            invalidClass        : 'form-control-danger',
107            warningClass        : 'form-control-warning',
108            groupClass          : 'is-group',
109            header              : '.site-header',
110            messages: {
111                badInput        : 'badInputMessage',
112                patternMismatch : 'patternMessage',
113                rangeOverflow   : 'overflowMessage',
114                rangeUnderflow  : 'underflowMessage',
115                stepMismatch    : 'stepMissmatchMessage',
116                tooLong         : 'tooLongMessage',
117                tooShort        : 'tooShortMessage',
118                typeMismatch    : 'typeMessage',
119                valueMissing    : 'requiredMessage',
120                equalto         : 'equaltoMessage',
121                dateFormat      : 'dateFormatMessage',
122                dateTimeFormat  : 'dateTimeFormatMessage',
123                dateNoFuture    : 'dateNoFutureMessage',
124                ibanTest        : 'ibanMessage',
125            }
126        }
127        this.options = options;
128        this.ibanFormats = {
129            DE: '^DE\\d{2}[ ]\\d{4}[ ]\\d{4}[ ]\\d{4}[ ]\\d{4}[ ]\\d{2}|DE\\d{20}$',
130            IT: '^IT\\d{2}[A-Z]\\d{22}$',
131            CY: 'CY\\d{10}[0-9A-Z]{16}$',
132            NL: 'NL\\d{2}[A-Z]{4}\\d{10}$',
133            LV: 'LV\\d{2}[A-Z]{4}\\d{13}$',
134            BG: 'BG\\d{2}[A-Z]{4}\\d{14}$',
135            GB: 'GB\\d{2}[A-Z]{4}\\d{14}$',
136            IE: 'IE\\d{2}[A-Z]{4}\\d{14}$',
137            RO: 'RO\\d{2}[A-Z]{4}[0-9A-Z]{16}$',
138            SE: 'SE\\d{22}$',
139            SI: 'SI\\d{17}$',
140            SK: 'SK\\d{22}$',
141            KW: 'KW\\d{2}[A-Z]{4}\\d{22}$',
142            MT: 'MT\\d{2}[A-Z]{4}\\d{5}[0-9A-Z]{18}$',
143            NO: 'NO\\d{13}$',
144            DK: 'DK\\d{16}$',
145            FI: 'FI\\d{16}$',
146            AT: 'AT\\d{18}$',
147            EE: 'EE\\d{18}$',
148            LU: 'LU\\d{18}$',
149            HR: 'HR\\d{19}$',
150            LI: 'LI\\d{19}$',
151            CH: 'CH(\\d{19}|\\d{18}[A-Z]{1})$',
152            LT: 'LT\\d{18}$',
153            CZ: 'CZ\\d{22}$',
154            ES: 'ES\\d{22}$',
155            PT: 'PT\\d{23}$',
156            IS: 'IS\\d{24}$',
157            BE: 'BE\\d{14}$',
158            FR: 'FR\\d{25}$',
159            PL: 'PL\\d{26}$',
160            HU: 'HU\\d{26}$',
161            GR: 'GR\\d{9}[0-9A-Z]{16}$',
162        }
163        this.settings = Object.assign({}, this.defaultOptions, this.options);
164
165        this.form = form;
166        this.header = document.querySelector(this.settings.header);
167        this.toggleElements = this.form.querySelectorAll(this.settings.toggleElements);
168        this.requiredElements = this.form.querySelectorAll(this.settings.requiredElements);
169
170        Validate.instance = this;
171    }
172
173    isElementVisible(element) {
174        if (!element) return false;
175
176        const style = window.getComputedStyle(element);
177        if (style.display === 'none' || style.visibility === 'hidden') return false;
178
179        const rect = element.getBoundingClientRect();
180        const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
181        const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
182
183        return (
184            rect.bottom > 0 &&
185            rect.right > 0 &&
186            rect.top < viewportHeight &&
187            rect.left < viewportWidth &&
188            (rect.height > 0 || rect.width > 0)
189        );
190    }
191
192    matchMessages(validityState, element) {
193        const messages = this.settings.messages;
194
195        for (let key in validityState) {
196            if (messages.hasOwnProperty(key)
197                && this.settings.exclude.indexOf(key) === -1
198                && validityState[key] === true) {
199                return element.dataset[messages[key]];
200            }
201        }
202        return '';
203    }
204
205    toggleRequired(element, duration) {
206        const setDuration = duration || 0;
207
208        setTimeout(() => {
209            if (this.isElementVisible(element) && element.classList.contains(this.settings.requiredClass)) {
210                if ((element.type === 'checkbox' || element.type === 'radio') && element.closest('.' + this.settings.groupClass)) {
211                    const groupName = element.getAttribute('name');
212                    const checkedInputs = document.querySelectorAll('[name="' + groupName + '"]:checked');
213                    if (checkedInputs.length > 0) {
214                        return false;
215                    }
216                }
217                element.required = true;
218                element.classList.remove(this.settings.requiredClass);
219            }
220            if (!this.isElementVisible(element) && element.required) {
221                element.required = false;
222                element.classList.add(this.settings.requiredClass);
223                element.checkValidity();
224            }
225        }, setDuration);
226
227        return true;
228    }
229
230    toggleInvalidClass(element) {
231        let settingClass = this.settings.warningClass;
232
233        setTimeout(() => {
234            element.classList.remove(this.settings.warningClass, this.settings.invalidClass);
235
236            if (!element.validity.valid && this.getFirstInvalidElement() === element) {
237                if (element.validity.valueMissing === true) {
238                    settingClass = this.settings.invalidClass;
239                }
240                if (element.closest('.' + this.settings.groupClass)) {
241                    element.closest('.' + this.settings.groupClass).classList.add(settingClass);
242                } else {
243                    element.classList.add(settingClass);
244                }
245            }
246
247            if (element.validity.valid && this.isElementVisible(element) && element.closest('.' + this.settings.groupClass)) {
248                element.closest('.' + this.settings.groupClass).classList.remove(this.settings.warningClass, this.settings.invalidClass);
249            }
250        }, 50);
251
252        return true;
253    }
254
255    getValidators(type, element) {
256        const validators = {
257            dateFormat: (element) => {
258                const moment = window.moment;
259                if (element.dataset.requiredDateFormat !== undefined && !element.validity.valueMissing) {
260                    const testDate = moment(element.value.trim(), this.settings.dateFormat, true);
261
262                    element.validity.dateFormat = !testDate.isValid();
263                    return element.validity.dateFormat;
264                }
265                return true;
266            },
267            dateTimeFormat: (element) => {
268                const moment = window.moment;
269                if (element.dataset.requiredDateTimeFormat !== undefined && !element.validity.valueMissing) {
270                    const testDate = moment(element.value.trim(), this.settings.dateFormat, true);
271                    const testDateTime = moment(element.value.trim(), this.settings.dateTimeFormat, true);
272
273                    element.validity.dateTimeFormat = !(testDate.isValid() || testDateTime.isValid());
274                    return element.validity.dateTimeFormat;
275                }
276                return true;
277            },
278            dateNoFuture: (element) => {
279                const moment = window.moment;
280                if (element.dataset.dateNoFuture !== undefined && !element.validity.valueMissing) {
281                    const testDate = moment(element.value.trim(), this.settings.dateFormat, true);
282                    const testDateTime = moment(element.value.trim(), this.settings.dateTimeFormat, true);
283                    const noFutureDate = moment(element.value.trim(), this.settings.dateFormat, true).diff(moment.now());
284                    const noFutureDateTime = moment(element.value.trim(), this.settings.dateTimeFormat, true).diff(moment.now());
285
286                    element.validity.dateNoFuture = !((testDate.isValid() && noFutureDate <= 0) ||
287                        (testDateTime.isValid()) && noFutureDateTime <= 0);
288                    return element.validity.dateNoFuture;
289                }
290                return true;
291            },
292            iban: (element) => {
293                let ibanAjaxValid = true;
294
295                if (element.dataset.ibantest !== undefined) {
296                    if (element.dataset.ibantest === 'optional' && (element.validity.valueMissing || !element.value.length)) {
297                        element.validity.ibanTest = false;
298                        return element.validity.ibanTest;
299                    }
300                }
301
302                if (element.dataset.ibantest !== undefined && !element.validity.valueMissing) {
303                    let format = () => {
304                            if (element.dataset.ibanFormat !== undefined && this.ibanFormats[element.dataset.ibanFormat] !== undefined) {
305                                return new RegExp(this.ibanFormats[element.dataset.ibanFormat], 'gi');
306                            }
307                            return new RegExp(this.ibanFormats['DE'], 'gi');
308                        },
309                        newIban = element.value.toUpperCase(),
310                        modulo = (dividend, divisor) => {
311                            let cDividend = '',
312                                cRest = '';
313
314                            for (let i in dividend) {
315                                let cChar = dividend[i],
316                                    cOperator = cRest + '' + cDividend + '' + cChar;
317
318                                if (cOperator < parseInt(divisor)) {
319                                    cDividend += '' + cChar;
320                                } else {
321                                    cRest = cOperator % divisor;
322                                    if (cRest === 0) {
323                                        cRest = '';
324                                    }
325                                    cDividend = '';
326                                }
327                            }
328                            cRest += '' + cDividend;
329                            if (cRest === '') {
330                                cRest = 0;
331                            }
332                            return cRest;
333                        };
334
335                    if (newIban.search(format()) < 0) {
336                        element.validity.ibanTest = true;
337                        return element.validity.ibanTest;
338                    }
339
340                    newIban = newIban.substring(4) + newIban.substring(0, 4);
341
342                    newIban = newIban.replace(/[A-Z]/g, function(match) {
343                        return match.charCodeAt(0) - 55;
344                    });
345
346                    if (element.dataset.getBank !== undefined) {
347                        ibanAjaxValid = false;
348                        if (element.dataset.isValidBank !== undefined) {
349                            ibanAjaxValid = true;
350                        }
351                    }
352                    element.validity.ibanTest = parseInt(modulo(newIban, 97), 10) !== 1 && ibanAjaxValid === false;
353                    return element.validity.ibanTest;
354                }
355                return true;
356            },
357            equalTo: (element) => {
358                if (element.dataset.equalto !== undefined && !element.validity.valueMissing) {
359                    element.validity.equalto = element.value !== document.querySelector(element.dataset.equalto).value;
360                }
361                return element.validity.equalto;
362            },
363            groups: (element) => {
364                let moreThanOneChecked = false;
365
366                const elementName = element.name;
367                const control = (element, elementName) => {
368                    moreThanOneChecked = element.closest('.' + this.settings.groupClass).querySelectorAll('input[name="' + elementName + '"]:checked').length;
369                    document.querySelectorAll('[name="' + elementName + '"]').forEach(item => {
370                        if (moreThanOneChecked) {
371                            if (item.required) {
372                                item.required = false;
373                                item.classList.add(this.settings.requiredClass);
374                            }
375                            setTimeout(() => {
376                                item.checkValidity();
377                            },250);
378                        } else {
379                            if (item.classList.contains(this.settings.requiredClass)) {
380                                item.required = true;
381                                item.classList.remove(this.settings.requiredClass);
382                            }
383                        }
384                    });
385                };
386
387                if (element.closest('.' + this.settings.groupClass) && ['checkbox', 'radio'].includes(element.type)) {
388                    document.querySelectorAll('[name="' + elementName + '"]:not(.js-init-required-change-hook)').forEach((item) => {
389                        item.addEventListener('change', () => {
390                            control(element, elementName);
391                        });
392                        item.classList.add('js-init-required-change-hook');
393                    });
394                    control(element, elementName);
395                }
396                return moreThanOneChecked;
397            },
398            setCustomValidityStates: (element) => {
399                Object.keys(this.settings.customErrors).forEach(index => {
400                    element.validity[this.settings.customErrors[index]] = false;
401                });
402            }
403        };
404
405        return validators[type](element);
406    }
407
408    observe() {
409        const isFormElement = (element) => {
410            return ['INPUT', 'SELECT', 'TEXTAREA'].includes(element.tagName);
411        };
412
413        const observer = new MutationObserver((mutations) => {
414            mutations.forEach((mutation) => {
415                if ((mutation.target === this.form || this.form.contains(mutation.target)) && !isFormElement(mutation.target)) {
416                    this.form.querySelectorAll('[required], .' + this.settings.requiredClass).forEach((element) => {
417                        this.toggleRequired(element, 0);
418                    });
419                }
420            });
421        });
422
423        const config = {
424            attributes: true,
425            childList: true,
426            subtree: true
427        };
428
429        observer.observe(this.form, config);
430    }
431
432    getFirstInvalidElement() {
433        return this.form.querySelectorAll(':invalid')[0] || null;
434    }
435
436    validate() {
437        let isValidating = false; // Flag variable
438
439        const validateHandler = (event) => {
440            if (isValidating) return; // Return if already validating
441            isValidating = true;
442
443            let customError = false;
444            let settedMessage = '';
445            const thatElement = (event instanceof Event)
446                ? event.target
447                : event;
448
449            this.getValidators('setCustomValidityStates', thatElement);
450            this.getValidators('iban', thatElement);
451            this.getValidators('dateNoFuture', thatElement);
452            this.getValidators('dateFormat', thatElement);
453            this.getValidators('dateTimeFormat', thatElement);
454            this.getValidators('equalTo', thatElement);
455            this.getValidators('groups', thatElement);
456
457            this.settings.customErrors.forEach((error, index) => {
458                customError = false;
459                if (typeof thatElement.validity[this.settings.customErrors[index]] !== 'undefined' &&
460                    thatElement.validity[this.settings.customErrors[index]]) {
461                    customError = true;
462                    return false;
463                }
464            });
465
466            this.toggleInvalidClass(thatElement);
467
468            thatElement.setCustomValidity('');
469            settedMessage = this.matchMessages(thatElement.validity, thatElement);
470
471            if ((!thatElement.validity.valid || customError) &&
472                typeof settedMessage !== 'undefined') {
473                thatElement.setCustomValidity(settedMessage);
474                thatElement.reportValidity();
475            }
476
477            isValidating = false; // Reset flag after validation
478        }
479
480        this.requiredElements.forEach(element => {
481            this.toggleRequired(element, 500);
482            this.getValidators('groups', element);
483        });
484
485        this.toggleElements.forEach((element) => {
486            element.addEventListener('change', (event) => {
487                this.form.querySelectorAll('[required], .' + this.settings.requiredClass).forEach((element) => {
488                    this.toggleRequired(element, 0);
489                });
490            });
491        });
492
493        ['input', 'invalid'].forEach((eventType, index) => {
494            this.form.querySelectorAll('input, textarea, select').forEach((element) => {
495                element.addEventListener(eventType, (event) => {
496                    validateHandler(event);
497                });
498            });
499        });
500
501        this.form.addEventListener('submit', () => {
502            this.form.querySelectorAll('input, textarea, select').forEach((element) => {
503                validateHandler(element);
504            });
505        });
506
507        this.observe();
508    }
509}
510
511class FormHandler {
512    constructor(form, options = {}, callbacks = []) {
513        if (!(form instanceof HTMLFormElement)) {
514            throw new Error('Provided element is not a valid form.');
515        }
516
517        // Formular und Variablen registrieren
518        this.form = form;
519        this.formSubmitBtn = this.form.querySelector('[type="submit"]');
520        this.clearOnSuccess = this.form.hasAttribute('data-clear-on-success');
521        this.showLoaderInButton = this.form.hasAttribute('data-show-button-loader');
522        this.noneClass = 'd-none';
523
524        // Standardoptionen festlegen
525        this.defaultOptions = {
526            mode: 'cors',
527            credentials: 'include',
528            ...options,
529        };
530
531        // Callbacks validieren und speichern
532        if (!Array.isArray(callbacks)) {
533            throw new Error('Callbacks must be an array of functions.');
534        }
535        this.callbacks = callbacks.filter(function (cb) {
536            return typeof cb === 'function';
537        });
538
539        // Ereignis für Submit binden
540        this.form.addEventListener('submit', this.handleSubmit.bind(this));
541    }
542
543    /**
544     * Erstellt die HTML-Struktur für eine Nachricht.
545     * @param {string} message - Die anzuzeigende Nachricht.
546     * @param {string} messageType - Der Typ der Nachricht (success|warning|danger).
547     * @returns {string}
548     */
549    buildMessageHtml(message, messageType) {
550        if (typeof message !== 'string' || message.trim() === '') {
551            throw new Error('Invalid message parameter in buildMessageHtml!');
552        }
553
554        if (typeof messageType !== 'string' || messageType.trim() === '') {
555            throw new Error('Invalid messageType parameter in buildMessageHtml!');
556        }
557
558        const role = messageType === 'success' ? 'polite' : 'assertive';
559
560        return `<div class="text-start alert alert-${messageType}
560 mw-100 w-100 lsg" role="status" aria-live="${role}" id="resMessage">${message}</div>`;
561    }
562
563    /**
564     * Zeigt eine Antwortnachricht im Formular an.
565     * @param {Object} response - Die Antwortdaten des Servers.
566     * @param {string} messageType - Der Typ der Nachricht.
567     * @param {number} timeout - Die Zeit in Millisekunden, nach der die Nachricht verschwindet.
568     */
569    renderResponseMessage(response, messageType, timeout = 3000) {
570        if (!this.form || !(this.form instanceof HTMLElement)) {
571            throw new Error('Invalid form reference in renderResponseMessage!');
572        }
573
574        if (!response || typeof response !== 'object') {
575            throw new Error('Invalid response parameter in renderResponseMessage!');
576        }
577
578        if (typeof response.message !== 'string' || response.message.trim() === '') {
579            console.warn('No valid message found in response object.');
580            return;
581        }
582
583        const existingMessage = document.getElementById('resMessage');
584        if (existingMessage) {
585            existingMessage.remove();
586        }
587
588        const messageHtml = this.buildMessageHtml(response.message, messageType);
589        this.form.insertAdjacentHTML('afterbegin', messageHtml);
590
591        const htmlMessage = document.getElementById('resMessage');
592        if (htmlMessage && timeout > 0) {
593            setTimeout(function () {
594                if (htmlMessage && htmlMessage.parentNode) {
595                    htmlMessage.remove();
596                }
597            }, timeout);
598        }
599    }
600
601    /**
602     * Aktiviert oder deaktiviert den Ladezustand des Buttons.
603     */
604    toggleLoaderInButton() {
605        if (this.formSubmitBtn) {
606            const spinner = this.formSubmitBtn.querySelector('.spinner-border');
607
608            if (spinner) {
609                if (!this.formSubmitBtn.disabled && spinner.classList.contains(this.noneClass)) {
610                    this.formSubmitBtn.disabled = true;
611                    spinner.classList.remove(this.noneClass);
612                } else {
613                    this.formSubmitBtn.disabled = false;
614                    spinner.classList.add(this.noneClass);
615                }
616            }
617        }
618    }
619
620    /**
621     * Behandelt das Submit-Ereignis des Formulars.
622     * @param {Event} event - Das Submit-Ereignis.
623     */
624    handleSubmit(event) {
625        event.preventDefault();
626
627        const htmlMessage = document.getElementById('resMessage');
628        if (htmlMessage) {
629            htmlMessage.remove();
630        }
631
632        if (this.showLoaderInButton) {
633            this.toggleLoaderInButton();
634        }
635
636        this.sendForm()
637            .then(function (response) {
638                if (!response || !response.ok) {
639                    throw new Error(`HTTP Error: ${response?.status}`);
640                }
641                return response.json();
642            })
643            .then(this.handleResponse.bind(this))
644            .catch(this.handleError.bind(this))
645            .finally();
646    }
647
648    /**
649     * Sendet das Formular über Fetch.
650     * @returns {Promise<Response>}
651     */
652    sendForm() {
653        const url = this.form.action;
654        const method = this.form.method.toUpperCase() || 'POST';
655        const formData = new FormData(this.form);
656
657        return fetch(url, {
658            ...this.defaultOptions,
659            method: method,
660            body: formData,
661        });
662    }
663
664    /**
665     * Behandelt die Antwort des Servers.
666     * @param {Object} response - Die JSON-Antwort des Servers.
667     */
668    handleResponse(response) {
669        if (response.redirectRoute) {
670            setTimeout(function () {
671                window.location.href = response.redirectRoute;
672            }, 3000);
673        }
674
675        if (response.reload) {
676            if (response?.message) {
677                this.renderResponseMessage(response, 'success');
678                setTimeout(() => {
679                    window.location.reload();
680                }, 3000);
681                return;
682            } else {
683                window.location.reload();
684            }
685        }
686
687        if (response.success) {
688            if (this.clearOnSuccess) {
689                this.form.reset();
690            }
691            this.renderResponseMessage(response, 'success');
692        } else {
693            this.renderResponseMessage(response, 'danger');
694        }
695
696        if (this.showLoaderInButton) {
697            this.toggleLoaderInButton();
698        }
699
700        this.executeCallbacks(response);
701    }
702
703    /**
704     * Führt alle registrierten Callbacks aus.
705     * @param {Object} response - Die Antwort des Servers.
706     */
707    executeCallbacks(response) {
708        for (const callback of this.callbacks) {
709            try {
710                callback(response);
711            } catch (error) {
712                console.error('Callback execution error:', error);
713            }
714        }
715    }
716
717    /**
718     * Behandelt Fehler bei der Anfrage.
719     * @param {Error} error - Der Fehler.
720     */
721    handleError(error) {
722        console.error('An error occurred:', error.message || 'Unknown error');
723        if (this.showLoaderInButton) {
724            this.toggleLoaderInButton();
725        }
726        this.renderResponseMessage({ message: 'An unexpected error occurred.' }, 'danger');
727    }
728}
729
730class FragmentMatcher
731{
732    constructor() {
733        this.availableKeys = {
734            openInsuranceAddLayer:               'openLayer',
735            openInsuranceAddLayerWithKontocheck: 'openLayerKC',
736            showInboxDocument:                   'inboxDocument',
737            insuranceBoxFoldout:                 'insuranceBox',
738            searchInsuranceBranch:               'insuranceBranch',
739            openPersData:                        'openPersData',
740            bankData:                            'bankData',
741            addressData:                         'addressData',
742            openQuarterlyReports:                'quarterlyReports',
743            openCostReports:                     'costReports',
744        }
745    }
746
747    /**
748     *
749     * @param key
750     * @returns {*}
751     */
752    regExpKeys(key = null) {
753        const availableFragmentsRegExp = {
754            openLayer:        new RegExp('^openInsuranceAddLayer$'),
755            openLayerKC:      new RegExp('^openLayerKC$'),
756            inboxDocument:    new RegExp('document-', 'gi'),
757            insuranceBox:     new RegExp('^insurance-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', 'i'),
758            insuranceBranch:  new RegExp('^[a-z_\\- ]*$', 'i'),
759            openPersData:     new RegExp('^openPersData$'),
760            bankData:         new RegExp('^bankData$'),
761            addressData:      new RegExp('^addressData$'),
762            quarterlyReports: new RegExp('^quarterlyReports$'),
763            costReports:      new RegExp('^costReports$'),
764        };
765
766        if (!availableFragmentsRegExp.hasOwnProperty(key)) {
767            throw `Key ${key} in class FragmentMatcher - regExpKey not exist!`;
768        }
769        return availableFragmentsRegExp[key];
770    }
771
772    /**
773     *
774     * @returns {null}
775     */
776    getFragmentValue() {
777        const url = window.location.href;
778
779        let urlObj = new URL(url);
780        let params = new URLSearchParams(urlObj.search);
781        let fragmentValue = null;
782
783        params.forEach((value, key) => {
784            if (key === 'fragment' && !value.includes('dest')) {
785                fragmentValue = value;
786            }
787        });
788
789        return fragmentValue;
790    }
791
792    /**
793     *
794     * @param objectKey
795     * @returns {*|null}
796     */
797    getSpecific(objectKey = null) {
798        const fragment = this.getFragmentValue();
799
800        if (objectKey && this.regExpKeys(objectKey).test(fragment)) {
801            return fragment;
802        }
803        return null;
804    }
805}
806
807class Alttarif
808{
809    /**
810     *
811     */
812    constructor() {
813        this.paramsData = [
814            'type',
815            'insurer',
816            'tariffgeneration',
817            'tariffname',
818            'options',
819            'actualTariff',
820            'bestTariff'
821        ];
822
823        // JS Classes
824        this.utilities = new Utilities();
825
826        this.init();
827    }
828
829    /**
830     *
831     * @param object
832     * @param key
833     * @returns {*|string}
834     */
835    getKeyFromDetails (object = {}, key = '') {
836        if (object.constructor === Object && object.hasOwnProperty(key)) {
837            return object[key];
838        }
839        return '';
840    }
841
842    /**
843     *
844     * @param url
845     * @param text
846     * @returns {string}
847     */
848    buildOnlineOfferBtn(url = null, text = '') {
849        if (!url) {
850            return ``;
851        }
852        return `<button
853            onclick="location.href = '${this.utilities.escapeStr(url)}';"
854            class="btn btn-colored-default-xl"
855            title="${text}">${text}</button>`;
856    }
857
858    /**
859     *
860     * @param filter
861     * @param insurer
862     * @param title
863     * @param buttonId
864     * @return {string}
865     */
866    buildAddInsuranceBtn(
867        filter = '',
868        insurer = '',
869        title = 'Vertrag hinzufügen',
870        buttonId = 'customAtbButtonId'
871    ) {
872        return `<button
873            type="button"
874            id="${buttonId}"
875            class="js-open-add-insurance-layer btn btn-bordered-default-xl js-gtm-event-click"
876            title="${title}"
877            data-label="${filter}"
878            data-action="add contract/start"
879            data-value="in Alttarifbewertung - Result"
880            data-filter="${filter}"
881            data-get-insurer="${insurer}"
882            data-auto-event-observed="true"
883            disabled>${title}</button>`;
884    }
885
886    /**
887     *
888     * @param insuranceAddBtn
889     * @param onlineOfferBtn
890     * @returns {string}
891     */
892    buildBtnContent(insuranceAddBtn = '', onlineOfferBtn = '') {
893        let onlineOfferButtonRenderedHTML = '';
894
895        if (onlineOfferBtn) {
896            onlineOfferButtonRenderedHTML = `<div class="col-12 col-lg text-center text-lg-end ps-lg-1">
897                    ${onlineOfferBtn ? `<small class="d-block mb-4">${this.utilities.getTextFromWindowVars('texts.atbButtonDescriptionCompare')}:</small>
898                        ${onlineOfferBtn}` : ''}
899                </div>`;
900        }
901
902        return `<div class="btn-container atb__result-container mt-0">
903                <div class="row justify-content-center justify-content-lg-between">
904                    <div class="col-12 col-lg mb-4 mb-lg-0 text-center text-lg-start pe-lg-1">
905                        <small class="d-block mb-4">${this.utilities.getTextFromWindowVars('texts.atbButtonDescriptionAddInsurance')}:</small>
906                        ${insuranceAddBtn}
907                    </div>
908                    ${onlineOfferButtonRenderedHTML}
909                </div>
910            </div>`;
911    }
912
913    // public section
914
915    /**
916     *
917     */
918    customEventCheck() {
919        document.addEventListener('custom_atb_result_event', (event) => {
920            const data = event?.detail?.trim();
921            if (data && window.hasOwnProperty('branchMap')) {
922                const appendElement = document.querySelector('.atb__addition');
923                const buttonId = `customButtonId${new Date().getTime()}`;
924                let handleObj = null;
925
926                if (window.branchMap.hasOwnProperty(data) && appendElement) {
927                    handleObj = window.branchMap[data];
928
929                    const insurerSelect = document.querySelector('.atb__form-select[name="company"]');
930                    const insurerClean = insurerSelect.options[insurerSelect.selectedIndex].text.trim() || null;
931                    const fetchUrl = document?.querySelector('.atb__base-container')?.closest('[data-fetch-url]')?.dataset.fetchUrl ?? '';
932                    const fetchData = new FormData();
933                    fetchData.set('branch', handleObj?.branch ?? null);
934
935                    fetch(fetchUrl, {
936                        method: 'POST',
937                        body: fetchData,
938                        mode: 'cors',
939                        credentials: 'include',
940                    })
941                        .then(response => response.json())
942                        .then(response => {
943                            if (response?.success) {
944                                appendElement.insertAdjacentHTML(
945                                    'afterbegin',
946                                    this.buildBtnContent(
947                                        this.buildAddInsuranceBtn(
948                                            handleObj.branch,
949                                            (window.hasOwnProperty('insurerMap') && insurerClean)
950                                                ? window.insurerMap[insurerClean] :
951                                                '',
952                                            this.utilities.getTextFromWindowVars('texts.addInsurance.insertExistingInsurance'),
953                                            buttonId
954                                        ),
955                                        this.buildOnlineOfferBtn(response?.url ?? null, response?.btnText ?? '')
956                                    )
957                                );
958
959                                const button = document.getElementById(buttonId);
960                                window.addEventListener('insurerFilled', event => {
961                                    if (button && Object.keys(event.detail).length) {
962                                        const insurerSelectLayer = event.detail.insurerSelect || null;
963                                        const insurerToSelect = button.dataset.getInsurer;
964                                        if (insurerSelectLayer) {
965                                            for (let i = 0; i < insurerSelectLayer.options.length; i++) {
966                                                if (insurerSelectLayer.options[i].text.trim() === insurerToSelect) {
967                                                    const setValue = insurerSelectLayer.options[i].value;
968                                                    setTimeout(() => {
969                                                        insurerSelectLayer.value = setValue;
970                                                        insurerSelectLayer.dispatchEvent(new Event('change'));
971                                                    }, 300);
972                                                    return;
973                                                }
974                                            }
975                                        }
976                                    }
977                                });
978
979                                new AddInsuranceLayer();
980                            }
981                        })
982                        .catch(error => {
983                            console.error(error);
984                        });
985                }
986            }
987        });
988    }
989
990    /**
991     *
992     */
993    triggerEvent() {
994        const calculatorTrackingUrl = (window.hasOwnProperty('application') && window.application.hasOwnProperty('atbTrackingUrl'))
995                ? window.application.atbTrackingUrl
996                : null;
997
998        document.addEventListener('jdcatb.event', (event) => {
999            if (typeof event.detail !== 'undefined' && calculatorTrackingUrl) {
1000                const data = new FormData();
1001
1002
1003                this.paramsData.forEach(paramName => {
1004                    data.set(paramName, this.getKeyFromDetails(event.detail, paramName));
1005                });
1006                fetch(calculatorTrackingUrl, {
1007                    method: 'POST',
1008                    body: data,
1009                    mode: 'cors',
1010                    credentials: 'include',
1011                })
1012                    .then(response => response.json())
1013                    .then(response => {
1014                        if (!response.hasOwnProperty('success') || !response.success) {
1015                            console.error('Fetch error on ATB event trigger "jdcatb.event"!');
1016                        }
1017                    })
1018                    .catch(error => {
1019                        console.error(error);
1020                    });
1021            }
1022        }, false);
1023    }
1024
1025    /**
1026     *
1027     * @returns {boolean}
1028     */
1029    init() {
1030        this.triggerEvent();
1031        this.customEventCheck();
1032        return true;
1033    }
1034}
1035
1036document.addEventListener('DOMContentLoaded', () => {
1037    new Alttarif();
1038});
1039
1040class RegisterInterestedParty
1041{
1042    constructor() {
1043        this.select = document.querySelector('select[name="customerType"]');
1044        this.privateSection = document.getElementById('sectionPrivate');
1045        this.businessSection = document.getElementById('sectionBusiness');
1046        this.hiddenClass = 'd-none';
1047        this.events = ['initial', 'change'];
1048    }
1049
1050    // private section
1051    showHideSection(event) {
1052        if (event) {
1053            event.preventDefault();
1054        }
1055        const value = this.select.value;
1056        if (value) {
1057            this.privateSection.classList.add(this.hiddenClass);
1058            this.businessSection.classList.add(this.hiddenClass);
1059            this[value + 'Section'].classList.remove(this.hiddenClass);
1060        }
1061    }
1062
1063    // public section
1064    showHideSectionAtEvent() {
1065        if (this.select && this.privateSection && this.businessSection) {
1066            this.events.forEach(eventType => {
1067                if (eventType === 'initial') {
1068                    this.showHideSection(event);
1069                } else {
1070                    this.select.addEventListener(eventType, event => {
1071                        this.showHideSection(event);
1072                    });
1073                }
1074            });
1075        }
1076    }
1077}
1078
1079const RegisterInterestedPartyClass = new RegisterInterestedParty();
1080RegisterInterestedPartyClass.showHideSectionAtEvent();
1081
1082'use strict';
1083
1084class BusinessClient
1085{
1086    constructor() {
1087        this.listInsuranceSelect = document.querySelector('.list-insurance-select');
1088        this.listBusinessInsuranceResult = document.querySelector('.list-business-insurance-result');
1089        this.bisinessSearchForm = document.getElementById('businessSearchForm');
1090        this.hideClass = 'd-none';
1091        this.activeClass = 'active';
1092        this.disabledClass = 'disabled';
1093        this.currentPage = 1;
1094        this.totalPages = 0;
1095        this.pagingUrl = '';
1096    }
1097
1098    // private section
1099
1100    /**
1101     *
1102     * @param target
1103     * @param checkTagName
1104     * @returns {boolean}
1105     */
1106    getTagName(target, checkTagName) {
1107        const tagName = target.tagName.toLowerCase();
1108        if (tagName) {
1109            return tagName === checkTagName;
1110        }
1111        return false;
1112    }
1113
1114    /**
1115     *
1116     * @param response
1117     * @param links
1118     * @param target
1119     * @param errorType
1120     * @param url
1121     */
1122    renderHtmlResult(response, links, target, errorType, url = null) {
1123        if (response.hasOwnProperty('redirectRoute')) {
1124            window.location.href = response.redirectRoute;
1125            return;
1126        }
1127
1128        if (response.hasOwnProperty('success')) {
1129            if (response.success && response.hasOwnProperty('content')) {
1130                const listElements = this.listBusinessInsuranceResult.querySelectorAll('li');
1131                if (response.hasOwnProperty('pages')) {
1132                    this.buildPageIdentifyer(response.pages, (target) ? target.dataset.ajax : url);
1133                }
1134
1135                if (listElements.length) {
1136                    listElements.forEach((element, key) => {
1137                        if (key > 0) {
1138                            element.remove();
1139                        }
1140                    });
1141
1142                    links.forEach(link => {
1143                        link.classList.remove(this.activeClass);
1144                    });
1145
1146                    if (target) {
1147                        target.classList.add(this.activeClass);
1148                        target.classList.remove(this.disabledClass);
1149                        target.closest('li').querySelector('.spinner').classList.toggle(this.hideClass);
1150
1151                        document.querySelector('.js-business-search-head').textContent = target.textContent;
1152                    } else {
1153                        document.querySelector('.js-business-search-head').textContent = 'Suchergebnis';
1154                    }
1155                    this.listBusinessInsuranceResult.innerHTML += response.content;
1156
1157                    new DocumentViewCounter();
1158
1159                    if (window.hasOwnProperty('base')) {
1160                        if (typeof window.base.initShowMoreDocs === 'function') {
1161                            window.base.initShowMoreDocs();
1162                        }
1163                    }
1164
1165                    document.querySelectorAll('.show-more-trigger').forEach(function (button) {
1166                        const checkboxId = button.dataset.checkboxId;
1167                        const checkbox = document.getElementById(checkboxId);
1168                        const contentId = button.getAttribute('aria-controls');
1169                        const content = document.getElementById(contentId);
1170                        const textSpan = button.querySelector('.button-text');
1171
1172                        if (!checkbox || !content) return;
1173
1174                        function updateState() {
1175                            const isExpanded = checkbox.checked;
1176                            button.setAttribute('aria-expanded', String(isExpanded));
1177                            content.setAttribute('aria-hidden', String(!isExpanded));
1178
1179                            const branchName = button.dataset.branchName || '';
1180                            const text = isExpanded ? button.dataset.textHide : button.dataset.textShow;
1181
1182                            if (textSpan) {
1183                                textSpan.textContent = text;
1184                            }
1185
1186                            button.setAttribute('aria-label', `${text} ${branchName}`.trim());
1187                        }
1188
1189                        checkbox.addEventListener('change', updateState);
1190
1191                        button.addEventListener('click', function () {
1192                            checkbox.checked = !checkbox.checked;
1193                            checkbox.dispatchEvent(new Event('change', { bubbles: true }));
1194                        });
1195
1196                        // Initial state
1197                        updateState();
1198                    });
1199
1200                    return;
1201                }
1202            }
1203        }
1204        if (target) {
1205            target.classList.remove(this.disabledClass);
1206            target.closest('li').querySelector('.spinner').classList.toggle(this.hideClass);
1207        }
1208    }
1209
1210    /**
1211     *
1212     * @param pages
1213     * @param url
1214     * @returns {void}
1215     */
1216    buildPageIdentifyer(pages, url) {
1217        this.pagingUrl = url;
1218        this.currentPage = 1;
1219        this.totalPages = pages;
1220    }
1221
1222    async loadMoreContent() {
1223        const totalPages = parseInt(this.totalPages);
1224        const fetchUrl = this.pagingUrl;
1225
1226        let currentPage = parseInt(this.currentPage);
1227
1228        if (currentPage < totalPages) {
1229            this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1230
1231            try {
1232                const response = await fetch(fetchUrl + '&page=' + (currentPage + 1));
1233                if (!response.ok) {
1234                    throw new Error('Fehler beim Abrufen der Daten');
1235                }
1236
1237                const newData = await response.json();
1238
1239                if (newData.hasOwnProperty('redirectRoute')) {
1240                    window.location.href = newData.redirectRoute;
1241                    return;
1242                }
1243
1244                if (newData.hasOwnProperty('success')) {
1245                    if (newData.success && newData.hasOwnProperty('content')) {
1246                        this.listBusinessInsuranceResult.innerHTML += newData.content;
1247                        currentPage++;
1248                        this.currentPage = currentPage;
1249
1250                        if (typeof $.fn.ajaxDownload === 'function') {
1251                            $('[data-role="ajaxdownload"]').each(function () {
1252                                $(this).ajaxDownload();
1253                            });
1254                        }
1255                        if (window.hasOwnProperty('base')) {
1256                            if (typeof window.base.initShowMoreDocs 
1256=== 'function') {
1257                                window.base.initShowMoreDocs();
1258                            }
1259                        }
1260                    }
1261                }
1262
1263            } catch (error) {
1264                console.error('Fehler beim Laden weiterer Inhalte:', error);
1265            } finally {
1266                this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1267            }
1268        }
1269    }
1270
1271    // public section
1272
1273    /**
1274     *
1275     * @returns {void}
1276     */
1277    getDetailsFromLink() {
1278        if (this.listInsuranceSelect) {
1279            const links = this.listInsuranceSelect.querySelectorAll('li button');
1280            const eventsToShow = ['click', 'custom_click'];
1281
1282            if (links.length) {
1283                links.forEach(link => {
1284                    eventsToShow.forEach(setEvent => {
1285                        link.addEventListener(setEvent, (event) => {
1286                            event.preventDefault();
1287                            if (this.getTagName(event.target, 'button')) {
1288                                if ((event.target.classList.contains(this.activeClass)
1289                                    || event.target.classList.contains(this.disabledClass))
1290                                    && event.type !== 'custom_click') {
1291                                    return false;
1292                                }
1293
1294                                event.target.classList.add(this.disabledClass);
1295
1296                                if (event.type !== 'custom_click') {
1297                                    event.target.closest('li').querySelector('.spinner').classList.toggle(this.hideClass);
1298                                    this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1299                                }
1300                                try {
1301                                    fetch(event.target.dataset.ajax, {
1302                                        method: 'GET',
1303                                        mode: 'cors',
1304                                        credentials: 'include',
1305                                    })
1306                                    .then(response => response.json())
1307                                    .then(response => {
1308                                        return this.renderHtmlResult(response, links, event.target, 'getDetailsFromLink');
1309                                    })
1310                                    .catch(error => {
1311                                        console.error(error);
1312                                    })
1313                                    .finally(() => {
1314                                        this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1315                                    });
1316                                } catch (error) {
1317                                    console.error(error);
1318                                    this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1319                                }
1320                            }
1321                        });
1322                    });
1323
1324                    if (link.classList.contains('active')) {
1325                        link.dispatchEvent(new Event('custom_click'));
1326                    }
1327                });
1328            }
1329        }
1330    }
1331
1332    /**
1333     *
1334     * @returns {void}
1335     */
1336    searchForm() {
1337        if (this.bisinessSearchForm) {
1338            const searchInput = this.bisinessSearchForm.querySelector('#businessSearchString');
1339            const submitBtn = this.bisinessSearchForm.querySelector('button[type="submit"]');
1340            const spinner = submitBtn.querySelector('.spinner-border');
1341            const links = this.listInsuranceSelect.querySelectorAll('li button');
1342
1343            submitBtn.addEventListener('click', event => {
1344                const fetchUri = this.bisinessSearchForm.action + '?' + new URLSearchParams({
1345                    searchQuery: searchInput.value
1346                });
1347
1348                searchInput.setCustomValidity('');
1349                if (!this.bisinessSearchForm.checkValidity()) {
1350                    searchInput.setCustomValidity(searchInput.dataset.errorMessage);
1351                    searchInput.reportValidity();
1352                } else {
1353                    spinner.classList.toggle(this.hideClass);
1354                    searchInput.readOnly = true;
1355                    submitBtn.disabled = true;
1356                    this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1357
1358                    try {
1359                        fetch(fetchUri, {
1360                            method: 'GET',
1361                            mode: 'cors',
1362                            credentials: 'include'
1363                        })
1364                        .then(response => response.json())
1365                        .then(response => {
1366                            spinner.classList.toggle(this.hideClass);
1367                            searchInput.readOnly = false;
1368                            submitBtn.disabled = false;
1369                            return this.renderHtmlResult(response, links, null, 'searchForm', fetchUri);
1370                        })
1371                        .catch(error => {
1372                            console.error(error);
1373                        })
1374                        .finally(() => {
1375                            this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1376                        });
1377                    } catch (error) {
1378                        console.error(error);
1379                        this.listBusinessInsuranceResult.querySelector('.list-element-loader').classList.toggle(this.hideClass);
1380                    }
1381                }
1382                event.preventDefault();
1383            });
1384        }
1385    }
1386
1387    slide() {
1388        const slider = document.querySelectorAll('.js-insurance-slider');
1389        const slickDefaultConfig = {
1390            slidesToShow:   1,
1391            slidesToScroll: 1,
1392            dots:           true,
1393            centerMode:     false,
1394            focusOnSelect:  false,
1395            variableWidth:  true,
1396            infinite:       true,
1397            arrows:         true,
1398        };
1399
1400        if (slider.length) {
1401            slider.forEach(slide => {
1402                if (typeof $.fn.slick !== 'function') {
1403                    console.error('Slick-slide not found!');
1404                    return;
1405                }
1406
1407                $(slide).slick(slickDefaultConfig);
1408
1409                $(slide).on('afterChange', function (a, b, c) {
1410                    const slides = b.$slideTrack.get(0).querySelectorAll('.slick-slide');
1411
1412                    if (slides.length) {
1413                        slides.forEach(thatSlide => {
1414                            let inp = thatSlide.querySelector('input');
1415                            let lab = thatSlide.querySelector('label');
1416
1417                            if (inp && lab) {
1418                                let rand = Math.floor(100000 + Math.random() * 900000);
1419                                inp.id = rand;
1420                                lab.setAttribute('for', rand);
1421                            }
1422                        });
1423                    }
1424                });
1425
1426                slide.addEventListener('setPosition', function () {
1427                    const thatJquery = $(this);
1428                    let sliderItemsCount = parseFloat(this.dataset.itemCount);
1429                    let winInnerWidth = window.innerWidth;
1430
1431                    if (sliderItemsCount === 2 && winInnerWidth > 768) {
1432                        thatJquery.slick('slickSetOption', {
1433                            slidesToShow:   1,
1434                            slidesToScroll: 1,
1435                            centerMode: false,
1436                        });
1437                    }
1438
1439                    if (sliderItemsCount > 2 && winInnerWidth > 768) {
1440                        thatJquery.slick('slickSetOption', {
1441                            slidesToShow:   1,
1442                            slidesToScroll: 1,
1443                            centerMode: true,
1444                        });
1445                    }
1446
1447                    if (sliderItemsCount >= 2 && winInnerWidth < 768) {
1448                        thatJquery.slick('slickSetOption', {
1449                            slidesToShow:   1,
1450                            slidesToScroll: 1,
1451                            centerMode: true,
1452                        });
1453                    }
1454                });
1455
1456                setTimeout(function () {
1457                    slide.dispatchEvent(new CustomEvent('setPosition'));
1458                }, 500);
1459            });
1460
1461            window.addEventListener('resize', () => {
1462                slider.forEach(slide => {
1463                    slide.dispatchEvent(new CustomEvent('setPosition'));
1464                });
1465            });
1466        }
1467    }
1468
1469    /**
1470     *
1471     * @returns {void}
1472     */
1473    loadByScroll() {
1474        let isLoading = false;
1475        const element = this.listBusinessInsuranceResult.closest('.box-default');
1476
1477        window.addEventListener('scroll', async () => {
1478            const windowHeight = window.innerHeight;
1479            const scrollY = window.scrollY;
1480            const elementOffsetTop = element.offsetTop;
1481            const elementHeight = element.offsetHeight;
1482            const elementBottom = elementOffsetTop + elementHeight;
1483
1484            if (!isLoading && (scrollY + windowHeight) >= elementBottom) {
1485                isLoading = true;
1486                await this.loadMoreContent();
1487                isLoading = false;
1488            }
1489        });
1490    }
1491}
1492
1493// init
1494document.addEventListener('DOMContentLoaded', () => {
1495    const BusinessClass = new BusinessClient();
1496    BusinessClass.slide();
1497
1498    if (!document.querySelector('.has-insurances')) {
1499        return false;
1500    }
1501
1502    BusinessClass.getDetailsFromLink();
1503    BusinessClass.searchForm();
1504    BusinessClass.loadByScroll();
1505});
1506
1507class AuthorizedSignatories
1508{
1509    constructor(form = null) {
1510        this.form = form;
1511        this.hideClass = 'd-none';
1512
1513        if (this.form) {
1514            this.signatoryElements = this.form.querySelectorAll('[class*="signatory"]');
1515        }
1516    }
1517
1518    /**
1519     *
1520     * @returns void
1521     */
1522    showSignatories() {
1523        if (this.form) {
1524            const selectCount = this.form.querySelector('[id^="countAuthorizedSignatories"]');
1525            const moreSignatoriesRadios = this.form.querySelectorAll('[name="exists_more_signatories"]');
1526            const selectCountSignatoriesContent = this.form.querySelector('[id^="selectCountSignatories"]');
1527
1528            if (moreSignatoriesRadios.length) {
1529                moreSignatoriesRadios.forEach((element) => {
1530                    element.addEventListener('change', (event) => {
1531                        if (event.target.checked) {
1532                            if (event.target.value === 'Y') {
1533                                selectCountSignatoriesContent.classList.remove(this.hideClass);
1534                                return;
1535                            }
1536                            selectCount.value = '';
1537                            selectCount.dispatchEvent(new Event('change'));
1538                            selectCountSignatoriesContent.classList.add(this.hideClass);
1539                        }
1540                    });
1541                });
1542                moreSignatoriesRadios.forEach((element) => {
1543                    element.dispatchEvent(new Event('change'));
1544                });
1545            }
1546
1547            if (selectCount) {
1548                const layer = document.querySelector('.layer-add-insurance.is-business');
1549
1550                selectCount.addEventListener('change', event => {
1551                    const thatValue = event.currentTarget.value;
1552
1553                    this.signatoryElements.forEach(formGroup => {
1554                        formGroup.classList.add(this.hideClass);
1555                    });
1556                    for (let i = 1; i <= parseInt(thatValue); i++) {
1557                        let element = this.signatoryElements[i - 1];
1558                        if (element) {
1559                            if (element.classList.contains(`signatory-${i}`)) {
1560                                element.classList.remove(this.hideClass);
1561                            }
1562                        }
1563                    }
1564                });
1565
1566                setTimeout(() => {
1567                    selectCount.dispatchEvent(new Event('change'));
1568                }, 250);
1569            }
1570        }
1571    }
1572
1573    sendMail(link = null, event = null) {
1574        if (link) {
1575            const spinner = link.querySelector('.spinner-border');
1576            const check = link.querySelector('.fa-check');
1577            const cross = link.querySelector('.fa-close');
1578
1579            if (link.classList.contains('disabled')) {
1580                event.preventDefault();
1581                return false;
1582            }
1583
1584            event.preventDefault();
1585            spinner.classList.remove('d-none');
1586            link.classList.add('disabled');
1587
1588            fetch(link.href, {
1589                method: 'GET',
1590                mode: 'cors',
1591                credentials: 'include',
1592            })
1593                .then(response => response.json())
1594                .then(response => {
1595                    if (response.hasOwnProperty('success') && response.success) {
1596                        check.classList.remove('d-none');
1597                        setTimeout(() => {
1598                            check.classList.add('d-none');
1599                        }, 2000);
1600                    } else {
1601                        cross.classList.remove('d-none');
1602                        setTimeout(() => {
1603                            cross.classList.add('d-none');
1604                        }, 2000);
1605                    }
1606                })
1607                .finally(() => {
1608                    spinner.classList.add('d-none');
1609                    link.classList.remove('disabled');
1610                })
1611                .catch(error => {
1612                    console.error(error);
1613                });
1614        }
1615    }
1616}
1617
1618document.addEventListener('DOMContentLoaded', () => {
1619    const forms = document?.querySelectorAll('.authorizedSignatoriesForm');
1620    if (forms.length) {
1621        forms.forEach(form => {
1622            new AuthorizedSignatories(form).showSignatories();
1623        });
1624    }
1625});
1626
1627class InsuranceAnchor
1628{
1629    constructor() {
1630        this.headerClassOrId = '.header-main';
1631        this.showMoreTriggerClassOrId = '.show-more-trigger';
1632        this.scrollBehavior = 'smooth';
1633
1634        this.fragmentMatcher = new FragmentMatcher();
1635
1636        this.init();
1637    }
1638
1639    /**
1640     *
1641     * @param fragment
1642     * @returns {boolean}
1643     */
1644    checkFragment(fragment = '') {
1645        const regex = /^insurance-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1646
1647        return regex.test(fragment);
1648    }
1649
1650    /**
1651     *
1652     * @param hashClean
1653     * @returns {Element|null}
1654     */
1655    getBox(hashClean = '') {
1656        return document.querySelector(`[data-insurance-anchor="${hashClean}"]`) || null;
1657    }
1658
1659    /**
1660     *
1661     * @param box
1662     */
1663    scrollToBox(box = null) {
1664        const header = document.querySelector(this.headerClassOrId);
1665        const headerHeight = (header) ? header.offsetHeight + 10 : 10;
1666        const boxPosition = box.getBoundingClientRect().top + window.scrollY;
1667        const offsetPosition = boxPosition - headerHeight;
1668
1669        window.scrollTo({
1670            top: offsetPosition,
1671            behavior: this.scrollBehavior,
1672        });
1673
1674        this.openBox(box);
1675        this.flashBox(box);
1676    }
1677
1678    /**
1679     *
1680     * @param box
1681     */
1682    openBox(box = null) {
1683        const triggerOpenInBox = box.querySelector(this.showMoreTriggerClassOrId);
1684
1685        if (triggerOpenInBox) {
1686            const inputElement = document.querySelector(`input[id="${triggerOpenInBox.getAttribute('for')}"]`);
1687
1688            inputElement.checked = true;
1689            inputElement.dispatchEvent(new Event('change'));
1690        }
1691    }
1692
1693    /**
1694     *
1695     * @param box
1696     */
1697    flashBox(box = null) {
1698        if (!box) {
1699            console.error("Box is null or undefined");
1700            return;
1701        }
1702
1703        const style = document.createElement('style');
1704        style.innerHTML = `
1705                @keyframes flash {
1706                    0% { opacity: 1; }
1707                    25% { opacity: .25; }
1708                    50% { opacity: 1; }
1709                    75% { opacity: .25; }
1710                    100% { opacity: 1; }
1711                }
1712                .box-insurance.flash {
1713                    animation: flash 750ms ease-in-out;
1714                }
1715            `;
1716
1717        document.head.appendChild(style);
1718
1719        setTimeout(() => {
1720            box.classList.add('flash');
1721
1722            setTimeout(() => {
1723                box.classList.remove('flash');
1724                document.head.removeChild(style);
1725            }, 1200);
1726        }, 750);
1727    }
1728
1729    /**
1730     *
1731     */
1732    checkWindowHash() {
1733        const specificFragment = this.fragmentMatcher.getSpecific(this.fragmentMatcher.availableKeys.insuranceBoxFoldout);
1734
1735        if (specificFragment) {
1736            if (this.getBox(specificFragment)) {
1737                this.scrollToBox(this.getBox(specificFragment));
1738            }
1739        }
1740    }
1741
1742    /**
1743     *
1744     */
1745    init() {
1746        this.checkWindowHash();
1747    }
1748}
1749
1750document.addEventListener('DOMContentLoaded', () => {
1751    new InsuranceAnchor();
1752});
1753
1754// Screenreader: .show-more-trigger eingeklappt/ausgeklappt Signalisierung, Text Fokus
1755document.addEventListener('DOMContentLoaded', () => {
1756    document.querySelectorAll('.show-more-trigger').forEach(label => {
1757        const inputId = label.getAttribute('for');
1758        const input = document.getElementById(inputId);
1759        const targetId = label.getAttribute('aria-controls');
1760        const target = targetId ? document.getElementById(targetId) : null;
1761
1762        if (input && target) {
1763            const updateState = () => {
1764                const expanded = input.checked;
1765                label.setAttribute('aria-expanded', expanded.toString());
1766                target.setAttribute('aria-hidden', (!expanded).toString());
1767            };
1768
1769            // Set initial state
1770            updateState();
1771
1772            // Update on toggle
1773            input.addEventListener('change', updateState);
1774        }
1775    });
1776});
1777
1778class InsuranceContactLayer
1779{
1780    constructor() {
1781        this.trigger = document.querySelectorAll('.js-insurance-contact');
1782        this.getLayer();
1783    }
1784
1785    /**
1786     *
1787     */
1788    getLayer() {
1789        if (this.trigger.length) {
1790            this.trigger.forEach(trigger => {
1791                trigger.addEventListener('click', event => {
1792                    event.preventDefault();
1793
1794                    try {
1795                        const data = new FormData();
1796                        data.set('insuranceId', trigger.dataset.id);
1797
1798                        this.setUnsetLoader();
1799                        fetch(trigger.href, {
1800                            method: 'POST',
1801                            mode: 'cors',
1802                            credentials: 'include',
1803                            body: data,
1804                        })
1805                            .then(response => response.json())
1806                            .then(response => {
1807                                this.redirectToUrl(response);
1808                                if (response.hasOwnProperty('success')) {
1809                                    if (response.success && response.hasOwnProperty('content')) {
1810                                        document.body.insertAdjacentHTML('afterbegin', response.content);
1811                                        setTimeout(() => {
1812                                            const layer = document.getElementById('contactLayer');
1813                                            const form = layer.querySelector('#broker-contact');
1814
1815                                            layer.classList.add('is-shown');
1816                                            this.close(layer);
1817                                            this.prepareForm(form);
1818                                        }, 500);
1819                                        return;
1820                                    }
1821                                    this.renderFlashMessage(response, 'error');
1822                                }
1823                            })
1824                            .catch(error => {
1825                                this.setUnsetLoader('hide');
1826                                this.renderFlashMessage({}, 'error');
1827                                console.error(error);
1828                            })
1829                            .finally(() => {
1830                                setTimeout(() => {
1831                                    this.setUnsetLoader('hide');
1832                                }, 500)
1833                            });
1834                    } catch (error) {
1835                        this.setUnsetLoader('hide');
1836                        this.renderFlashMessage({}, 'error');
1837                        console.error(error);
1838                    }
1839                });
1840                trigger.classList.remove('disabled');
1841            });
1842        }
1843    }
1844
1845    /**
1846     *
1847     * @param layer
1848     */
1849    close(layer = null) {
1850        if (layer) {
1851            const closeTrigger = layer.querySelector('.btn-close');
1852            if (closeTrigger) {
1853                closeTrigger.addEventListener('click', event => {
1854                    event.preventDefault();
1855
1856                    layer.classList.remove('is-shown');
1857                    setTimeout(() => {
1858                        layer.remove();
1859                    }, 750)
1860                });
1861            }
1862        }
1863    }
1864
1865    /**
1866     *
1867     * @param form
1868     */
1869    prepareForm(form = null) {
1870        if (form) {
1871            const upload = form.querySelector('.js-upload');
1872
1873            (upload) ? $(upload).fileUpload() : null;
1874            $(form).validateBase();
1875
1876            this.sendForm(form);
1877        }
1878    }
1879
1880    /**
1881     *
1882     * @param form
1883     */
1884    sendForm(form = null) {
1885        form.addEventListener('submit', event => {
1886            event.preventDefault();
1887
1888            if (form.checkValidity()) {
1889                try {
1890                    const hiddenFields = form.querySelectorAll('input[type="hidden"]');
1891                    const data = new FormData();
1892
1893                    // add Fields
1894                    data.set('description', form.querySelector('[name="description"]').value);
1895                    if (hiddenFields.length) {
1896                        hiddenFields.forEach(field => {
1897                            data.set(field.name, field.value);
1898                        });
1899                    }
1900
1901                    this.setUnsetLoader('show', true);
1902
1903                    // init xhr
1904                    const xhr = new XMLHttpRequest();
1905
1906                    xhr.open('POST', form.action, true);
1907                    xhr.withCredentials = true;
1908
1909                    // xhr Progress
1910                    xhr.upload.addEventListener('progress', (event) => {
1911                        if (event.lengthComputable) {
1912                            $.fn.progress.progressData(event);
1913                        }
1914                    });
1915
1916                    // xhr Load
1917                    xhr.addEventListener('load', () => {
1918                        this.setUnsetLoader('hide');
1919                        if (xhr.status >= 200 && xhr.status < 300) {
1920                            try {
1921                                const response = JSON.parse(xhr.responseText);
1922                                this.redirectToUrl(response);
1923                                if (response.hasOwnProperty('success')) {
1924                                    if (response.success) {
1925                                        this.renderSuccessContainer(form);
1926                                        return;
1927                                    }
1928                                    this.renderErrorContainer(form);
1929                                }
1930                            } catch (parseError) {
1931                                this.renderErrorContainer(form);
1932                                console.error('Error parsing JSON response:', parseError);
1933                            }
1934                        } else {
1935                            this.renderErrorContainer(form);
1936                            console.error('Upload failed with status:', xhr.status);
1937                        }
1938                    });
1939
1940                    // xhr Error
1941                    xhr.addEventListener('error', () => {
1942                        this.setUnsetLoader('hide');
1943                        this.renderErrorContainer(form);
1944                        console.error('An error occurred during the upload');
1945                    });
1946
1947                    // xhr Send
1948                    xhr.send(data);
1949                } catch (error) {
1950                    this.setUnsetLoader('hide');
1951                    this.renderErrorContainer(form);
1952                    console.error(error);
1953                }
1954            }
1955        });
1956    }
1957
1958    /**
1959     *
1960     * @param showOrHide
1961     * @param progress
1962     * @returns {*|jQuery|boolean|number}
1963     */
1964    setUnsetLoader(
1965        showOrHide = 'show',
1966        progress   = false
1967    ) {
1968        if (typeof $.fn.progress === 'function') {
1969            if (showOrHide === 'show') {
1970                return $.fn.progress.renderProgressLayer(progress)
1971            }
1972            return $.fn.progress.removeProgressLayer();
1973        }
1974        return true;
1975    }
1976
1977    /**
1978     *
1979     * @param object
1980     * @param type
1981     */
1982    renderFlashMessage(
1983        object = {},
1984        type   = 'success'
1985    ) {
1986        if (typeof $.fn.flash === 'function') {
1987            $.fn.flash.render(object, type);
1988        }
1989    }
1990
1991    /**
1992     *
1993     * @param form
1994     */
1995    renderSuccessContainer(form = null) {
1996        if (form) {
1997            const successContent = form.closest('#contactLayer').querySelector('.contact-layer-success');
1998            if (successContent) {
1999                form.remove();
2000                successContent.classList.remove('d-none');
2001            }
2002        }
2003    }
2004
2005    /**
2006     *
2007     * @param form
2008     */
2009    renderErrorContainer(form = null) {
2010        if (form) {
2011            const errorContent = form.closest('#contactLayer').querySelector('.contact-layer-error');
2012            if (errorContent) {
2013                form.classList.add('d-none');
2014                errorContent.querySelector('img').src = './img/failure_circle.svg';
2015                errorContent.classList.remove('d-none');
2016                setTimeout(() => {
2017                    errorContent.classList.add('d-none');
2018                    errorContent.querySelector('img').src = '';
2019                    form.classList.remove('d-none');
2020                }, 3500);
2021            }
2022        }
2023    }
2024
2025    redirectToUrl(response) {
2026        const mandator = window?.application?.mandant ?? '';
2027        if (response?.redirectRoute) {
2028            const delimiter = response.redirectRoute.includes('?') ? '&' : '?';
2029            window.location.href = `//${location.host}${response.redirectRoute}${delimiter}mandator=${mandator}`;
2030            return false;
2031        }
2032        return true;
2033    }
2034}
2035
2036document.addEventListener('DOMContentLoaded', () => {
2037    new InsuranceContactLayer();
2038});
2039
2040class AddInsuranceLayer {
2041    constructor() {
2042        // DOM Elements
2043        this.layerOpenButtons = document.querySelectorAll('.js-open-add-insurance-layer');
2044        this.layer            = document.querySelector('.insurance-add-layer');
2045        this.layerHeader      = document.querySelector('.insurance-add-layer .message-content-header');
2046        this.formContent      = document.querySelector('.insurance-add-layer .form-content');
2047        this.progress         = document.querySelector('.insurance-add-layer .add-insurance-progress');
2048
2049        // CSS Classes
2050        this.loaderClass        = '.layer-loader';
2051        this.closeTriggerClass  = '.js-close-insurance-layer';
2052        this.contentThanksClass = 'content-thanks';
2053        this.shownClass         = 'is-shown';
2054        this.disabledClass      = 'disabled';
2055        this.noneClass          = 'd-none';
2056        this.activeStepClass    = 'active';
2057        this.initCSSClass       = 'add-insurance-button-initialized';
2058
2059        // Event Handlers
2060        this.boundOpenLayerClickHandler  = this.openLayerClickHandler.bind(this);
2061        this.boundCloseLayerClickHandler = this.closeLayerClickHandler.bind(this);
2062
2063        // Intervals
2064        this.checkInterval        = null;
2065        this.errorTimeout         = null;
2066        this.intervalDuration     = 3000;
2067        this.errorTimeoutDuration = 5000;
2068
2069        // Other Properties
2070        this.filter               = null;
2071        this.step                 = null;
2072        this.lastActiveTrigger    = null;
2073
2074        // JS Classes
2075        this.utilities            = new Utilities();
2076
2077        this.init();
2078    }
2079
2080    // Initialization
2081    init() {
2082        this.enableOpenButton();
2083        this.openLayer();
2084        this.openLayerOnFragment();
2085    }
2086
2087    /**
2088     * Focus auf das letzte Element, welches den Layer öffnete
2089     */
2090    focusLastOpenTrigger() {
2091        if (this.lastActiveTrigger) {
2092            this.lastActiveTrigger?.focus();
2093            setTimeout(() => {
2094                this.lastActiveTrigger = null;
2095            }, 250);
2096        }
2097    }
2098
2099    checkIfElementInSpecificContent(content = null, idOrClass = null) {
2100        const parser = new DOMParser();
2101        if (content) {
2102            const doc = parser.parseFromString(content, 'text/html');
2103
2104            return doc?.querySelector(idOrClass);
2105        }
2106        return false;
2107    }
2108
2109    enableOpenButton() {
2110        if (this.layerOpenButtons?.length && this.layer) {
2111            this.layerOpenButtons.forEach(button => {
2112                if (button.classList.contains(this.initCSSClass)) {
2113                    return;
2114                }
2115                button.classList.remove(this.disabledClass);
2116                button.classList.add(this.initCSSClass);
2117                button.disabled = false;
2118            });
2119        }
2120    }
2121
2122    openLayer() {
2123        if (this.layerOpenButtons?.length && this.layer) {
2124            this.layerOpenButtons.forEach(button => {
2125                button.removeEventListener('click', this.boundOpenLayerClickHandler);
2126                button.addEventListener('click', this.boundOpenLayerClickHandler);
2127            });
2128        }
2129    }
2130
2131    closeLayer() {
2132        setTimeout(() => {
2133            const closeTriggers = this.layer?.querySelectorAll(this.closeTriggerClass);
2134
2135            if (closeTriggers?.length) {
2136                closeTriggers.forEach(close => {
2137                    close.removeEventListener('click', this.boundCloseLayerClickHandler);
2138                    close.addEventListener('click', this.boundCloseLayerClickHandler);
2139                });
2140            }
2141        }, 500);
2142    }
2143
2144    openLayerClickHandler(event) {
2145        event.preventDefault();
2146
2147        if (this.formContent) {
2148            this.lastActiveTrigger = event.currentTarget;
2149
2150            this.filter = event.currentTarget?.dataset?.filter ?? '';
2151            this.progress?.classList.add(this.noneClass);
2152            this.layer?.classList.add(this.shownClass);
2153            this.layer?.classList.add('small');
2154
2155            this.layer?.focus();
2156
2157            this.closeLayer();
2158
2159            const data = new FormData();
2160            data.set('filter', this.filter);
2161
2162            this.standardFetch(
2163                this.layer?.dataset?.startUri,
2164                {
2165                    body: this.mergeFormDataWithSelectedInsuranceFormData(
2166                        data,
2167                        this.getFormDataFromWindow()
2168                    )
2169                },
2170                event.currentTarget
2171            );
2172            setTimeout(() => {
2173                this.layer?.focus();
2174            }, 200);
2175        }
2176    }
2177
2178    closeLayerClickHandler(event) {
2179        event.preventDefault();
2180
2181        this.layer?.classList.remove(this.shownClass);
2182
2183        this.focusLastOpenTrigger();
2184
2185        setTimeout(() => {
2186            this.formContent.innerHTML = '';
2187        }, 300);
2188    }
2189
2190    // Loader and Visibility Handling
2191    showLoader() {
2192        this.layer?.querySelector(this.loaderClass)?.classList.remove(this.noneClass);
2193    }
2194
2195    hideLoader() {
2196        this.layer?.querySelector(this.loaderClass)?.classList.add(this.noneClass);
2197    }
2198
2199    hideShowClose(type = 'hide') {
2200        const close = this.layerHeader?.querySelector(this.closeTriggerClass);
2201        if (close) {
2202            if (type === 'show') {
2203                close.classList.remove(this.noneClass);
2204            } else {
2205                close.classList.add(this.noneClass);
2206            }
2207        }
2208    }
2209
2210    hideLayerHeader() {
2211        this.layerHeader?.classList.add(this.noneClass);
2212    }
2213
2214    showLayerHeader() {
2215        this.layerHeader?.classList.remove(this.noneClass);
2216    }
2217
2218    showAndHideLoaderInButton(btnOrLink = null) {
2219        if (btnOrLink) {
2220            const spinner = btnOrLink?.querySelector('.spinner-border');
2221            const isButton = btnOrLink.tagName === 'BUTTON';
2222
2223            if (spinner) {
2224                if (!btnOrLink.disabled && spinner.classList.contains(this.noneClass)) {
2225                    if (isButton) {
2226                        btnOrLink.disabled = true;
2227                    }
2228                    btnOrLink.classList.add(this.disabledClass);
2229                    btnOrLink?.querySelector('.spinner-border')?.classList.remove(this.noneClass);
2230                    return;
2231                }
2232                if (isButton) {
2233                    btnOrLink.disabled = false;
2234                }
2235                btnOrLink.classList.remove(this.disabledClass);
2236                btnOrLink?.querySelector('.spinner-border')?.classList.add(this.noneClass);
2237
2238                if (btnOrLink.dataset?.hideClosest && spinner.classList.contains(this.noneClass)) {
2239                    btnOrLink.closest(btnOrLink.dataset?.hideClosest)?.classList.add(this.noneClass);
2240                }
2241            }
2242        }
2243    }
2244
2245    setError(error = null) {
2246        if (error) {
2247            this.formContent?.classList.remove(this.noneClass);
2248            this.hideLoader();
2249            this.hideShowClose('show');
2250            this.showErrorMessage(error);
2251        }
2252    }
2253
2254    showErrorMessage(error) {
2255        const errorContent = this.layer?.querySelector('.js-add-insurance-error-content');
2256        if (!errorContent) return;
2257
2258        const alert = errorContent.querySelector('.alert');
2259        const alertHeader = alert?.querySelector('.alert-heading');
2260        const alertText = alert?.querySelector('p');
2261        const defaultHeader = this.utilities.getTextFromWindowVars('messages.errorAlert');
2262        const defaultMessage = this.utilities.getTextFromWindowVars('messages.errorGlobal');
2263
2264        if (alertHeader) {
2265            alertHeader.textContent = error?.header || defaultHeader;
2266        }
2267
2268        if (alertText) {
2269            alertText.textContent = (error.constructor === Object && error.message)
2270                ? error?.message
2271                : defaultMessage;
2272        }
2273
2274        errorContent.classList.add(this.shownClass);
2275
2276        if (this.errorTimeout) {
2277            clearTimeout(this.errorTimeout);
2278            this.errorTimeout = null;
2279        }
2280
2281        this.errorTimeout = setTimeout(() => {
2282            errorContent.classList.remove(this.shownClass);
2283            this.errorTimeout = null;
2284        }, this.errorTimeoutDuration);
2285    }
2286
2287    closeErrorMessage() {
2288        const errorContent = this.layer?.querySelector('.js-add-insurance-error-content');
2289        if (!errorContent) return;
2290
2291        errorContent.classList.remove(this.shownClass);
2292
2293        if (this.errorTimeout) {
2294            clearTimeout(this.errorTimeout);
2295            this.errorTimeout = null;
2296        }
2297    }
2298
2299    // Form Handling
2300    validateFormInLayer() {
2301        if (typeof $.fn?.validateBase === 'function') {
2302            const validateFormClasses = [
2303                '.layer-validate-form',
2304                '.js-form-validation-base',
2305            ];
2306
2307            validateFormClasses.forEach(className => {
2308                $(this.layer).find(className).validateBase();
2309            });
2310        }
2311    }
2312
2313    sendForm() {
2314        const forms = this.formContent?.querySelectorAll('form');
2315        if (forms?.length) {
2316            forms.forEach(form => {
2317                const handleSubmitBound = this.handleSubmit.bind(this);
2318
2319                form.removeEventListener('submit', handleSubmitBound);
2320
2321                form.addEventListener('submit', handleSubmitBound);
2322            });
2323        }
2324    }
2325
2326    handleSubmit(event) {
2327        event.preventDefault();
2328
2329        const form = event.target;
2330        const dataForm = new FormData(form);
2331
2332        this.formContent?.classList.add(this.noneClass);
2333        this.showLoader();
2334        this.hideShowClose('hide');
2335
2336        this.standardFetch(
2337            form?.action,
2338            {
2339                method: form?.method ?? 'POST',
2340                body: this.mergeFormDataWithSelectedInsuranceFormData(
2341                    dataForm,
2342                    this.getFormDataFromWindow()
2343                )
2344            },
2345            form
2346        );
2347    }
2348
2349    updateFormContent(content) {
2350        // select
2351        const selectInsuranceForm = this.formContent?.querySelector('#selectInsuranceForm');
2352        let isNormallyUpdate = false;
2353
2354        if (selectInsuranceForm && this.checkIfElementInSpecificContent(content, '.js-add-insurance')) {
2355            selectInsuranceForm?.lastElementChild?.insertAdjacentHTML('beforebegin', content);
2356        }
2357        // normally
2358        else {
2359            this.formContent.innerHTML = content;
2360            this.formContent.classList.remove(this.noneClass);
2361            isNormallyUpdate = true;
2362        }
2363
2364        this.deleteSelectContent();
2365        this.setIframeSize();
2366
2367        this.initPluginsAndMethods();
2368
2369        this.validateFormInLayer();
2370        this.initFetchElements();
2371
2372        if (isNormallyUpdate) {
2373            this.checkPermanentlyEmailVerificationStatus();
2374            if (this?.checkInterval) {
2375                clearInterval(this.checkInterval);
2376            }
2377
2378            this.sendForm();
2379        }
2380
2381        if (this.formContent?.querySelector(`.${this.contentThanksClass}`)) {
2382            this.hideLayerHeader();
2383        } else {
2384            this.showLayerHeader();
2385        }
2386
2387        if (this.formContent?.querySelector(`#signatureForm`)) {
2388            const procurationClass = new Procuration();
2389            procurationClass.resizeDocument();
2390        }
2391    }
2392
2393    redirectToUrl(response) {
2394        const mandator = window?.application?.mandant ?? '';
2395        if (response?.redirectRoute) {
2396            const delimiter = response.redirectRoute.includes('?') ? '&' : '?';
2397            this.layer?.classList.remove(this.shownClass);
2398            let redirectRoute = response.redirectRoute;
2399            if (! redirectRoute.startsWith('https://') && ! redirectRoute.startsWith('http://')) {
2400                redirectRoute = `//${location.host}${response.redirectRoute}${delimiter}mandator=${mandator}`;
2401            }
2402            window.location.href = redirectRoute;
2403            return false;
2404        }
2405        return true;
2406    }
2407
2408    // Plugins and Methods
2409    initPluginsAndMethods() {
2410        const addInsuranceContainer = document.querySelectorAll('.js-add-insurance');
2411        const signatoryForms = document.querySelectorAll('.authorizedSignatoriesForm');
2412        const setMinDateInput = document.querySelectorAll('input[type="date"][data-set-min]');
2413        const setMaxDateInput = document.querySelectorAll('input[type="date"][data-set-max]');
2414        const setDefaultDateInput = document.querySelectorAll('input[type="date"][data-default-date]');
2415        const dateSet = new Set([
2416            ...setMinDateInput,
2417            ...setMaxDateInput
2418        ]);
2419
2420        dateSet.forEach(arr => {
2421            arr.forEach(input => {
2422                if (typeof $.fn.setDate === 'function') {
2423                    $(input).setDate();
2424                }
2425            });
2426        });
2427
2428        setDefaultDateInput?.forEach(input => {
2429            if (typeof $.fn.setDefaultDate === 'function') {
2430                $(input).setDefaultDate();
2431            }
2432        });
2433
2434        addInsuranceContainer?.forEach(formContent => {
2435            formContent.querySelectorAll('.js-insurance-add-select')?.forEach(select => {
2436                this.initSelect(select);
2437            });
2438        });
2439
2440        signatoryForms?.forEach(form => {
2441            new AuthorizedSignatories(form).showSignatories();
2442        });
2443    }
2444
2445    setIframeSize() {
2446        const iframe = document?.getElementById('psd2Iframe');
2447
2448        if (!iframe) {
2449            return;
2450        }
2451
2452        const handleMessage = (event) => {
2453            if (event?.data?.height) {
2454                let newHeight = event.data.height;
2455                iframe.style.height = newHeight + 'px';
2456            }
2457        }
2458
2459        window.removeEventListener('message', handleMessage);
2460        window.addEventListener('message', handleMessage);
2461    }
2462
2463    initFetchElements() {
2464        const fetchElements = this.layer?.querySelectorAll('[data-add-insurance-ajax]');
2465        if (fetchElements?.length) {
2466            fetchElements.forEach(element => {
2467                const oldHandler = element._clickHandler;
2468
2469                if (oldHandler) {
2470                    element.removeEventListener('click', oldHandler);
2471                }
2472
2473                const showOrHideClose = (element?.dataset?.hasOwnProperty('showOrHideClose'))
2474                    ? element?.dataset?.showOrHideClose
2475                    : null
2476
2477                const newHandler = event => {
2478                    event.preventDefault();
2479
2480                    if (element?.dataset?.hasOwnProperty('clearParams')) {
2481                        window.selectInsuranceFormData = null;
2482                    }
2483
2484                    this.standardFetch(
2485                        element?.dataset?.url ?? '',
2486                        {body: this.getFormDataFromWindow()},
2487                        element,
2488                        element?.dataset?.hasOwnProperty('loaderAnimateOff') ?? false,
2489                        showOrHideClose
2490                    );
2491                };
2492
2493                element._clickHandler = newHandler;
2494
2495                element.addEventListener('click', newHandler);
2496            });
2497        }
2498    }
2499
2500    // FormData Handling
2501    mergeFormDataWithSelectedInsuranceFormData(formData1, formData2) {
2502        if (!(formData1 instanceof FormData) || !(formData2 instanceof FormData)) {
2503            return null;
2504        }
2505
2506        formData2?.forEach((value, key) => {
2507            formData1?.set(key, value);
2508        });
2509
2510        return formData1;
2511    }
2512
2513    saveFormDataToWindow(el = null) {
2514        if (el?.tagName === 'FORM' && el?.id === 'selectInsuranceForm') {
2515            window.selectInsuranceFormData = new FormData(el);
2516        }
2517    }
2518
2519    getFormDataFromWindow() {
2520        const filterData = new FormData();
2521        if (this.filter) {
2522            filterData.set('filter', this.filter);
2523        }
2524        return this.mergeFormDataWithSelectedInsuranceFormData(
2525            filterData,
2526            window?.selectInsuranceFormData ?? new FormData()
2527        );
2528    }
2529
2530    // Email Verification Status
2531    checkPermanentlyEmailVerificationStatus() {
2532        const verifyElement = this.formContent?.querySelector('[data-check-fetch]');
2533
2534        if (verifyElement) {
2535            const url = verifyElement?.dataset?.url ?? '';
2536
2537            if (this.checkInterval) {
2538                clearInterval(this.checkInterval);
2539                this.checkInterval = null;
2540            }
2541
2542            setTimeout(() => {
2543                this.checkStatusFetch(url);
2544                this.checkInterval = setInterval(() => {
2545                    this.checkStatusFetch(url);
2546                }, this.intervalDuration);
2547            }, 500);
2548        }
2549    }
2550
2551    checkStatusFetch(url = null) {
2552        this.standardFetch(
2553            url,
2554            {
2555                body: this.getFormDataFromWindow(),
2556            },
2557            null,
2558            true
2559        );
2560    }
2561
2562    // Fetch Handling
2563    standardFetch(url = null, conf = {}, el = null, loaderAnimateOff = false, showOrHideClose = null) {
2564        const defaultOptions = {
2565            method: 'POST',
2566            mode: 'cors',
2567            credentials: 'include',
2568        };
2569
2570        const options = Object.assign({}, defaultOptions, conf);
2571
2572        if (!loaderAnimateOff) {
2573            this.showLoader();
2574            this.hideShowClose('hide');
2575        }
2576        this.showAndHideLoaderInButton(el);
2577
2578        if (showOrHideClose !== null) {
2579            this.hideShowClose(showOrHideClose);
2580        }
2581
2582        fetch(url, options)
2583            .then(response => {
2584                if (!response.ok) {
2585                    throw new Error('Network response was not ok');
2586                }
2587                return response.json();
2588            })
2589            .then(response => {
2590                this.redirectToUrl(response);
2591                if (response?.redirectRoute) {
2592                    return;
2593                }
2594                this.saveFormDataToWindow(el);
2595
2596                this.step = response?.step || this.step;
2597                this.handleProgress();
2598
2599                if (response?.content) {
2600                    this.updateFormContent(response.content);
2601
2602                    const selects = this.formContent?.querySelectorAll('.js-insurance-add-select');
2603                    if (selects.length) {
2604                        selects.forEach(select => new AccessibleComboSelect(select));
2605                    }
2606                }
2607
2608                if (response?.largeLayer) {
2609                    this.layer.classList.remove('small');
2610                } else {
2611                    this.layer.classList.add('small');
2612                }
2613
2614                if (!response?.success) {
2615                    this.setError(response);
2616                }
2617            })
2618            .catch(error => {
2619                this.setError(error);
2620            })
2621            .finally(() => {
2622                if (!loaderAnimateOff) {
2623                    this.hideLoader();
2624                    this.hideShowClose('show');
2625                }
2626                this.showAndHideLoaderInButton(el);
2627            });
2628    }
2629
2630    // selectionhandling
2631
2632    initSelect(select) {
2633
2634        if (select._changeEv) {
2635            select.removeEventListener(select._changeEv);
2636        }
2637
2638        select._changeEv = select.addEventListener('change', (event) => {
2639            event.preventDefault();
2640
2641            const closestContainer = select.closest('.js-add-insurance');
2642            const insurerSelect = closestContainer?.querySelector('select[data-insurance-select-type="insurer"]');
2643            this.icrmBranchId = select.value;
2644
2645            if (select.value) {
2646                select.setCustomValidity('');
2647                select.checkValidity();
2648            }
2649
2650            if (select.dataset.insuranceSelectType === 'branch') {
2651                this.getSelectOptions(insurerSelect, select.value);
2652            }
2653        });
2654
2655        select.classList.add('js-add-insurance-init');
2656    }
2657
2658    getSelectOptions(select, value = '') {
2659        if (!select) return;
2660
2661        select.previousElementSibling?.classList?.contains('alert') && select.previousElementSibling.remove();
2662
2663        select.innerHTML = (!value)
2664            ? `<option value="">${this.utilities.getTextFromWindowVars('texts.addInsurance.chooseInsuranceBranch')}</option>`
2665            : `<option value="">${this.utilities.getTextFromWindowVars('texts.pleaseWait')}</option>`;
2666        select.disabled = true;
2667
2668        if (!value) return;
2669
2670        const data = new FormData();
2671
2672        data.set('iCrmBranchICrmId', this.icrmBranchId);
2673        data.set('type', 'insurer');
2674
2675        select.closest('.mb-3')?.querySelector('.spinner-border')?.classList.remove(this.noneClass);
2676
2677        if (this.currentAbortController) {
2678            this.currentAbortController.abort();
2679        }
2680
2681        this.currentAbortController = new AbortController();
2682        const signal = this.currentAbortController.signal;
2683
2684        fetch(select.dataset.ajaxUri, {
2685            method: 'POST',
2686            body: this.mergeFormDataWithSelectedInsuranceFormData(
2687                data,
2688                this.getFormDataFromWindow()
2689            ),
2690            mode: 'cors',
2691            credentials: 'include',
2692            signal: signal,
2693        })
2694            .then(response => response.json())
2695            .then(response => {
2696                this.redirectToUrl(response);
2697                if (response?.redirectRoute) {
2698                    return;
2699                }
2700
2701                if (response?.content) {
2702                    select.innerHTML = '';
2703                    select.insertAdjacentHTML('afterbegin', response.content);
2704
2705                    if (select.dataset.insuranceSelectType === 'insurer') {
2706                        window.dispatchEvent(new CustomEvent('insurerFilled', {
2707                            detail: { insurerSelect: select }
2708                        }));
2709                    }
2710
2711                    if (this.filter && select.options.length === 1) {
2712                        let counter = 0;
2713
2714                        const interval = setInterval(() => {
2715                            if (counter >= 20) {
2716                                clearInterval(interval);
2717                            }
2718                            if (select.querySelectorAll('option').length === 2) {
2719                                select.selectedIndex = 1;
2720                                select.dispatchEvent(new Event('change'));
2721                                clearInterval(interval);
2722                            }
2723                            counter++;
2724                        }, 10);
2725                    }
2726                    select.disabled = false;
2727                    select.closest('.mb-3')?.querySelector('.spinner-border')?.classList.add(this.noneClass);
2728                }
2729
2730                if (!response?.content) {
2731                    select.insertAdjacentHTML('beforebegin', `<div class="alert alert-warning lc mb-0" role="alert"><p class="mb-0"><span class="icon-app info me-2"></span>${this.texts('noCompanyFound')}</p></div>`);
2732                    select.closest('.mb-3')?.querySelector('.spinner-border')?.classList.add(this.noneClass);
2733                }
2734
2735                if (!response?.success) {
2736                    this.setError(response);
2737                }
2738            })
2739            .catch((error) => {
2740                if (error.name !== 'AbortError') {
2741                    this.setError();
2742                }
2743            });
2744    }
2745
2746    deleteSelectContent() {
2747        const triggers = this.formContent?.querySelectorAll('.insurance-add-delete');
2748
2749        triggers?.forEach(trigger => {
2750            if (!trigger.classList.contains('init')) {
2751                ['click', 'keydown'].forEach(eventType => {
2752                    trigger.addEventListener(eventType, (event) => {
2753                        if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') {
2754                            event.preventDefault();
2755
2756                            const prevElement = trigger.closest('.js-add-insurance')?.previousElementSibling;
2757                            const nextElement = trigger.closest('.js-add-insurance')?.nextElementSibling;
2758                            const nextElementAddContent = nextElement?.nextElementSibling;
2759
2760                            if (prevElement?.classList.contains('load-more-link-content') &&
2761                                (!nextElementAddContent || !nextElementAddContent?.classList.contains('js-add-insurance'))) {
2762                                prevElement.classList.remove(this.noneClass);
2763                            }
2764                            if (nextElement?.classList.contains('load-more-link-content')) {
2765                                nextElement.remove();
2766                            }
2767                            trigger.closest('.js-add-insurance')?.remove();
2768                        }
2769                    });
2770                });
2771                trigger.classList.add('init');
2772            }
2773        });
2774    }
2775
2776    // Progress
2777    handleProgress() {
2778        const progressBarSteps = this.progress?.querySelectorAll('.add-insurance-progress-step');
2779
2780        if (progressBarSteps.length && this.step) {
2781            progressBarSteps.forEach((progressStep) => {
2782                progressStep.classList.remove(this.activeStepClass);
2783                if (this.step === 'procuration' && progressStep.dataset.step === 'selectInsurance') {
2784                    progressStep.classList.add(this.activeStepClass);
2785                }
2786            });
2787        }
2788
2789        if (this.step !== 'confirmation' && this.step !== 'chooseMode') {
2790            this.progress.classList.remove(this.noneClass);
2791        } else {
2792            this.progress.classList.add(this.noneClass);
2793        }
2794    }
2795
2796    openLayerOnFragment() {
2797        const triggers = document?.querySelectorAll('.js-open-add-insurance-layer:not([data-filter])');
2798        const fragmentMatcher = new FragmentMatcher();
2799        const trigger = (triggers.length) ? triggers[0] : null;
2800        const intervalMethod = () => {
2801            let counter = 0;
2802            let intervalId;
2803
2804            const intervalCallback = () => {
2805                let btn = this?.layer?.querySelector('.change-add-insurance-type')?.querySelectorAll('button')[1];
2806                if (counter >= 30) {
2807                    clearInterval(intervalId);
2808                }
2809                if (btn) {
2810                    btn.dispatchEvent(new Event('click'));
2811                    clearInterval(intervalId);
2812                }
2813                counter++;
2814            };
2815            intervalId = setInterval(intervalCallback, 250);
2816        };
2817
2818        // normally open
2819        if (fragmentMatcher.getSpecific(fragmentMatcher.availableKeys.openInsuranceAddLayer) && trigger) {
2820            trigger.dispatchEvent(new Event('click'));
2821        }
2822
2823        // open Kontocheck
2824        if (fragmentMatcher.getSpecific(fragmentMatcher.availableKeys.openInsuranceAddLayerWithKontocheck) && trigger) {
2825            trigger.dispatchEvent(new Event('click'));
2826            intervalMethod();
2827        }
2828    }
2829}
2830
2831document.addEventListener('DOMContentLoaded', () => {
2832    new AddInsuranceLayer();
2833});
2834
2835class InteractiveFocusHandler {
2836    constructor() {
2837        this.exeptedKeys    = ['Enter'];
2838        this.selectors      = [
2839            'label[tabindex]',
2840            'div[tabindex]',
2841            'span[tabindex]',
2842            'img[tabindex]',
2843        ];
2844        this.dialogSelector = '[role="dialog"]:not([aria-hidden="true"]), [role="region"]:not(.accordion-panel):not(.collapse)';
2845        this.elements       = [];
2846
2847        this.init();
2848        this.observeMutations();
2849        this.bindFocusTrap();
2850    }
2851
2852    init() {
2853        this.selectors.forEach(selector => {
2854            const elements = document.querySelectorAll(selector);
2855            elements.forEach(element => {
2856                if (!element.hasAttribute('data-listener-set')) {
2857                    this.addEventListeners(element);
2858                }
2859            });
2860            this.elements.push(...elements);
2861        });
2862    }
2863
2864    addEventListeners(element) {
2865        element.addEventListener('focus', this.onFocus);
2866        element.addEventListener('focusout', this.outFocus);
2867        element.addEventListener('keydown', this.onKeyDown.bind(this));
2868        element.setAttribute('data-listener-set', 'true');
2869    }
2870
2871    onFocus(event) {
2872        const el = event?.target;
2873        if (el?.classList) {
2874            el.classList.add('focused');
2875            el.setAttribute('data-has-focus', 'true'); // Beispiel für weiteres Attr
2876        }
2877    }
2878
2879    outFocus(event) {
2880        const el = event?.target;
2881        if (el?.classList) {
2882            el.classList.remove('focused');
2883            el.removeAttribute('data-has-focus');
2884        }
2885    }
2886
2887    onKeyDown(event) {
2888        if (this.exeptedKeys.includes(event.key)) {
2889            event.preventDefault();
2890            event.target.click();
2891        }
2892    }
2893
2894    observeMutations() {
2895        const observer = new MutationObserver(mutations => {
2896            mutations.forEach(mutation => {
2897                this.initializeElementsWithoutListener();
2898
2899                mutation.addedNodes.forEach(node => {
2900                    if (node.nodeType === Node.ELEMENT_NODE && this.isMatchingElement(node)) {
2901                        if (!node.hasAttribute('data-listener-set')) {
2902                            this.addEventListeners(node);
2903                        }
2904                    }
2905                });
2906
2907                if (mutation.type === 'attributes') {
2908                    const target = mutation.target;
2909                    if (this.isMatchingElement(target) && !target.hasAttribute('data-listener-set')) {
2910                        this.addEventListeners(target);
2911                    }
2912                }
2913            });
2914        });
2915
2916        observer.observe(document.body, {
2917            childList: true,
2918            subtree: true,
2919            attributes: true,
2920            attributeFilter: ['tabindex', 'style', 'class']
2921        });
2922    }
2923
2924    initializeElementsWithoutListener() {
2925        const elements = document.querySelectorAll(this.selectors.join(', '));
2926        elements.forEach(element => {
2927            if (!element.hasAttribute('data-listener-set')) {
2928                this.addEventListeners(element);
2929            }
2930        });
2931    }
2932
2933    isMatchingElement(element) {
2934        return this.selectors.some(selector => element.matches(selector));
2935    }
2936
2937    bindFocusTrap() {
2938        document.addEventListener('focusin', (event) => {
2939            const openDialogs = Array.from(document.querySelectorAll(this.dialogSelector))
2940                .filter(dialog => this.isDialogVisible(dialog));
2941
2942            openDialogs.forEach(dialog => {
2943                if (!dialog.contains(event.target)) {
2944                    const focusables = this.getFocusableElements(dialog);
2945                    if (focusables.length > 0) {
2946                        event.preventDefault();
2947                        focusables[0].focus();
2948                    }
2949                }
2950            });
2951        });
2952    }
2953
2954    isDialogVisible(element) {
2955        if (typeof element.checkVisibility === 'function') {
2956            return element.checkVisibility({
2957                opacityProperty: true,
2958                visibilityProperty: true,
2959                contentVisibilityAuto: true,
2960            });
2961        }
2962
2963        const style = window.getComputedStyle(element);
2964
2965        return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
2966    }
2967
2968    getFocusableElements(container) {
2969        return Array.from(
2970            container.querySelectorAll(
2971                'a[href], button, textarea, input[type="text"], input[type="radio"], input[type="checkbox"], select, [tabindex]:not([tabindex="-1"])'
2972            )
2973        ).filter(el => !el.disabled && el.offsetParent !== null);
2974    }
2975}
2976
2977document.addEventListener('DOMContentLoaded', () => {
2978    new InteractiveFocusHandler();
2979});
2980
2981class VisibilityFocusHandler {
2982    constructor() {
2983        this.classSelectors = [
2984            '.layer-default:not(.embedded):not(.analysis-start-layer):not(.insurance-add-layer):not(.email-outdated-layer):not(.layer-cookie-approval.top):not(.crop-layer)',
2985        ];
2986
2987        this.elements = [];
2988        this.layerState = new WeakMap(); // { visible: bool, lastFocused: Element|null }
2989
2990        this.collectElements();
2991        this.initLayerState();
2992        this.checkVisibilityAndFocus(true); // initial
2993        this.observeMutations();
2994        this.checkCheckedBoxes();
2995        this.enableLayerFocusTrap();
2996        this.bindFocusReturn();
2997    }
2998
2999    collectElements() {
3000        this.elements = [];
3001        this.classSelectors.forEach(selector => {
3002            this.elements.push(...document.querySelectorAll(selector));
3003        });
3004    }
3005
3006    initLayerState() {
3007        this.elements.forEach(element => {
3008            this.layerState.set(element, {
3009                visible: this.isElementVisible(element),
3010                lastFocused: null,
3011                initialized: false
3012            });
3013        });
3014    }
3015
3016    isElementVisible(element) {
3017        const style = window.getComputedStyle(element);
3018
3019        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
3020            return false;
3021        }
3022
3023        const rect = element.getBoundingClientRect();
3024
3025        // Modal kann größer als Viewport sein -> nur "irgendein Teil sichtbar" reicht!
3026        return rect.bottom >= 0 && rect.right >= 0 &&
3027            rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
3028            rect.left <= (window.innerWidth || document.documentElement.clientWidth);
3029    }
3030
3031    getFocusableIn(element) {
3032        const selectors = [
3033            'a[href]',
3034            'button:not([disabled])',
3035            'textarea:not([disabled])',
3036            'input:not([type="hidden"]):not([disabled])',
3037            'select:not([disabled])',
3038            '[contenteditable="true"]',
3039            '[tabindex]:not([tabindex="-1"])'
3040        ].join(',');
3041
3042        return Array.from(element.querySelectorAll(selectors))
3043            .filter(node => !node.hasAttribute('disabled') && this.isNodeVisible(node));
3044    }
3045
3046    isNodeVisible(node) {
3047        const computedStyle = window.getComputedStyle(node);
3048
3049        if (computedStyle.visibility === 'hidden' || computedStyle.display === 'none') {
3050            return false;
3051        }
3052
3053        const rect = node.getBoundingClientRect();
3054
3055        return (rect.width > 0 || rect.height > 0);
3056    }
3057
3058    focusFirstMeaningful(element) {
3059        const list = this.getFocusableIn(element);
3060
3061        if (list.length) {
3062            list[0].focus();
3063        }
3064    }
3065
3066    checkVisibilityAndFocus(isInit = false) {
3067
3068        this.elements.forEach(element => {
3069            const visibleNow = this.isElementVisible(element);
3070            const state = this.layerState.get(element) || {
3071                visible: false,
3072                lastFocused: null,
3073                initialized: false
3074            };
3075
3076            // einmalig tabindex am Container, aber Container NICHT fokussieren
3077            if (!state.initialized) {
3078                if (!element.hasAttribute('tabindex')) {
3079                    element.setAttribute('tabindex', '-1');
3080                }
3081
3082                state.initialized = true;
3083            }
3084
3085            if (!state.visible && visibleNow) {
3086                // eben sichtbar geworden -> Fokus INS Layer (erstes sinnvolles Ziel oder letzter gespeicherter)
3087                const candidate = (state.lastFocused && element.contains(state.lastFocused))
3088                    ? state.lastFocused
3089                    : null;
3090
3091                setTimeout(() => {
3092                    if (candidate) {
3093                        candidate.focus();
3094                    } else {
3095                        this.focusFirstMeaningful(element);
3096                    }
3097                }, isInit ? 0 : 10);
3098            }
3099
3100            state.visible = visibleNow;
3101            this.layerState.set(element, state);
3102        });
3103    }
3104
3105    enableLayerFocusTrap() {
3106        document.addEventListener('keydown', (event) => {
3107            if (event.key !== 'Tab') {
3108                return;
3109            }
3110
3111            const visibleLayer = this.getTopMostVisibleLayer();
3112
3113            if (!visibleLayer) {
3114                return;
3115            }
3116
3117            const focusables = this.getFocusableIn(visibleLayer);
3118
3119            if (!focusables.length) {
3120                return;
3121            }
3122
3123            const first = focusables[0];
3124            const last  = focusables[focusables.length - 1];
3125            const active = document.activeElement;
3126            const activeIsContainer = active === visibleLayer;
3127
3128            if (event.shiftKey) {
3129                if (activeIsContainer || active === first || !visibleLayer.contains(active)) {
3130                    event.preventDefault();
3131                    last.focus();
3132                }
3133            } else {
3134                if (activeIsContainer || active === last || !visibleLayer.contains(active)) {
3135                    event.preventDefault();
3136                    first.focus();
3137                }
3138            }
3139        });
3140
3141        // track last focused inside layer
3142        document.addEventListener('focusin', (event) => {
3143            const layer = this.getTopMostVisibleLayer();
3144
3145            if (!layer) {
3146                return;
3147            }
3148
3149            if (layer.contains(event.target)) {
3150                const state = this.layerState.get(layer) || {};
3151
3152                state.lastFocused = event.target;
3153                this.layerState.set(layer, state);
3154            }
3155        }, true);
3156    }
3157
3158    bindFocusReturn() {
3159        // Wenn der Fokus außerhalb landet (z.B. Klick in den Hintergrund) und danach ein focusin auf etwas außerhalb passiert,
3160        // hole den Fokus zurück in den sichtbaren Layer - aber nur, wenn überhaupt ein Layer sichtbar ist.
3161        document.addEventListener('focusin', (event) => {
3162            const layer = this.getTopMostVisibleLayer();
3163
3164            if (!layer) {
3165                return;
3166            }
3167
3168            if (layer.contains(event.target)) {
3169                return;
3170            }
3171
3172            const focusables = this.getFocusableIn(layer);
3173
3174            if (!focusables.length) {
3175                return;
3176            }
3177
3178            const state = this.layerState.get(layer) || {};
3179            const candidate = (state.lastFocused && layer.contains(state.lastFocused)) ? state.lastFocused : focusables[0];
3180
3181            // wichtig: verhindern, dass der Browser außerhalb bleibt
3182            event.preventDefault();
3183            candidate.focus();
3184        }, true);
3185    }
3186
3187    getTopMostVisibleLayer() {
3188        const layers = this.elements.filter(element => this.isElementVisible(element));
3189
3190        if (!layers.length) {
3191            return null;
3192        }
3193
3194        // letzte = vermeintlich oberste (DOM-Order, reicht hier idR)
3195        return layers[layers.length - 1];
3196    }
3197
3198    setTabindexForTag(checkbox, tags) {
3199        if (checkbox?.checked) {
3200            tags.forEach(tag => tag.setAttribute('tabindex', '0'));
3201        } else {
3202            tags.forEach(tag => tag.setAttribute('tabindex', '-1'));
3203        }
3204    }
3205
3206    checkCheckedBoxes() {
3207        const checkboxes = document.querySelectorAll('.box-insurance [type="checkbox"]');
3208
3209        if (!checkboxes.length) {
3210            return;
3211        }
3212
3213        checkboxes.forEach(checkbox => {
3214            const moreContent = checkbox.closest('.box-insurance')?.querySelector('.more-content');
3215
3216            if (!moreContent) {
3217                return;
3218            }
3219
3220            const tags = moreContent.querySelectorAll('a, button, label[for]');
3221
3222            this.setTabindexForTag(checkbox, tags);
3223            checkbox.addEventListener('change', () => this.setTabindexForTag(checkbox, tags));
3224        });
3225    }
3226
3227    observeMutations() {
3228        const observer = new MutationObserver((mutations) => {
3229            let needsRecollect = false;
3230            let visibilityMaybeChanged = false;
3231
3232            for (const mutation of mutations) {
3233                if (mutation.type === 'childList') {
3234                    mutation.addedNodes.forEach(node => {
3235                        if (node.nodeType === Node.ELEMENT_NODE &&
3236                            this.classSelectors.some(selections => node.matches(selections))) {
3237                            needsRecollect = true;
3238                        }
3239                    });
3240                    mutation.removedNodes.forEach(node => {
3241                        if (node.nodeType === Node.ELEMENT_NODE &&
3242                            this.classSelectors.some(selections => node.matches(selections))) {
3243                            needsRecollect = true;
3244                        }
3245                    });
3246                    visibilityMaybeChanged = true;
3247                } else if (mutation.type === 'attributes') {
3248                    // class/style ändern evtl. Sichtbarkeit
3249                    if (this.classSelectors.some(selections => mutation.target.matches(selections))) {
3250                        visibilityMaybeChanged = true;
3251                    }
3252                }
3253            }
3254
3255            if (needsRecollect) {
3256                this.collectElements();
3257                this.initLayerState();
3258            }
3259
3260            if (visibilityMaybeChanged) {
3261                // Wichtig: NICHT immer fokusieren - nur Übergänge behandeln
3262                this.checkVisibilityAndFocus();
3263            }
3264        });
3265
3266        observer.observe(document.body, {
3267            childList: true,
3268            subtree: true,
3269            attributes: true,
3270            attributeFilter: ['style', 'class']
3271        });
3272    }
3273}
3274
3275document.addEventListener('DOMContentLoaded', () => {
3276    new VisibilityFocusHandler();
3277
3278    //////// Focus Success-Layer ////////
3279    const successMsg = document.getElementById('success-message');
3280
3281    if (successMsg) {
3282        successMsg.focus();
3283    }
3284});
3285
3286class DynamicSave {
3287    constructor() {
3288        // Singleton-Pattern
3289        if (DynamicSave.instance) {
3290            return DynamicSave.instance;
3291        }
3292
3293        // Classes
3294        this.fragmentMatcherClass = new FragmentMatcher();
3295
3296        // HTML-Elemente
3297        this.actionBtns = document?.querySelectorAll('.action-btn');
3298        this.bySideContent = document?.querySelector('.resolution-by-side-content');
3299        this.bySideContentFill = this.bySideContent?.querySelector('.box-default');
3300
3301        // Klassen-/String-Definitionen
3302        this.activeClass = 'dynamic-save-active';
3303        this.replaceContentClass = 'data-replace-content';
3304        this.formContentClass = 'dynamic-save-content';
3305        this.noneClass = 'd-none';
3306        this.disabledClass = 'disabled';
3307
3308        // Konfigurationen
3309        this.bySideResolution = 768; // Schwellenwert für Layoutumschaltung
3310        this.resizeTimeout    = 100; // Zeit in ms für Resize-Timeout
3311
3312        this.init();
3313
3314        // Instanz speichern
3315        DynamicSave.instance = this;
3316    }
3317
3318    /**
3319     * Leitet zu einer bestimmten URL weiter.
3320     * @param {Object} response - Serverantwort.
3321     * @returns {boolean} Ob die Weiterleitung erfolgreich war.
3322     */
3323    redirectToUrl(response) {
3324        const mandator = window?.application?.mandant ?? '';
3325        if (response?.redirectRoute) {
3326            const delimiter = response.redirectRoute.includes('?') ? '&' : '?';
3327            window.location.href = `//${location.host}${response.redirectRoute}${delimiter}mandator=${mandator}`;
3328            return false;
3329        }
3330        return true;
3331    }
3332
3333    /**
3334     * Setzt den HTML-Inhalt und die entsprechenden Klassen.
3335     * @param {string|null} content - Neuer HTML-Inhalt.
3336     * @param {HTMLElement|null} formContent - Formularinhalt.
3337     * @param {HTMLElement|null} appender - Ziel-Element.
3338     */
3339    setHtmlAndClasses(content = null, formContent = null, appender = null) {
3340        const activeReplaceContent = formContent?.querySelector(`.${this.replaceContentClass}`);
3341
3342        if (!appender || !formContent) {
3343            throw new Error('Invalid parameters passed to setHtmlAndClasses!');
3344        }
3345
3346        appender.innerHTML = content;
3347        formContent.classList.add(this.activeClass);
3348
3349        if (activeReplaceContent) {
3350            if (!this.isResolutionForAside()) {
3351                this.bySideContent?.classList.add(this.noneClass);
3352                activeReplaceContent.classList.remove(this.noneClass);
3353                this.updatejQueryDateInput(activeReplaceContent);
3354                this.updateFormValidation(activeReplaceContent);
3355                this.updatejQueryIbanPlugin(activeReplaceContent);
3356                this.scrollTo(appender);
3357            } else {
3358                this.bySideContent?.classList.remove(this.noneClass);
3359                activeReplaceContent.classList.add(this.noneClass);
3360                this.updatejQueryDateInput(this.bySideContent);
3361                this.updateFormValidation(this.bySideContent);
3362                this.updatejQueryIbanPlugin(this.bySideContent);
3363                this.scrollTo(formContent);
3364            }
3365
3366            setTimeout(() => {
3367                const autofocusElement = formContent.querySelector('[autofocus]') || appender.querySelector('[autofocus]');
3368                if (autofocusElement) {
3369                    autofocusElement.focus();
3370                }
3371            }, 250);
3372            return;
3373        }
3374
3375        throw new Error('Error in setHtmlAndClasses: Active replace content missing.');
3376    }
3377
3378    /**
3379     * Aktualisiert Inhalte basierend auf der Serverantwort.
3380     * @param {string|null} content - Neuer HTML-Inhalt.
3381     * @param {HTMLElement|null} formContent - Formularinhalt.
3382     * @param {HTMLElement|null} appender - Ziel-Element.
3383     */
3384    updateContent(content = null, formContent = null, appender = null) {
3385        if (!content || !formContent || !appender) {
3386            throw new Error('Missing parameters in updateContent!');
3387        }
3388
3389        this.clearContents();
3390        this.setHtmlAndClasses(content, formContent, appender);
3391        this.positioningContent();
3392
3393        if (typeof FormHandler === 'function') {
3394            new FormHandler(appender.querySelector('form'));
3395        }
3396    }
3397
3398    /**
3399     * Wechselt den Ladezustand eines Buttons/Links.
3400     * @param {HTMLElement|null} btnOrLink - Ziel-Element.
3401     */
3402    changeStateOfLoaderInButton(btnOrLink = null) {
3403        if (!btnOrLink) return;
3404
3405        const spinner = btnOrLink?.querySelector('.spinner-border');
3406        const isButton = btnOrLink.tagName === 'BUTTON';
3407
3408        if (spinner) {
3409            const isHidden = spinner.classList.contains(this.noneClass);
3410            btnOrLink.disabled = isHidden;
3411            btnOrLink.classList.toggle(this.disabledClass, isHidden);
3412            spinner?.classList.toggle(this.noneClass, !isHidden);
3413        }
3414    }
3415
3416    /**
3417     * Standard-Fetch-Aufruf mit optionaler Konfiguration.
3418     */
3419    standardFetch(url = null, formContent = null, appender = null, buttonOrLink = null, conf = {}) {
3420        if (!url || !formContent || !appender) {
3421            throw new Error('Invalid parameters passed to standardFetch!');
3422        }
3423
3424        const defaultOptions = {
3425            method: 'POST',
3426            mode: 'cors',
3427            credentials: 'include',
3428        };
3429        const options = { ...defaultOptions, ...conf };
3430
3431        this.changeStateOfLoaderInButton(buttonOrLink);
3432
3433        fetch(url, options)
3434            .then((response) => {
3435                if (!response.ok) throw new Error('Network response was not ok');
3436                return response.json();
3437            })
3438            .then((response) => {
3439                if (!this.redirectToUrl(response) || !response?.content) return;
3440                this.updateContent(response.content, formContent, appender);
3441                if (!response?.success) this.setError();
3442            })
3443            .catch((error) => this.setError(error))
3444            .finally(() => this.changeStateOfLoaderInButton(buttonOrLink));
3445    }
3446
3447    /**
3448     * Gibt Fehler bei Fetches zurück.
3449     */
3450    setError() {
3451        // @todo: Later!
3452        return true;
3453    }
3454
3455    /**
3456     * Aktualisiert Formularvalidierung für ein Element.
3457     * @param {HTMLElement|null} element - Ziel-Element.
3458     */
3459    updateFormValidation(element = null) {
3460        if (typeof $.fn?.validateBase === 'function' && element) {
3461            const validateFormClasses = ['.js-form-validation-base'];
3462            validateFormClasses.forEach((className) => {
3463                $(element).find(className).validateBase();
3464            });
3465        }
3466    }
3467
3468    updatejQueryDateInput(contentFill = null) {
3469        if (typeof $.fn?.ibanComplete === 'function' && contentFill) {
3470            const setMinDateInput = contentFill?.querySelectorAll('input[type="date"][data-set-min]');
3471            const setMaxDateInput = contentFill?.querySelectorAll('input[type="date"][data-set-max]');
3472            const setDefaultDateInput = contentFill?.querySelectorAll('input[type="date"][data-default-date]');
3473
3474            [setMinDateInput, setMaxDateInput]?.forEach(arr => {
3475                arr?.forEach(input => {
3476                    if (typeof $.fn.setDate === 'function') {
3477                        $(input).setDate();
3478                    }
3479                });
3480            });
3481
3482            setDefaultDateInput?.forEach(input => {
3483                if (typeof $.fn.setDefaultDate === 'function') {
3484                    $(input).setDefaultDate();
3485                }
3486            });
3487        }
3488    }
3489
3490    updatejQueryIbanPlugin(contentFill = null) {
3491        if (typeof $.fn?.ibanComplete === 'function' && contentFill) {
3492            const thatContentFill = $(contentFill);
3493            thatContentFill.find('[data-get-bank]').each(function () {
3494                const that = this;
3495                $(that).ibanComplete();
3496                setTimeout(() => {
3497                    if (that.value) {
3498                        that.dispatchEvent(new Event('keyup'));
3499                    }
3500                }, 150);
3501            });
3502        }
3503
3504        if (typeof new IbanMask === 'function') {
3505            new IbanMask();
3506        }
3507    }
3508
3509    /**
3510     * Prüft, ob die Auflösung groß genug für das Seitenlayout ist.
3511     * @returns {boolean}
3512     */
3513    isResolutionForAside() {
3514        return window.innerWidth >= this.bySideResolution;
3515    }
3516
3517    /**
3518     * Aktualisiert Inhalte basierend auf der Bildschirmauflösung.
3519     */
3520    replacementContent() {
3521        const activeContent = document?.querySelector(`.${this.activeClass}`);
3522        const activeReplaceContent = activeContent?.querySelector(`.${this.replaceContentClass}`);
3523        let form = null;
3524
3525        if (!this.isResolutionForAside() && activeReplaceContent
3526            && (activeReplaceContent.children.length === 0 && activeReplaceContent.textContent.trim() === '')) {
3527            activeReplaceContent.innerHTML = this.bySideContentFill?.innerHTML;
3528            this.bySideContentFill.innerHTML = '';
3529            this.bySideContent?.classList.add(this.noneClass);
3530            activeReplaceContent.classList.remove(this.noneClass);
3531            this.updatejQueryDateInput(activeReplaceContent);
3532            this.updateFormValidation(activeReplaceContent);
3533            this.updatejQueryIbanPlugin(activeReplaceContent);
3534            this.scrollTo(activeReplaceContent);
3535            form = activeReplaceContent.querySelector('form');
3536        } else if (this.isResolutionForAside() && activeReplaceContent
3537            && (this.bySideContentFill.children.length === 0 && this.bySideContentFill.textContent.trim() === '')) {
3538            this.bySideContentFill.innerHTML = activeReplaceContent.innerHTML;
3539            activeReplaceContent.innerHTML = '';
3540            this.bySideContent?.classList.remove(this.noneClass);
3541            activeReplaceContent.classList.add(this.noneClass);
3542            this.updatejQueryDateInput(this.bySideContent);
3543            this.updateFormValidation(this.bySideContent);
3544            this.updatejQueryIbanPlugin(this.bySideContent);
3545            this.scrollTo(activeContent);
3546            form = this.bySideContent?.querySelector('form');
3547        }
3548
3549        if (typeof FormHandler === 'function' && form) {
3550            new FormHandler(form);
3551        }
3552    }
3553
3554    /**
3555     * Positioniert den Inhalt basierend auf der Auflösung.
3556     */
3557    positioningContent() {
3558        const activeContent = document?.querySelector(`.${this.activeClass}`);
3559
3560        if (!this.isResolutionForAside()) {
3561            this.bySideContent?.removeAttribute('style');
3562        } else if (this.isResolutionForAside() && activeContent) {
3563            this.bySideContent.style.paddingTop = `${activeContent.offsetTop}px`;
3564        }
3565    }
3566
3567    /**
3568     * Beendet eine Resize-Aktion und führt Ersatzaktionen aus.
3569     */
3570    onResizeEnd() {
3571        this.replacementContent();
3572        this.positioningContent();
3573    }
3574
3575    /**
3576     * Scrollt smooth zum Zielelement
3577     * @param elementToScroll
3578     * @returns {boolean}
3579     */
3580    scrollTo(elementToScroll = null) {
3581        if (!elementToScroll) {
3582            return false;
3583        }
3584
3585        setTimeout(() => {
3586            const offset = 90;
3587            const top = elementToScroll.getBoundingClientRect().top + window.scrollY - offset;
3588
3589            window.scrollTo({ top, behavior: "smooth" });
3590        }, 500)
3591    }
3592
3593   /**
3594     *
3595     */
3596    openByFragment() {
3597       const openTriggerPersData = document.getElementById('contactDataTrigger');
3598       const openBankData = document.getElementById('bankDataTrigger');
3599       const openAddressData = document.getElementById('addressDataTrigger');
3600
3601       const persDataActive    = this.fragmentMatcherClass.getSpecific(this.fragmentMatcherClass.availableKeys.openPersData);
3602       const bankDataActive    = this.fragmentMatcherClass.getSpecific(this.fragmentMatcherClass.availableKeys.bankData);
3603       const addressDataActive = this.fragmentMatcherClass.getSpecific(this.fragmentMatcherClass.availableKeys.addressData);
3604
3605       let button;
3606
3607       if (bankDataActive) {
3608           button = persDataActive ? openTriggerPersData : openBankData;
3609       } else if (addressDataActive) {
3610           button = persDataActive ? openTriggerPersData : openAddressData;
3611       } else {
3612           button = persDataActive ? openTriggerPersData : null;
3613       }
3614
3615       if (button) {
3616           const formContent = button.closest(`.${this.formContentClass}`);
3617           const appender = this.isResolutionForAside()
3618               ? this.bySideContentFill
3619               : formContent?.querySelector(`.${this.replaceContentClass}`);
3620
3621           this.standardFetch(button?.dataset?.url, formContent, appender, button);
3622       }
3623    }
3624
3625    /**
3626     * Löscht alle aktiven Inhalte und setzt den Zustand zurück.
3627     */
3628    clearContents() {
3629        const actuallyActiveContent = document?.querySelector(`.${this.activeClass}`);
3630        const replaceContents = document?.querySelectorAll(`.${this.replaceContentClass}`);
3631
3632        if (actuallyActiveContent) {
3633            replaceContents?.forEach((content) => {
3634                content.innerHTML = '';
3635                content.classList.add(this.noneClass);
3636            });
3637            this.bySideContentFill.innerHTML = '';
3638            actuallyActiveContent.classList.remove(this.activeClass);
3639            this.bySideContent?.classList.add(this.noneClass);
3640        }
3641    }
3642
3643    /**
3644     * Fügt den Klick-Handler für Aktionen hinzu.
3645     */
3646    addAction() {
3647        this?.actionBtns?.forEach((actionBtn) => {
3648            actionBtn.addEventListener('click', (ev) => {
3649                const formContent = actionBtn?.closest(`.${this.formContentClass}`);
3650                const appender = this.isResolutionForAside()
3651                    ? this.bySideContentFill
3652                    : formContent?.querySelector(`.${this.replaceContentClass}`);
3653
3654                ev.preventDefault();
3655
3656                this.standardFetch(actionBtn.href || actionBtn?.dataset?.url, formContent, appender, actionBtn);
3657            });
3658        });
3659    }
3660
3661    /**
3662     * Fügt Resize-Event-Handling hinzu.
3663     */
3664    resize() {
3665        let timeoutForResize;
3666
3667        window.addEventListener('resize', () => {
3668            clearTimeout(timeoutForResize);
3669            timeoutForResize = setTimeout(() => {
3670                this.onResizeEnd();
3671            }, this.resizeTimeout);
3672        });
3673    }
3674
3675    /**
3676     * Initialisiert die Klasse.
3677     */
3678    init() {
3679        this.addAction();
3680        this.resize();
3681        setTimeout(() => {
3682            this.openByFragment();
3683        }, 750);
3684    }
3685}
3686
3687document.addEventListener('DOMContentLoaded', () => {
3688    new DynamicSave();
3689});
3690
3691class IbanMask {
3692    constructor() {
3693        this.ibanInputs = document.querySelectorAll('.iban-mask');
3694
3695        // IBAN-Formate pro Land: Jedes Format gibt die Gruppengrößen an
3696        this.ibanFormats = {
3697            AT: [4, 4, 4, 4, 4],          // Österreich (AT)
3698            BE: [4, 4, 4, 4],             // Belgien (BE)
3699            BG: [4, 4, 4, 4, 4, 2],       // Bulgarien (BG)
3700            CH: [4, 4, 4, 4, 4, 1],       // Schweiz (CH)
3701            CY: [4, 4, 4, 4, 4, 4, 4],    // Zypern (CY)
3702            CZ: [4, 4, 4, 4, 4, 4],       // Tschechien (CZ)
3703            DE: [4, 4, 4, 4, 4, 2],       // Deutschland (DE)
3704            DK: [4, 4, 4, 4, 2],          // Dänemark (DK)
3705            EE: [4, 4, 4, 4, 4],          // Estland (EE)
3706            ES: [4, 4, 4, 4, 4, 4],       // Spanien (ES)
3707            FI: [4, 4, 4, 4, 2],          // Finnland (FI)
3708            FR: [4, 4, 4, 4, 4, 4, 3],    // Frankreich (FR)
3709            GB: [4, 4, 4, 4, 4, 2],       // Großbritannien (GB)
3710            GR: [4, 4, 4, 4, 4, 4, 3],    // Griechenland (GR)
3711            HR: [4, 4, 4, 4, 4, 1],       // Kroatien (HR)
3712            HU: [4, 4, 4, 4, 4, 4, 4],    // Ungarn (HU)
3713            IE: [4, 4, 4, 4, 4, 2],       // Irland (IE)
3714            IS: [4, 4, 4, 4, 4, 4, 2],    // Island (IS)
3715            IT: [4, 4, 4, 4, 4, 4, 3],    // Italien (IT)
3716            LI: [4, 4, 4, 4, 4, 1],       // Liechtenstein (LI)
3717            LT: [4, 4, 4, 4, 4],          // Litauen (LT)
3718            LU: [4, 4, 4, 4, 4],          // Luxemburg (LU)
3719            LV: [4, 4, 4, 4, 4, 1],       // Lettland (LV)
3720            MT: [4, 4, 4, 4, 4, 4, 4, 3], // Malta (MT)
3721            NL: [4, 4, 4, 4, 2],          // Niederlande (NL)
3722            NO: [4, 4, 4, 3],             // Norwegen (NO)
3723            PL: [4, 4, 4, 4, 4, 4, 4],    // Polen (PL)
3724            PT: [4, 4, 4, 4, 4, 4, 1],    // Portugal (PT)
3725            RO: [4, 4, 4, 4, 4, 4],       // Rumänien (RO)
3726            SE: [4, 4, 4, 4, 4, 4],       // Schweden (SE)
3727            SI: [4, 4, 4, 4, 3],          // Slowenien (SI)
3728            SK: [4, 4, 4, 4, 4, 4]        // Slowakei (SK)
3729        };
3730
3731        if (!this.ibanInputs.length) {
3732            return;
3733        }
3734
3735        this.init();
3736    }
3737
3738    // IBAN maskieren basierend auf dem Ländercode und Format
3739    maskIban(ibanInput) {
3740        const input = ibanInput;
3741        let value = input.value.replace(/\s+/g, '').toUpperCase(); // Leerzeichen entfernen und Großbuchstaben erzwingen
3742        const countryCode = value.slice(0, 2);
3743        const format = this.ibanFormats[countryCode];
3744
3745        if (format) {
3746            let maskedIban = '';
3747            let startIndex = 0;
3748
3749            format.forEach((blockSize, index) => {
3750                if (startIndex >= value.length) return;
3751                const block = value.slice(startIndex, startIndex + blockSize);
3752                maskedIban += block + (index < format.length - 1 ? ' ' : '');
3753                startIndex += blockSize;
3754            });
3755
3756            input.value = maskedIban.trim();
3757        } else {
3758            input.value = value.replace(/(.{4})/g, '$1 ').trim();
3759        }
3760    }
3761
3762    init() {
3763        this.ibanInputs.forEach((ibanInput) => {
3764            if (ibanInput.classList.contains('iban-mask-init')) {
3765                return;
3766            }
3767            ibanInput.addEventListener('input', (event) => {
3768                this.maskIban(event.target);
3769            });
3770
3771            this.maskIban(ibanInput);
3772
3773            ibanInput.classList.add('iban-mask-init');
3774        });
3775    }
3776}
3777
3778document.addEventListener('DOMContentLoaded', () => {
3779    new IbanMask();
3780});
3781
3782class SelectExistIban {
3783    constructor() {
3784        this.checkboxes = document.querySelectorAll('.select-exist-iban');
3785        this.hiddenClass = 'd-none';
3786
3787        if (!this.checkboxes.length) {
3788            return;
3789        }
3790
3791        this.init();
3792    }
3793
3794    isChecked(checkbox) {
3795        return checkbox.checked;
3796    }
3797
3798    handleFieldsRequiredAndHideOrShow(containers = null, show = true) {
3799        if (containers.length) {
3800            containers.forEach((item) => {
3801                if (show) {
3802                    item.classList.remove(this.hiddenClass);
3803                    item?.querySelectorAll('[data-required]')?.forEach((field) => {
3804                        field.setAttribute('required', '');
3805                        field.removeAttribute('data-required');
3806                    });
3807                } else {
3808                    item.classList.add(this.hiddenClass);
3809                    item?.querySelectorAll('[required]')?.forEach((field) => {
3810                        field.setAttribute('data-required', '');
3811                        field.removeAttribute('required');
3812                    });
3813                }
3814            });
3815        }
3816    }
3817
3818    handleCheckboxChange(checkbox) {
3819        const checkboxClosestForm = checkbox.closest('form');
3820        const hideIfChecked = checkboxClosestForm?.querySelectorAll(checkbox?.dataset?.hideIban) ?? 0;
3821        if (hideIfChecked.length) {
3822            if (this.isChecked(checkbox)) {
3823                this.handleFieldsRequiredAndHideOrShow(hideIfChecked, false);
3824            } else {
3825                this.handleFieldsRequiredAndHideOrShow(hideIfChecked, true);
3826            }
3827        }
3828    }
3829
3830    init() {
3831        this.checkboxes.forEach(checkbox => {
3832            if (checkbox.classList.contains('select-exist-iban-init')) {
3833                return;
3834            }
3835            this.handleCheckboxChange(checkbox);
3836            checkbox.addEventListener('change', e => {
3837                this.handleCheckboxChange(checkbox);
3838            });
3839
3840            checkbox.classList.contains('select-exist-iban-init');
3841        });
3842    }
3843}
3844
3845document.addEventListener('DOMContentLoaded', e => {
3846    new SelectExistIban();
3847});
3848
3849class ScrollToContentInvestment {
3850    constructor() {
3851        this.scrollToHash(this.getHash());
3852        window.addEventListener('hashchange', event => {
3853            this.scrollToHash(this.getHash());
3854            event.stopPropagation();
3855        });
3856
3857        this.scrollAndOpenByClick();
3858        this.scrollToFragment();
3859    }
3860
3861    getHash() {
3862        if (window?.location?.hash) {
3863            return window?.location?.hash;
3864        }
3865        return null;
3866    }
3867
3868    scrollToHash(hash = null) {
3869        if (hash) {
3870            const targetElement = document?.querySelector(`[data-reporting-type="${hash}"]`);
3871            const params = new URLSearchParams(window.location.search);
3872            const open = params.get('fragment');
3873            const openElement = document.querySelector(`[data-reporting-type="${open}"]`);
3874            const hasReportingType = typeof targetElement?.dataset?.reportingType !== 'undefined';
3875
3876            if (openElement) {
3877                this.openAccordion(hasReportingType, openElement);
3878            }
3879
3880            if (targetElement) {
3881                this.scrollSmooth(targetElement);
3882            }
3883        }
3884    }
3885
3886    scrollToFragment() {
3887        const params = new URLSearchParams(window.location.search);
3888        const scrollTo = params.get('fragment');
3889
3890        if (scrollTo) {
3891            const targetElement =
3892                document.querySelector(`[data-scrollto="${scrollTo}"]`) ||
3893                document.querySelector(`[data-reporting-type="${scrollTo}"]`);
3894            const hasReportingType = typeof targetElement.dataset.reportingType !== 'undefined';
3895
3896            if (targetElement) {
3897                this.scrollSmooth(targetElement);
3898                this.openAccordion(hasReportingType, targetElement);
3899            }
3900        }
3901    }
3902
3903    openAccordion(hasReportingType = false, targetElement = null) {
3904        if (!hasReportingType || !targetElement) {
3905            return;
3906        }
3907        const trigger = targetElement.querySelector('.accordion-trigger:not([aria-expanded="true"])');
3908
3909        setTimeout(() => {
3910            trigger?.click();
3911        }, 500);
3912    }
3913
3914    scrollAndOpenByClick() {
3915        const links   = document.querySelectorAll('.js-slide-to-reportings');
3916
3917        links.forEach(link => {
3918            link.addEventListener('click', event => {
3919                this.scrollToHash(this.getHash());
3920                event.stopPropagation();
3921            });
3922        });
3923    }
3924
3925    scrollSmooth(element = null, offset = 90) {
3926        if (!element) {
3927            return;
3928        }
3929
3930        setTimeout(() => {
3931            const elementPosition = element.getBoundingClientRect().top + window.scrollY - offset;
3932
3933            window.scrollTo({
3934                top: elementPosition,
3935                behavior: 'smooth'
3936            });
3937        }, 1000);
3938    }
3939}
3940
3941document.addEventListener('DOMContentLoaded', () => {
3942    new ScrollToContentInvestment();
3943});
3944
3945class OutdatedEmail
3946{
3947    constructor() {
3948        // Test Singleton
3949        if (OutdatedEmail.instance) {
3950            return OutdatedEmail.instance;
3951        }
3952        // DOM Elements
3953        this.layer                       = document.querySelector('.email-outdated-layer');
3954        this.errorContent                = this.layer?.querySelector('.js-email-outdated-error-content');
3955        this.formContent                 = this.layer?.querySelector('.form-content');
3956        this.layerTrigger                = null;
3957        // CSS Classes
3958        this.loaderClass                 = '.layer-loader';
3959        this.closeTriggerClass           = '.js-close-email-outdated-layer';
3960        this.shownClass                  = 'is-shown';
3961        this.noneClass                   = 'd-none';
3962        // Intervals
3963        this.checkInterval               = null;
3964        this.errorTimeout                = null;
3965        this.intervalDuration            = 1000;
3966        this.errorTimeoutDuration        = 5000;
3967        // Event Handlers
3968        this.boundCloseLayerClickHandler = this.closeLayerClickHandler.bind(this);
3969        // JS Classes
3970        this.utilities                   = new Utilities();
3971        // INIT
3972        this.init();
3973        // Singleton
3974        OutdatedEmail.instance           = this;
3975    }
3976
3977    init() {
3978        if (this.layer) {
3979            this.checkInterval = setInterval(() => {
3980                if (!document.querySelectorAll('.layer-default.is-shown').length) {
3981                    this.openLayer();
3982                    clearInterval(this.checkInterval);
3983                    this.checkInterval = null;
3984                }
3985            }, this.intervalDuration);
3986        }
3987    }
3988
3989    validateFormInLayer() {
3990        if (typeof $.fn?.validateBase === 'function') {
3991            const validateFormClasses = [
3992                '.email-outdated-layer-validate-form',
3993            ];
3994
3995            validateFormClasses.forEach(className => {
3996                $(this.layer).find(className).validateBase();
3997            });
3998        }
3999    }
4000
4001    openLayer() {
4002        this.layer.classList.add(this.shownClass);
4003        setTimeout(() => {
4004            this.layer?.focus();
4005        }, 500);
4006
4007        this.validateFormInLayer();
4008        this.handleSubmit();
4009        this.closeLayer();
4010    }
4011
4012    closeLayer() {
4013        setTimeout(() => {
4014            const closeTriggers = this.layer?.querySelectorAll(this.closeTriggerClass);
4015
4016            if (this.layerTrigger) {
4017                this.layerTrigger?.focus();
4018
4019                setTimeout(() => {
4020                    this.layerTrigger = null;
4021                }, 300);
4022            }
4023
4024            if (closeTriggers?.length) {
4025                closeTriggers.forEach(close => {
4026                    close.removeEventListener('click', this.boundCloseLayerClickHandler);
4027                    close.addEventListener('click', this.boundCloseLayerClickHandler);
4028                });
4029            }
4030        }, 500);
4031    }
4032
4033    showErrorMessage(error) {
4034        if (!this.errorContent) return;
4035
4036        const alert = this.errorContent?.querySelector('.alert');
4037        const alertHeader = alert?.querySelector('.alert-heading');
4038        const alertText = alert?.querySelector('p');
4039        const defaultHeader = this.utilities.getTextFromWindowVars('messages.errorAlert');
4040        const defaultMessage = this.utilities.getTextFromWindowVars('messages.errorGlobal');
4041
4042        if (alertHeader) {
4043            alertHeader.textContent = error?.header || defaultHeader;
4044        }
4045
4046        if (alertText) {
4047            alertText.textContent = (error.constructor === Object && error.message)
4048                ? error?.message
4049                : defaultMessage;
4050        }
4051
4052        this.errorContent.classList.add(this.shownClass);
4053
4054        if (this.errorTimeout) {
4055            clearTimeout(this.errorTimeout);
4056            this.errorTimeout = null;
4057        }
4058
4059        this.errorTimeout = setTimeout(() => {
4060            this.errorContent.classList.remove(this.shownClass);
4061            this.errorTimeout = null;
4062        }, this.errorTimeoutDuration);
4063    }
4064
4065    closeErrorMessage() {
4066        if (!this.errorContent) return;
4067
4068        this.errorContent.classList.remove(this.shownClass);
4069
4070        if (this.errorTimeout) {
4071            clearTimeout(this.errorTimeout);
4072            this.errorTimeout = null;
4073        }
4074    }
4075
4076    setError(error = null) {
4077        if (error) {
4078            this.showFormContent();
4079            this.hideLoader();
4080            this.showErrorMessage(error);
4081        }
4082    }
4083
4084    showLoader() {
4085        this.layer.querySelector(this.loaderClass).classList.remove(this.noneClass);
4086    }
4087
4088    hideLoader() {
4089        this.layer.querySelector(this.loaderClass).classList.add(this.noneClass);
4090    }
4091
4092    hideFormContent() {
4093        this.formContent.classList.add(this.noneClass);
4094    }
4095
4096    showFormContent() {
4097        this.formContent.classList.remove(this.noneClass);
4098    }
4099
4100    closeLayerClickHandler(event) {
4101        event.preventDefault();
4102
4103        this.layer.classList.remove(this.shownClass);
4104
4105        setTimeout(() => {
4106            this.layer?.querySelector('form')?.reset();
4107
4108            this.closeErrorMessage();
4109
4110            setTimeout(() => {
4111                const closeTriggers = this.layer?.querySelectorAll(this.closeTriggerClass);
4112
4113                this.hideLoader();
4114
4115                closeTriggers?.forEach(close => {
4116                    close.removeEventListener('click', this.boundCloseLayerClickHandler);
4117                });
4118            }, 250);
4119        }, 250);
4120    }
4121
4122    handleSubmit() {
4123        const form = this.layer?.querySelector('form');
4124
4125        if (form) {
4126            form.addEventListener('submit', (event) => {
4127                event.preventDefault();
4128                this.hideFormContent();
4129                this.showLoader();
4130
4131                fetch(form.action, {
4132                    method: 'POST',
4133                    mode: 'cors',
4134                    credentials: 'include',
4135                    body: new FormData(form),
4136                })
4137                .then(response => {
4138                    if (!response.ok) {
4139                        throw new Error('Network response was not ok');
4140                    }
4141                    return response.json();
4142                })
4143                .then(response => {
4144                    if (response?.success) {
4145                        this.layer.classList.remove(this.shownClass);
4146                        setTimeout(() => {
4147                            this.layer.remove();
4148                        }, 1000);
4149                    }
4150
4151                    if (!response?.success) {
4152                        this.setError(response);
4153                        this.showFormContent();
4154                        this.hideLoader();
4155                    }
4156                })
4157                .catch(error => {
4158                    this.setError(error);
4159                    this.showFormContent();
4160                    this.hideLoader();
4161                });
4162            });
4163        }
4164    }
4165}
4166
4167document.addEventListener('DOMContentLoaded', () => {
4168    new OutdatedEmail();
4169});
4170
4171class PowerGas
4172{
4173    constructor() {
4174        this.form = document.querySelector('form[name="power_gas"]');
4175
4176        this.observeMutations();
4177    }
4178
4179    observeMutations() {
4180        const observer = new MutationObserver(mutations => {
4181            mutations.forEach(mutation => {
4182
4183                if (mutation.type === 'childList' && this.form !== null) {
4184                    const hasDocuments = document.querySelectorAll('.file-upload-preview').length > 0;
4185
4186                    if (hasDocuments) {
4187                        this.form.querySelectorAll('[required]').forEach(requiredElement => {
4188                            requiredElement.setAttribute('data-was-required', 'true');
4189                            requiredElement.required = false;
4190                            requiredElement.classList.remove('form-control-danger');
4191                            requiredElement.classList.remove('form-control-warning');
4192                        });
4193                        this.form.querySelectorAll('[data-ibantest]').forEach(requiredElement => {
4194                            requiredElement.setAttribute('data-was-ibantest', 'true');
4195                            requiredElement.removeAttribute('data-ibantest');
4196                            requiredElement.classList.remove('form-control-danger');
4197                            requiredElement.classList.remove('form-control-warning');
4198                        });
4199                    } else {
4200                        this.form.querySelectorAll('[data-was-required="true"]').forEach(requiredElement => {
4201                            requiredElement.required = true;
4202                            requiredElement.removeAttribute('data-was-required');
4203                        });
4204                        this.form.querySelectorAll('[data-was-ibantest]').forEach(requiredElement => {
4205                            requiredElement.setAttribute('data-ibantest', 'optional');
4206                            requiredElement.removeAttribute('data-was-ibantest');
4207                        });
4208                    }
4209                }
4210            });
4211        });
4212
4213        observer.observe(document.body, {
4214            childList: true,
4215            subtree: true,
4216            attributes: false,
4217        });
4218    }
4219
4220    updateFormValidation() {
4221        if (typeof $.fn?.validateBase === 'function') {
4222            const validateFormClasses = ['.js-form-validation-base'];
4223            validateFormClasses.forEach(className => {
4224                $(className).validateBase();
4225            });
4226        }
4227    }
4228}
4229
4230document.addEventListener('DOMContentLoaded', () => {
4231    new PowerGas();
4232});
4233
4234class LoadUserImageForm
4235{
4236    constructor() {
4237        this.init();
4238    }
4239
4240    init() {
4241        this.loadForm();
4242    }
4243
4244    loadForm() {
4245        const triggerElements = document.querySelectorAll('.js-account-image-trigger');
4246        const formContainer = document.querySelector('.js-customer-image-upload');
4247
4248        triggerElements.forEach(trigger => {
4249            trigger.addEventListener('click', event => this.handleTriggerClick(event, formContainer));
4250        });
4251    }
4252
4253    handleTriggerClick(event, formContainer) {
4254        event.preventDefault();
4255
4256        if (!formContainer || this.uploadFormExists(formContainer)) return;
4257
4258        fetch(event.currentTarget?.dataset?.url, {
4259            method: 'POST',
4260            mode: 'cors',
4261            credentials: 'include',
4262        })
4263            .then(response => this.handleResponse(response))
4264            .then(response => this.processResponse(response, formContainer))
4265            .catch(error => this.setError(error));
4266    }
4267
4268    uploadFormExists(formContainer) {
4269        return formContainer.querySelectorAll('form').length >= 1;
4270    }
4271
4272    handleResponse(response) {
4273        if (!response.ok) throw new Error('Network response was not ok');
4274        return response.json();
4275    }
4276
4277    processResponse(response, formContainer) {
4278        if (!this.redirectToUrl(response) || !response?.content) return;
4279
4280        formContainer.insertAdjacentHTML('beforeend', response.content);
4281        this.initializePlugins();
4282        this.setupCloseEvent(formContainer);
4283        formContainer.classList.add('active');
4284
4285        if (!response?.success) this.setError();
4286    }
4287
4288    initializePlugins() {
4289        if (typeof $.fn.fileUpload === 'function') {
4290            $('.js-customer-image-upload .js-upload').each(function () {
4291                $(this).fileUpload();
4292            });
4293        }
4294
4295        if (typeof $.fn.ajaxForm === 'function') {
4296            $('.js-customer-image-upload .js-send-as-ajax').each(function () {
4297                $(this).ajaxForm();
4298            });
4299        }
4300    }
4301
4302    setupCloseEvent(formContainer) {
4303        const closeTrigger = formContainer.querySelector('.js-customer-image-upload-close');
4304        if (!closeTrigger) return;
4305
4306        closeTrigger.removeEventListener('click', () => {});
4307        closeTrigger.addEventListener('click', event => {
4308            event.preventDefault();
4309            formContainer.classList.remove('active');
4310            formContainer.querySelector('form')?.remove();
4311        });
4312    }
4313
4314    redirectToUrl(response) {
4315        const mandator = window?.application?.mandant ?? '';
4316        if (response?.redirectRoute) {
4317            const delimiter = response.redirectRoute.includes('?') ? '&' : '?';
4318            window.location.href = `//${location.host}${response.redirectRoute}${delimiter}mandator=${mandator}`;
4319            return false;
4320        }
4321        return true;
4322    }
4323
4324    setError() {
4325        // @todo: Later!
4326        return true;
4327    }
4328}
4329
4330document.addEventListener('DOMContentLoaded', () => {
4331    new LoadUserImageForm();
4332});
4333
4334class Tooltip {
4335    constructor({ single = true } = {}) {
4336        this.activeTooltips = new Set();
4337        this.single         = single; // optional: nur ein Tooltip gleichzeitig offen
4338
4339        // Init vorhandener
4340        document.querySelectorAll('.app-tooltip:not(.tooltip-initialized)').forEach(tooltip => {
4341                if (tooltip?.dataset?.single === 'false') {
4342                    this.single = false;
4343                }
4344
4345                this.init(tooltip);
4346            });
4347
4348        this.observeDOM(document.body);
4349
4350        // ein globaler ESC-Handler
4351        this._onDocKeydown = (event) => {
4352            if (event.key === 'Escape') {
4353                this.closeAll();
4354            }
4355        };
4356
4357        document.addEventListener('keydown', this._onDocKeydown);
4358        this._updateScheduled  = false;
4359        this._onViewportChange = () => {
4360            if (this._updateScheduled) {
4361                return;
4362            }
4363
4364            this._updateScheduled = true;
4365
4366            requestAnimationFrame(() => {
4367                this._updateScheduled = false;
4368                this.updateActiveTooltipPositions();
4369            });
4370        };
4371
4372        window.addEventListener('resize', this._onViewportChange);
4373        window.addEventListener('scroll',  this._onViewportChange, true);
4374    }
4375
4376    open = (tooltip) => {
4377        if (!tooltip || tooltip.classList.contains('active')) {
4378            return;
4379        }
4380
4381        if (this.single) {
4382            this.closeAll();
4383        }
4384
4385        const inner = tooltip.parentElement.querySelector('.app-tooltip-inner');
4386        const close = tooltip.parentElement.querySelector('.app-tooltip-close');
4387
4388        tooltip.classList.add('active');
4389
4390        if (inner) {
4391            if (!inner.id) {
4392                inner.id = 'tt-' + Math.random().toString(36).slice(2);
4393            }
4394
4395            tooltip.setAttribute('aria-describedby', inner.id);
4396        }
4397
4398        // Fokus für A11y
4399        close?.focus();
4400
4401        this.setPosition(tooltip);
4402        this.activeTooltips.add(tooltip);
4403    }
4404
4405    close = (tooltip) => {
4406        try {
4407            if (!tooltip || !tooltip.classList.contains('active')) {
4408                return;
4409            }
4410
4411            const inner = tooltip.parentElement.querySelector('.app-tooltip-inner');
4412            const close = tooltip.parentElement.querySelector('.app-tooltip-close');
4413
4414            tooltip.classList.remove('active', 'focused');
4415            tooltip.removeAttribute('aria-describedby');
4416
4417            this.activeTooltips.delete(tooltip);
4418
4419            // Fokus für A11y - zurücklegen auf Dialog-Öffnendes-Element
4420            tooltip.focus();
4421        } catch (error) {
4422            console.error('Error in close():', error);
4423        }
4424    }
4425
4426    closeAll = () => {
4427        Array.from(this.activeTooltips).forEach(t => this.close(t));
4428    }
4429
4430    updateActiveTooltipPositions = () => {
4431        this.activeTooltips.forEach(tooltip => {
4432            if (!document.body.contains(tooltip)) {
4433                this.activeTooltips.delete(tooltip);
4434
4435                return;
4436            }
4437            if (tooltip.classList.contains('active')) {
4438                this.setPosition(tooltip);
4439            }
4440        });
4441    }
4442
4443    setPosition(tooltip) {
4444        const inner = tooltip.parentElement.querySelector('.app-tooltip-inner');
4445        if (!inner) {
4446            return;
4447        }
4448
4449        tooltip.classList.remove('left', 'top', 'bottom', 'right');
4450
4451        const allowed = new Set(['left','right','top','bottom']);
4452        const rawPreferred = (tooltip.dataset.position || '').trim();
4453        const preferred = allowed.has(rawPreferred) ? rawPreferred : ''; // ← nie leer adden
4454        const disableDynamic = tooltip.dataset.tooltipDisablePositioning === 'true';
4455        const staticPos = tooltip.dataset.static === 'true';
4456
4457        if (staticPos) {
4458            tooltip.classList.add(preferred || 'bottom');
4459
4460            return;
4461        }
4462        if (preferred && disableDynamic) {
4463            tooltip.classList.add(preferred);
4464
4465            return;
4466        }
4467
4468        const fitsInViewport = (pos) => {
4469            const gutter = window.innerWidth > 500 ? 80 : 0;
4470
4471            tooltip.classList.add(pos);
4472
4473            const rect = inner.getBoundingClientRect();
4474
4475            tooltip.classList.remove(pos);
4476
4477            return (
4478                rect.left   >= gutter &&
4479                rect.top    >= 0 &&
4480                rect.right  <= window.innerWidth &&
4481                rect.bottom <= window.innerHeight
4482            );
4483        };
4484
4485        if (preferred && fitsInViewport(preferred)) {
4486            tooltip.classList.add(preferred);
4487
4488            return;
4489        }
4490        for (const pos of ['left','right','bottom','top']) {
4491            if (fitsInViewport(pos)) {
4492                tooltip.classList.add(pos);
4493
4494                return;
4495            }
4496        }
4497
4498        tooltip.classList.add('top');
4499    }
4500
4501    toggleTooltip = (tooltip) => {
4502        if (tooltip.classList.contains('onclosed')) {
4503            setTimeout(() => {
4504                tooltip.classList.remove('onclosed')
4505            }, 250);
4506
4507            return;
4508        }
4509
4510        if (tooltip.classList.contains('active')) {
4511            this.close(tooltip);
4512        } else {
4513            this.open(tooltip);
4514        }
4515    }
4516
4517    init = (tooltip) => {
4518        if (!tooltip || tooltip.classList.contains('tooltip-initialized')) {
4519            return;
4520        }
4521
4522        const close = tooltip.parentElement.querySelector('.app-tooltip-close');
4523
4524        // Toggle nur, wenn nicht der Close-Button geklickt wurde
4525        ['click', 'keydown'].forEach(eventType => {
4526            tooltip.addEventListener(eventType, (event) => {
4527                if (event.type === 'click' || event.key === 'Enter' || event.key === ' ') {
4528                    event.preventDefault();
4529                    event.stopPropagation();
4530                    this.toggleTooltip(tooltip);
4531                }
4532            });
4533        });
4534
4535        // ENTER auf Root öffnen
4536        tooltip.addEventListener('keydown', (event) => {
4537            if (event.key === 'Enter' && !tooltip.classList.contains('active')) {
4538                this.open(tooltip);
4539            }
4540        });
4541
4542        // Close per Klick
4543        close?.addEventListener('click', (event) => {
4544            event.stopPropagation();
4545            this.close(tooltip);
4546        });
4547
4548        // Close per Enter/Space auf Close
4549        close?.addEventListener('keydown', (event) => {
4550            if (event.key === 'Enter' || event.key === ' ') {
4551                event.preventDefault();
4552                event.stopPropagation();
4553                this.close(tooltip);
4554            }
4555        });
4556
4557        tooltip.classList.add('tooltip-initialized');
4558    }
4559
4560    observeDOM(container) {
4561        const observer = new MutationObserver(mutations => {
4562            for (const mutationRecord of mutations) {
4563                mutationRecord.addedNodes.forEach(node => {
4564                    if (node.nodeType !== 1) {
4565                        return;
4566                    }
4567
4568                    if (node.classList?.contains('app-tooltip') && !node.classList.contains('tooltip-initialized')) {
4569                        this.init(node);
4570                    }
4571
4572                    node.querySelectorAll?.('.app-tooltip:not(.tooltip-initialized)').forEach(tooltip => {
4573                        this.init(tooltip);
4574                    });
4575                });
4576            }
4577        });
4578
4579        observer.observe(container, {
4580            childList: true,
4581            subtree: true
4582        });
4583    }
4584}
4585
4586document.addEventListener('DOMContentLoaded', () => {
4587    new Tooltip(); // {single:true}
4588});
4589
4590class SlidingFormContent {
4591    constructor() {
4592        this.observer = null;
4593        this.initExistingContainers();
4594        this.observeNewContainers();
4595    }
4596
4597    initExistingContainers() {
4598        document.querySelectorAll('.sliding-container').forEach((container) => {
4599            this.initContainer(container);
4600        });
4601    }
4602
4603    initContainer(container) {
4604        const nextBtns = container.querySelectorAll('[data-slide-next]');
4605        const prevBtns = container.querySelectorAll('[data-slide-prev]');
4606
4607        nextBtns.forEach((btn) => {
4608            btn.addEventListener('click', event => {
4609                this.next(event);
4610            });
4611        });
4612
4613        prevBtns.forEach((btn) => {
4614            btn.addEventListener('click', event => {
4615                this.prev(event);
4616            });
4617        });
4618
4619        container.setAttribute('data-sliding-init', '1'); // Markiert als bereits initialisiert
4620    }
4621
4622    // Für neue dynamisch eingefügte Container
4623    observeNewContainers() {
4624        this.observer = new MutationObserver((mutations) => {
4625            mutations.forEach((mutation) => {
4626                mutation.addedNodes.forEach((node) => {
4627                    if (!(node instanceof HTMLElement)) {
4628                        return;
4629                    }
4630
4631                    if (node.classList?.contains('sliding-container')) {
4632                        if (!node.hasAttribute('data-sliding-init')) {
4633                            this.initContainer(node);
4634                        }
4635                    }
4636
4637                    const nested = node.querySelectorAll?.('.sliding-container') || [];
4638
4639                    nested.forEach((el) => {
4640                        if (!el.hasAttribute('data-sliding-init')) {
4641                            this.initContainer(el);
4642                        }
4643                    });
4644                });
4645            });
4646        });
4647
4648        this.observer.observe(document.body, {
4649            childList: true,
4650            subtree:   true,
4651        });
4652    }
4653
4654    validateActiveStep(container) {
4655        const activeSpan = container.querySelector('span.sliding-form-identifyer[data-active="true"]');
4656
4657        if (!activeSpan) {
4658            return false;
4659        }
4660
4661        const index      = activeSpan.id?.split('-').pop();
4662        const activeStep = container.querySelector(`.step-container[data-index="${index}"]`);
4663
4664        if (!activeStep) {
4665            return false;
4666        }
4667
4668        const fields = activeStep.querySelectorAll('input:not([type="hidden"]), select, textarea, .js-upload[data-required="true"]');
4669
4670        for (const field of fields) {
4671            if (!field.checkValidity()) {
4672                if (typeof field.reportValidity === 'function') {
4673                    field.reportValidity();
4674                }
4675
4676                return false;
4677            }
4678        }
4679
4680        return true;
4681    }
4682
4683    next(event) {
4684        const nextStep         = event.currentTarget.getAttribute('aria-controls');
4685        const nextStepInput    = document.getElementById(nextStep);
4686        const closestContainer = event.currentTarget?.closest('.sliding-container');
4687        const closestForm      = closestContainer?.closest('form');
4688
4689        if (closestForm.length) {
4690            if (this.validateActiveStep(closestContainer) && nextStepInput) {
4691                closestForm.querySelectorAll('span[data-active="true"]').forEach((el) => {
4692                    el.removeAttribute('data-active');
4693                });
4694
4695                nextStepInput.setAttribute('data-active', 'true');
4696            }
4697        }
4698    }
4699
4700    prev(event) {
4701        const prevStep         = event.currentTarget.getAttribute('aria-controls');
4702        const prevStepInput     = document.getElementById(prevStep);
4703        const closestContainer = event.currentTarget?.closest('.sliding-container');
4704        const closestForm      = closestContainer?.closest('form');
4705
4706        if (prevStepInput && closestForm.length) {
4707            closestForm.querySelectorAll('span[data-active="true"]').forEach((el) => {
4708                el.removeAttribute('data-active');
4709            });
4710            prevStepInput.setAttribute('data-active', 'true');
4711        }
4712    }
4713}
4714
4715document.addEventListener('DOMContentLoaded', () => {
4716    new SlidingFormContent();
4717});
4718
4719class AccessibleAccordion {
4720    constructor(root, { allowMultiple = null, allowToggle = null, allowTabindex = false } = {}) {
4721        if (!root) throw new Error('Accordion root not found');
4722        this.root              = root;
4723
4724        const ds               = root.dataset;
4725        this.allowMultiple     = allowMultiple ?? (ds.allowMultiple === 'true');
4726        this.allowToggle       = allowToggle   ?? (ds.allowToggle !== 'false');
4727        this.allowTabindex     = allowTabindex ?? (ds.allowTabindex !== 'false');
4728
4729        // nur Items, die direkt zu diesem Accordion gehören
4730        this.items             = Array.from(root.querySelectorAll('[data-accordion-item]'))
4731            .filter(item => item.closest('[data-accordion]') === this.root);
4732
4733        // Trigger/Panel aus den Items ableiten
4734        this.triggers          = this.items.map(item =>
4734 item.querySelector('[data-accordion-trigger]')).filter(Boolean);
4735        this.panels            = this.items.map(item => item.querySelector('[data-accordion-panel]')).filter(Boolean);
4736
4737        this.#initA11y();
4738        this.#bindEvents();
4739    }
4740
4741    open(indexOrButton) {
4742        const btn = this.#resolveTrigger(indexOrButton);
4743
4744        if (!btn) {
4745            return;
4746        }
4747
4748        const panel = this.#panelFor(btn);
4749
4750        if (!panel) {
4751            return;
4752        }
4753
4754        if (!this.allowMultiple) {
4755            this.triggers.forEach(trigger => {
4756                if (trigger !== btn) {
4757                    this.close(trigger);
4758                }
4759            });
4760        }
4761
4762        btn.setAttribute('aria-expanded', 'true');
4763
4764        panel.hidden = false;
4765    }
4766
4767    close(indexOrButton) {
4768        const btn = this.#resolveTrigger(indexOrButton);
4769
4770        if (!btn) {
4771            return;
4772        }
4773
4774        const panel = this.#panelFor(btn);
4775
4776        if (!panel) {
4777            return;
4778        }
4779
4780        if (!this.allowToggle && this.#expandedCount() === 1) {
4781            return;
4782        }
4783
4784        btn.setAttribute('aria-expanded', 'false');
4785
4786        panel.hidden = true;
4787    }
4788
4789    toggle(indexOrButton) {
4790        const btn = this.#resolveTrigger(indexOrButton);
4791
4792        if (!btn) {
4793            return;
4794        }
4795
4796        const expanded = btn.getAttribute('aria-expanded') === 'true';
4797
4798        if (expanded) {
4799            this.close(btn);
4800        } else {
4801            this.open(btn);
4802        }
4803    }
4804
4805    // private
4806    #initA11y() {
4807        this.items.forEach((item, i) => {
4808            const btn   = item.querySelector('[data-accordion-trigger]');
4809            const panel = item.querySelector('[data-accordion-panel]');
4810
4811            if (!btn || !panel) {
4812                return;
4813            }
4814
4815            if (!btn.id) {
4816                btn.id   = `acc-trigger-${this.#uid()}-${i}`;
4817            }
4818
4819            if (!panel.id) {
4820                panel.id = `acc-panel-${this.#uid()}-${i}`;
4821            }
4822
4823            // btn
4824            btn.setAttribute('aria-controls', panel.id);
4825            btn.setAttribute('aria-expanded', 'false');
4826            btn.setAttribute('type', 'button');
4827            // panel
4828            panel.setAttribute('role', 'region');
4829            panel.setAttribute('aria-labelledby', btn.id);
4830
4831            panel.hidden   = true;
4832
4833            if (item.hasAttribute('data-open')) {
4834                btn.setAttribute('aria-expanded', 'true');
4835                panel.hidden = false;
4836            }
4837        });
4838
4839        if (!this.allowToggle && this.#expandedCount() === 0 && this.triggers[0]) {
4840            this.open(this.triggers[0]);
4841        }
4842    }
4843
4844    #bindEvents() {
4845        let kbdBtn = null;
4846
4847        this.root.addEventListener('keydown', (event) => {
4848            const btn = event.target.closest('[data-accordion-trigger]');
4849
4850            if (!btn || !this.triggers.includes(btn)) {
4851                return;
4852            }
4853
4854            // space scrollt sonst
4855            if (event.key === ' ') {
4856                event.preventDefault();
4857            }
4858
4859            if (event.key === 'Enter' || event.key === ' ') {
4860                kbdBtn = btn;
4861            }
4862        });
4863
4864        this.root.addEventListener('keyup', (event) => {
4865            const btn = event.target.closest('[data-accordion-trigger]');
4866
4867            if (!btn || !this.triggers.includes(btn)) {
4868                return;
4869            }
4870
4871            if ((event.key === 'Enter' || event.key === ' ') && kbdBtn === btn) {
4872                event.preventDefault();
4873
4874                this.toggle(btn);
4875
4876                // erst nach dem evtl. Click leeren
4877                setTimeout(() => {
4878                    kbdBtn = null;
4879                }, 0);
4880            }
4881        });
4882
4883        this.root.addEventListener('click', (event) => {
4884            const btn = event.target.closest('[data-accordion-trigger]');
4885
4886            if (!btn || !this.triggers.includes(btn)) {
4887                return;
4888            }
4889
4890            if (kbdBtn === btn) {
4891                event.preventDefault();
4892
4893                return;
4894            }
4895
4896            event.preventDefault();
4897
4898            this.toggle(btn);
4899        });
4900    }
4901
4902    #panelFor(btn) {
4903        const id = btn.getAttribute('aria-controls');
4904
4905        if (id) {
4906            return document.getElementById(id);
4907        } else {
4908            return null;
4909        }
4910    }
4911
4912    #expandedCount() {
4913        return this.triggers.filter(triggerFilter => triggerFilter.getAttribute('aria-expanded') === 'true').length;
4914    }
4915
4916    #resolveTrigger(indexOrButton) {
4917        if (typeof indexOrButton === 'number') {
4918            return this.triggers[indexOrButton] || null;
4919        }
4920
4921        if (indexOrButton instanceof HTMLElement) {
4922            return indexOrButton.matches('[data-accordion-trigger]') ? indexOrButton : null;
4923        }
4924
4925        return null;
4926    }
4927
4928    #uid() {
4929        if (!this._uidBase) {
4930            this._uidBase = Math.random().toString(36).slice(2);
4931        }
4932
4933        return this._uidBase;
4934    }
4935}
4936
4937document.addEventListener('DOMContentLoaded', () => {
4938    // default
4939    document.querySelectorAll('[data-accordion]').forEach(accordion => {
4940        const options = {
4941            allowMultiple: accordion.dataset.allowMultiple === 'true',
4942            allowToggle:   accordion.dataset.allowToggle !== 'false',
4943            allowTabindex: accordion.dataset.allowTabindex !== 'false',
4944        };
4945        accordion._accordion = new AccessibleAccordion(accordion, options);
4946    });
4947});
4948
4949class Faq {
4950    constructor() {
4951        this.faqSearchForms = document.querySelectorAll(".faq-search-form");
4952    }
4953
4954    search() {
4955        this.faqSearchForms.forEach(form => {
4956            const input = form.querySelector('input[type="search"], input[type="text"]');
4957            const container = form.closest('.box-default') || document;
4958            const metaText = form?.dataset?.metaText;
4959            const accordion = container.querySelector('[data-accordion]') || container.querySelector('.accordion');
4960
4961            if (!accordion) {
4962                return;
4963            }
4964
4965            const items = Array.from(accordion.querySelectorAll('[data-accordion-item], .accordion-item'));
4966            let meta = container.querySelector('.faq-search-meta');
4967
4968            if (!meta) {
4969                meta           = document.createElement('div');
4970                meta.className = 'faq-search-meta';
4971
4972                meta.setAttribute('role', 'status');
4973                meta.setAttribute('aria-live', 'polite');
4974
4975                meta.hidden    = true;
4976
4977                form.insertAdjacentElement('afterend', meta);
4978            }
4979
4980            const normalize = s => (s || '')
4981                .toLowerCase()
4982                .normalize('NFD')
4983                .replace(/\p{Diacritic}/gu, '') // ä→a ...
4984                .replace(/ß/g, 'ss');
4985
4986            const matches = (item, query) => {
4987                const hay   = normalize(item.textContent || '');
4988                const words = normalize(query).trim().split(/\s+/).filter(Boolean);
4989
4990                return words.every(word => hay.includes(word)); // AND search
4991            };
4992
4993            const doSearch = (query) => {
4994                const q = (query || '').trim();
4995
4996                if (!q) {
4997                    items.forEach(item => {
4998                        item.classList.remove('is-hidden');
4999                        item.hidden = false;
5000                    });
5001
5002                    meta.hidden = true;
5003                    meta.textContent = '';
5004
5005                    accordion.dispatchEvent(new CustomEvent('faq:search', {
5006                        detail: {
5007                            query: '',
5008                            hits: items.length
5009                        }
5010                    }));
5011
5012                    return;
5013                }
5014
5015                let hits = 0;
5016
5017                items.forEach(item => {
5018                    const ok = matches(item, q);
5019
5020                    item.classList.toggle('is-hidden', !ok);
5021                    item.hidden = !ok;
5022
5023                    if (ok) {
5024                        hits++;
5025                    }
5026                });
5027
5028                meta.hidden      = false;
5029                meta.textContent = '';
5030
5031                requestAnimationFrame(() => {
5032                    this.renderMeta(meta, metaText, hits)
5033                });
5034
5035                accordion.dispatchEvent(new CustomEvent('faq:search', {
5036                    detail: {
5037                        query: q,
5038                        hits
5039                    }
5040                }));
5041            };
5042
5043            form.addEventListener('submit', e => {
5044                e.preventDefault();
5045
5046                doSearch(input ? input.value : '');
5047            });
5048
5049            // optional: live search
5050            if (input) {
5051                let timeD;
5052
5053                input.addEventListener('input', () => {
5054                    clearTimeout(timeD);
5055                    timeD = setTimeout(() => {
5056                        doSearch(input.value)
5057                    }, 150);
5058                });
5059
5060                // ESC = zurücksetzen
5061                input.addEventListener('keydown', event => {
5062                    if (event.key === 'Escape') {
5063                        input.value = '';
5064
5065                        doSearch('');
5066
5067                        input.blur();
5068                    }
5069                });
5070            }
5071        });
5072    }
5073
5074    renderMeta(meta, metaText, hits) {
5075        const [pre = '', post = ''] = metaText.split('%1$s');
5076        const span = document.createElement('span');
5077
5078        span.textContent = String(hits);
5079
5080        const nodes = [];
5081
5082        if (pre) {
5083            nodes.push(document.createTextNode(pre));
5084        }
5085
5086        nodes.push(span);
5087
5088        if (post) {
5089            nodes.push(document.createTextNode(post));
5090        }
5091
5092        meta.replaceChildren(...nodes); // triggert aria-live
5093    }
5094}
5095
5096// init
5097document.addEventListener('DOMContentLoaded', () => {
5098    const faq = new Faq();
5099    faq.search();
5100});
5101
5102class FakeRadioHandler {
5103    constructor() {
5104        this.handleFakeRadios();
5105    }
5106
5107    handleFakeRadios() {
5108        const selector = ".js-select-insurance";
5109
5110        const setup = button => {
5111            if (button.dataset.bound) return;
5112            button.dataset.bound = "true";
5113
5114            button.addEventListener("click", () => {
5115                document.querySelectorAll(selector).forEach(btn => {
5116                    btn.setAttribute("aria-checked", "false");
5117                });
5118                button.setAttribute("aria-checked", "true");
5119            });
5120        };
5121
5122        // Initiales Setup
5123        document.querySelectorAll(selector).forEach(setup);
5124
5125        // Beobachter für neue Elemente
5126        const observer = new MutationObserver(mutations => {
5127            mutations.forEach(mutation => {
5128                mutation.addedNodes.forEach(node => {
5129                    if (!(node instanceof HTMLElement)) return;
5130
5131                    if (node.matches(selector)) {
5132                        setup(node);
5133                    }
5134
5135                    node.querySelectorAll?.(selector)?.forEach(setup);
5136                });
5137            });
5138        });
5139
5140        observer.observe(document.body, {
5141            childList: true,
5142            subtree: true
5143        });
5144    }
5145}
5146
5147document.addEventListener('DOMContentLoaded', () => {
5148    new FakeRadioHandler();
5149});
5150
5151class DocumentViewCounter {
5152    constructor() {
5153        this.elements  = document.querySelectorAll('[data-set-viewcounter]');
5154        this.utilities = new Utilities();
5155
5156        if (this.elements.length) {
5157            this.init();
5158        }
5159    }
5160
5161    init() {
5162        this.elements.forEach((element) => {
5163            element.addEventListener('click', (event) => {
5164                if (element.hasAttribute('data-set-viewcounter')) {
5165                    // Marker direkt vor dem geklickten Element entfernen (falls vorhanden)
5166                    const prev = element.previousElementSibling;
5167                    const marker = prev && prev.matches('[data-marker-new-document]') ? prev : null;
5168
5169                    // aktive Hint-Bubbles in allen Navigationen sammeln
5170                    const activeBubbles = [];
5171
5172                    document.querySelectorAll('.navigation-list').forEach(list => {
5173                        const active = list.querySelector('a.active');
5174
5175                        if (!active) {
5176                            return;
5177                        }
5178
5179                        const bubble = active.querySelector('.nav-hint-bubble');
5180
5181                        if (bubble) {
5182                            activeBubbles.push(bubble);
5183                        }
5184                    });
5185
5186                    const navBadge = document.querySelector('.js-contracts-unread');
5187                    const listInsuranceSelect = document.querySelector('.list-insurance-select');
5188                    const closestDocCounter = element.closest('.doc-counter');
5189
5190                    if (marker) {
5191                        marker.remove();
5192                    }
5193
5194                    // Globale Badge runterzählen/entfernen
5195                    if (navBadge) {
5196                        const badgeCount = parseInt(navBadge.textContent, 10);
5197                        const newBadgeCount = (isNaN(badgeCount) ? 0 : badgeCount - 1);
5198                        if (newBadgeCount > 0) {
5199                            navBadge.textContent = String(newBadgeCount);
5200                        } else {
5201                            navBadge.remove();
5202                        }
5203                    }
5204
5205                    // Listen-spezifische Badge (neben aktivem Link) herunterzählen/entfernen
5206                    if (listInsuranceSelect) {
5207                        const thatBadge = listInsuranceSelect.querySelector('button.active .document-badge');
5208
5209                        if (thatBadge) {
5210                            const listBadgeCount = parseInt(thatBadge.textContent, 10);
5211                            const newListBadgeCount = (isNaN(listBadgeCount)
5212                                ? 0
5213                                : listBadgeCount - 1);
5214
5215                            if (newListBadgeCount > 0) {
5216                                thatBadge.textContent = String(newListBadgeCount);
5217                            } else {
5218                                thatBadge.closest('.document-line').remove();
5219                            }
5220                        } else {
5221                            listInsuranceSelect?.querySelector('button.active .document-line')?.remove();
5222                        }
5223                    }
5224
5225                    // Innerhalb des nächsten .doc-counter prüfen, ob noch "attention"-Marker existieren
5226                    if (closestDocCounter) {
5227                        let counter = 0;
5228
5229                        closestDocCounter.querySelectorAll('[data-set-viewcounter]').forEach(viewCounter => {
5230                            const sibling = viewCounter.previousElementSibling;
5231                            if (sibling && sibling.matches('.attention-o')) {
5232                                counter++;
5233                            }
5234                        });
5235
5236                        // Wenn keine Marker mehr vorhanden, alle passenden Boxen (ALLES, nicht nur erste) bereinigen
5237                        if (counter === 0) {
5238                            const boxes = this.utilities.closestAll(closestDocCounter, '.box-default, .list-element-detail');
5239                            // Fallback: wenn keine passende Box gefunden, nimm den doc-counter selbst
5240                            const targets = boxes.length
5241                                ? boxes
5242                                : [closestDocCounter];
5243
5244                            targets.forEach(box => {
5245                                box.querySelectorAll('[class$="new-document-badge"]').forEach(badges => {
5246                                    badges.remove()
5247                                });
5248                            });
5249                        }
5250                    }
5251
5252                    // Wenn es GAR KEINE New-Document-Marker mehr gibt, alle aktiven Bubbles entfernen
5253                    if (!document.querySelectorAll('[data-marker-new-document]').length) {
5254                        activeBubbles.forEach(bubble => {
5255                            bubble.remove()
5256                        });
5257                    }
5258
5259                    element.removeAttribute('data-set-viewcounter');
5260                }
5261            });
5262        });
5263    }
5264}
5265
5266document.addEventListener('DOMContentLoaded', () => {
5267    new DocumentViewCounter();
5268});
5269
5270class SmartDownload {
5271    constructor(options) {
5272        options = options || {};
5273        this.selector = options.selector || 'a.js-download-link';
5274    }
5275
5276    // --- Environment Detection ---
5277    isIOS() {
5278        return /iPad|iPhone|iPod/i.test(navigator.userAgent);
5279    }
5280
5281    isAndroid() {
5282        return /Android/i.test(navigator.userAgent);
5283    }
5284
5285    isStandaloneIOS() {
5286        return !!(window.navigator && window.navigator.standalone);
5287    }
5288
5289    isAndroidWebViewUA(ua) {
5290        return /\bwv\b/.test(ua) ||
5291            (/\bVersion\/\d+\.\d+/.test(ua) && ua.indexOf('Chrome') !== -1 && ua.indexOf('Safari') !== -1);
5292    }
5293
5294    isIOSWebViewUA(ua) {
5295        const isIOSLike = /iPad|iPhone|iPod/.test(ua);
5296        const hasSafariToken = /Safari\//.test(ua);
5297
5298        return isIOSLike && !hasSafariToken;
5299    }
5300
5301    isWebView() {
5302        const ua = navigator.userAgent || '';
5303
5304        if (this.isAndroid() && this.isAndroidWebViewUA(ua)) {
5305            return true;
5306        }
5307
5308        if (this.isIOS() && !this.isStandaloneIOS() && this.isIOSWebViewUA(ua)) {
5309            return true;
5310        }
5311
5312        return false;
5313    }
5314
5315    // --- Bridges ---
5316    tryOpenViaKnownBridges(url) {
5317        if (globalThis?.Capacitor?.Plugins?.Dogtorance) {
5318            const absoluteUrl = new URL(url, window.location);
5319            globalThis.Capacitor.Plugins.Dogtorance.download({url: absoluteUrl.toString()});
5320            return true;
5321        }
5322
5323        if (window.Capacitor?.Browser?.open) {
5324            return window.Capacitor.Browser.open({url});
5325        }
5326
5327        if (window.flutter_inappwebview?.callHandler) {
5328            return window.flutter_inappwebview.callHandler('openExternal', url);
5329        }
5330
5331        if (window.ReactNativeWebView?.postMessage) {
5332            window.ReactNativeWebView.postMessage(JSON.stringify({type: 'OPEN_EXTERNAL', url}));
5333            return Promise.resolve();
5334        }
5335
5336        if (window.webkit?.messageHandlers?.openExternal?.postMessage) {
5337            window.webkit.messageHandlers.openExternal.postMessage({url});
5338            return Promise.resolve();
5339        }
5340
5341        return null;
5342    }
5343
5344    openExternalBestEffort(url) {
5345        const bridged = this.tryOpenViaKnownBridges(url);
5346
5347        if (bridged) {
5348            return bridged;
5349        }
5350
5351        const w = window.open(url, '_blank', 'noopener,noreferrer');
5352
5353        if (w) {
5354            return Promise.resolve();
5355        }
5356
5357        window.location.href = url;
5358
5359        return Promise.resolve();
5360    }
5361
5362    // --- Hauptlogik: Links patchen & Click abfangen ---
5363    patchAnchors() {
5364        const isWV = this.isWebView();
5365        const anchors = document.querySelectorAll(this.selector);
5366
5367        anchors.forEach(a => {
5368            if (isWV) {
5369                a.removeAttribute('download');
5370                a.setAttribute('target', '_blank');
5371
5372                let rel = (a.getAttribute('rel') || '').split(/\s+/);
5373
5374                ['external', 'noopener', 'noreferrer'].forEach(t => {
5375                    if (!rel.includes(t)) {
5376                        rel.push(t);
5377                    }
5378                });
5379
5380                a.setAttribute('rel', rel.join(' ').trim());
5381            }
5382        });
5383
5384        document.addEventListener('click', (e) => {
5385            const link = e.target.closest(this.selector);
5386
5387            if (!link) {
5388                return;
5389            }
5390
5391            if (this.isWebView()) {
5392                e.preventDefault();
5393                this.openExternalBestEffort(link.href);
5394            }
5395
5396            // Im Browser greift normales Download-Verhalten
5397        }, {passive: false});
5398    }
5399}
5400
5401document.addEventListener('DOMContentLoaded', function () {
5402    const dl = new SmartDownload();
5403    dl.patchAnchors();
5404});
5405class SetIdentToUrl {
5406    constructor() {
5407        this.links = document.querySelectorAll('.js-set-ident-to-document');
5408        this.utillities = new Utilities();
5409
5410        if (this.links) {
5411            this.init();
5412        }
5413    }
5414
5415    init() {
5416        const loginIdentField = document.getElementById('loginIdent');
5417
5418        if (loginIdentField) {
5419            this.links.forEach(link => {
5420                link.addEventListener('click', () => {
5421                    const loginIdentValue = loginIdentField.value || '';
5422                    let url = link.href;
5423
5424                    if (loginIdentValue.length) {
5425                        link.href = this.utillities.appendParamsToUrl(url, {
5426                            'loginIdent' : loginIdentValue
5427                        });
5428                    }
5429                });
5430            });
5431        }
5432    }
5433}
5434
5435document.addEventListener('DOMContentLoaded', () => {
5436    new SetIdentToUrl();
5437});
5438
5439class CommunicationDeclaration {
5440    constructor() {
5441        this.link = document.querySelector('.js-show-communicationDeclaration');
5442        this.linkHref = this.link?.href || '';
5443        this.utilities = new Utilities();
5444
5445        if (this.link) {
5446            this.init();
5447        }
5448    }
5449
5450    init() {
5451        const loginIdentField = document.getElementById('loginIdent');
5452        const approval = document.getElementById('communicationDeclarationApproval');
5453
5454        if (!loginIdentField || !approval) {
5455            return;
5456        }
5457
5458        this.link.addEventListener('click', (e) => {
5459
5460            const loginIdentValue = loginIdentField.value || '';
5461            const params = {};
5462            let checkedMedia = [];
5463
5464            if (approval.checked) {
5465                const mediaNodes = document.getElementsByName('communicationMedia[]');
5466                checkedMedia = Array.from(mediaNodes)
5467                    .filter(node =>
5467 node instanceof HTMLInputElement && node.checked)
5468                    .map(node => node.value);
5469            }
5470
5471            if (loginIdentValue) {
5472                params.loginIdent = loginIdentValue;
5473            }
5474
5475            if (checkedMedia.length) {
5476                params.communicationMedia = checkedMedia.join(',');
5477            }
5478
5479            this.link.href = this.utilities.appendParamsToUrl(this.linkHref, params);
5480        });
5481    }
5482}
5483
5484document.addEventListener('DOMContentLoaded', () => {
5485    new CommunicationDeclaration();
5486});
5487
5488class ProcurationPreview {
5489    constructor() {
5490        this.utilities = new Utilities();
5491        this.onClick   = this.onClick.bind(this);
5492
5493        document.addEventListener('click', this.onClick);
5494    }
5495
5496    onClick(event) {
5497        const link = event.target.closest('.js-procuration-preview');
5498
5499        if (!link || !document.contains(link)) {
5500            return;
5501        }
5502
5503        event.preventDefault();
5504
5505        const data = new FormData();
5506        const signatureFieldForm = document.querySelector('.signature-field-form');
5507
5508        if (!signatureFieldForm) {
5509            return;
5510        }
5511
5512        const imageField = signatureFieldForm.querySelector('.signature-image-input');
5513
5514        if (imageField) {
5515            if (imageField.type === 'file' && imageField.files && imageField.files[0]) {
5516                data.append('signature-image', imageField.files[0], imageField.files[0].name || 'signature');
5517            } else {
5518                const val = imageField.value ?? '';
5519
5520                if (val !== '') {
5521                    data.append('signature-image', val);
5522                }
5523            }
5524        }
5525
5526        link.classList.add('disabled');
5527
5528        document.body.insertAdjacentHTML("beforeend", this.getLoaderLayerHtml());
5529
5530        fetch(link.href, {
5531            method: 'POST',
5532            body: data,
5533            credentials: 'same-origin',
5534        })
5535            .then(response => {
5536                if (!response || !response.ok) {
5537                    throw new Error(`HTTP Error: ${response?.status}`);
5538                }
5539
5540                return response.json().catch((error) => {
5541                    console.error(error);
5542                });
5543            })
5544            .then(json => {
5545                if (json?.success && json?.previewUrl) {
5546                    window.open(json.previewUrl, link?.getAttribute('target'));
5547                }
5548            })
5549            .catch(err => {
5550                throw new Error(`Procuration Document Error: ${err.toString()}!`);
5551            })
5552            .finally(() => {
5553                link.classList.remove('disabled');
5554                this.removeLoaderLayer();
5555            });
5556    }
5557
5558    getLoaderLayerHtml() {
5559        const svg = `<svg class="loader-layer-circle" width="80" height="80" version="1.1" xmlns="http://www.w3.org/2000/svg">
5560                <circle cx="40" cy="40" r="35">
5561            </svg>`;
5562        const hint = `<p>${window.texts.pleaseWait || ""}</p>`;
5563
5564        return `<div class="layer-default loader-layer is-shown" role="status" aria-live="polite" id="procurationLoaderLayer">
5565                <div class="cell">
5566                    <div class="loader-layer-content">${svg}${hint}</div>
5567                </div>
5568            </div>`;
5569    }
5570
5571    removeLoaderLayer() {
5572        const layer = document.getElementById('procurationLoaderLayer');
5573
5574        if (!layer) {
5575            return;
5576        }
5577
5578        layer.classList.remove('is-shown');
5579
5580        setTimeout(() => {
5581            layer.remove();
5582        }, 500);
5583    }
5584
5585    destroy() {
5586        document.removeEventListener('click', this.onClick);
5587    }
5588}
5589
5590document.addEventListener('DOMContentLoaded', () => {
5591    new ProcurationPreview();
5592});
5593
5594/**
5595 * Layer Controller
5596 *
5597 * Features:
5598 * - Öffnen per [data-open-layer="<id>"]
5599 * - Schließen per [data-close-layer] (optional mit [data-remove-layer])
5600 * - ESC schließt sichtbare Layer
5601 * - DOM-Änderungen werden beobachtet (MutationObserver) -> Rebinding von Close-Handlern
5602 * - Custom Events auf dem Layer-Element:
5603 *     - 'layer.open'  mit detail: { layer, trigger }
5604 *     - 'layer.close' mit detail: { layer, trigger, removed }
5605 *
5606 * Nutzung:
5607 *   const layer = document.getElementById('myLayer');
5608 *   layer.addEventListener('layer.open',  (e) => console.log('opened', e.detail));
5609 *   layer.addEventListener('layer.close', (e) => console.log('closed', e.detail));
5610 */
5611class Layer {
5612    constructor() {
5613        /** CSS-Selektoren für relevante Layer */
5614        this.classSelectors = [
5615            '.layer-default:not(.embedded):not(.analysis-start-layer):not(.email-
5615outdated-layer):not(.layer-cookie-approval.top)',
5616        ];
5617
5618        /** Trigger zum Öffnen/Schließen */
5619        this.showLayerTrigger  = null;
5620        this.closeLayerTrigger = null;
5621        this.layerTrigger      = null;
5622
5623        /** Klasse, die Sichtbarkeit markiert */
5624        this.shownClass = 'is-shown';
5625        /** Klasse, die das Schließen auf Body initialisiert */
5626        this.closeOnBodyClass = 'js-close-on-body';
5627
5628        // Init
5629        this.observeMutations();
5630        this.showLayerByClick();
5631        this.closeLayerOnEsc();
5632        this.closeLayerByClick();
5633        this.closeLayerOnBody();
5634    }
5635
5636    /**
5637     * Setzt den Focus auf den zuletzt betätigten Trigger, der das Modal geöffnet hat
5638     */
5639    focusLayerTrigger() {
5640        if (this.layerTrigger) {
5641            setTimeout(() => {
5642                this.layerTrigger?.focus();
5643                this.layerTrigger = null;
5644            }, 300);
5645        }
5646    }
5647
5648    /**
5649     * Prüft, ob ein Layer sichtbar ist.
5650     * Nutzt checkVisibility wenn vorhanden, sonst Fallbacks.
5651     */
5652    isLayerVisible(layer = null) {
5653        if (!layer) {
5654            return false;
5655        }
5656
5657        if (typeof layer.checkVisibility === 'function') {
5658            return layer.checkVisibility({
5659                opacityProperty: true,
5660                visibilityProperty: true,
5661                contentVisibilityAuto: true,
5662            });
5663        }
5664
5665        const style = window.getComputedStyle(layer);
5666
5667        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
5668            return false;
5669        }
5670
5671        if (layer.getClientRects().length === 0) {
5672            return false;
5673        }
5674
5675        return layer.offsetParent !== null || (layer.offsetWidth + layer.offsetHeight) > 0;
5676    }
5677
5678    canCloseByClickOnBody(child) {
5679        return child.classList.contains('cell');
5680
5681    }
5682
5683    /**
5684     * Öffnet einen Layer: Klasse setzen, Events dispatchen, Close-Handler binden.
5685     */
5686    openLayer(layer, trigger = null) {
5687        if (!layer) {
5688            return;
5689        }
5690
5691        // Bereits offen?
5692        const wasShown = layer.classList.contains(this.shownClass);
5693
5694        layer.classList.add(this.shownClass);
5695        layer.setAttribute('aria-modal', 'true');
5696        layer.setAttribute('role', 'dialog');
5697
5698        // Nur feuern, wenn tatsächlich ein Wechsel stattfand
5699        if (!wasShown) {
5700            layer.dispatchEvent(new CustomEvent('layer.open', {
5701                bubbles: true,
5702                detail: {
5703                    layer,
5704                    trigger
5705                }
5706            }));
5707        }
5708
5709        // Sicherstellen, dass Close-Trigger gebunden sind
5710        this.closeLayerByClick();
5711    }
5712
5713    /**
5714     * Schließt einen Layer: Klasse entfernen, optional entfernen, Events dispatchen.
5715     */
5716    closeLayer(layer, trigger = null) {
5717        if (!layer) {
5718            return;
5719        }
5720
5721        const wasShown = layer.classList.contains(this.shownClass);
5722        const shouldRemove = trigger?.hasAttribute?.('data-remove-layer');
5723
5724        layer.classList.remove(this.shownClass);
5725
5726        const dispatchClose = () => {
5727            if (wasShown) {
5728                layer.dispatchEvent(new CustomEvent('layer.close', {
5729                    bubbles: true,
5730                    detail: {
5731                        layer,
5732                        trigger,
5733                        removed: !!shouldRemove
5734                    }
5735                }));
5736            }
5737        };
5738
5739        if (shouldRemove) {
5740            // Delay erlaubt einfache CSS-Transitions (z.B. Fade-Out)
5741            setTimeout(() => {
5742                // Vor dem Remove noch das Event feuern, Element existiert noch
5743                dispatchClose();
5744                layer.remove();
5745            }, 500);
5746        } else {
5747            dispatchClose();
5748        }
5749
5750        this.focusLayerTrigger();
5751    }
5752
5753    /**
5754     * Beobachtet DOM-Änderungen, um neue/entfernte Layer/Trigger zu handhaben.
5755     */
5756    observeMutations() {
5757        const observer = new MutationObserver((mutations) => {
5758            let layerModified = false;
5759
5760            mutations.forEach(mutation => {
5761                if (
5762                    mutation.type === 'attributes' &&
5763                    (mutation.attributeName === 'style' || mutation.attributeName === 'class')
5764                ) {
5765                    if (this.classSelectors.some(selector => mutation.target.matches?.(selector))) {
5766                        layerModified = true;
5767                    }
5768                } else if (mutation.type === 'childList') {
5769                    mutation.addedNodes.forEach(node => {
5770                        if (node.nodeType === Node.ELEMENT_NODE && this.classSelectors.some(selector => node.matches?.(selector))) {
5771                            layerModified = true;
5772                        }
5773                    });
5774                    mutation.removedNodes.forEach(node => {
5775                        if (node.nodeType === Node.ELEMENT_NODE && this.classSelectors.some(selector => node.matches?.(selector))) {
5776                            layerModified = true;
5777                        }
5778                    });
5779                }
5780            });
5781
5782            if (layerModified) {
5783                this.closeLayerByClick();
5784                this.showLayerByClick();
5785            }
5786        });
5787
5788        observer.observe(document.body, {
5789            childList: true,
5790            subtree: true,
5791            attributes: true,
5792            attributeFilter: ['style', 'class'],
5793        });
5794    }
5795
5796    /**
5797     * ESC schließt alle sichtbaren Layer (oder nur die sichtbaren im DOM).
5798     */
5799    closeLayerOnEsc() {
5800        if (this._escBound) {
5801            return;
5802        }
5803        this._escBound = true;
5804
5805        document.addEventListener('keydown', (event) => {
5806            if (event.key !== 'Escape') return;
5807
5808            const layers = document.querySelectorAll('.layer-default');
5809
5810            layers.forEach(layer => {
5811                if (layer && this.isLayerVisible(layer)) {
5812                    const closeBtns = layer.querySelectorAll('[data-close-layer]');
5813
5814                    if (closeBtns.length) {
5815                        closeBtns[0].click();
5816                    }
5817
5818                    this.focusLayerTrigger();
5819                }
5820            });
5821        });
5822    }
5823
5824    closeLayerOnBody() {
5825        if (this._bodyBound) {
5826            return;
5827        }
5828        this._bodyBound = true;
5829
5830        document.body.addEventListener('click', (event) => {
5831            const layers = document.querySelectorAll('.layer-default');
5832            const target = event.target;
5833
5834            layers.forEach(layer => {
5835                if (!this.canCloseByClickOnBody(target)) {
5836                    return;
5837                }
5838
5839                if (!layer.classList.contains(this.closeOnBodyClass)) {
5840                    return;
5841                }
5842
5843                if (layer && this.isLayerVisible(layer)) {
5844                    const closeBtns = layer.querySelectorAll('[data-close-layer]');
5845
5846                    if (closeBtns.length) {
5847                        closeBtns[0].click();
5848                    }
5849
5850                    this.focusLayerTrigger();
5851                }
5852            });
5853        });
5854    }
5855
5856    /**
5857     * Bindet Klick-Handler für alle [data-close-layer].
5858     */
5859    closeLayerByClick() {
5860        this.closeLayerTrigger = document.querySelectorAll('[data-close-layer]');
5861
5862        if (!this.closeLayerTrigger.length) {
5863            return;
5864        }
5865
5866        this.closeLayerTrigger.forEach(trigger => {
5867            if (trigger._closeEvent) {
5868                trigger.removeEventListener('click', trigger._closeEvent);
5869            }
5870
5871            trigger._closeEvent = (event) => {
5872                const layer = event.currentTarget.closest('.layer-default') || null;
5873
5874                if (!layer) {
5875                    return;
5876                }
5877
5878                event.preventDefault();
5879                this.closeLayer(layer, trigger);
5880            };
5881
5882            trigger.addEventListener('click', trigger._closeEvent);
5883        });
5884    }
5885
5886    /**
5887     * Bindet Klick-Handler für alle [data-open-layer].
5888     * Erwartet die Ziel-ID im Attribut, z.B. data-open-layer="myLayer"
5889     */
5890    showLayerByClick() {
5891        this.showLayerTrigger = document.querySelectorAll('[data-open-layer]');
5892
5893        if (!this.showLayerTrigger) {
5894            return;
5895        }
5896
5897        this.showLayerTrigger.forEach(trigger => {
5898            if (trigger._openEvent) {
5899                trigger.removeEventListener('click', trigger._openEvent);
5900            }
5901
5902            trigger._openEvent = (event) => {
5903                const targetId = event.currentTarget?.dataset?.openLayer || '';
5904
5905                this.layerTrigger = event.currentTarget || null;
5906
5907                if (!targetId) {
5908                    return;
5909                }
5910
5911                const layer = document.getElementById(targetId);
5912
5913                if (!layer) {
5914                    return;
5915                }
5916
5917                event.preventDefault();
5918                this.openLayer(layer, trigger);
5919            };
5920
5921            trigger.addEventListener('click', trigger._openEvent);
5922        });
5923    }
5924}
5925
5926document.addEventListener('DOMContentLoaded', () => new Layer());
5927
5928class AddressAutocomplete {
5929    constructor(opts = {}) {
5930        this.autoCompletePluginUtils = {
5931            escapeHtml: (str) => {
5932                if (str === null || str === undefined) {
5933                    return '';
5934                }
5935
5936                return String(str)
5937                    .replace(/&/g, "&amp;")
5938                    .replace(/</g, "&lt;")
5939                    .replace(/>/g, "&gt;")
5940                    .replace(/"/g, "&quot;")
5941                    .replace(/'/g, "&#039;");
5942            },
5943            getSettings: (element, options, defaults) => {
5944                // Merged: defaults <- data-* <- options
5945                const data = element.dataset || {};
5946                const flat = {};
5947
5948                // einfache data-* Übernahme (dataCity => city, dataClosest => closest, etc.)
5949                Object.keys(data).forEach((k) => {
5950                    const key = k.replace(/^[A-Z]/, (m) => m.toLowerCase());
5951
5952                    try {
5953                        // JSON im data-* erlaubt
5954                        flat[key] = JSON.parse(data[k]);
5955                    } catch {
5956                        flat[key] = data[k];
5957                    }
5958                });
5959
5960                const deep = (a, b) => {
5961                    const out = {...a};
5962
5963                    Object.keys(b || {}).forEach((k) => {
5964                        if (b[k] && typeof b[k] === 'object' && !Array.isArray(b[k])) {
5965                            out[k] = deep(out[k] || {}, b[k]);
5966                        } else {
5967                            out[k] = b[k];
5968                        }
5969                    });
5970                    return out;
5971                };
5972                return deep(deep(defaults, flat), options || {});
5973            }
5974        };
5975
5976        this.jsonpUrl = 'https://www.mr-money.de/AJAX_Server/AJAX_Serv_HR_Adresse_JSON.php';
5977        this.selectors = {
5978            city: opts.citySelector || '.js-city-complete',
5979            street: opts.streetSelector || '.js-street-complete',
5980        };
5981        this.initialized = new WeakSet();
5982        this.observer = null;
5983
5984        if (opts.autoObserve) {
5985            // optional DOMContentLoaded warten, falls vor DOM ready instanzierst
5986            if (document.readyState === 'loading') {
5987                document.addEventListener('DOMContentLoaded', () => {
5988                    this.scan();
5989                    this.startObserver();
5990                });
5991            } else {
5992                this.scan();
5993                this.startObserver();
5994            }
5995        }
5996    }
5997
5998    attachCities(selectorOrElements, options) {
5999        const list = this.toElements(selectorOrElements);
6000
6001        list.forEach((element) => this.initCities(element, options));
6002    }
6003
6004    attachStreets(selectorOrElements, options) {
6005        const list = this.toElements(selectorOrElements);
6006
6007        list.forEach((element) => this.initStreets(element, options));
6008    }
6009
6010    toElements(selectorOrElements) {
6011        if (!selectorOrElements) {
6012            return [];
6013        }
6014
6015        if (typeof selectorOrElements === 'string') {
6016            return Array.from(document.querySelectorAll(selectorOrElements));
6017        }
6018
6019        if (selectorOrElements instanceof Element) {
6020            return [selectorOrElements];
6021        }
6022
6023        return Array.from(selectorOrElements);
6024    }
6025
6026    createDatalist(targetInput, closestClass, prefix) {
6027        const id = `${prefix}-${Math.floor(Math.random() * 10214)}`;
6028
6029        targetInput.setAttribute('list', id);
6030        targetInput.setAttribute('spellcheck', 'false');
6031
6032        const wrap = targetInput.closest(`.${closestClass}`) || targetInput.parentElement || targetInput;
6033        const dataList = document.createElement('datalist');
6034
6035        dataList.id = id;
6036        wrap.appendChild(dataList);
6037
6038        return id;
6039    }
6040
6041    setDatalistHTML(datalistId, html) {
6042        const dataList = document.getElementById(datalistId);
6043
6044        if (dataList) {
6045            dataList.innerHTML = html || '';
6046        }
6047    }
6048
6049    optionHTML(value, text) {
6050        const escaped = this.autoCompletePluginUtils.escapeHtml;
6051
6052        return `<option value="${escaped(value)}">${escaped(text)}</option>`;
6053    }
6054
6055    // JSONP mit abort (Vanilla JS Style)
6056    jsonp({
6057        url,
6058        params,
6059        callbackParam = 'jsonp_callback',
6060        beforeSend,
6061        onSuccess
6062    }) {
6063        const cbName = `jsonp_cb_${Date.now()}_${Math.floor(Math.random() * 1e6)}`;
6064        const query = new URLSearchParams(params || {});
6065
6066        query.set(callbackParam, cbName);
6067
6068        const src = `${url}${url.includes('?') ? '&' : '?'}${query.toString()}`;
6069
6070        let done = false;
6071        const script = document.createElement('script');
6072
6073        const cleanup = () => {
6074            if (window[cbName]) {
6075                delete window[cbName];
6076            }
6077
6078            if (script.parentNode) {
6079                script.parentNode.removeChild(script);
6080            }
6081        };
6082
6083        const abort = () => {
6084            done = true;
6085            cleanup();
6086        };
6087
6088        window[cbName] = (data) => {
6089            if (done) {
6090                return;
6091            }
6092
6093            done = true;
6094
6095            try {
6096                onSuccess && onSuccess(data);
6097            } finally {
6098                cleanup();
6099            }
6100        };
6101
6102        script.src = src;
6103        script.async = true;
6104        script.onerror = () => { cleanup(); };
6105
6106        beforeSend && beforeSend();
6107
6108        document.head.appendChild(script);
6109
6110        return {
6111            abort
6112        };
6113    }
6114
6115    initCities(postalInput, options) {
6116        const defaults = {
6117            url: this.jsonpUrl,
6118            data: {
6119                menu: 'ort'
6120            },
6121            closest: 'mb-3',
6122            city: null,
6123            street: null
6124        };
6125        const settings = this.autoCompletePluginUtils.getSettings(postalInput, options, defaults);
6126
6127        const cityInput = typeof settings.city === 'string'
6128            ? document.querySelector(settings.city)
6129            : settings.city;
6130
6131        if (!cityInput) {
6132            console.warn('[AddressAutocomplete:cities] city-Input fehlt (settings.city).');
6133            return;
6134        }
6135
6136        const datalistId = this.createDatalist(cityInput, settings.closest, 'cities');
6137
6138        let activeAjax = null;
6139        let lastCityVal = null;
6140        let lastPostalCodeVal = null;
6141
6142        postalInput.setAttribute('spellcheck', 'false');
6143        postalInput.setAttribute('maxlength', '5');
6144
6145        const setCityDefaultEmpty = () => {
6146            cityInput.value = '';
6147            cityInput.dispatchEvent(new Event('blur', {
6148                bubbles: true
6149            }));
6150            this.setDatalistHTML(datalistId, '');
6151
6152            return true;
6153        };
6154
6155        if (!cityInput.value.length) {
6156            setCityDefaultEmpty();
6157        } else {
6158            lastCityVal = cityInput.value;
6159            setTimeout(() => {
6160                postalInput.dispatchEvent(new Event('input', {
6161                    bubbles: true
6162                }));
6163            }, 250);
6164        }
6165
6166        const handle = (event) => {
6167            event.preventDefault();
6168
6169            const val = (postalInput.value || '').trim();
6170
6171            if (val.length < 5) {
6172                setCityDefaultEmpty();
6173                lastPostalCodeVal = null;
6174
6175                return;
6176            }
6177
6178            if (val.length === 5) {
6179                if (val === lastPostalCodeVal) return;
6180                lastPostalCodeVal = val;
6181
6182                if (activeAjax && typeof activeAjax.abort === 'function') {
6183                    activeAjax.abort();
6184                    activeAjax = null;
6185                }
6186
6187                activeAjax = this.jsonp({
6188                    url: settings.url,
6189                    params: {
6190                        ...(settings.data || {}),
6191                        PLZ: val
6192                    },
6193                    callbackParam: 'jsonp_callback',
6194                    beforeSend: () => this.setDatalistHTML(datalistId, ''),
6195                    onSuccess: (response) => {
6196                        try {
6197                            if (response && Object.prototype.hasOwnProperty.call(response, 'options')) {
6198                                let optionsHTML = '';
6199
6200                                if (response.options.length === 1) {
6201                                    const option = response.options[0];
6202
6203                                    if (option.value !== 'Bitte zuerst PLZ eintragen') {
6204                                        optionsHTML = this.optionHTML(option.value, option.text);
6205                                        cityInput.value = this.autoCompletePluginUtils.escapeHtml(option.value);
6206                                        cityInput.dispatchEvent(new Event('blur', {
6207                                            bubbles: true
6208                                        }));
6209                                    }
6210                                } else {
6211                                    cityInput.dispatchEvent(new Event('focus', {
6212                                        bubbles: true
6213                                    }));
6214                                    cityInput.click();
6215
6216                                    for (let i = 0; i < response.options.length; i++) {
6217                                        const option = response.options[i];
6218
6219                                        if (option.value !== 'Bitte zuerst PLZ eintragen') {
6220                                            if (lastCityVal !== null && lastCityVal === option.value) {
6221                                                cityInput.value = lastCityVal;
6222                                            }
6223
6224                                            optionsHTML += this.optionHTML(option.value, option.text);
6225                                        }
6226                                    }
6227                                    if (lastCityVal === cityInput.value) {
6228                                        setTimeout(() => {
6229                                            cityInput.dispatchEvent(new Event('blur', {
6230                                                bubbles: true
6231                                            }));
6232                                        }, 250);
6233                                    }
6234                                }
6235
6236                                this.setDatalistHTML(datalistId, optionsHTML);
6237                            }
6238                        } finally {
6239                            activeAjax = null;
6240                        }
6241                    }
6242                });
6243            }
6244        };
6245
6246        postalInput.addEventListener('input', handle);
6247        postalInput.dispatchEvent(new Event('blur', {
6248            bubbles: true
6249        }));
6250    }
6251
6252    initStreets(cityInput, options) {
6253        const defaults = {
6254            url: this.jsonpUrl,
6255            data: { menu: 'strasse' },
6256            closest: 'mb-3',
6257            street: null,
6258            postalCode: null
6259        };
6260        const settings = this.autoCompletePluginUtils.getSettings(cityInput, options, defaults);
6261        const streetInput = typeof settings.street === 'string'
6262            ? document.querySelector(settings.street)
6263            : settings.street;
6264        const postalInput = typeof settings.postalCode === 'string'
6265            ? document.querySelector(settings.postalCode)
6266            : settings.postalCode;
6267
6268        if (!streetInput) {
6269            console.warn('[AddressAutocomplete:streets] street-Input fehlt (settings.street).');
6270            return;
6271        }
6272
6273        if (!postalInput) {
6274            console.warn('[AddressAutocomplete:streets] postalCode-Input fehlt (settings.postalCode).');
6275            return;
6276        }
6277
6278        const datalistId = this.createDatalist(streetInput, settings.closest, 'streets');
6279
6280        let activeAjax = null;
6281        let lastStreetVal = null;
6282        let lastCityVal = null;
6283
6284        streetInput.setAttribute('spellcheck', 'false');
6285
6286        const setStreetDefaultEmpty = () => {
6287            streetInput.dispatchEvent(new Event('blur', {
6288                bubbles: true
6289            }));
6290            this.setDatalistHTML(datalistId, '');
6291
6292            return true;
6293        };
6294
6295        if (!streetInput.value.length) {
6296            setStreetDefaultEmpty();
6297        } else {
6298            lastStreetVal = streetInput.value;
6299        }
6300
6301        const uniqueByRef = (arr) => arr.filter((c, idx) => arr.indexOf(c) === idx);
6302        const handle = (event) => {
6303            event.preventDefault();
6304
6305            const val = (cityInput.value || '').trim();
6306
6307            if (val.length === 0) {
6308                lastCityVal = null;
6309
6310                return;
6311            }
6312
6313            if (val === lastCityVal) {
6314                if (lastStreetVal !== null) {
6315                    streetInput.value = lastStreetVal;
6316                    streetInput.dispatchEvent(new Event('focus', {
6317                        bubbles: true
6318                    }));
6319                    streetInput.dispatchEvent(new Event('blur', {
6320                        bubbles: true
6321                    }));
6322                    streetInput.value = lastStreetVal;
6323                }
6324
6325                return;
6326            }
6327            lastCityVal = val;
6328
6329            if (activeAjax && typeof activeAjax.abort === 'function') {
6330                activeAjax.abort();
6331                activeAjax = null;
6332            }
6333
6334            activeAjax = this.jsonp({
6335                url: settings.url,
6336                params: {
6337                    ...(settings.data || {}),
6338                    Ort: val,
6339                    PLZ: (postalInput.value || '').trim()
6340                },
6341                callbackParam: 'jsonp_callback',
6342                beforeSend: () => this.setDatalistHTML(datalistId, ''),
6343                onSuccess: (response) => {
6344                    try {
6345                        if (response && Object.prototype.hasOwnProperty.call(response, 'options')) {
6346                            let optionsHTML = '';
6347
6348                            if (response.options.length === 1) {
6349                                const option = response.options[0];
6350                                if (option.value !== 'Bitte zuerst Ort wählen') {
6351                                    optionsHTML = this.optionHTML(option.value, option.text);
6352                                    streetInput.value = this.autoCompletePluginUtils.escapeHtml(option.value);
6353                                    streetInput.dispatchEvent(new Event('blur', { bubbles: true }));
6354                                }
6355                            } else {
6356                                streetInput.dispatchEvent(new Event('focus', { bubbles: true }));
6357                                streetInput.click();
6358
6359                                const unique = uniqueByRef(response.options);
6360
6361                                for (let i = 0; i < unique.length; i++) {
6362                                    const option = unique[i];
6363
6364                                    if (option.value !== 'Bitte zuerst Ort wählen') {
6365                                        if (lastStreetVal !== null && lastStreetVal === option.value) {
6366                                            streetInput.value = lastStreetVal;
6367                                        }
6368
6369                                        optionsHTML += this.optionHTML(option.value, option.text);
6370                                    }
6371                                }
6372                            }
6373
6374                            this.setDatalistHTML(datalistId, optionsHTML);
6375                        }
6376                    } finally {
6377                        activeAjax = null;
6378                    }
6379                }
6380            });
6381        };
6382
6383        cityInput.addEventListener('blur', handle);
6384        cityInput.dispatchEvent(new Event('blur', {
6385            bubbles: true
6386        }));
6387    }
6388
6389    scan(root = document) {
6390        root
6391            .querySelectorAll(`${this.selectors.city}, ${this.selectors.street}`)
6392            .forEach((element) => this.initIfNeeded(element));
6393    }
6394
6395    startObserver() {
6396        if (this.observer) {
6397            return;
6398        }
6399        this.observer = new MutationObserver((mutations) => {
6400            for (const mutation of mutations) {
6401                mutation.addedNodes.forEach((node) => {
6402                    if (!(node instanceof HTMLElement)) {
6403                        return;
6404                    }
6405
6406                    this.initIfNeeded(node);
6407
6408                    node
6409                        .querySelectorAll?.(`${this.selectors.city}, ${this.selectors.street}`)
6410                        .forEach((element) => this.initIfNeeded(element));
6411                });
6412            }
6413        });
6414
6415        this.observer.observe(document.documentElement, {
6416            childList: true,
6417            subtree: true,
6418        });
6419    }
6420
6421    stopObserver() {
6422        if (!this.observer) {
6423            return;
6424        }
6425
6426        this.observer.disconnect();
6427        this.observer = null;
6428    }
6429
6430    initIfNeeded(element) {
6431        if (this.initialized.has(element)) {
6432            return;
6433        }
6434
6435        if (element.matches(this.selectors.city)) {
6436            this.attachCities(element);
6437            this.initialized.add(element);
6438        } else if (element.matches(this.selectors.street)) {
6439            this.attachStreets(element);
6440            this.initialized.add(element);
6441        }
6442    }
6443}
6444
6445document.addEventListener('DOMContentLoaded', () => {
6446    // Dynamisches Initialisieren mit update auf dynamischen Content
6447    new AddressAutocomplete({
6448        autoObserve: true,
6449        citySelector: '.js-city-complete',
6450        streetSelector: '.js-street-complete',
6451    });
6452
6453    // Normales initialisieren
6454    //const autocomplete = new AddressAutocomplete();
6455
6456    // Erwartet: data-city / data-street / data-postal-code an den Inputs oder Optionen separat setzen.
6457    // Beispiel: <input class="js-city-complete" data-city="#city">
6458    //autocomplete.attachCities(document.querySelectorAll('.js-city-complete'));
6459
6460    // Beispiel: <input class="js-street-complete" data-street="#street" data-postal-code="#postal">
6461    //autocomplete.attachStreets(document.querySelectorAll('.js-street-complete'));
6462
6463    // Wenn andere Selektoren genutzt werden, per data-* setzen:
6464    // <input className="js-city-complete" id="postal" data-city="#city"/>
6465    // <input className="js-street-complete" id="city" data-street="#street" data-postal-code="#postal"/>
6466    // <input id="street"/>
6467
6468    // Ohne data-*
6469    // const autocomplete = new AddressAutocomplete();
6470    // autocomplete.attachCities('#postal', { city: '#city' });
6471    // autocomplete.attachStreets('#city', { street: '#street', postalCode: '#postal' });
6472});
6473
6474class ScrollButton {
6475    constructor(options = {}) {
6476        this.defaults = {
6477            excludeElements: ['.teaser-overlay'],
6478            container: document.body,
6479            buttonClass: 'scroll-top-btn',
6480            showClass: 'shown',
6481            distance: 110,
6482            ariaLabel: 'Nach oben scrollen',
6483            title: 'Nach oben',
6484            buttonHtml: (btnClass, ariaLabel, title) =>
6485                `<button
6486                    type="button"
6487                    class="${btnClass}"
6488                    aria-label="${ariaLabel}"
6489                    title="${title}"
6490                    tabindex="-1"
6491                    aria-hidden="true"
6492                    disabled
6493                >
6494                    <span aria-hidden="true" class="icon-app chevron-line-up"></span>
6495                </button>`
6496        };
6497
6498        this.settings = Object.assign({}, this.defaults, options);
6499        this.button = null;
6500
6501        if (this.settings.excludeElements.some(sel => document.querySelector(sel))) {
6502            return;
6503        }
6504
6505        this.render();
6506
6507        window.addEventListener('scroll', () => this.onScroll(), { passive: true });
6508
6509        this.onScroll();
6510    }
6511
6512    render() {
6513        if (document.querySelector(`.${this.settings.buttonClass}`)) {
6514            this.button = document.querySelector(`.${this.settings.buttonClass}`);
6515
6516            return;
6517        }
6518
6519        this.settings.container.insertAdjacentHTML(
6520            'beforeend',
6521            this.settings.buttonHtml(this.settings.buttonClass, this.settings.ariaLabel, this.settings.title)
6522        );
6523
6524        this.button = document.querySelector(`.${this.settings.buttonClass}`);
6525
6526        this.button.addEventListener('click', () => this.scrollToTop());
6527
6528        if (typeof window.InteractiveFocusHandler === 'function') {
6529            new window.InteractiveFocusHandler();
6530        }
6531    }
6532
6533    scrollToTop() {
6534        const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
6535
6536        if (prefersReduced) {
6537            window.scrollTo(0, 0);
6538        } else {
6539            window.scrollTo({ top: 0, behavior: 'smooth' });
6540        }
6541
6542        this.button.focus({ preventScroll: true });
6543    }
6544
6545    onScroll() {
6546        if (!this.button) {
6547            return;
6548        }
6549
6550        const y = window.scrollY || document.documentElement.scrollTop;
6551
6552        if (y >= this.settings.distance) {
6553            this.button.removeAttribute('disabled');
6554            this.button.removeAttribute('tabindex');
6555            this.button.setAttribute('aria-hidden', 'false');
6556            this.button.classList.add(this.settings.showClass);
6557        } else {
6558            this.button.setAttribute('disabled', '');
6559            this.button.setAttribute('tabindex', '-1');
6560            this.button.setAttribute('aria-hidden', 'true');
6561            this.button.classList.remove(this.settings.showClass);
6562        }
6563    }
6564
6565    show() {
6566        this.button.removeAttribute('disabled');
6567        this.button.removeAttribute('tabindex');
6568        this.button.setAttribute('aria-hidden', 'false');
6569        this.button.classList.add(this.settings.showClass);
6570    }
6571
6572    hide() {
6573        this.button.setAttribute('disabled', '');
6574        this.button.setAttribute('tabindex', '-1');
6575        this.button.setAttribute('aria-hidden', 'true');
6576        this.button.classList.remove(this.settings.showClass);
6577    }
6578}
6579
6580document.addEventListener('DOMContentLoaded', () => {
6581    new ScrollButton();
6582});
6583
6584/**
6585 * Accessible, such-/keyboardfähige Combobox für ein <select>.
6586 * Ersetzt das Select im DOM (non-destructive) und spiegelt Wert/Status bidirektional.
6587 *
6588 * @typedef {Object} AccessibleComboOptions
6589 * @property {boolean} [search=true]            - Tippen filtert die Optionen.
6590 * @property {boolean} [openShowsAll=true]      - Beim Öffnen alle Optionen anzeigen (ohne Query).
6591 * @property {string}  [placeholder='…']        - Platzhalter im Input.
6592 * @property {string}  [nonResultText='…']      - Text, wenn keine Optionen vorhanden sind.
6593 */
6594class AccessibleComboSelect {
6595    /**
6596     * @param {HTMLSelectElement} selectEl - Ausgangs-Select.
6597     * @param {AccessibleComboOptions} [options]
6598     * @throws {Error} wenn kein <select> übergeben wird.
6599     */
6600    constructor(selectEl, options = {}) {
6601        if (!(selectEl instanceof HTMLSelectElement)) {
6602            throw new Error('Pass a <select>');
6603        }
6604
6605        // Wenn das Select schon initialisiert wurde (Mutation Observer blocken)
6606        if (selectEl.classList.contains('accessible-combo-select-init')) {
6607            return;
6608        }
6609
6610        this.select = selectEl;
6611        this.select.classList.add('accessible-combo-select-init');
6612
6613        // Merge Options
6614        this.options = {
6615            search:        true,
6616            openShowsAll:  true,
6617            placeholder:   window?.texts?.tipToSearch || 'Tippen zum Suchen…',
6618            labelForPlaceholder: window?.texts?.labelTipToSearch || 'Suchfeld für Auswahlliste',
6619            nonResultText: window?.texts?.searchNoResultsFound || 'Es wurden keine Ergebnisse gefunden!',
6620            dangerClass:   'form-control-danger',
6621            ...options
6622        };
6623
6624        this.idBase            = this.select.id || `combo-${Math.random().toString(36).slice(2)}`;
6625        this.activeIndex       = -1;
6626        this.items             = [];
6627        this.userQueried       = false;
6628        this.suppressNextInput = false;
6629
6630        this.#build();
6631        this.#syncFromSelect();
6632        this.#applyDisabledState();
6633        this.#observeSelect();
6634    }
6635
6636    /**
6637     * Öffnet die Liste (public API).
6638     */
6639    open()  {
6640        this.#open();
6641    }
6642
6643    /**
6644     * Schließt die Liste (public API).
6645     */
6646    close() {
6647        this.#close();
6648    }
6649
6650    /**
6651     * Räumt auf: Observer/Listener entfernen und Original-Select zurücksetzen.
6652     */
6653    destroy() {
6654        this.#disconnectObserver();
6655
6656        if (this.label && this.onLabelClick) {
6657            this.label.removeEventListener('click', this.onLabelClick);
6658        }
6659
6660        this.select.classList.remove('visually-hidden');
6661        this.wrapper.replaceWith(this.select);
6662    }
6663
6664    /**
6665     * Baut DOM-Struktur (Wrapper, Input, Listbox, Status), bindet Events
6666     * und initialisiert die Optionsliste aus dem <select>.
6667     * @private
6668     */
6669    #build() {
6670        this.wrapper           = document.createElement('div');
6671        this.wrapper.className = 'combo';
6672
6673        this.input             = document.createElement('input');
6674        this.input.type        = 'text';
6675        this.input.className   = 'combo-input';
6676        this.input.id          = `${this.idBase}-input`;
6677        this.input.setAttribute('role', 'combobox');
6678        this.input.setAttribute('aria-autocomplete', 'list');
6679        this.input.setAttribute('aria-expanded', 'false');
6680        this.input.setAttribute('aria-haspopup', 'listbox');
6681        this.input.setAttribute('autocomplete', 'off');
6682        this.input.setAttribute('spellcheck', 'false');
6683
6684        const labelForComboInput  = document.createElement('label');
6685        labelForComboInput.className   = 'visually-hidden';
6686        labelForComboInput.setAttribute('for', this.input.id);
6687
6688        if (this.options.labelForPlaceholder) {
6689            labelForComboInput.innerText = this.options.labelForPlaceholder;
6690        }
6691
6692        if (this.options.placeholder) {
6693            this.input.placeholder = this.options.placeholder;
6694        }
6695
6696        const label = this.#findLabel();
6697
6698        if (label) {
6699            if (!label.id) {
6700                label.id = `${this.idBase}-label`;
6701            }
6702
6703            // Label-Klick soll den sichtbaren Combobox-Input fokussieren
6704            this.label        = label;
6705            this.onLabelClick = (event) => {
6706                event.preventDefault();
6707
6708                if (this.#isDisabled && this.#isDisabled()) {
6709                    return;
6710                }
6711
6712                this.#open();
6713            };
6714
6715            label.addEventListener('click', this.onLabelClick);
6716        }
6717
6718
6719        this.list = document.createElement('div');
6720        this.list.className    = 'combo-list';
6721        this.list.id           = `${this.idBase}-listbox`;
6722        this.list.dataset.open = 'false';
6723        this.list.setAttribute('aria-labelledby', label ? label.id : this.input.id);
6724        this.list.setAttribute('role', 'listbox');
6725        this.list.setAttribute('tabindex', '-1');
6726
6727        this.input.setAttribute('aria-controls', this.list.id);
6728
6729        this.status            = document.createElement('div');
6730        this.status.className  = 'combo-status';
6731        this.status.setAttribute('aria-live', 'polite');
6732        this.status.setAttribute('aria-atomic', 'true');
6733
6734        this.select.parentNode.append(this.wrapper);
6735
6736        this.wrapper.appendChild(labelForComboInput);
6737        this.wrapper.appendChild(this.input);
6738        this.wrapper.appendChild(this.list);
6739        this.wrapper.appendChild(this.status);
6740
6741        this.select.classList.add('visually-hidden');
6742        this.select.setAttribute('tabindex', '-1');
6743
6744        this.#bindEvents();
6745
6746        if (this.select.options && this.select.options.length) {
6747            this.#rebuildFromSelect();
6748        } else {
6749            this.input.placeholder = this.options.nonResultText;
6750        }
6751    }
6752
6753    /**
6754     * Sucht das zugehörige Label (for-Attribut oder umschließendes <label>).
6755     * @returns {HTMLLabelElement|null}
6756     * @private
6757     */
6758    #findLabel() {
6759        // 1) explizites Label: <label for="selectId">
6760        if (this.select.id) {
6761            const label = document.querySelector(`label[for="${this.select.id}"]`);
6762
6763            if (label) {
6764                return label;
6765            }
6766        }
6767
6768        // 2) umschließendes Label: <label> ... <select> ... </label>
6769        return this.select.closest('label') || null;
6770    }
6771
6772    /**
6773     * Bindet alle Event-Listener (Input, Doku-Klick, Select-Change, List-Mousedown).
6774     * @private
6775     */
6776    #bindEvents() {
6777        this.input.addEventListener('focus', () => this.#open());
6778        this.input.addEventListener('click', () => this.#open());
6779
6780        if (this.options.search) {
6781            this.input.addEventListener('input', (event) => {
6782                if (this.suppressNextInput) {
6783                    this.suppressNextInput = false;
6784                    return;
6785                }
6786
6787                if (this.#isDisabled()) {
6788                    return;
6789                }
6790
6791                this.userQueried = true;
6792
6793                this.#filter(event.target.value);
6794                this.#open();
6795            });
6796        }
6797
6798        this.input.addEventListener('keydown', (event) => this.#onInputKeydown(event));
6799
6800        this.onDocClick = (event) => {
6801            if (!this.wrapper.contains(event.target)) {
6802                this.#close();
6803            }
6804        };
6805
6806        document.addEventListener('click', this.onDocClick);
6807
6808        this.select.addEventListener('change', () => {
6809            this.#syncFromSelect();
6810            this.#close();
6811        });
6812
6813        this.list.addEventListener('mousedown', (event) => {
6814            if (this.#isDisabled()) {
6815                return;
6816            }
6817
6818            const optEl = event.target.closest('[role="option"]');
6819
6820            if (optEl) {
6821                event.preventDefault();
6822                this.#commitById(optEl.id);
6823                this.#close();
6824            }
6825        });
6826    }
6827
6828    /**
6829     * Tastatursteuerung im Combobox-Input (Öffnen/Navigation/Commit/Schließen).
6830     * @param {KeyboardEvent} event
6831     * @private
6832     */
6833    #onInputKeydown(event) {
6834        if (this.#isDisabled()) {
6835            return;
6836        }
6837
6838        const key     = event.key;
6839        const visible = this.#isOpen();
6840
6841        switch (key) {
6842            case 'ArrowDown':
6843                event.preventDefault();
6844                if (!visible) {
6845                    this.#open();
6846                }
6847
6848                this.#moveActive(1);
6849                break;
6850            case 'ArrowUp':
6851                event.preventDefault();
6852
6853                if (!visible) {
6854                    this.#open();
6855                }
6856
6857                this.#moveActive(-1);
6858                break;
6859            case 'Home':
6860                event.preventDefault();
6861
6862                this.#setActive(0);
6863                break;
6864            case 'End':
6865                event.preventDefault();
6866
6867                this.#setActive(this.#visibleOptions().length - 1);
6868                break;
6869            case 'Enter':
6870                event.preventDefault();
6871                setTimeout(() => {
6872                    let cur = this.#getActiveEl() || this.#visibleOptions()[0];
6873
6874                    if (cur) {
6875                        this.#commitById(cur.id);
6876                    }
6877
6878                    this.#close();
6879                }, 100);
6880                break;
6881            case 'Escape':
6882                event.preventDefault();
6883
6884                this.#close();
6885                break;
6886            case 'Tab':
6887                this.#close();
6888                break;
6889            default:
6890                break;
6891        }
6892    }
6893
6894    /**
6895     * Liest Optionen aus dem <select> und baut internes Items-Array neu auf.
6896     * @private
6897     */
6898    #rebuildFromSelect() {
6899        this.items = Array.from(this.select.options).map((opt, idx) => ({
6900            value:    opt.value,
6901            label:    opt.textContent.trim(),
6902            disabled: opt.disabled,
6903            img:      opt.getAttribute('data-image') || null,
6904            alt:      opt.getAttribute('data-image-alt') || null,
6905            content:  opt.getAttribute('data-content') || null,
6906            id:       `${this.idBase}-opt-${idx}`
6907        }));
6908
6909        this.#renderList(this.items);
6910        this.#syncFromSelect();
6911    }
6912
6913    /**
6914     * Rendert die Liste (Listbox) aus einer Item-Quelle, inkl. Bild/HTML-Content.
6915     * Markiert Auswahl und setzt aktives Element neu.
6916     * @param {Array<{value:string,label:string,disabled?:boolean,img?:string,alt?:string,content?:string,id:string}>} sourceItems
6917     * @private
6918     */
6919    #renderList(sourceItems) {
6920        this.list.innerHTML = '';
6921
6922        sourceItems.forEach((item) => {
6923            const div = document.createElement('div');
6924
6925            div.className     = 'combo-option';
6926            div.id            = item.id;
6927            div.dataset.value = item.value;
6928            div.setAttribute('role', 'option');
6929
6930            if (item.disabled) {
6931                div.setAttribute('aria-disabled', 'true');
6932            }
6933
6934            if (item.img) {
6935                const img = document.createElement('img');
6936
6937                img.alt = item.alt || '';
6938                img.src = item.img;
6939
6940                div.appendChild(img);
6941            }
6942
6943            if (item.content) {
6944                const iconSpan = document.createElement('span');
6945
6946                iconSpan.insertAdjacentHTML('afterbegin', item.content);
6947                div.appendChild(iconSpan);
6948            }
6949
6950            const span = document.createElement('span');
6951
6952            span.textContent = item.label;
6953
6954            div.appendChild(span);
6955
6956            this.list.appendChild(div);
6957        });
6958
6959        this.#announce(`${sourceItems.length} Optionen`);
6960        this.#resetActive();
6961        this.#markSelectedInList();
6962    }
6963
6964    /**
6965     * Filtert Items anhand einer Query (case-insensitiv) und rendert neu.
6966     * @param {string} sQuery
6967     * @private
6968     */
6969    #filter(sQuery) {
6970        const querry   = (sQuery || '').trim().toLowerCase();
6971        const filtered = !querry
6972            ? this.items
6973            : this.items.filter(it => it.label.toLowerCase().includes(querry));
6974
6975        this.#renderList(filtered);
6976        this.#open();
6977    }
6978
6979    /**
6980     * Liefert alle sichtbaren, nicht-disabled Options-Elemente.
6981     * @returns {HTMLElement[]}
6982     * @private
6983     */
6984    #visibleOptions() {
6985        return Array.from(this.list.querySelectorAll('[role="option"]:not([aria-disabled="true"])'));
6986    }
6987
6988    /**
6989     * Öffnet die Listbox, optional mit kompletter Liste (wenn konfiguriert).
6990     * Setzt ARIA-Attribute und markiert ausgewählte Option.
6991     * @private
6992     */
6993    #open() {
6994        if (this.#isDisabled() || this.#isOpen()) {
6995            return;
6996        }
6997
6998        if (this.options.search && this.options.openShowsAll) {
6999            this.#renderList(this.items);
7000        }
7001
7002        this.list.dataset.open = 'true';
7003
7004        this.input.setAttribute('aria-expanded', 'true');
7005        this.input.value = '';
7006
7007        this.#markSelectedInList();
7008    }
7009
7010    /**
7011     * Schließt die Listbox, setzt ARIA zurück und spiegelt selektierten Text in den Input.
7012     * @private
7013     */
7014    #close() {
7015        if (!this.#isOpen()) {
7016            return;
7017        }
7018
7019        this.list.dataset.open = 'false';
7020
7021        this.input.setAttribute('aria-expanded', 'false');
7022        this.input.removeAttribute('aria-activedescendant');
7023        this.input.removeAttribute('aria-owns');
7024
7025        const opt = this.select.selectedOptions[0];
7026
7027        if (opt) {
7028            this.input.value = opt.textContent.trim();
7029        }
7030
7031        this.userQueried = false;
7032    }
7033
7034    /**
7035     * Ob die Listbox offen ist.
7036     * @returns {boolean}
7037     * @private
7038     */
7039    #isOpen() {
7040        return this.list.dataset.open === 'true';
7041    }
7042
7043    /**
7044     * Verschiebt das aktive Element relativ (mit Bounds).
7045     * @param {number} delta - +1 / -1 etc.
7046     * @private
7047     */
7048    #moveActive(delta) {
7049        const options = this.#visibleOptions();
7050
7051        if (!options.length) {
7052            return;
7053        }
7054
7055        let next = this.activeIndex + delta;
7056
7057        if (next < 0) {
7058            next = 0;
7059        }
7060
7061        if (next >= options.length) {
7062            next = options.length - 1;
7063        }
7064
7065        this.#setActive(next);
7066    }
7067
7068    /**
7069     * Setzt das aktive Element absolut (mit Scroll-Into-View-Logik).
7070     * @param {number} index
7071     * @private
7072     */
7073    #setActive(index) {
7074        const options = this.#visibleOptions();
7075
7076        if (!options.length) {
7077            return;
7078        }
7079
7080        if (index < 0) {
7081            index = 0;
7082        }
7083
7084        if (index >= options.length) {
7085            index = options.length - 1;
7086        }
7087
7088        const prev = this.#getActiveEl();
7089
7090        if (prev) {
7091            prev.removeAttribute('data-active');
7092        }
7093
7094        const element = options[index];
7095
7096        element.setAttribute('data-active', 'true');
7097
7098        this.activeIndex = index;
7099        this.input.setAttribute('aria-activedescendant', element.id);
7100        this.input.setAttribute('aria-owns', this.list.id || '');
7101
7102        const elTop      = element.offsetTop;
7103        const elBottom   = elTop + element.offsetHeight;
7104        const viewTop    = this.list.scrollTop;
7105        const viewBottom = viewTop + this.list.clientHeight;
7106
7107        if (elTop < viewTop) {
7108            this.list.scrollTop = elTop;
7109        } else if (elBottom > viewBottom) {
7110            this.list.scrollTop = elBottom - this.list.clientHeight;
7111        }
7112    }
7113
7114    /**
7115     * Holt das aktuell aktive List-Element (falls vorhanden).
7116     * @returns {HTMLElement|null}
7117     * @private
7118     */
7119    #getActiveEl() {
7120        return this.list.querySelector('[data-active="true"]');
7121    }
7122
7123    /**
7124     * Setzt aktives Element zurück; bevorzugt die aktuell ausgewählte Option.
7125     * @private
7126     */
7127    #resetActive() {
7128        this.activeIndex = -1;
7129
7130        const current = this.#selectedOptionEl();
7131
7132        if (current) {
7133            const options = this.#visibleOptions();
7134            const idx     = options.indexOf(current);
7135
7136            this.#setActive(idx >= 0 ? idx : 0);
7137        } else {
7138            this.#setActive(0);
7139        }
7140    }
7141
7142    /**
7143     * Gibt das Element der derzeit im <select> ausgewählten Option in der Listbox zurück.
7144     * @returns {HTMLElement|null}
7145     * @private
7146     */
7147    #selectedOptionEl() {
7148        const value = this.select.value;
7149
7150        if (!value) {
7151            return null;
7152        }
7153
7154        return Array.from(this.list.children).find(el => el.dataset.value === value) || null;
7155    }
7156
7157    /**
7158     * Spiegelt ein Listbox-Element per DOM-Index auf das korrespondierende Item.
7159     * @param {number} domIndex
7160     * @returns {{value:string,label:string,id:string}|null}
7161     * @private
7162     */
7163    #mirrorByDomIndex(domIndex) {
7164        const element = this.list.children[domIndex];
7165
7166        if (!element) {
7167            return null;
7168        }
7169
7170        const value = element.dataset.value;
7171
7172        return this.items.find(it => it.value === value) || null;
7173    }
7174
7175    /**
7176     * Commit der Auswahl anhand der Option-ID (setzt Select-Wert, feuert change).
7177     * Aktualisiert Anzeige, kündigt via ARIA an.
7178     * @param {string} optionId
7179     * @private
7180     */
7181    #commitById(optionId) {
7182        const item = this.items.find(it => it.id === optionId)
7183            || this.items.find(it => document.getElementById(it.id) === document.getElementById(optionId));
7184
7185        if (!item || item.disabled) {
7186            return;
7187        }
7188
7189        this.select.value = item.value;
7190
7191        this.select.classList.remove(this.options.dangerClass);
7192        if (this.select.required === true && this.select.value.trim() === '') {
7193            this.select.classList.add(this.options.dangerClass);
7194        }
7195
7196        this.select.dispatchEvent(new Event('change', {
7197            bubbles: true
7198        }));
7199
7200        this.#markSelectedInList();
7201
7202        this.#close();
7203
7204        this.suppressNextInput = true;
7205        this.input.value       = item.label;
7206
7207        this.#announce(`${item.label} ausgewählt`);
7208    }
7209
7210    /**
7211     * Markiert in der Listbox das aktuell im <select> gesetzte value per aria-selected.
7212     * @private
7213     */
7214    #markSelectedInList() {
7215        const val = this.select.value;
7216
7217        Array.from(this.list.children).forEach((element, i) => {
7218            const mirror   = this.#mirrorByDomIndex(i);
7219            const selected = mirror && mirror.value === val;
7220
7221            element.setAttribute('aria-selected', selected ? 'true' : 'false');
7222        });
7223    }
7224
7225    /**
7226     * Spiegelt den aktuellen <select>-Wert in den sichtbaren Input-Text.
7227     * @private
7228     */
7229    #syncFromSelect() {
7230        const opt = this.select.selectedOptions[0];
7231
7232        this.input.value = opt
7233            ? opt.textContent.trim()
7234            : '';
7235    }
7236
7237    /**
7238     * SR-only Statusmeldung setzen (mit rAF für Live-Region-Update).
7239     * @param {string} msg
7240     * @private
7241     */
7242    #announce(msg) {
7243        this.status.textContent = '';
7244
7245        requestAnimationFrame(() => this.status.textContent = msg);
7246    }
7247
7248    /**
7249     * Ob das zugrundeliegende <select> disabled ist.
7250     * @returns {boolean}
7251     * @private
7252     */
7253    #isDisabled() {
7254        return this.select.disabled === true;
7255    }
7256
7257    /**
7258     * Wendet Disabled-Status auf Wrapper/Input an und schließt ggf. die Listbox.
7259     * @private
7260     */
7261    #applyDisabledState() {
7262        const disabled = this.#isDisabled();
7263
7264        this.wrapper.setAttribute('aria-disabled', disabled ? 'true' : 'false');
7265
7266        this.input.disabled = disabled;
7267        this.input.setAttribute('aria-disabled', disabled ? 'true' : 'false');
7268
7269        if (disabled) {
7270            if (this.select.classList.contains(this.options.dangerClass)) {
7271                this.select.classList.remove(this.options.dangerClass);
7272            }
7273
7274            this.#close();
7275        }
7276    }
7277
7278    // ---- Observe ----
7279
7280    /**
7281     * Beobachtet das <select> auf Änderungen (Kinder/Attribute) und synchronisiert die Listbox.
7282     * Rebuild bei Options-/Inhaltsänderungen; bei disabled-Änderung nur State anwenden.
7283     * @private
7284     */
7285    #observeSelect() {
7286        const cfg = {
7287            childList: true,
7288            subtree: false,
7289            attributes: true,
7290            attributeFilter: ['disabled', 'data-content', 'label']
7291        };
7292
7293        this.observer = new MutationObserver((mutations) => {
7294            let needsRebuild    = false;
7295            let disabledChanged = false;
7296
7297            for (const m of mutations) {
7298                if (m.type === 'attributes' && m.attributeName === 'disabled') {
7299                    disabledChanged = true;
7300                } else {
7301                    needsRebuild = true;
7302                }
7303            }
7304
7305            if (disabledChanged) {
7306                this.#applyDisabledState();
7307            }
7308
7309            if (needsRebuild) {
7310                if (this.select.options && this.select.options.length) {
7311                    this.#rebuildFromSelect();
7312                } else {
7313                    this.input.placeholder = this.options.nonResultText;
7314                }
7315
7316                // WICHTIG: nur re-filtern, wenn der User gerade sucht
7317                if (this.options.search && this.userQueried && this.input.value.trim()) {
7318                    this.#filter(this.input.value);
7319                } else {
7320                    this.#syncFromSelect();
7321                }
7322            }
7323        });
7324
7325        this.observer.observe(this.select, cfg);
7326    }
7327
7328    /**
7329     * Trennt den MutationObserver (Cleanup).
7330     * @private
7331     */    #disconnectObserver() {
7332        if (this.observer) {
7333            this.observer.disconnect();
7334        }
7335    }
7336}
7337
7338document.addEventListener('DOMContentLoaded', () => {
7339    /**
7340     * Initialisiert alle .js-combo-select-Elemente mit Standard-Optionen.
7341     */
7342    const selects = document.querySelectorAll('.js-combo-select');
7343
7344    if (selects.length) {
7345        selects.forEach(select => new AccessibleComboSelect(select, {
7346            openShowsAll: true
7347        }));
7348    }
7349});
7350
7351class DocResultList {
7352    constructor() {
7353        this.selector    = '.doc-result-list';
7354        this.initClass   = 'doc-result-list-init';
7355        this.hideClass   = 'd-none';
7356        this.observer    = null;
7357        this.resultLists = Array.from(document.querySelectorAll(this.selector));
7358
7359        if (this.resultLists.length) {
7360            this.initLists(this.resultLists);
7361        }
7362
7363        this.initObserver();
7364    }
7365
7366    initLists(lists) {
7367        lists.forEach(list => this.initList(list));
7368    }
7369
7370    initList(list) {
7371        if (!list || list.classList.contains(this.initClass)) {
7372            return;
7373        }
7374
7375        const ul = list.querySelector('ul');
7376
7377        if (!ul) {
7378            return;
7379        }
7380
7381        const listItems = Array.from(ul.querySelectorAll('li'));
7382
7383        if (!listItems.length) {
7384            return;
7385        }
7386
7387        if (ul.hasAttribute('data-is-static')) {
7388            this.initStaticList(list, ul, listItems);
7389        } else {
7390            this.initDynamicList(list, ul, listItems);
7391        }
7392
7393        list.classList.add(this.initClass);
7394    }
7395
7396    initStaticList(list, ul, listItems) {
7397        const moreLessBtn = list.querySelector('.doc-result-list-expand-button');
7398
7399        // initial hide
7400        this.hideStaticItems(ul, listItems, moreLessBtn);
7401
7402        if (moreLessBtn) {
7403            moreLessBtn.removeEventListener('click', moreLessBtn._docResultHandler);
7404            moreLessBtn._docResultHandler = () => {
7405                const expanded = ul.getAttribute('aria-expanded') === 'true';
7406
7407                if (expanded) {
7408                    this.hideStaticItems(ul, listItems, moreLessBtn);
7409                } else {
7410                    this.showStaticItems(ul, listItems, moreLessBtn);
7411                }
7412            };
7413
7414            moreLessBtn.addEventListener('click', moreLessBtn._docResultHandler);
7415        }
7416    }
7417
7418    showStaticItems(list = null, listItems = [], button = null) {
7419        if (!list || !listItems.length || !button) {
7420            return;
7421        }
7422
7423        listItems.forEach(item => item.classList.remove(this.hideClass));
7424        list.setAttribute('aria-expanded', 'true');
7425        button.textContent = (window.texts && window.texts.showLess)
7426            ? window.texts.showLess
7427            : 'Weniger anzeigen';
7428    }
7429
7430    hideStaticItems(list = null, listItems = [], button = null) {
7431        if (!list || !listItems.length) {
7432            return;
7433        }
7434
7435        if (!button && listItems.length < 3) {
7436            return;
7437        }
7438
7439        if (listItems.length < 3 && button) {
7440            button.remove();
7441            return;
7442        }
7443
7444        listItems.forEach((item, index) => {
7445            if (index > 1) {
7446                item.classList.add(this.hideClass);
7447            } else {
7448                item.classList.remove(this.hideClass);
7449            }
7450        });
7451
7452        list.setAttribute('aria-expanded', 'false');
7453
7454        if (button) {
7455            button.textContent = (window.texts && window.texts.showMore)
7456                ? window.texts.showMore
7457                : 'Mehr anzeigen';
7458            button.classList.remove(this.hideClass);
7459        }
7460    }
7461
7462    initDynamicList(list, ul, listItems) {
7463        listItems.forEach(listItem => {
7464            const trigger = listItem.querySelector('.doc-result-trigger');
7465
7466            if (!trigger) {
7467                return;
7468            }
7469
7470            const closestItem = trigger.closest('.doc-result-list-item') || listItem;
7471
7472            // cache result elements safely
7473            trigger.result = {
7474                spinner:  closestItem.querySelector('.doc-result-spinner'),
7475                srStatus: closestItem.querySelector('.doc-result-status'),
7476                error:    closestItem.querySelector('.doc-result-error'),
7477                results:  closestItem.querySelector('.doc-result-content'),
7478                list:     closestItem.querySelector('.doc-result-fetch-list'),
7479                heading:  closestItem.querySelector('.doc-result-list-header'),
7480            };
7481
7482            trigger.removeEventListener('click', trigger._docResultHandler);
7483            trigger._docResultHandler = async event => {
7484                event.preventDefault();
7485                await this.handleTriggerClick(trigger);
7486            };
7487
7488            trigger.addEventListener('click', trigger._docResultHandler);
7489        });
7490    }
7491
7492    async handleTriggerClick(trigger) {
7493        const result = trigger.result;
7494
7495        if (!result || !result.results || !result.list) {
7496            return;
7497        }
7498
7499        trigger.disabled = true;
7500        trigger.setAttribute('aria-expanded', 'true');
7501
7502        if (result.spinner) {
7503            result.spinner.hidden = false;
7504        }
7505
7506        result.results && result.results.setAttribute('aria-busy', 'true');
7507
7508        if (result.error) {
7509            result.error.hidden = true;
7510            result.error.textContent = '';
7511        }
7512
7513        if (result.srStatus) {
7514            result.srStatus.textContent = (window.texts && window.texts.pleaseWait)
7515                ? window.texts.pleaseWait
7516                : 'Bitte warten...';
7517        }
7518
7519        const url = trigger.dataset.docLoadUrl;
7520
7521        if (!url) {
7522            this._handleFetchError(trigger, result, 'Keine URL zum Laden vorhanden.');
7523            return;
7524        }
7525
7526        try {
7527            const response = await fetch(url, {
7528                method: 'POST',
7529                mode: 'cors',
7530                credentials: 'include',
7531                headers: {
7532                    'Accept': 'application/json'
7533                }
7534            });
7535
7536            if (!response.ok) {
7537                throw new Error(`HTTP ${response.status}`);
7538            }
7539
7540            const data = await response.json();
7541
7542            if (!Array.isArray(data)) {
7543                throw new Error('Unerwartetes Antwortformat.');
7544            }
7545
7546            // @todo: Muss noch angepasst werden, sobald Icrm API live ist
7547            result.list.innerHTML = data.map(item => {
7548                if (item && (item.title || item.url)) {
7549                    const title = item.title || '';
7550                    const url = item.url ? `<a href="${this._escapeAttr(item.url)}">${this._escapeHtml(title)}</a>` : this._escapeHtml(title);
7551                    return `<li>${url}</li>`;
7552                }
7553
7554                return `<li>${this._escapeHtml(String(item))}</li>`;
7555            }).join('');
7556
7557            const loadedText = (window.texts && window.texts.numberOfLoadingDocuments)
7558                ? window.texts.numberOfLoadingDocuments.replace('##', String(data.length))
7559                : `${data.length} Dokumente geladen.`;
7560
7561            if (result.srStatus) {
7562                result.srStatus.textContent = loadedText;
7563            }
7564
7565            if (result.heading && typeof result.heading.focus === 'function') {
7566                result.heading.focus();
7567            } else if (result.results && typeof result.results.focus === 'function') {
7568                result.results.focus();
7569            }
7570        } catch (err) {
7571            this._handleFetchError(trigger, result, err.message || 'Beim Laden ist ein Fehler aufgetreten.');
7572        } finally {
7573            if (result.results) {
7574                result.results.setAttribute('aria-busy', 'false');
7575            }
7576
7577            if (result.spinner) {
7578                result.spinner.hidden = true;
7579            }
7580
7581            trigger.disabled = false;
7582        }
7583    }
7584
7585    _handleFetchError(trigger, result, message) {
7586        if (result && result.error) {
7587            result.error.textContent = (window.texts && window.texts.loadError)
7588                ? window.texts.loadError
7589                : 'Beim Laden ist ein Fehler aufgetreten.';
7590            result.error.hidden = false;
7591        }
7592
7593        if (result && result.srStatus) {
7594            result.srStatus.textContent = (window.texts && window.texts.loadError)
7595                ? window.texts.loadError
7596                : 'Beim Laden ist ein Fehler aufgetreten.';
7597        }
7598
7599        trigger.setAttribute('aria-expanded', 'false');
7600
7601        console.warn('DocResultList fetch error:', message);
7602    }
7603
7604    _escapeHtml(str) {
7605        return String(str)
7606            .replace(/[&<>"']/g, s => ({
7607                '&':'&amp;',
7608                '<':'&lt;',
7609                '>':'&gt;',
7610                '"':'&quot;',
7611                "'":'&#39;'
7612            }[s]));
7613    }
7614
7615    _escapeAttr(str) {
7616        return String(str)
7617            .replace(/"/g, '&quot;');
7618    }
7619
7620    // --- MutationObserver ---
7621    initObserver() {
7622        if (this.observer) {
7623            return;
7624        }
7625
7626        this.observer = new MutationObserver(mutations => {
7627            const added = [];
7628
7629            for (const m of mutations) {
7630                if (m.type !== 'childList') {
7631                    continue;
7632                }
7633
7634                for (const node of m.addedNodes) {
7635                    if (!(node instanceof Element)) {
7636                        continue;
7637                    }
7638
7639                    if (node.matches && node.matches(this.selector)) {
7640                        added.push(node);
7641                    }
7642
7643                    added.push(...Array.from(
7644                        node.querySelectorAll
7645                        ? node.querySelectorAll(this.selector)
7646                        : [])
7647                    );
7648                }
7649            }
7650
7651            const unique = Array.from(new Set(added)).filter(n => !n.classList.contains(this.initClass));
7652
7653            if (unique.length) {
7654                this.initLists(unique);
7655            }
7656        });
7657
7658        this.observer.observe(document.documentElement || document.body, {
7659            childList: true,
7660            subtree: true
7661        });
7662    }
7663
7664    // optional: stop observing
7665    disconnectObserver() {
7666        if (this.observer) {
7667            this.observer.disconnect();
7668            this.observer = null;
7669        }
7670    }
7671}
7672
7673document.addEventListener('DOMContentLoaded', () => {
7674    window.docResultList = new DocResultList();
7675});
7676
7677class ContractFields {
7678    constructor() {
7679        if (ContractFields.instance) {
7680            return ContractFields.instance;
7681        }
7682        ContractFields.instance = this;
7683
7684        // JS Classes
7685        this.utilities = new Utilities();
7686    }
7687
7688    buildIndividualDataHtml(obj = {}) {
7689        const acceptedKeys = [
7690            'AktuellerVertragswert',
7691            'GeleisteterBonus',
7692            'SummeDerAusschuettungen',
7693            'SummeDerBruttobeitragszahlungen',
7694            'SummeDerNettobeitragszahlungen'
7695        ];
7696
7697        if (!obj || typeof obj !== 'object' || Object.keys(obj).length === 0) {
7698            return '';
7699        }
7700
7701        const formatter = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' });
7702        let html = '';
7703
7704        for (const key of acceptedKeys) {
7705            if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key] != null) {
7706                const label = this.utilities.escapeStr(this.utilities.getTextFromWindowVars(`texts.contractFields.${key}`) || key);
7707                const value = (() => {
7708                    const v = obj[key];
7709
7710                    if (typeof v === 'number') {
7711                        return formatter.format(v);
7712                    }
7713
7714                    const parsed = Number(v);
7715                    return Number.isFinite(parsed) ? formatter.format(parsed) : this.utilities.escapeStr(String(v));
7716                })();
7717                html += `<div class="labeled-content">
7718                        <span class="check-label">${label}</span>
7719                        <span class="check-value">${this.utilities.escapeStr(value)}</span>
7720                    </div>`;
7721            }
7722        }
7723
7724        return html;
7725    }
7726
7727    buildFundFactsHtml(obj = {}) {
7728        if (!obj || typeof obj !== 'object' || Object.keys(obj).length === 0) {
7729            return '';
7730        }
7731
7732        const tooltipId = `app-tooltip-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;
7733        const tooltip = `<div class="app-tooltip-wrapper">
7734            <div class="app-tooltip"
7735                role="button"
7736                aria-controls="${this.utilities.escapeStr(tooltipId)}"
7737                aria-label="${this.utilities.escapeStr(this.utilities.getTextFromWindowVars('texts.fundFactsTooltipAriaLabel') || '')}"
7738                data-position="right"
7739                data-static="false"
7740                aria-haspopup="dialog"
7741                data-tooltip-disable-positioning="false"
7742                tabindex="0">
7743                    <span class="icon-app info" aria-hidden="true" style="font-size: 1rem;"></span>
7744                </div>
7745                <div class="app-tooltip-inner"
7746                    id="${this.utilities.escapeStr(tooltipId)}"
7747                    role="dialog">
7748                    <div class="app-tooltip-description">
7749                        <span class="app-tooltip-header">${this.utilities.escapeStr(this.utilities.getTextFromWindowVars('texts.warning') || '')}</span>
7750                        <p>${this.utilities.escapeStr(this.utilities.getTextFromWindowVars('texts.fondsfactsInfo') || '')}</p>
7751                        <span class="app-tooltip-close" role="button" tabindex="-1">${this.utilities.escapeStr(this.utilities.getTextFromWindowVars('texts.closeWindow') || '')}</span>
7752                    </div>
7753                </div>
7754            </div>`;
7755
7756        let listElements = '';
7757        for (const [, fundFactData] of Object.entries(obj)) {
7758            if (fundFactData && typeof fundFactData === 'object' && fundFactData.factSheetName) {
7759                const name = this.utilities.escapeStr(fundFactData.name || fundFactData.factSheetName || '');
7760                const url = this.utilities.escapeStr(fundFactData.url || '#');
7761                const title = this.utilities.escapeStr(`${fundFactData.name || ''} | ${window.texts?.linkOpenInNewTab || ''}`);
7762
7763                listElements += `<li class="doc-result-list-item">
7764                        <a href="${url}" title="${title}" target="_blank" rel="noopener noreferrer" class="has-pdf-icon js-download-link">
7765                            <span class="icon-app pdf" aria-hidden="true"></span>
7766                            ${name}
7767                        </a>
7768                    </li>`;
7769            }
7770        }
7771
7772        if (!listElements) {
7773            return '';
7774        }
7775
7776        return `<div class="insurance-more-content-list highlighted">
7777                <div class="doc-result-list">
7778                    <h2 class="d-inline-block doc-result-list-header js-tooltip-appender position-relative">
7779                        ${this.utilities.escapeStr(this.utilities.getTextFromWindowVars('texts.fondsfacts') || '')}
7780                    </h2>
7781                    <span class="position-relative">${tooltip}</span>
7782                    <ul aria-expanded="true" data-is-static>${listElements}</ul>
7783                    <button type="button" class="d-none is-link doc-result-list-expand-button">${this.utilities.escapeStr(window.texts?.showMore || 'Mehr anzeigen')}</button>
7784                </div>
7785            </div>`;
7786    }
7787
7788    async getAjax(url = null, insuranceId = null, that = null) {
7789        if (!url) {
7790            return Promise.reject(new Error('Missing URL'));
7791        }
7792
7793        if (that && that.getAttribute && that.getAttribute('data-contract-fields-loaded') !== null) {
7794            return Promise.resolve();
7795        }
7796
7797        const body = new FormData();
7798        body.append('insuranceId', insuranceId ?? '');
7799
7800        if (window.$ && window.$.fn && typeof window.$.fn.progress === 'function') {
7801            window.$.fn.progress.renderProgressLayer();
7802        }
7803
7804        try {
7805            const resp = await fetch(url, {
7806                method: 'POST',
7807                body,
7808                credentials: 'same-origin'
7809            });
7810            const text = await resp.text();
7811            let data;
7812
7813            try {
7814                data = text
7815                    ? JSON.parse(text)
7816                    : {};
7817            } catch (e) {
7818                throw new Error('Invalid JSON response');
7819            }
7820
7821            if (!data || typeof data !== 'object') {
7822                throw new Error('Unexpected response');
7823            }
7824
7825            if (data.success) {
7826                let html = '';
7827
7828                if (that && !(that instanceof Element)) {
7829                    throw new Error('Provided "that" is not a DOM Element');
7830                }
7831
7832                if (that && that.setAttribute) {
7833                    that.setAttribute('data-contract-fields-loaded', '');
7834                }
7835
7836                if (data.individualInsuranceData && typeof data.individualInsuranceData === 'object') {
7837                    html = this.buildIndividualDataHtml(data.individualInsuranceData);
7838                }
7839
7840                if (data.fundFacts && typeof data.fundFacts === 'object') {
7841                    html += this.buildFundFactsHtml(data.fundFacts);
7842                }
7843
7844                if (html) {
7845                    if (that && that.closest) {
7846                        const parent = that.closest('.labeled-content');
7847
7848                        if (parent && parent.parentNode) {
7849                            const fragment = this._htmlToFragment(html);
7850
7851                            parent.parentNode.replaceChild(fragment, parent);
7852                        }
7853                    }
7854                }
7855            } else {
7856                if (window.$ && window.$.fn && typeof window.$.fn.flash === 'function') {
7857                    window.$.fn.flash.render({
7858                        message: data.message ?? ''
7859                    }, 'error');
7860                }
7861            }
7862
7863            return data;
7864        } catch (err) {
7865            if (window.$ && window.$.fn && typeof window.$.fn.flash === 'function') {
7866                window.$.fn.flash.render({
7867                    message: {
7868                        header: window.messages?.errorAlert ?? '',
7869                        text: window.messages?.errorGlobal ?? ''
7870                    }
7871                }, 'error');
7872            }
7873
7874            return Promise.reject(err);
7875        } finally {
7876            if (window.$ && window.$.fn && typeof window.$.fn.progress === 'function') {
7877                window.$.fn.progress.removeProgressLayer();
7878            }
7879        }
7880    }
7881
7882    _htmlToFragment(html) {
7883        const template = document.createElement('template');
7884        template.innerHTML = html.trim();
7885
7886        const frag = document.createDocumentFragment();
7887
7888        while (template.content.firstChild) {
7889            frag.appendChild(template.content.firstChild);
7890        }
7891
7892        return frag;
7893    }
7894
7895    load() {
7896        const buttons = document.querySelectorAll('[data-contract-fields]');
7897
7898        if (!buttons || buttons.length === 0) {
7899            return true;
7900        }
7901
7902        buttons.forEach(button => {
7903            const thatInsuranceId = button.getAttribute('data-contract-field-id') || '';
7904            const thatUrl = button.getAttribute('data-contract-fields-uri') || '';
7905            const checkUri = button.getAttribute('data-check-additional-data-exists-uri');
7906
7907            if (!checkUri) {
7908                return;
7909            }
7910
7911            const body = new FormData();
7912            body.append('insuranceId', thatInsuranceId);
7913
7914            fetch(checkUri, {
7915                method: 'POST',
7916                body,
7917                credentials: 'same-origin'
7918            })
7919                .then(r => r.json())
7920                .then(response => {
7921                    const additionalData = !!(response && response.hasAdditionalData);
7922
7923                    if (additionalData) {
7924                        const labeled = button.closest('.labeled-content');
7925
7926                        if (labeled) {
7927                            labeled.classList.remove('d-none');
7928                        }
7929
7930                        button.addEventListener('click', (event) => {
7931                            event.preventDefault();
7932
7933                            if (button.getAttribute('data-contract-fields-loaded') !== null) {
7934                                return;
7935                            }
7936
7937                            this.getAjax(thatUrl, thatInsuranceId, button).catch(()=>{/* already handled */});
7938                        }, { once: false });
7939                    }
7940                })
7941                .catch(() => {
7942                    // optional logging
7943                });
7944        });
7945
7946        return true;
7947    }
7948}
7949
7950document.addEventListener('DOMContentLoaded', () => {
7951    const cf = new ContractFields();
7952    cf.load();
7953});
7954
7955class Procuration {
7956    constructor() {
7957        this.message = {
7958            state: 'completed',
7959            form: null,
7960        };
7961
7962        this.resize = {
7963            trigger: null,
7964            closeTrigger: null,
7965            layer: document.querySelector('.layer-default.embedded.static'),
7966            embeddedClass: 'embedded',
7967            staticClass: 'static',
7968            fullSizeClass: 'full-size',
7969        }
7970    }
7971
7972    // Methods for Message-Handler
7973
7974    sendAfterMessage() {
7975        this.getMessageEvent();
7976    }
7977
7978    getMessageEvent() {
7979        window.addEventListener('message', ev => this.checkMessage(ev));
7980    }
7981
7982    checkMessage(event) {
7983        const message = event.data?.osign?.message || null;
7984
7985        if (message === this.message.state) {
7986            this.getSubmitTypeByForm();
7987            this.setFormToBusy();
7988        }
7989    }
7990
7991    getSubmitTypeByForm() {
7992        this.message.form = document.getElementById('signatureForm');
7993
7994        return this.message.form.dataset.procurationType === 'procurationSubmit'
7995            ? this.message.form.submit()
7996            : this.message.form.requestSubmit();
7997    }
7998
7999    setFormToBusy() {
8000        this.message.form = document.getElementById('signatureForm');
8001        const form = this.message.form;
8002        form.setAttribute('aria-busy', 'true');
8003
8004        form.querySelector('.layer-loader').classList.remove('d-none');
8005        form.querySelector('iframe').style.opacity = 0;
8006        form.querySelector('iframe').style.visibility = 'hidden';
8007    }
8008
8009    // Methods for Resize Document
8010
8011    resizeDocument() {
8012        this.resize.trigger = document.getElementById('procurationFullSizeButton');
8013        this.resize.closeTrigger = document.getElementById('procurationNormalSizeButton');
8014
8015        if (this.resize.trigger) {
8016            this.resize.trigger.addEventListener('click', ev => this.resizeDocumentToFullSize(ev));
8017        }
8018
8019        if (this.resize.closeTrigger) {
8020            this.resize.closeTrigger.addEventListener('click', ev => this.resizeDocumentToNormalSize(ev));
8021        }
8022    }
8023
8024    resizeDocumentToFullSize(event) {
8025        event.preventDefault();
8026        const layer = this.resize.layer;
8027
8028        if (layer) {
8029            layer.classList.remove(this.resize.embeddedClass);
8030            layer.classList.remove(this.resize.staticClass);
8031            layer.classList.add(this.resize.fullSizeClass);
8032
8033            this.resize.trigger.closest('div').style.display = 'none';
8034        }
8035    }
8036
8037    resizeDocumentToNormalSize(event) {
8038        event.preventDefault();
8039        const layer = this.resize.layer;
8040
8041        if (layer) {
8042            layer.classList.remove(this.resize.fullSizeClass);
8043            layer.classList.add(this.resize.staticClass);
8044            layer.classList.add(this.resize.embeddedClass);
8045
8046            this.resize.trigger.closest('div').style = null;
8047        }
8048    }
8049}
8050
8051document.addEventListener('DOMContentLoaded', () => {
8052    new Procuration().sendAfterMessage();
8053    new Procuration().resizeDocument();
8054})
8055
8056class MultiplyField {
8057    constructor(container, options = {}) {
8058        this.container = container;
8059
8060        const defaults = {
8061            maxItems:                  null,
8062            addButtonSelector:         '.multiply-field-button button',
8063            rowSelector:               '.multiply-field-row',
8064            insertBeforeSelector:      '.multiply-field-button',
8065            removeButtonClass:         'multiply-field-remove-row',
8066            removeButtonTextContent:   '–',
8067            createRemoveButton:        true,
8068            removeBtnInsertMethod:     'prepend',
8069            showRemoveOnFirst:         false,
8070            showRemoveOnFirstCallback: false,
8071            liveRegionAddNewWording:   'Neue Person hinzugefügt',
8072            liveRegionRemoveWording:   'Person entfernt',
8073            ariaLabelRemoveWording:    'Person %1$s entfernen'
8074        };
8075
8076        const dataOptions = this.getDataOptions(container);
8077
8078        this.options = {
8079            ...defaults,
8080            ...dataOptions,
8081            ...options, // höchste Priorität
8082        };
8083
8084        this.removeOnFirstCallbacks = {
8085            'accident': this.accidentRemoveOnFirst,
8086        }
8087
8088        this.addButton  = container.querySelector(this.options.addButtonSelector);
8089        this.maxItems   = this.options.maxItems;
8090
8091        this.liveRegion = document.createElement('div');
8092        this.liveRegion.setAttribute('aria-live', 'polite');
8093        this.liveRegion.setAttribute('aria-atomic', 'true');
8094
8095        this.liveRegion.className = 'visually-hidden';
8096
8097        this.container.appendChild(this.liveRegion);
8098
8099        this.init();
8100    }
8101
8102    /**
8103     *
8104     */
8105    init() {
8106        if (!this.container || !this.addButton){
8107            return;
8108        }
8109
8110        this.addButton.addEventListener('click', () => this.addRow());
8111
8112        // Event für Remove
8113        if (this.options.createRemoveButton) {
8114            this.container.addEventListener('click', (e) => {
8115                if (e.target.classList.contains(this.options.removeButtonClass)) {
8116                    this.removeRow(e.target);
8117                }
8118            });
8119        }
8120
8121        // erste Row optional ohne Button
8122        const firstRow = this.container.querySelectorAll(this.options.rowSelector);
8123        const removeBtn = firstRow[0]?.querySelector(`.${this.options.removeButtonClass}`);
8124        if (removeBtn && this.options.showRemoveOnFirst) {
8125            if (this.options.showRemoveOnFirstCallback) {
8126                removeBtn.addEventListener('click', this.removeOnFirstCallbacks[this.options.showRemoveOnFirstCallback]);
8127            }
8128        } else {
8129            removeBtn.remove();
8130        }
8131    }
8132
8133    /**
8134     *
8135     * @param element
8136     * @return {{}}
8137     */
8138    getDataOptions(element) {
8139        const options = {};
8140
8141        for (const [key, value] of Object.entries(element.dataset)) {
8142            options[key] = this.parseValue(value);
8143        }
8144
8145        return options;
8146    }
8147
8148    /**
8149     *
8150     * @param value
8151     * @return {*|number|null|boolean}
8152     */
8153    parseValue(value) {
8154        if (value === 'true') {
8155            return true;
8156        }
8157
8158        if (value === 'false') {
8159            return false;
8160        }
8161
8162        if (value === 'null') {
8163            return null;
8164        }
8165
8166        // Number erkennen
8167        if (!isNaN(value) && value.trim() !== '') {
8168            return Number(value);
8169        }
8170
8171        return value;
8172    }
8173
8174    /**
8175     *
8176     * @return {*}
8177     */
8178    getRows() {
8179        return this.container.querySelectorAll(this.options.rowSelector);
8180    }
8181
8182    /**
8183     *
8184     * @return {*}
8185     */
8186    getNextIndex() {
8187        return this.getRows().length;
8188    }
8189
8190    /**
8191     *
8192     */
8193    addRow() {
8194        const rows = this.getRows();
8195
8196        if (this.maxItems && rows.length >= this.maxItems) {
8197            return;
8198        }
8199
8200        const lastRow  = rows[rows.length - 1];
8201        const clone    = lastRow.cloneNode(true);
8202        const newIndex = this.getNextIndex();
8203
8204        clone.querySelectorAll('input, select, textarea').forEach(field => {
8205            const newID = Math.random().toString(16).slice(2);
8206            const label = clone.querySelector(`label[for="${field.id}"]`);
8207
8208            // Reset value
8209            if (field.tagName === 'SELECT') {
8210                field.selectedIndex = 0;
8211            } else {
8212                field.value = '';
8213            }
8214
8215            // Name anpassen
8216            if (field.name) {
8217                field.name = field.name.replace(/_\d+/, `_${newIndex}`);
8218            }
8219
8220            // ID & for anpassen
8221            if (label) {
8222                label.setAttribute('for', newID);
8223            }
8224            field.id = newID;
8225        });
8226
8227        const removeClasses = ['form-control-danger', 'form-control-warning', 'form-control-success', 'label-init'];
8228        clone.querySelectorAll('input, select, textarea').forEach(el => {
8229            const label = clone.querySelector(`label[for="${el.id}"]`);
8230
8231            if (label) {
8232                el.dataset.placeholder = label.textContent;
8233
8234                removeClasses.forEach(remove => {
8235                    el.classList.remove(remove);
8236                });
8237            }
8238        });
8239
8240        if (this.options.createRemoveButton) {
8241            this.ensureRemoveButton(clone, newIndex);
8242        }
8243
8244        this.container.insertBefore(
8245            clone,
8246            this.container.querySelector(this.options.insertBeforeSelector)
8247        );
8248
8249        if (this.container.closest('form').classList.contains('js-form-validation-base')) {
8250            if (typeof $ !== 'undefined' && typeof $.fn.validateBase === 'function') {
8251                $(this.container.closest('form')).validateBase();
8252                $.fn.validateBase.update($(this.container.closest('form')));
8253            }
8254        }
8255
8256        this.liveRegion.textContent = this.options.liveRegionAddNewWording;
8257    }
8258
8259    /**
8260     *
8261     */
8262    setNewAria() {
8263        let removeBtns = this.container.querySelectorAll(`.${this.options.removeButtonClass}`);
8264
8265        removeBtns.forEach((btn, index) => {
8266            btn.setAttribute('aria-label', this.options.ariaLabelRemoveWording.replace('%1$s', index + 1));
8267        });
8268    }
8269
8270    /**
8271     *
8272     * @param button
8273     */
8274    removeRow(button) {
8275        const rows = this.getRows();
8276
8277        if (rows.length <= 1) {
8278            return;
8279        }
8280
8281        const row = button.closest(this.options.rowSelector);
8282
8283        if (row) {
8284            row.remove();
8285
8286            this.liveRegion.textContent = this.options.liveRegionRemoveWording;
8287            this.setNewAria();
8288        }
8289    }
8290
8291    /**
8292     *
8293     * @param row
8294     * @param index
8295     */
8296    ensureRemoveButton(row, index) {
8297        let btn = row.querySelector(`.${this.options.removeButtonClass}`);
8298
8299        if (this.options.showRemoveOnFirst) {
8300            btn.remove();
8301            btn = null;
8302        }
8303
8304        // wenn nicht vorhanden → erstellen
8305        if (!btn) {
8306            btn = document.createElement('button');
8307            btn.type = 'button';
8308            btn.className = this.options.removeButtonClass;
8309            btn.textContent = this.options.removeButtonTextContent;
8310            btn.setAttribute('aria-label', this.options.ariaLabelRemoveWording.replace('%1$s', index + 1));
8311
8312            if (this.options.removeBtnInsertMethod === 'prepend') {
8313                row.insertBefore(btn, row.firstChild);
8314            } else {
8315                row.appendChild(btn);
8316            }
8317        }
8318
8319        btn.setAttribute('aria-label', this.options.ariaLabelRemoveWording.replace('%1$s', index + 1));
8320    }
8321
8322    /*********************** Callbackmethoden beim Klick (whatever) auf ersten Löschen Button *******************/
8323    /************************################################################################**
8323******************/
8324
8325    /**
8326     *
8327     * @param event
8328     */
8329    accidentRemoveOnFirst(event) {
8330        const closestContainer = event.target?.closest('.multiply-field');
8331        const checkboxes = document.getElementsByName('injuredPersons[]');
8332
8333        if (closestContainer) {
8334            document.getElementById('addMorePersons').classList.remove('d-none');
8335            closestContainer.classList.add('d-none');
8336            closestContainer.querySelectorAll('.multiply-field-row')?.forEach((row, key) => {
8337                if (key > 0) {
8338                    row.remove()
8339                }
8340
8341                row.querySelectorAll('input, select, textarea').forEach(field => {
8342                    // Reset value
8343                    if (field.tagName === 'SELECT') {
8344                        field.selectedIndex = 0;
8345                    } else {
8346                        field.value = '';
8347                    }
8348                });
8349            });
8350
8351            if (checkboxes.length) {
8352                checkboxes.forEach((checkbox) => {
8353                    checkbox.setAttribute('required', '');
8354                });
8355            }
8356
8357            if (typeof $.fn.validateBase === 'function') {
8358                $.fn.validateBase.update($(closestContainer.closest('form')));
8359            }
8360        }
8361    }
8362}
8363
8364document.addEventListener('DOMContentLoaded', () => {
8365
8366    const initMultiplyFields = (root = document) => {
8367        const containers = root.querySelectorAll('.multiply-field');
8368
8369        containers.forEach(container => {
8370            // schon initialisiert -> überspringen
8371            if (container.dataset.initialized === 'true') {
8372                return;
8373            }
8374
8375            new MultiplyField(container);
8376
8377            container.dataset.initialized = 'true';
8378        });
8379    };
8380
8381    // initial laufen lassen
8382    initMultiplyFields();
8383
8384    // Observer
8385    const observer = new MutationObserver((mutations) => {
8386        mutations.forEach(mutation => {
8387            mutation.addedNodes.forEach(node => {
8388                // nur Element Nodes
8389                if (node.nodeType !== 1) {
8390                    return;
8391                }
8392
8393                // falls direkt der Container ist
8394                if (node.classList?.contains('multiply-field')) {
8395                    initMultiplyFields(node.parentNode);
8396                } else {
8397                    // falls irgendwo drin enthalten
8398                    initMultiplyFields(node);
8399                }
8400            });
8401        });
8402    });
8403
8404    observer.observe(document.body, {
8405        childList: true,
8406        subtree: true
8407    });
8408});
8409
8410class Cancellation {
8411    constructor() {
8412        this.cancellationForm = document.getElementById('cancellationForm');
8413
8414        if (!this.cancellationForm) {
8415            return;
8416        }
8417
8418        this.noneClass            = 'd-none';
8419        this.errorTimeout         = null;
8420        this.errorTimeoutDuration = 5000;
8421        this.utilities            = new Utilities();
8422        this.abortControllers     = new WeakMap();
8423        this.isForCustomers       = this.cancellationForm.dataset.isForCustomers === 'true';
8424
8425        this._submitButton = this.cancellationForm.querySelector('button[type="submit"]');
8426        this._alert        = this.cancellationForm.querySelector('.alert');
8427        this._alertHeader  = this._alert?.querySelector('.alert-heading') || null;
8428        this._alertText    = this._alert?.querySelector('p') || null;
8429
8430        this.init();
8431    }
8432
8433    init() {
8434        this.initSelects();
8435        this.initIdToggle();
8436        this.initSubmit();
8437    }
8438
8439    initSelects() {
8440        const selects = this.cancellationForm.querySelectorAll('.js-insurance-add-select');
8441
8442        if (!selects.length) {
8443            return;
8444        }
8445
8446        const fieldsToHide = this.getCustomerFields();
8447
8448        selects.forEach(select => {
8449            new AccessibleComboSelect(select);
8450
8451            switch (select.dataset.insuranceSelectType) {
8452                case 'contract':
8453                    this.initContractSelect(select, fieldsToHide);
8454                    break;
8455
8456                case 'branch':
8457                    this.initBranchSelect(select, fieldsToHide);
8458                    break;
8459
8460                default:
8461                    break;
8462            }
8463        });
8464    }
8465
8466    initContractSelect(select, fieldsToHide) {
8467        const radioGroup = this.getCancellationRadioGroup();
8468        const proposalNumberField = document.getElementById('proposalNumberField');
8469        const policyNumberField = document.getElementById('policyNumberField');
8470
8471        if (!radioGroup || !proposalNumberField || !policyNumberField) {
8472            return;
8473        }
8474
8475        select.addEventListener('change', event => {
8476            const contractSelect = event.currentTarget;
8477            const selectedOption = contractSelect.options[contractSelect.selectedIndex];
8478            const contractValue = contractSelect.value;
8479
8480            if (this.isForCustomers) {
8481                this.hideFields(fieldsToHide);
8482            }
8483
8484            if (!contractValue) {
8485                return;
8486            }
8487
8488            const optionData = this.parseJson(selectedOption?.dataset.extra);
8489
8490            this.handleProviderFallback(optionData);
8491            this.handleContractNumberFields(optionData, radioGroup, policyNumberField);
8492        });
8493    }
8494
8495    initBranchSelect(select, fieldsToHide) {
8496        select.addEventListener('change', event => {
8497            event.preventDefault();
8498
8499            const branchSelect = event.currentTarget;
8500            const branchValue = branchSelect.value;
8501            const targetSelect = this.getBranchTargetSelect();
8502
8503            this.hideErrorMessage();
8504
8505            if (!targetSelect) {
8506                return;
8507            }
8508
8509            if (this.isForCustomers) {
8510                this.hideFields(fieldsToHide);
8511            }
8512
8513            if (branchValue) {
8514                targetSelect.setCustomValidity('');
8515                targetSelect.checkValidity();
8516            }
8517
8518            this.setSelectPlaceholder(targetSelect, branchValue ? this.getText('texts.pleaseWait') : this.getText('texts.addInsurance.chooseInsuranceBranch'));
8519            targetSelect.disabled = true;
8520
8521            if (!branchValue) {
8522                return;
8523            }
8524
8525            const data = new FormData();
8526            data.set('branchICrmId', branchValue);
8527
8528            this.fetchForSelect(targetSelect, data);
8529        });
8530    }
8531
8532    handleProviderFallback(optionData) {
8533        if (optionData?.hasProviderICrmId) {
8534            return;
8535        }
8536
8537        const chooseProviderSelect = document.getElementById('chooseProviderSelect');
8538        const providerSelect = chooseProviderSelect?.querySelector('select');
8539        const branchSelect = this.cancellationForm.querySelector('select[data-insurance-select-type="branch"]');
8540        const branchValue = branchSelect?.value || null;
8541
8542        if (!chooseProviderSelect || !providerSelect || !branchValue) {
8543            return;
8544        }
8545
8546        const data = new FormData();
8547        data.set('branchICrmId', branchValue);
8548
8549        chooseProviderSelect.classList.remove(this.noneClass);
8550        this.fetchForSelect(providerSelect, data);
8551    }
8552
8553    handleContractNumberFields(optionData, radioGroup, policyNumberField) {
8554        if (optionData?.policyNumber) {
8555            const policyInput = policyNumberField.querySelector('input');
8556
8557            policyNumberField.classList.remove(this.noneClass);
8558
8559            if (policyInput) {
8560                policyInput.value = optionData.policyNumber;
8561                policyInput.readOnly = true;
8562            }
8563
8564            return;
8565        }
8566
8567        radioGroup.classList.remove(this.noneClass);
8568        radioGroup.querySelectorAll('input[type="radio"]').forEach(radio => {
8569            radio.disabled = false;
8570        });
8571    }
8572
8573    async fetchForSelect(select, data) {
8574        if (!select?.dataset.ajaxUri) {
8575            return;
8576        }
8577
8578        const spinner = select.closest('.mb-3')?.querySelector('.spinner-border') || null;
8579        const previousController = this.abortControllers.get(select);
8580        const abortController = new AbortController();
8581
8582        previousController?.abort();
8583        this.abortControllers.set(select, abortController);
8584        spinner?.classList.remove(this.noneClass);
8585
8586        try {
8587            const response = await fetch(select.dataset.ajaxUri, {
8588                method: 'POST',
8589                body: data,
8590                mode: 'cors',
8591                credentials: 'include',
8592                signal: abortController.signal,
8593                headers: {
8594                    'X-Requested-With': 'XMLHttpRequest',
8595                },
8596            });
8597
8598            if (!response.ok) {
8599                throw new Error(`Request failed with status ${response.status}`);
8600            }
8601
8602            const payload = await response.json();
8603
8604            if (abortController.signal.aborted) {
8605                return;
8606            }
8607
8608            if (payload?.content) {
8609                this.updateSelectOptions(select, payload.content);
8610                return;
8611            }
8612
8613            this.showNoCompanyFound(select);
8614        } catch (error) {
8615            if (error.name !== 'AbortError') {
8616                this.setError(error);
8617            }
8618        } finally {
8619            if (this.abortControllers.get(select) === abortController) {
8620                this.abortControllers.delete(select);
8621            }
8622
8623            spinner?.classList.add(this.noneClass);
8624        }
8625    }
8626
8627    updateSelectOptions(select, content) {
8628        this.removeNoCompanyMessage(select);
8629
8630        select.innerHTML = content;
8631        select.disabled = false;
8632
8633        this.autoSelectSingleOption(select);
8634    }
8635
8636    autoSelectSingleOption(select) {
8637        const selectableOptions = Array.from(select.options).filter(option => option.value !== '');
8638
8639        if (selectableOptions.length !== 1) {
8640            return;
8641        }
8642
8643        select.value = selectableOptions[0].value;
8644        select.dispatchEvent(new Event('change', { bubbles: true }));
8645    }
8646
8647    showNoCompanyFound(select) {
8648        this.removeNoCompanyMessage(select);
8649
8650        const message = this.getText('texts.addInsurance.noCompanyFound');
8651
8652        select.insertAdjacentHTML('beforebegin', `
8653            <div class="alert alert-warning lc mb-0" role="alert" data-js="no-company-found">
8654                <p class="mb-0"><span class="icon-app info me-2"></span>${message}</p>
8655            </div>
8656        `);
8657    }
8658
8659    removeNoCompanyMessage(select) {
8660        const previousElement = select.previousElementSibling;
8661
8662        if (previousElement?.matches('[data-js="no-company-found"]')) {
8663            previousElement.remove();
8664        }
8665    }
8666
8667    hideFields(fields = []) {
8668        fields.filter(Boolean).forEach(field => {
8669            field.classList.add(this.noneClass);
8670
8671            field.querySelectorAll('input, select').forEach(inputOrSelect => {
8672                if (inputOrSelect.tagName === 'INPUT') {
8673                    this.resetInput(inputOrSelect);
8674                    return;
8675                }
8676
8677                if (inputOrSelect.tagName === 'SELECT') {
8678                    inputOrSelect.disabled = true;
8679                }
8680            });
8681        });
8682    }
8683
8684    resetInput(input) {
8685        switch (input.type) {
8686            case 'text':
8687                input.value = '';
8688                input.readOnly = false;
8689                break;
8690
8691            case 'radio':
8692                input.checked = false;
8693                input.disabled = true;
8694                break;
8695
8696            default:
8697                break;
8698        }
8699    }
8700
8701    initIdToggle() {
8702        const group = this.getCancellationRadioGroup();
8703
8704        if (!group) {
8705            return;
8706        }
8707
8708        group.addEventListener('change', event => {
8709            const radio = event.target;
8710
8711            if (radio.type !== 'radio') {
8712                return;
8713            }
8714
8715            const isPolicyNumber = radio.value === 'policyNumber';
8716
8717            document.getElementById('policyNumberField')?.classList.toggle(this.noneClass, !isPolicyNumber);
8718            document.getElementById('proposalNumberField')?.classList.toggle(this.noneClass, isPolicyNumber);
8719        });
8720    }
8721
8722    initSubmit() {
8723        this.cancellationForm.addEventListener('submit', async event => {
8724            event.preventDefault();
8725
8726            this.hideErrorMessage();
8727            this.toggleButton(true);
8728
8729            let shouldEnableButton = true;
8730
8731            try {
8732                const response = await fetch(this.cancellationForm.action, {
8733                    method: 'POST',
8734                    mode: 'cors',
8735                    credentials: 'same-origin',
8736                    headers: {
8737                        'X-Requested-With': 'XMLHttpRequest',
8738                    },
8739                    body: new FormData(this.cancellationForm),
8740                });
8741
8742                if (!response.ok) {
8743                    throw new Error(`Request failed with status ${response.status}`);
8744                }
8745
8746                const payload = await response.json();
8747
8748                if (payload?.success && payload?.confirmationLineHtml) {
8749                    shouldEnableButton = false;
8750                    this.buildConfirmation(payload.confirmationLineHtml);
8751                    return;
8752                }
8753
8754                this.setError(payload);
8755            } catch (error) {
8756                this.setError(error);
8757            } finally {
8758                if (shouldEnableButton) {
8759                    this.toggleButton(false);
8760                }
8761            }
8762        });
8763    }
8764
8765    buildConfirmation(confirmationLineHtml = '') {
8766        const boxDefault = document.querySelector('.cancellation-form-outer .box-default');
8767
8768        if (!boxDefault || !confirmationLineHtml) {
8769            return;
8770        }
8771
8772        const headline = this.getText('texts.cancellation.cancellationConfirmationHeadline');
8773
8774        boxDefault.innerHTML = `
8775            <div class="cancellation-confirmation-content">
8776                <h2 class="cancellation-confirmation-header">${headline}</h2>
8777                <p>${confirmationLineHtml}</p>
8778            </div>
8779        `;
8780    }
8781
8782    setError(error = null) {
8783        this.showErrorMessage(error);
8784    }
8785
8786    toggleButton(isDisabled = false) {
8787        if (!this._submitButton) {
8788            return;
8789        }
8790
8791        this._submitButton.disabled = isDisabled;
8792        this._submitButton.querySelector('.spinner-border')?.classList.toggle(this.noneClass, !isDisabled);
8793    }
8794
8795    showErrorMessage(error = null) {
8796        if (!this._alert) {
8797            return;
8798        }
8799
8800        const defaultHeader = this.getText('messages.errorAlert');
8801        const defaultMessage = this.getText('messages.errorGlobal');
8802
8803        if (this._alertHeader) {
8804            this._alertHeader.textContent = error?.header || defaultHeader;
8805        }
8806
8807        if (this._alertText) {
8808            this._alertText.textContent = error?.message || defaultMessage;
8809        }
8810
8811        this._alert.classList.remove(this.noneClass);
8812
8813        if (this.errorTimeout) {
8814            clearTimeout(this.errorTimeout);
8815        }
8816
8817        this.errorTimeout = setTimeout(() => {
8818            this._alert.classList.add(this.noneClass);
8819            this.errorTimeout = null;
8820        }, this.errorTimeoutDuration);
8821    }
8822
8823    hideErrorMessage() {
8824        this._alert?.classList.add(this.noneClass);
8825
8826        if (this.errorTimeout) {
8827            clearTimeout(this.errorTimeout);
8828            this.errorTimeout = null;
8829        }
8830    }
8831
8832    getBranchTargetSelect() {
8833        return this.cancellationForm.querySelector('select[data-insurance-select-type="contract"]')
8834            || this.cancellationForm.querySelector('select[data-insurance-select-type="provider"]');
8835    }
8836
8837    getCustomerFields() {
8838        return [
8839            this.getCancellationRadioGroup(),
8840            document.getElementById('proposalNumberField'),
8841            document.getElementById('policyNumberField'),
8842            document.getElementById('chooseProviderSelect'),
8843        ];
8844    }
8845
8846    getCancellationRadioGroup() {
8847        return document.getElementById('cellationRadioGroup')
8848            || document.getElementById('cancellationRadioGroup');
8849    }
8850
8851    setSelectPlaceholder(select, text) {
8852        select.replaceChildren(new Option(text || '', ''));
8853    }
8854
8855    getText(path) {
8856        return this.utilities.getTextFromWindowVars(path) || '';
8857    }
8858
8859    parseJson(json = '{}') {
8860        try {
8861            return JSON.parse(json || '{}');
8862        } catch (error) {
8863            return {};
8864        }
8865    }
8866}
8867
8868document.addEventListener('DOMContentLoaded', () => new Cancellation());
8869
8870class PasswordRequirements {
8871    /**
8872     * @param {HTMLInputElement} input
8873     * @param {Object} options
8874     */
8875    constructor(input, options = {}) {
8876        if (!(input instanceof HTMLInputElement)) {
8877            throw new TypeError('PasswordRequirements requires an HTMLInputElement.');
8878        }
8879
8880        this.input = input;
8881
8882        const texts = window.texts?.passwordRequirements ?? {};
8883
8884        this.settings = {
8885            numCharacters: 8,
8886
8887            useLowercase: true,
8888            useUppercase: true,
8889            useNumbers: true,
8890            useSpecial: true,
8891
8892            infoMessage: texts.prHeader ?? '',
8893            characterLengthMessage: texts.prCharacterLength ?? '',
8894            lowerCaseMessage: texts.prLowerCase ?? '',
8895            capitalLetterMessage: texts.prCapitalLetter ?? '',
8896            numberMessage: texts.prNumber ?? '',
8897            specialCharacterMessage: texts.prSpecialCharacter ?? '',
8898
8899            closestAppender: '.mb-3',
8900
8901            boxClass: 'pr-box',
8902            boxTopClass: 'pr-top',
8903            boxBottomClass: 'pr-bottom',
8904            boxActiveClass: 'pr-active',
8905
8906            boxLengthOfNumbersClass: 'pr-num-characters',
8907            boxLowerCaseClass: 'pr-use-lowercase',
8908            boxUpperCaseClass: 'pr-use-uppercase',
8909            boxNumberClass: 'pr-use-numbers',
8910            boxSpecialClass: 'pr-use-special',
8911            boxSpecialCharWrapperClass: 'pr-special-char-wrapper',
8912
8913            successClass: 'pr-ok',
8914
8915            iconClass: 'icon-app',
8916            iconNotSuccess: 'attention-o',
8917            iconSuccess: 'check',
8918
8919            ...options
8920        };
8921
8922        /**
8923         * Pattern direkt aus dem Input lesen.
8924         */
8925        this.pattern = this.input.getAttribute('pattern') ?? '';
8926
8927        /**
8928         * Sonderzeichen dynamisch aus dem Pattern extrahieren.
8929         *
8930         * Beispiel:
8931         *
8932         * (?=.*[@#€\(\)\[\]\{\}?!$%&=*+~,.;:<>_\-])
8933         *
8934         * wird zu:
8935         *
8936         * @#€()[]{}?!$%&=*+~,.;:<>_-
8937         */
8938        this.specialCharacters = this.extractSpecialCharacters(this.pattern);
8939
8940        /**
8941         * Wenn das Pattern gar keine Sonderzeichen-Anforderung enthält,
8942         * brauchen wir die Sonderzeichen-Prüfung auch nicht.
8943         */
8944        if (this.specialCharacters.length === 0) {
8945            this.settings.useSpecial = false;
8946        }
8947
8948        this.lowerCaseRegex = /[a-z]/;
8949        this.upperCaseRegex = /[A-Z]/;
8950        this.numberRegex = /[0-9]/;
8951
8952        this.state = {
8953            numCharacters: false,
8954            lowercase: !this.settings.useLowercase,
8955            uppercase: !this.settings.useUppercase,
8956            numbers: !this.settings.useNumbers,
8957            special: !this.settings.useSpecial
8958        };
8959
8960        this.appender = this.input.closest(this.settings.closestAppender);
8961
8962        if (!this.appender) {
8963            throw new Error(
8964                `PasswordRequirements: closest appender "${this.settings.closestAppender}" not found.`
8965            );
8966        }
8967
8968        this.handleInput = this.handleInput.bind(this);
8969        this.handleFocus = this.handleFocus.bind(this);
8970        this.handleBlur = this.handleBlur.bind(this);
8971        this.handleWindowChange = this.handleWindowChange.bind(this);
8972
8973        this.init();
8974    }
8975
8976    /**
8977     * Initialisierung.
8978     */
8979    init() {
8980        this.createMessage();
8981
8982        this.input.addEventListener('input', this.handleInput);
8983        this.input.addEventListener('keyup', this.handleInput);
8984        this.input.addEventListener('focus', this.handleFocus);
8985        this.input.addEventListener('blur', this.handleBlur);
8986
8987        window.addEventListener('scroll', this.handleWindowChange, {
8988            passive: true
8989        });
8990
8991        window.addEventListener('resize', this.handleWindowChange);
8992
8993        /**
8994         * Aktuellen Wert direkt prüfen, falls das Feld bereits
8995         * vorausgefüllt ist.
8996         */
8997        this.validate(false);
8998    }
8999
9000    /**
9001     * Alle Lookahead-Zeichenklassen auslesen.
9002     *
9003     * Beispiel:
9004     *
9005     * (?=.*[a-z])
9006     * (?=.*[A-Z])
9007     * (?=.*[@#€\(\)])
9008     */
9009    extractLookaheadCharacterClasses(pattern) {
9010        const classes = [];
9011        const marker = '(?=.*[';
9012
9013        let searchIndex = 0;
9014
9015        while (true) {
9016            const markerIndex = pattern.indexOf(marker, searchIndex);
9017
9018            if (markerIndex === -1) {
9019                break;
9020            }
9021
9022            const start = markerIndex + marker.length;
9023
9024            let content = '';
9025            let escaped = false;
9026            let end = start;
9027
9028            for (let i = start; i < pattern.length; i++) {
9029                const char = pattern[i];
9030
9031                if (escaped) {
9032                    content += `\\${char}`;
9033                    escaped = false;
9034                    end = i + 1;
9035                    continue;
9036                }
9037
9038                if (char === '\\') {
9039                    escaped = true;
9040                    continue;
9041                }
9042
9043                if (char === ']') {
9044                    end = i + 1;
9045                    break;
9046                }
9047
9048                content += char;
9049                end = i + 1;
9050            }
9051
9052            classes.push(content);
9053            searchIndex = end;
9054        }
9055
9056        return classes;
9057    }
9058
9059    /**
9060     * Sonderzeichen aus dem Pattern ermitteln.
9061     */
9062    extractSpecialCharacters(pattern) {
9063        if (!pattern) {
9064            return [];
9065        }
9066
9067        const characterClasses =
9068            this.extractLookaheadCharacterClasses(pattern);
9069
9070        for (const characterClass of characterClasses) {
9071            /**
9072             * Bekannte Zeichenbereiche entfernen.
9073             *
9074             * [a-z]
9075             * [A-Z]
9076             * [0-9]
9077             */
9078            const withoutRanges = characterClass
9079                .replace(/a-z/g, '')
9080                .replace(/A-Z/g, '')
9081                .replace(/0-9/g, '');
9082
9083            const characters =
9084                this.decodeCharacterClass(withoutRanges);
9085
9086            /**
9087             * Eine Lookahead-Klasse mit echten Sonderzeichen gefunden.
9088             */
9089            if (characters.length > 0) {
9090                return [...new Set(characters)];
9091            }
9092        }
9093
9094        return [];
9095    }
9096
9097    /**
9098     * Escapes einer Regex-Zeichenklasse in echte Zeichen umwandeln.
9099     *
9100     * \(  -> (
9101     * \)  -> )
9102     * \[  -> [
9103     * \]  -> ]
9104     * \{  -> {
9105     * \}  -> }
9106     * \-  -> -
9107     */
9108    decodeCharacterClass(characterClass) {
9109        const characters = [];
9110
9111        for (let i = 0; i < characterClass.length; i++) {
9112            const char = characterClass[i];
9113
9114            if (char !== '\\') {
9115                characters.push(char);
9116                continue;
9117            }
9118
9119            const escaped = characterClass[i + 1];
9120
9121            if (escaped === undefined) {
9122                continue;
9123            }
9124
9125            /**
9126             * Regex-Zeichenklassen sind keine einzelnen Sonderzeichen.
9127             */
9128            if (['s', 'S', 'd', 'D', 'w', 'W'].includes(escaped)) {
9129                i++;
9130                continue;
9131            }
9132
9133            characters.push(escaped);
9134            i++;
9135        }
9136
9137        return characters;
9138    }
9139
9140    /**
9141     * UI erzeugen.
9142     */
9143    createMessage() {
9144        const {
9145            boxClass,
9146            boxLengthOfNumbersClass,
9147            boxLowerCaseClass,
9148            boxUpperCaseClass,
9149            boxNumberClass,
9150            boxSpecialClass,
9151            boxSpecialCharWrapperClass,
9152            iconClass,
9153            iconNotSuccess
9154        } = this.settings;
9155
9156        const wrapper = document.createElement('div');
9157        wrapper.classList.add(boxClass);
9158
9159        const arrow = document.createElement('i');
9160        wrapper.appendChild(arrow);
9161
9162        const inner = document.createElement('div');
9163        inner.classList.add(`${boxClass}-inner`);
9164
9165        const headline = document.createElement('p');
9166
9167        headline.textContent = this.settings.infoMessage.replace(
9168            '%1$s',
9169            this.settings.numCharacters
9170        );
9171
9172        inner.appendChild(headline);
9173
9174        const list = document.createElement('ul');
9175
9176        list.appendChild(
9177            this.createRequirementItem(
9178                boxLengthOfNumbersClass,
9179                this.settings.characterLengthMessage.replace(
9180                    '%1$s',
9181                    this.settings.numCharacters
9182                )
9183            )
9184        );
9185
9186        if (this.settings.useLowercase) {
9187            list.appendChild(
9188                this.createRequirementItem(
9189                    boxLowerCaseClass,
9190                    this.settings.lowerCaseMessage
9191                )
9192            );
9193        }
9194
9195        if (this.settings.useUppercase) {
9196            list.appendChild(
9197                this.createRequirementItem(
9198                    boxUpperCaseClass,
9199                    this.settings.capitalLetterMessage
9200                )
9201            );
9202        }
9203
9204        if (this.settings.useNumbers) {
9205            list.appendChild(
9206                this.createRequirementItem(
9207                    boxNumberClass,
9208                    this.settings.numberMessage
9209                )
9210            );
9211        }
9212
9213        if (this.settings.useSpecial) {
9214            const item = document.createElement('li');
9215            item.classList.add(boxSpecialClass);
9216
9217            const icon = document.createElement('span');
9218            icon.classList.add(iconClass, iconNotSuccess);
9219
9220            item.appendChild(icon);
9221
9222            const text = document.createElement('span');
9223            text.classList.add(boxSpecialCharWrapperClass);
9224
9225            const message = this.settings.specialCharacterMessage;
9226            const [before, after = ''] = message.split('%1$s');
9227
9228            text.append(document.createTextNode(before));
9229
9230            const strong = document.createElement('strong');
9231
9232            strong.textContent = this.specialCharacters.join('');
9233
9234            text.appendChild(strong);
9235            text.append(document.createTextNode(after));
9236
9237            item.appendChild(text);
9238            list.appendChild(item);
9239        }
9240
9241        inner.appendChild(list);
9242        wrapper.appendChild(inner);
9243
9244        this.appender.appendChild(wrapper);
9245
9246        this.messageBox = wrapper;
9247    }
9248
9249    /**
9250     * Einzelnen Requirement-Eintrag erzeugen.
9251     */
9252    createRequirementItem(className, message) {
9253        const item = document.createElement('li');
9254        item.classList.add(className);
9255
9256        const icon = document.createElement('span');
9257
9258        icon.classList.add(
9259            this.settings.iconClass,
9260            this.settings.iconNotSuccess
9261        );
9262
9263        item.appendChild(icon);
9264        item.append(document.createTextNode(message));
9265
9266        return item;
9267    }
9268
9269    /**
9270     * Input prüfen.
9271     */
9272    validate(updateVisibility = true) {
9273        const value = this.input.value;
9274
9275        this.state.numCharacters =
9276            value.length >= this.settings.numCharacters;
9277
9278        if (this.settings.useLowercase) {
9279            this.state.lowercase =
9280                this.lowerCaseRegex.test(value);
9281        }
9282
9283        if (this.settings.useUppercase) {
9284            this.state.uppercase =
9285                this.upperCaseRegex.test(value);
9286        }
9287
9288        if (this.settings.useNumbers) {
9289            this.state.numbers =
9290                this.numberRegex.test(value);
9291        }
9292
9293        if (this.settings.useSpecial) {
9294            this.state.special =
9295                this.specialCharacters.some(character =>
9296                    value.includes(character)
9297                );
9298        }
9299
9300        this.updateRequirement(
9301            this.settings.boxLengthOfNumbersClass,
9302            this.state.numCharacters
9303        );
9304
9305        if (this.settings.useLowercase) {
9306            this.updateRequirement(
9307                this.settings.boxLowerCaseClass,
9308                this.state.lowercase
9309            );
9310        }
9311
9312        if (this.settings.useUppercase) {
9313            this.updateRequirement(
9314                this.settings.boxUpperCaseClass,
9315                this.state.uppercase
9316            );
9317        }
9318
9319        if (this.settings.useNumbers) {
9320            this.updateRequirement(
9321                this.settings.boxNumberClass,
9322                this.state.numbers
9323            );
9324        }
9325
9326        if (this.settings.useSpecial) {
9327            this.updateRequirement(
9328                this.settings.boxSpecialClass,
9329                this.state.special
9330            );
9331        }
9332
9333        if (updateVisibility) {
9334            this.checkCompleted();
9335        }
9336    }
9337
9338    /**
9339     * Status eines Requirement-Eintrags aktualisieren.
9340     */
9341    updateRequirement(className, success) {
9342        const item = this.messageBox.querySelector(`.${className}`);
9343
9344        if (!item) {
9345            return;
9346        }
9347
9348        const icon = item.querySelector(`.${this.settings.iconClass}`);
9349
9350        if (!icon) {
9351            return;
9352        }
9353
9354        icon.classList.toggle(
9355            this.settings.iconSuccess,
9356            success
9357        );
9358
9359        icon.classList.toggle(
9360            this.settings.successClass,
9361            success
9362        );
9363
9364        icon.classList.toggle(
9365            this.settings.iconNotSuccess,
9366            !success
9367        );
9368    }
9369
9370    /**
9371     * Sind alle Anforderungen erfüllt?
9372     */
9373    isCompleted() {
9374        return Object.values(this.state).every(Boolean);
9375    }
9376
9377    /**
9378     * Box entsprechend des aktuellen Status ein-/ausblenden.
9379     */
9380    checkCompleted() {
9381        if (this.isCompleted()) {
9382            this.hideMessage();
9383            return;
9384        }
9385
9386        if (document.activeElement === this.input) {
9387            this.showMessage();
9388        }
9389    }
9390
9391    /**
9392     * Box anzeigen.
9393     */
9394    showMessage() {
9395        if (this.isCompleted()) {
9396            return;
9397        }
9398
9399        this.messageBox.classList.add(
9400            this.settings.boxActiveClass
9401        );
9402
9403        this.setPositionClasses();
9404
9405        window.dispatchEvent(
9406            new CustomEvent('custom_password_requirements_init', {
9407                detail: {
9408                    input: this.input,
9409                    instance: this
9410                }
9411            })
9412        );
9413    }
9414
9415    /**
9416     * Box ausblenden.
9417     */
9418    hideMessage() {
9419        this.messageBox.classList.remove(
9420            this.settings.boxActiveClass
9421        );
9422    }
9423
9424    /**
9425     * Position bestimmen.
9426     */
9427    setPositionClasses() {
9428        if (!this.messageBox) {
9429            return;
9430        }
9431
9432        const messageRect =
9433            this.messageBox.getBoundingClientRect();
9434
9435        const inputRect =
9436            this.input.getBoundingClientRect();
9437
9438        const showAbove =
9439            inputRect.y - messageRect.height - 90 > 0;
9440
9441        this.messageBox.classList.toggle(
9442            this.settings.boxTopClass,
9443            showAbove
9444        );
9445
9446        this.messageBox.classList.toggle(
9447            this.settings.boxBottomClass,
9448            !showAbove
9449        );
9450    }
9451
9452    /**
9453     * Input Event.
9454     */
9455    handleInput() {
9456        this.validate();
9457    }
9458
9459    /**
9460     * Focus Event.
9461     */
9462    handleFocus() {
9463        this.validate(false);
9464        this.showMessage();
9465    }
9466
9467    /**
9468     * Blur Event.
9469     */
9470    handleBlur() {
9471        this.hideMessage();
9472    }
9473
9474    /**
9475     * Scroll / Resize.
9476     */
9477    handleWindowChange() {
9478        if (
9479            this.messageBox.classList.contains(
9480                this.settings.boxActiveClass
9481            )
9482        ) {
9483            this.setPositionClasses();
9484        }
9485    }
9486
9487    /**
9488     * Instanz sauber entfernen.
9489     */
9490    destroy() {
9491        this.input.removeEventListener(
9492            'input',
9493            this.handleInput
9494        );
9495
9496        this.input.removeEventListener(
9497            'keyup',
9498            this.handleInput
9499        );
9500
9501        this.input.removeEventListener(
9502            'focus',
9503            this.handleFocus
9504        );
9505
9506        this.input.removeEventListener(
9507            'blur',
9508            this.handleBlur
9509        );
9510
9511        window.removeEventListener(
9512            'scroll',
9513            this.handleWindowChange
9514        );
9515
9516        window.removeEventListener(
9517            'resize',
9518            this.handleWindowChange
9519        );
9520
9521        this.messageBox?.remove();
9522    }
9523
9524    /**
9525     * Bereits initialisierte Elemente.
9526     *
9527     * WeakMap verhindert doppelte Initialisierung und hält
9528     * entfernte DOM-Elemente nicht künstlich im Speicher.
9529     *
9530     * @type {WeakMap<HTMLInputElement, PasswordRequirements>}
9531     */
9532    static instances = new WeakMap();
9533
9534    /**
9535     * Einzelnes Element initialisieren.
9536     *
9537     * @param {HTMLInputElement} input
9538     * @param {Object} options
9539     *
9540     * @returns {PasswordRequirements|null}
9541     */
9542    static init(input, options = {}) {
9543        if (!(input instanceof HTMLInputElement)) {
9544            return null;
9545        }
9546
9547        if (PasswordRequirements.instances.has(input)) {
9548            return PasswordRequirements.instances.get(input);
9549        }
9550
9551        const instance = new PasswordRequirements(input, options);
9552
9553        PasswordRequirements.instances.set(input, instance);
9554
9555        return instance;
9556    }
9557
9558    /**
9559     * Alle vorhandenen Inputs initialisieren.
9560     *
9561     * @param {string} selector
9562     * @param {Object} options
9563     *
9564     * @returns {PasswordRequirements[]}
9565     */
9566    static initAll(selector = '.js-pwr', options = {}) {
9567        return [...document.querySelectorAll(selector)]
9568            .map(input => PasswordRequirements.init(input, options))
9569            .filter(Boolean);
9570    }
9571
9572    /**
9573     * DOM auf dynamisch hinzugefügte Passwortfelder überwachen.
9574     *
9575     * @param {string} selector
9576     * @param {Object} options
9577     * @param {Node} root
9578     *
9579     * @returns {MutationObserver}
9580     */
9581    static observe(
9582        selector = '.js-pwr',
9583        options = {},
9584        root = document.body
9585    ) {
9586        const observer = new MutationObserver(mutations => {
9587            mutations.forEach(mutation => {
9588                mutation.addedNodes.forEach(node => {
9589                    if (!(node instanceof Element)) {
9590                        return;
9591                    }
9592
9593                    /**
9594                     * Das hinzugefügte Element selbst könnte bereits
9595                     * das gesuchte Input sein.
9596                     */
9597                    if (node.matches(selector)) {
9598                        PasswordRequirements.init(node, options);
9599                    }
9600
9601                    /**
9602                     * Oder das Input befindet sich irgendwo innerhalb
9603                     * des neu hinzugefügten Elements.
9604                     */
9605                    node.querySelectorAll(selector).forEach(input => {
9606                        PasswordRequirements.init(input, options);
9607                    });
9608                });
9609            });
9610        });
9611
9612        observer.observe(root, {
9613            childList: true,
9614            subtree: true
9615        });
9616
9617        return observer;
9618    }
9619
9620    /**
9621     * Alle Instanzen initialisieren und anschließend das DOM beobachten.
9622     *
9623     * @param {string} selector
9624     * @param {Object} options
9625     * @param {Node} root
9626     *
9627     * @returns {MutationObserver}
9628     */
9629    static boot(
9630        selector = '.js-pwr',
9631        options = {},
9632        root = document.body
9633    ) {
9634        PasswordRequirements.initAll(selector, options);
9635
9636        return PasswordRequirements.observe(
9637            selector,
9638            options,
9639            root
9640        );
9641    }
9642}
9643
9644document.addEventListener('DOMContentLoaded', () => {
9645    PasswordRequirements.boot();
9646});

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.