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, "<") 43 .replace(/>/g, ">"); 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, "&") 5938 .replace(/</g, "<") 5939 .replace(/>/g, ">") 5940 .replace(/"/g, """) 5941 .replace(/'/g, "'"); 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 '&':'&', 7608 '<':'<', 7609 '>':'>', 7610 '"':'"', 7611 "'":''' 7612 }[s])); 7613 } 7614 7615 _escapeAttr(str) { 7616 return String(str) 7617 .replace(/"/g, '"'); 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.