1document.addEventListener('DOMContentLoaded', function () { 2 /** 3 * Formats a number with German thousands separator (dot) and optional decimals/comma. 4 * @param {number|string} val - value to format 5 * @param {number} decimals - number of decimals (default: 0) 6 * @returns {string} 7 */ 8 function formatNumber(val, decimals = 0) { 9 return Number(val) 10 .toLocaleString('de-DE', { 11 minimumFractionDigits: decimals, 12 maximumFractionDigits: decimals 13 }); 14 } 15 16 /** 17 * Binds an input[type="range"] slider with a paired <input type="number"> 18 * and updates WAI-ARIA attributes for accessibility. 19 * Synchronization works in both directions. 20 * 21 * @param {string} sliderId - id of the slider 22 * @param {string} valueId - id of the number input element 23 * @param {number} decimals - number of decimals 24 */ 25 function bindSlider(sliderId, valueId, decimals = 0) { 26 const slider = document.getElementById(sliderId); 27 const valueElem = document.getElementById(valueId); 28 if (!slider || !valueElem) return; 29 30 // Init ARIA 31 slider.setAttribute('role', 'slider'); 32 slider.setAttribute('aria-valuemin', slider.min); 33 slider.setAttribute('aria-valuemax', slider.max); 34 slider.setAttribute('aria-describedby', valueId); 35 36 function format(val) { 37 let formatted = formatNumber(val, decimals); 38 if (decimals > 0) formatted = formatted.replace('.', ','); 39 return formatted; 40 } 41 42 // Update value input from slider 43 function updateFromSlider() { 44 valueElem.value = format(slider.value); 45 slider.setAttribute('aria-valuenow', slider.value); 46 } 47 48 // Update slider from number input 49 function updateFromInput() { 50 let raw = valueElem.value.replace(',', '.'); 51 let val = parseFloat(raw); 52 if (isNaN(val)) return; 53 54 const min = parseFloat(slider.min); 55 const max = parseFloat(slider.max); 56 57 val = Math.min(Math.max(val, min), max); 58 59 // write to slider 60 slider.value = val; 61 62 // directly format and write back to the input 63 valueElem.value = format(val); 64 slider.setAttribute('aria-valuenow', val); 65 66 // update the track color 67 setTrackColor(); 68 } 69 70 // Track fill color 71 function setTrackColor() { 72 const min = +slider.min || 0; 73 const max = +slider.max || 100; 74 const val = +slider.value; 75 const percent = ((val - min) * 100) / (max - min); 76 slider.style.setProperty('--progress', percent); 77 } 78 79 // Events 80 slider.addEventListener('input', () => { 81 updateFromSlider(); 82 setTrackColor(); 83 }); 84 85 valueElem.addEventListener('blur', updateFromInput); 86 valueElem.addEventListener('change', updateFromInput); 87 valueElem.addEventListener('keydown', (e) => { 88 if (e.key === 'Enter') { 89 e.preventDefault(); 90 updateFromInput(); 91 valueElem.blur(); 92 } 93 }); 94 95 // Init 96 updateFromSlider(); 97 setTrackColor(); 98 } 99 100 // Allow only digits and one comma/dot in text inputs for rates 101 document.querySelectorAll('.repayment-value').forEach((field) => { 102 field.addEventListener('keydown', (e) => { 103 // Allow control keys 104 if ( 105 e.key === 'Backspace' || 106 e.key === 'Delete' || 107 e.key === 'ArrowLeft' || 108 e.key === 'ArrowRight' || 109 e.key === 'Tab' 110 ) { 111 return; 112 } 113 114 // Allow digits 115 if (/[0-9]/.test(e.key)) return; 116 117 // Allow comma or dot, but only one 118 if ((e.key === ',' || e.key === '.') && !field.value.includes(',') && !field.value.includes('.')) { 119 return; 120 } 121 122 // Otherwise block 123 e.preventDefault(); 124 }); 125 126 field.addEventListener('input', () => { 127 // Remove any characters except digits, comma and dot 128 field.value = field.value.replace(/[^0-9,\.]/g, ''); 129 }); 130 }); 131 132 // Bind all sliders with ARIA support 133 bindSlider('years', 'years-value', 0); // no decimals, e.g. 20 134 bindSlider('repayment', 'repayment-value', 2); // two decimals, e.g. 3,50 135 bindSlider('interest-rate', 'interest-rate-value', 2); // two decimals, e.g. 3,80 136 137 // --- Format thousands with dot in #amount input only --- 138 const amountInput = document.getElementById('amount'); 139 if (amountInput) { 140 // Format input as integer with thousands separator (dot) on every input 141 amountInput.addEventListener('input', function () { 142 // Remove all non-digit characters 143 let raw = this.value.replace(/\D/g, ''); 144 // Format with thousands separator (dot) if not empty 145 if (raw) { 146 this.value = formatNumber(raw, 0); 147 } else { 148 this.value = ''; 149 } 150 }); 151 152 // Block non-digit input (like dot, comma, letters) during typing 153 amountInput.addEventListener('keypress', function (e) { 154 if (e.key && !/[0-9]/.test(e.key)) { 155 e.preventDefault(); 156 } 157 }); 158 } 159 160 // --- Universal ARIA-valuenow update for all range inputs (in case of dynamic generation) --- 161 // This finds all input[type="range"] with aria-valuenow and updates on input 162 document.querySelectorAll('input[type="range"]').forEach(function (slider) { 163 slider.addEventListener('input', function () { 164 slider.setAttribute('aria-valuenow', slider.value); 165 }); 166 }); 167 168 // AJAX submit for the calculator form 169 const form = document.querySelector('.isa-calculator__form[data-ajax="1"]'); 170 if (!form) return; 171 172 form.addEventListener('submit', function (e) { 173 e.preventDefault(); 174 175 const formData = new FormData(form); 176 177 fetch(form.action, { 178 method: 'POST', 179 body: formData, 180 headers: { 'X-Requested-With': 'XMLHttpRequest' } 181 }) 182 .then(function (response) { return response.text(); }) 183 .then(function (html) { 184 const parser = new DOMParser(); 185 const doc = parser.parseFromString(html, 'text/html'); 186 const newResult = doc.querySelector('.isa-calculator__result'); 187 if (newResult) { 188 document.querySelector('.isa-calculator__result').replaceWith(newResult); 189 } 190 }); 191 }); 192});
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.