1jQuery(document).ready(function ($) { 2 const $form = $('#gg-skifahrt-form'); 3 const $wrapper = $('.gg-skifahrt-wrapper'); 4 5 // Navigation 6 $('.btn-next').on('click', function () { 7 if (validateStep($(this).closest('.form-step'))) { 8 const nextStep = $(this).data('to'); 9 goToStep(nextStep); 10 } 11 }); 12 13 $('.btn-prev').on('click', function () { 14 const prevStep = $(this).data('to'); 15 goToStep(prevStep); 16 }); 17 18 function goToStep(step) { 19 $('.form-step').removeClass('active'); 20 $('.form-step[data-step="' + step + '"]').addClass('active'); 21 22 $('.gg-progress-bar .step').removeClass('active'); 23 $('.gg-progress-bar .step[data-step="' + step + '"]').addClass('active'); 24 25 if (step == 3 && typeof resizeCanvas === 'function') { 26 setTimeout(resizeCanvas, 100); // Small delay to ensure display:block is applied 27 } 28 29 $('html, body').animate({ 30 scrollTop: $wrapper.offset().top - 100 31 }, 500); 32 } 33 34 // Live Validation on Blur/Change 35 $form.on('input change blur', '[required]', function () { 36 validateField($(this)); 37 }); 38 39 function validateField($input) { 40 const type = $input.attr('type'); 41 let isValidRaw = false; 42 43 if (type === 'checkbox' || type === 'radio') { 44 const name = $input.attr('name'); 45 const $group = $input.closest('.checkbox-group, .radio-group, .highlight-label'); 46 47 // Special case for 'terms_accepted': ALL checkboxes must be checked 48 if (name === 'terms_accepted[]') { 49 const totalCheckboxes = $group.find('input[type="checkbox"]').length; 50 const checkedCheckboxes = $group.find('input[type="checkbox"]:checked').length; 51 52 if (checkedCheckboxes === totalCheckboxes) { 53 isValidRaw = true; 54 $group.addClass('valid').css('border-color', '#9CCB3B'); 55 $input.closest('.form-group').find('.error-msg').remove(); 56 } else { 57 $group.removeClass('valid').css('border-color', ''); 58 } 59 } else { 60 // Default: at least one checked 61 if ($form.find('input[name="' + name + '"]:checked').length > 0) { 62 isValidRaw = true; 63 $group.addClass('valid').css('border-color', '#9CCB3B'); 64 $input.closest('.form-group').find('.error-msg').remove(); 65 } else { 66 $group.removeClass('valid').css('border-color', ''); 67 } 68 } 69 } else { 70 if ($input.val().trim() !== '') { 71 // Email check 72 if (type === 'email') { 73 if (isValidEmail($input.val())) { 74 isValidRaw = true; 75 } 76 } else { 77 isValidRaw = true; 78 } 79 } 80 81 if (isValidRaw) { 82 $input.addClass('valid').removeClass('error'); 83 $input.next('.error-msg').remove(); 84 } else { 85 $input.removeClass('valid'); 86 } 87 } 88 return isValidRaw; 89 } 90 91 // Validation 92 function validateStep($step) { 93 let isValid = true; 94 95 // Remove old errors 96 $step.find('.error').removeClass('error'); 97 $step.find('.error-msg').remove(); 98 99 // Check required fields 100 $step.find('[required]').each(function () { 101 const $input = $(this); 102 if (!validateField($input) && $input.attr('type') !== 'hidden') { 103 // Double check visibility - sometimes hidden tabs cause issues 104 if ($input.is(':visible') || $input.attr('type') === 'email' || $input.attr('type') === 'text') { 105 isValid = false; 106 const type = $input.attr('type'); 107 108 if (type === 'checkbox' || type === 'radio') { 109 $input.closest('.form-group').find('label').first().addClass('error-text'); 110 $input.closest('.form-group').append('<small class="error-msg">Bitte auswählen.</small>'); 111 $input.closest('.checkbox-group, .radio-group, .highlight-label').css('border', '1px solid #ff5252'); 112 } else { 113 $input.addClass('error'); 114 $input.after('<small class="error-msg">Bitte füllen Sie dieses Feld aus.</small>'); 115 } 116 console.log('Validation failed for:', $input.attr('name')); 117 } 118 } 119 }); 120 121 // Specific Email Validation not caught by above if field was optional (but here all are required mostly) 122 // Kept for robustness 123 $step.find('input[type="email"]').each(function () { 124 if ($(this).val() !== '' && !isValidEmail($(this).val())) { 125 isValid = false; 126 $(this).addClass('error').removeClass('valid'); 127 if ($(this).next('.error-msg').length === 0) { 128 $(this).after('<small class="error-msg">Bitte eine gültige E-Mail Adresse eingeben.</small>'); 129 } 130 } 131 }); 132 133 return isValid; 134 } 135 136 function isValidEmail(email) { 137 const pattern = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; 138 return pattern.test(email); 139 } 140 141 // Signature Pad Logic 142 const canvas = document.getElementById('signature-pad'); 143 let resizeCanvas; // Define here 144 145 if (canvas) { 146 const ctx = canvas.getContext('2d'); 147 let isDrawing = false; 148
149 // Resize canvas to fill parent width 150 resizeCanvas = function () { 151 const ratio = Math.max(window.devicePixelRatio || 1, 1); 152 canvas.width = canvas.offsetWidth * ratio; 153 canvas.height = canvas.offsetHeight * ratio; 154 ctx.scale(ratio, ratio); 155 }; 156 window.addEventListener("resize", resizeCanvas); 157 // Initial resize 158 resizeCanvas(); 159 160 function getPos(e) { 161 const rect = canvas.getBoundingClientRect(); 162 let clientX = e.clientX; 163 let clientY = e.clientY; 164 165 if (e.touches && e.touches.length > 0) { 166 clientX = e.touches[0].clientX; 167 clientY = e.touches[0].clientY; 168 } 169 170 return { 171 x: clientX - rect.left, 172 y: clientY - rect.top 173 }; 174 } 175 176 function startDraw(e) { 177 e.preventDefault(); 178 isDrawing = true; 179 const pos = getPos(e); 180 ctx.beginPath(); 181 ctx.moveTo(pos.x, pos.y); 182 183 // Mark validation 184 $('#signature').val('signed').trigger('change'); 185 $(canvas).parent().css('border-color', '#9CCB3B'); 186 } 187 188 function draw(e) { 189 if (!isDrawing) return; 190 e.preventDefault(); 191 const pos = getPos(e); 192 ctx.lineTo(pos.x, pos.y); 193 ctx.stroke(); 194 } 195 196 function stopDraw() { 197 isDrawing = false; 198 // Update hidden input with data URL 199 if (!isCanvasBlank(canvas)) { 200 $('#signature').val(canvas.toDataURL()); 201 } 202 } 203 204 // Mouse Events 205 canvas.addEventListener('mousedown', startDraw); 206 canvas.addEventListener('mousemove', draw); 207 canvas.addEventListener('mouseup', stopDraw); 208 canvas.addEventListener('mouseout', stopDraw); 209 210 // Touch Events 211 canvas.addEventListener('touchstart', startDraw); 212 canvas.addEventListener('touchmove', draw); 213 canvas.addEventListener('touchend', stopDraw); 214 215 // Clear Button 216 $('#clear-signature').on('click', function () { 217 ctx.clearRect(0, 0, canvas.width, canvas.height); 218 $('#signature').val(''); 219 $(canvas).parent().css('border-color', '#e0e0e0'); 220 }); 221 222 function isCanvasBlank(canvas) { 223 const blank = document.createElement('canvas'); 224 blank.width = canvas.width; 225 blank.height = canvas.height; 226 return canvas.toDataURL() === blank.toDataURL(); 227 } 228 } 229 230 // Submission 231 $form.on('submit', function (e) { 232 e.preventDefault(); 233 234 // Final validation of current step (Step 3) 235 if (!validateStep($('.form-step[data-step="3"]'))) { 236 return; 237 } 238 239 // Debug: Log all form values 240 console.log('Form serialization:', $form.serialize()); 241 242 // Manual collection to ensure no data loss involved with FormData on hidden fields 243 var dataObj = {}; 244 var formArray = $form.serializeArray(); 245 $.each(formArray, function (i, field) { 246 // Handle multiple values (checkboxes) 247 if (dataObj[field.name]) { 248 if (!Array.isArray(dataObj[field.name])) { 249 dataObj[field.name] = [dataObj[field.name]]; 250 } 251 dataObj[field.name].push(field.value); 252 } else { 253 dataObj[field.name] = field.value; 254 } 255 }); 256 257 // FORCE Manual check of critical fields (Step 1) using scoped find 258 dataObj['firstname'] = $form.find('input[name="firstname"]').val(); 259 dataObj['lastname'] = $form.find('input[name="lastname"]').val(); 260 dataObj['email'] = $form.find('input[name="email"]').val(); 261 dataObj['class'] = $form.find('select[name="class"]').val(); 262 263 // Critical Check 264 if (!dataObj['firstname'] || !dataObj['lastname'] || !dataObj['email']) {
265 alert("Kritischer Fehler: Pflichtfelder konnten nicht ausgelesen werden. Bitte Seite neu laden."); 266 return; 267 } 268 269 // Add action and nonce explicitly 270 dataObj['action'] = 'gg_skifahrt_submit'; 271 dataObj['nonce'] = ggSkifahrtAjax.nonce; 272 273 const $submitBtn = $(this).find('.btn-submit'); 274 $submitBtn.addClass('loading').prop('disabled', true).text('Wird gesendet...'); 275 276 $.ajax({ 277 url: ggSkifahrtAjax.ajax_url, 278 type: 'POST', 279 data: dataObj, // Use plain object 280 // processData: true, // Default is true, needed for object 281 // contentType: 'application/x-www-form-urlencoded; charset=UTF-8', // Default 282 success: function (response) { 283 $submitBtn.removeClass('loading').prop('disabled', false).text('Anmeldung abschicken'); 284 285 if (response.success) { 286 $form.hide(); 287 $('.form-success').fadeIn(); 288 $('.gg-progress-bar').hide(); 289 $('html, body').animate({ 290 scrollTop: $wrapper.offset().top - 100 291 }, 500); 292 } else { 293 alert('Fehler: ' + (response.data || 'Ein unbekannter Fehler ist aufgetreten.')); 294 } 295 }, 296 error: function (xhr, status, error) { 297 $submitBtn.removeClass('loading').prop('disabled', false).text('Anmeldung abschicken'); 298 console.error('AJAX Error:', error); 299 alert('Ein Netzwerkfehler ist aufgetreten: ' + error); 300 } 301 }); 302 }); 303 304 // Table Sorting 305 $('.gg-skifahrt-results-wrapper th[data-sort]').on('click', function () { 306 const $table = $(this).closest('table'); 307 const $tbody = $table.find('tbody'); 308 const rows = $tbody.find('tr').toArray(); 309 const index = $(this).index(); 310 const type = $(this).data('sort'); 311 const isAsc = !$(this).hasClass('asc'); // Toggle 312 313 $table.find('th').removeClass('asc desc'); 314 $(this).addClass(isAsc ? 'asc' : 'desc'); 315 316 rows.sort(function (a, b) { 317 const valA = $(a).find('td').eq(index).text().trim(); 318 const valB = $(b).find('td').eq(index).text().trim(); 319 320 if (type === 'date') { 321 // Simple date compare assuming format allows it or fallback to string 322 return isAsc ? new Date(valA) - new Date(valB) : new Date(valB) - new Date(valA); 323 } 324 325 return isAsc ? valA.localeCompare(valB) : valB.localeCompare(valA); 326 }); 327 328 $tbody.append(rows); 329 }); 330 331 // Delete All Results 332 $('#gg-skifahrt-delete-all').on('click', function (e) { 333 e.preventDefault(); 334 if (confirm('SIND SIE SICHER? \n\nDies löscht ALLE Anmeldungen unwiderruflich aus der Datenbank. Dies kann nicht rückgängig gemacht werden!')) { 335 if (confirm('Wirklich sicher?')) { 336 $.ajax({
337 url: ggSkifahrtAjax.ajax_url, 338 type: 'POST', 339 data: { 340 action: 'gg_skifahrt_delete_all', 341 nonce: ggSkifahrtAjax.nonce 342 }, 343 success: function (response) { 344 if (response.success) { 345 alert('Alle Daten wurden gelöscht.'); 346 location.reload(); 347 } else { 348 alert('Fehler: ' + response.data); 349 } 350 }, 351 error: function () { 352 alert('Netzwerkfehler beim Löschen.'); 353 } 354 }); 355 } 356 } 357 }); 358});
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.