1(function($) { 2 'use strict'; 3 4 function getOrCreateStorageValue(key, fallbackPrefix) { 5 try { 6 var existing = window.localStorage.getItem(key); 7 if (existing) { 8 return existing; 9 } 10 var value = fallbackPrefix + '-' + Math.random().toString(36).slice(2, 12); 11 window.localStorage.setItem(key, value); 12 return value; 13 } catch (e) { 14 return fallbackPrefix + '-' + Math.random().toString(36).slice(2, 12); 15 } 16 } 17 18 function currentVisitorId() { 19 return getOrCreateStorageValue('klicktipp_visitor_id', 'visitor'); 20 } 21 22 function currentSessionId() { 23 return getOrCreateStorageValue('klicktipp_session_id', 'session'); 24 } 25 26 function hashString(input) { 27 var value = String(input || ''); 28 var hash = 0; 29 for (var i = 0; i < value.length; i++) { 30 hash = ((hash << 5) - hash) + value.charCodeAt(i); 31 hash |= 0; 32 } 33 return String(Math.abs(hash)); 34 } 35 36 function showMessage($container, message, type) { 37 $container 38 .removeClass('kt-msg-success kt-msg-error') 39 .addClass(type === 'success' ? 'kt-msg-success' : 'kt-msg-error') 40 .text(String(message || '')) 41 .stop(true, true) 42 .fadeIn(120); 43 } 44 45 function refreshStartedTimestamps() { 46 $('.kt-optin-form input[name="kt_started"]').each(function() { 47 $(this).val(String(Date.now())); 48 }); 49 } 50 51 // The key identifies one logical submit attempt; it is not an 52 // authentication credential. It must be generated in the visitor's 53 // browser rather than PHP-rendered HTML because a full-page cache would 54 // otherwise hand the same key to different visitors. 55 function generateIdempotencyKey() { 56 var cryptoApi = window.crypto || window.msCrypto; 57 58 if (cryptoApi && typeof cryptoApi.randomUUID === 'function') { 59 return cryptoApi.randomUUID(); 60 } 61 62 if (cryptoApi && typeof cryptoApi.getRandomValues === 'function' && typeof window.Uint8Array === 'function') { 63 var bytes = new Uint8Array(16); 64 cryptoApi.getRandomValues(bytes); 65 bytes[6] = (bytes[6] & 0x0f) | 0x40; 66 bytes[8] = (bytes[8] & 0x3f) | 0x80; 67 68 var hex = []; 69 for (var i = 0; i < bytes.length; i++) { 70 hex.push((bytes[i] + 0x100).toString(16).slice(1)); 71 } 72 73 return hex.slice(0, 4).join('') + '-' + 74 hex.slice(4, 6).join('') + '-' + 75 hex.slice(6, 8).join('') + '-' + 76 hex.slice(8, 10).join('') + '-' + 77 hex.slice(10, 16).join(''); 78 } 79 80 // Extremely old browsers have no Web Crypto implementation. The 81 // server still validates and can replace this non-secret token; the 82 // timestamp/random fallback prevents a shared cached HTML value. 83 return 'kt-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 15); 84 } 85 86 function ensureFormIdempotencyKey($form) { 87 var $field = $form.find('input[name="kt_idempotency_key"]').first(); 88 if (!$field.length) { 89 return ''; 90 } 91 92 // Always replace a PHP-rendered legacy value once. This also protects 93 // visitors served old cached page HTML immediately after an update. 94 // This marker deliberately lives only on the DOM element, not in an 95 // HTML attribute. A page builder can clone markup after page load; 96 // its cloned field must receive its own key rather than inheriting 97 // the original field's marker and value. 98 if (!$field[0].__ktIdempotencyInitialized) { 99 $field.val(generateIdempotencyKey()); 100 $field[0].__ktIdempotencyInitialized = true; 101 } 102 103 return String($field.val() || ''); 104 } 105 106 function normalizeTemporalFieldValue(rawValue, inputType) { 107 var raw = String(rawValue || '').trim(); 108 var type = String(inputType || '').toLowerCase(); 109 if (!raw || (type !== 'date' && type !== 'time' && type !== 'datetime-local')) { 110 return raw; 111 } 112 113 if (/^-?[0-9]{1,12}$/.test(raw)) { 114 return raw; 115 } 116 117 if (type === 'datetime-local') { 118 var dt = new Date(raw); 119 if (!isNaN(dt.getTime())) { 120 return String(Math.floor(dt.getTime() / 1000)); 121 } 122 return raw; 123 } 124 125 if (type === 'date') { 126 var dateParts = raw.split('-'); 127 if (dateParts.length === 3) { 128 var y = parseInt(dateParts[0], 10); 129 var m = parseInt(dateParts[1], 10); 130 var d = parseInt(dateParts[2], 10); 131 if (!isNaN(y) && !isNaN(m) && !isNaN(d)) { 132 var dateObj = new Date(y, m - 1, d, 0, 0, 0, 0); 133 return String(Math.floor(dateObj.getTime() / 1000)); 134 } 135 } 136 return raw; 137 } 138 139 var timeMatch = raw.match(/^(\d{1,2}):(\d{2})(?::(\d{2}))?$/); 140 if (timeMatch) { 141 var now = new Date(); 142 var hh = parseInt(timeMatch[1], 10); 143 var mm = parseInt(timeMatch[2], 10); 144 var ss = parseInt(timeMatch[3] || '0', 10); 145 if (!isNaN(hh) && !isNaN(mm) && !isNaN(ss)) { 146 var timeObj = new Date( 147 now.getFullYear(), 148 now.getMonth(), 149 now.getDate(), 150 hh, 151 mm, 152 ss, 153 0 154 ); 155 return String(Math.floor(timeObj.getTime() / 1000)); 156 } 157 } 158 159 return raw; 160 } 161 162 function readFieldData($form) { 163 var payload = {}; 164 165 $form.find('[name^="kt_field_"]').each(function() { 166 var $input = $(this); 167 var name = String($input.attr('name') || ''); 168 if (!name) { 169 return; 170 } 171 172 var key = name.replace(/^kt_field_/, '').replace(/\[\]$/, ''); 173 if (!key) { 174 return; 175 } 176 177 var payloadKey = 'field_' + key; 178 179 if ($input.is(':checkbox')) { 180 if (!$input.is(':checked')) { 181 return; 182 } 183 if (!Array.isArray(payload[payloadKey])) { 184 payload[payloadKey] = []; 185 } 186 payload[payloadKey].push(String($input.val() || '').trim()); 187 return; 188 } 189 190 var value = $input.val(); 191 var inputType = String($input.attr('type') || '').toLowerCase(); 192 if (Array.isArray(value)) { 193 payload[payloadKey] = value.map(function(item) { 194 return String(item || '').trim(); 195 }
195).filter(Boolean); 196 } else { 197 payload[payloadKey] = normalizeTemporalFieldValue(value, inputType); 198 } 199 }); 200 201 return payload; 202 } 203 204 function isEmail(value) { 205 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); 206 } 207 208 /** 209 * INTWORD-19: client-side pre-submit validation. 210 * 211 * The rendered form markup carries `novalidate` (see class-klicktipp-form.php) 212 * because native browser validation UI can't be styled/localized consistently 213 * and doesn't cover the custom dropdown widgets below â so validation is 214 * implemented here instead, reading the same "required"/type markers the 215 * server already uses to build the form: 216 * - plain inputs/textareas: the `required` HTML attribute (only present 217 * when the admin marked the field required in the form editor). 218 * - single_select: the `required` attribute on the hidden 219 * `.kt-single-dropdown-input` that carries the selected value. 220 * - multi_select: a `data-required` attribute on the `.kt-multi-dropdown` 221 * wrapper (checkbox groups have no native singular "required" concept). 222 * This is a UX layer only. The server (KlickTipp_Plugin_Ajax::validate_submission()) 223 * remains the authoritative check and re-validates everything independently. 224 */ 225 function validateFieldFormat($control, value) { 226 var tag = String($control.prop('tagName') || '').toLowerCase(); 227 var type = 'textarea' === tag ? 'textarea' : String($control.attr('type') || 'text').toLowerCase(); 228 var name = String($control.attr('name') || ''); 229 var isEmailField = 'kt_field_email' === name || 'email' === type; 230 231 if (isEmailField) { 232 return isEmail(value) ? '' : klicktippFrontend.messages.email; 233 } 234 if ('url' === type) { 235 return /^https?:\/\/[^\s]+\.[^\s]+/i.test(value) ? '' : klicktippFrontend.messages.field_invalid_url; 236 } 237 if ('text' === type && 'decimal' === String($control.attr('inputmode') || '').toLowerCase()) { 238 // INTWORD-175 (option 2): renders as type="text" (see 239 // class-klicktipp-form.php) so inputmode="decimal" is the only 240 // signal that distinguishes this from a plain text field. 241 // Deliberately permissive â the server (KlickTipp_Plugin_API:: 242 // normalize_decimal_value(), reused by 243 // KlickTipp_Plugin_Ajax::validate_submission()) is the 244 // authoritative parser and already accepts either a comma or a 245 // dot as the decimal separator plus optional thousands 246 // separators; this only rejects obviously-not-a-number input 247 // (letters, symbols) rather than trying to fully replicate that 248 // disambiguation client-side. 249 return /^[+-]?[\d.,\s]+$/.test(value) ? '' : klicktippFrontend.messages.field_invalid_decimal; 250 } 251 if ('number' === type) { 252 // INTWORD-145: whole numbers only â a decimal point always means 253 // a fractional value and is rejected (previously this pattern 254 // accepted it, e.g. "3.5", and the value was silently mangled 255 // server-side instead of being caught at all). A thousands 256 // separator like "1,234" is still allowed here, matching the 257 // server's own leniency for it (see 258 // KlickTipp_Plugin_Ajax::validate_submission()) â only a literal 259 // "." is a genuine validation failure. Use a "decimal"-type 260 // field for values with a fractional part. 261 var isWholeNumber = value.indexOf('.') === -1 && /^[+-]?[\d,]+$/.test(value); 262 return isWholeNumber ? '' : klicktippFrontend.messages.field_invalid_number; 263 } 264 if ('tel' === type) { 265 return /^[0-9+\-\s()]{6,}$/.test(value) ? '' : klicktippFrontend.messages.field_invalid_phone; 266 } 267 if ('date' === type) { 268 return /^\d{4}-\d{2}-\d{2}$/.test(value) ? '' : klicktippFrontend.messages.field_invalid_date; 269 } 270 if ('time' === type) { 271 return /^\d{2}:\d{2}(:\d{2}
271)?$/.test(value) ? '' : klicktippFrontend.messages.field_invalid_time; 272 } 273 if ('datetime-local' === type) { 274 return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) ? '' : klicktippFrontend.messages.field_invalid_datetime; 275 } 276 277 return ''; 278 } 279 280 function clearFieldErrors($form) { 281 $form.find('.kt-field-group, .kt-gdpr-group').removeClass('kt-field-invalid'); 282 $form.find('.kt-field-error').remove(); 283 $form.find('[aria-invalid="true"]').removeAttr('aria-invalid'); 284 } 285 286 function buildFieldValidationErrors($form, data) { 287 var errors = []; 288 var $emailGroup = null; 289 290 $form.find('.kt-field-group').each(function() { 291 var $group = $(this); 292 293 // Multi-select: a checkbox group with no native "required" attribute. 294 var $multiDropdown = $group.find('.kt-multi-dropdown').not('.kt-single-dropdown'); 295 if ($multiDropdown.length) { 296 var isRequired = '1' === String($multiDropdown.attr('data-required') || ''); 297 var checkedCount = $multiDropdown.find('.kt-multi-dropdown-option:checked').length; 298 if (isRequired && 0 === checkedCount) { 299 errors.push({ 300 $group: $group, 301 $focusTarget: $multiDropdown.find('.kt-multi-dropdown-trigger').first(), 302 message: klicktippFrontend.messages.field_required 303 }); 304 } 305 return; 306 } 307 308 // Single-select: value lives on a hidden input carrying `required`. 309 var $singleInput = $group.find('.kt-single-dropdown-input'); 310 if ($singleInput.length) { 311 var singleValue = $.trim(String($singleInput.val() || '')); 312 if ($singleInput.prop('required') && '' === singleValue) { 313 errors.push({ 314 $group: $group, 315 $focusTarget: $group.find('.kt-single-dropdown .kt-multi-dropdown-trigger').first(), 316 message: klicktippFrontend.messages.field_required 317 }); 318 } 319 return; 320 } 321 322 // Plain text/email/tel/url/number/date/time/datetime-local/textarea. 323 var $control = $group.find('input.kt-input, textarea.kt-input').first(); 324 if (!$control.length) { 325 return; 326 } 327 328 // Identify the email field by its input type / explicit marker, 329 // never by name/key â the internal field key (INTWORD-36) is now 330 // derived from the KlickTipp field's own name, e.g. "email_address" 331 // for an English locale, so a hardcoded "kt_field_email" name no 332 // longer reliably matches it and silently broke this check. 333 if ('email' === String($control.attr('type') || '').toLowerCase() || $control.is('[data-kt-email-field="1"]')) { 334 $emailGroup = $group; 335 } 336 337 var value = $.trim(String($control.val() || '')); 338 var isControlRequired = !!$control.prop('required'); 339 340 if (isControlRequired && '' === value) { 341 errors.push({ $group: $group, $focusTarget: $control, message: klicktippFrontend.messages.field_required }); 342 return; 343 } 344 345 if ('' === value) { 346 return; 347 } 348 349 var formatError = validateFieldFormat($control, value); 350 if (formatError) { 351 errors.push({ $group: $group, $focusTarget: $control, message: formatError }); 352 } 353 }); 354 355 // The `email` field is always required server-side (see 356 // KlickTipp_Plugin_Ajax::validate_submission()) regardless of its 357 // "required" toggle in the form editor. When the email field is 358 // visible it was already fully checked above (required-or-not, 359 // format is always validated). Only fall back here when the email 360 // field is hidden (rendered outside .kt-field-group as a bare 361 // <input type="hidden">, e.g. a query-prefilled/locked value). 362 if (!$emailGroup) { 363 // Same reasoning as above: read the hidden email input directly 364 // via its key-independent marker rather than guessing the 365 // readFieldData() payload key ("field_" + internal key), which 366 // is no longer reliably "field_email". 367 var $hiddenEmailField = $form.find('input[data-kt-email-field="1"]'); 368 var fallbackEmail = $hiddenEmailField.length ? $.trim(String($hiddenEmailField.val() || '')) : ''; 369 if (!isEmail(fallbackEmail)) { 370 errors.push({ $group: null, $focusTarget: null, message: klicktippFrontend.messages.email }); 371 } 372 } 373 374 var $gdprInput = $form.find('input[name="kt_gdpr"]'); 375 // Note: data.gdpr is the string '0' or '1' (see the submit handler 376 // below), so a plain `!data.gdpr` truthiness check would never fire 377 // ('0' is a non-empty, truthy string in JS) â compare explicitly. 378 if ($gdprInput.length && '1' !== String(data.gdpr)) { 379 var gdprErrorMessage = String($form.attr('data-gdpr-error-message') || '').trim(); 380 errors.push({ 381 $group: $form.find('.kt-gdpr-group'), 382 $focusTarget: $gdprInput,
383 message: gdprErrorMessage || klicktippFrontend.messages.field_required 384 }); 385 } 386 387 return errors; 388 } 389 390 function applyFieldErrors(errors) { 391 errors.forEach(function(error) { 392 if (error.$group && error.$group.length) { 393 error.$group.addClass('kt-field-invalid'); 394 error.$group.append($('<div class="kt-field-error" role="alert"></div>').text(error.message)); 395 } 396 if (error.$focusTarget && error.$focusTarget.length) { 397 error.$focusTarget.attr('aria-invalid', 'true'); 398 } 399 }); 400 } 401 402 function trackFormViews() { 403 var visitorId = currentVisitorId(); 404 var sessionId = currentSessionId(); 405 406 $('.kt-optin-form').each(function() { 407 var $form = $(this); 408 var formId = String($form.find('input[name="kt_form_id"]').val() || '').trim(); 409 if (!formId) { 410 return; 411 } 412 413 var viewKey = 'klicktipp_view_' + formId + '_' + hashString(visitorId + ':' + sessionId); 414 try { 415 if (window.sessionStorage.getItem(viewKey)) { 416 return; 417 } 418 window.sessionStorage.setItem(viewKey, '1'); 419 } catch (e) { 420 // Session storage unavailable. 421 } 422 423 $.ajax({ 424 url: klicktippFrontend.ajax_url, 425 method: 'POST', 426 timeout: 3000, 427 data: { 428 action: 'klicktipp_track_view', 429 nonce: klicktippFrontend.nonce, 430 form_id: formId, 431 visitor_hash: hashString(visitorId), 432 session_id: sessionId, 433 // INTWORD-16: a view fired from the admin's own live-preview 434 // iframe must never inflate the visible Views stat â see 435 // isPreviewFrame() below and KlickTipp_Plugin_Insights::track_event(). 436 is_test: isPreviewFrame() ? '1' : '0' 437 } 438 }); 439 }); 440 } 441 442 function isPreviewFrame() { 443 return document.body && document.body.classList.contains('klicktipp-plugin-frontend-preview'); 444 } 445 446 function hidePreviewNoise() { 447 if (!isPreviewFrame()) { 448 return; 449 } 450 451 var selectors = [ 452 '#query-monitor-main', 453 '#query-monitor-container', 454 '#qm-wrapper', 455 '#qm-panel-menu', 456 '#qm-overview', 457 '#qm-admin-bar-menu', 458 '[id^="qm-"]', 459 '[class*="query-monitor"]' 460 ]; 461 462 selectors.forEach(function(selector) { 463 document.querySelectorAll(selector).forEach(function(node) { 464 if (node && node.parentNode) { 465 node.parentNode.removeChild(node); 466 } 467 }); 468 }); 469 } 470 471 function sendPreviewHeight() { 472 if (!isPreviewFrame()) { 473 return; 474 } 475 476 try { 477 hidePreviewNoise(); 478 479 var shell = document.querySelector('.kt-optin-preview-shell'); 480 var form = document.querySelector('.kt-optin-wrap'); 481 var shellHeight = shell ? Math.max(shell.scrollHeight || 0, shell.offsetHeight || 0) : 0; 482 var formHeight = form ? Math.max(form.scrollHeight || 0, form.offsetHeight || 0) : 0; 483 var fallbackHeight = Math.max( 484 document.body ? (document.body.scrollHeight || 0) : 0, 485 document.documentElement ? (document.documentElement.scrollHeight || 0) : 0 486 ); 487 488 window.parent.postMessage({ 489 type: 'ktOptinPreviewHeight', 490 height: Math.max(shellHeight, formHeight, fallbackHeight) 491 }, '*'); 492 } catch (e) {} 493 } 494 495 function installPreviewHeightSync() { 496 if (!isPreviewFrame()) { 497 return; 498 } 499 500 var queued = false; 501 function queuePreviewHeightSync() { 502 if (queued) { 503 return; 504 } 505 506 queued = true; 507 window.requestAnimationFrame(function() { 508 queued = false;
509 sendPreviewHeight(); 510 }); 511 } 512 513 if (typeof window.ResizeObserver === 'function') { 514 var resizeObserver = new ResizeObserver(function() { 515 queuePreviewHeightSync(); 516 }); 517 518 if (document.body) { 519 resizeObserver.observe(document.body); 520 } 521 522 if (document.documentElement) { 523 resizeObserver.observe(document.documentElement); 524 } 525 } 526 527 if (typeof window.MutationObserver === 'function' && document.body) { 528 var mutationObserver = new MutationObserver(function() { 529 queuePreviewHeightSync(); 530 }); 531 532 mutationObserver.observe(document.body, { 533 childList: true, 534 subtree: true, 535 attributes: true, 536 characterData: true 537 }); 538 } 539 540 window.addEventListener('resize', queuePreviewHeightSync); 541 window.addEventListener('orientationchange', queuePreviewHeightSync); 542 window.addEventListener('load', function() { 543 hidePreviewNoise(); 544 sendPreviewHeight(); 545 }); 546 547 setTimeout(sendPreviewHeight, 50); 548 setTimeout(sendPreviewHeight, 250); 549 } 550 551 function updateMultiDropdownLabel($dropdown) { 552 if (!$dropdown || !$dropdown.length) { 553 return; 554 } 555 556 var selectedLabels = []; 557 $dropdown.find('.kt-multi-dropdown-option:checked').each(function() { 558 var $option = $(this); 559 var labelText = String($option.closest('.kt-multi-dropdown-option-row').find('span').first().text() || '').trim(); 560 if (labelText) { 561 selectedLabels.push(labelText); 562 } 563 }); 564 565 var $value = $dropdown.find('.kt-multi-dropdown-value'); 566 var emptyLabel = String($value.attr('data-empty-label') || 'Choose options').trim(); 567 $value.text(selectedLabels.length ? selectedLabels.join(', ') : emptyLabel); 568 $value.attr('title', selectedLabels.length ? selectedLabels.join(', ') : ''); 569 } 570 571 function syncSingleDropdownSelection($dropdown, value) { 572 if (!$dropdown || !$dropdown.length) { 573 return; 574 } 575 var selectedValue = String(value || '').trim(); 576 var $options = $dropdown.find('.kt-single-dropdown-option'); 577 var hasMatch = false; 578 579 $options.each(function() { 580 var $option = $(this); 581 var optionValue = String($option.data('value') || '').trim(); 582 var isSelected = selectedValue !== '' && optionValue === selectedValue; 583 $option.toggleClass('is-selected', isSelected); 584 if (isSelected) { 585 hasMatch = true; 586 } 587 }); 588 589 var $input = $dropdown.find('.kt-single-dropdown-input'); 590 if ($input.length) { 591 $input.val(hasMatch ? selectedValue : ''); 592 } 593 594 var $value = $dropdown.find('.kt-multi-dropdown-value'); 595 var emptyLabel = String($value.attr('data-empty-label') || 'Please choose').trim(); 596 var labelText = ''; 597 598 if (hasMatch) { 599 var $selected = $options.filter('.is-selected').first(); 600 labelText = String($selected.text() || '').trim(); 601 } 602 603 $value.text(labelText || emptyLabel); 604 $value.attr('title', labelText || ''); 605 } 606 607 $(document).on('submit', '.kt-optin-form', function(e) { 608 e.preventDefault(); 609 610 var $form = $(this); 611 var $button = $form.find('.kt-submit-btn'); 612 var $buttonText = $button.find('.kt-btn-text'); 613 var $buttonLoading = $button.find('.kt-btn-loading'); 614 var $message = $form.find('.kt-optin-message'); 615 var hasRetried = false; 616 617 if ($button.prop('disabled')) { 618 return; 619 } 620 621 var unlockTimer = window.setTimeout(function() { 622 $button.prop('disabled', false); 623 $buttonLoading.hide(); 624 $buttonText.show(); 625 }, 10000); 626 627 // INTWORD-17 (captcha.eu): unlike reCAPTCHA's synchronously-readable 628 // textarea, captcha.eu's solution token is fetched asynchronously via 629 // KROT.getSolution(), so the rest of the submit flow is wrapped in a 630 // function invoked either immediately (no captcha / reCAPTCHA) or 631 // after that promise resolves (captcha.eu) â see the branch below. 632 function proceedWithSubmit(captchaEuSolution) { 633 var idempotencyKey = ensureFormIdempotencyKey($form); 634 var data = { 635 action: 'klicktipp_submit', 636 nonce: klicktippFrontend.nonce, 637 klicktipp_locale: (klicktippFrontend && klicktippFrontend.locale) ? String(klicktippFrontend.locale) : '', 638 form_id: $form.find('input[name="kt_form_id"]').val() || '', 639 // Generated once per form instance and captured in this 640 // payload, so a double-click or nonce-refresh retry reuses 641 // the same key without sharing it across cached page views. 642 idempotency_key: idempotencyKey, 643 tag_id: $form.find('input[name="kt_tag_id"]').val() || '', 644 list_id: $form.find('input[name="kt_list_id"]').val() || '', 645 gdpr: $form.find('input[name="kt_gdpr"]').is(':checked') ? '1' : '0', 646 started: $form.find('input[name="kt_started"]').val() || '', 647 hp: $form.find('input[name="kt_hp"]').val() || '', 648 visitor_hash: hashString(currentVisitorId()), 649 session_id: currentSessionId(), 650 'g-recaptcha-response': $form.find('textarea[name="g-recaptcha-response"]').val() || '', 651 captcha_eu_solution: captchaEuSolution || '', 652 // INTWORD-16: a submission made inside the admin's own live-preview 653 // iframe must never count as a real Submission/Subscribed conversion 654 // â see isPreviewFrame() above and KlickTipp_Plugin_Ajax::handle_submit(). 655 is_test: isPreviewFrame() ? '1' : '0' 656 }; 657 658 $.extend(data, readFieldData($form)); 659 660 clearFieldErrors($form);
661 var validationErrors = buildFieldValidationErrors($form, data); 662 if (validationErrors.length) { 663 applyFieldErrors(validationErrors); 664 showMessage($message, klicktippFrontend.messages.form_invalid || klicktippFrontend.messages.required, 'error'); 665 var $firstFocusTarget = validationErrors[0].$focusTarget; 666 if ($firstFocusTarget && $firstFocusTarget.length && typeof $firstFocusTarget.focus === 'function') { 667 $firstFocusTarget.focus(); 668 } 669 return; 670 } 671 672 $button.prop('disabled', true); 673 $buttonText.hide(); 674 $buttonLoading.show(); 675 $message.hide(); 676 var submitTimeoutMs = parseInt((klicktippFrontend && klicktippFrontend.submit_timeout_ms) ? klicktippFrontend.submit_timeout_ms : 9000, 10); 677 if (isNaN(submitTimeoutMs) || submitTimeoutMs < 4000) { 678 submitTimeoutMs = 9000; 679 } 680 681 function doSubmit() { 682 return $.ajax({ 683 url: klicktippFrontend.ajax_url, 684 method: 'POST', 685 timeout: submitTimeoutMs, 686 data: data 687 }); 688 } 689 690 function handleSubmitDone(response) { 691 if (response && response.success) { 692 if (response.data && response.data.redirect_url) { 693 if (isPreviewFrame()) { 694 window.parent.open(response.data.redirect_url, '_blank'); 695 } else { 696 window.location.href = response.data.redirect_url; 697 } 698 return; 699 } 700 showMessage($message, response.data.message, 'success'); 701 if (response.data && response.data.email_warning) { 702 $message.append( 703 $('<div class="kt-optin-warning">').text(String(response.data.email_warning || '')) 704 ); 705 } 706 if ($form.length) { 707 // Do not call native form.reset() here: query-prefill values are part of 708 // the initial DOM defaults, so resetting after submit would restore the 709 // first-load prefill and overwrite the values the visitor just entered. 710 $form.find('.kt-multi-dropdown').each(function() { 711 updateMultiDropdownLabel($(this)); 712 }); 713 } 714 return; 715 } 716 717 var code = (response && response.data && response.data.code) ? response.data.code : ''; 718 719 // On a security failure, fetch a fresh nonce and retry once automatically. 720 if (code === 'security' && !hasRetried) { 721 hasRetried = true; 722 $.ajax({ 723 url: klicktippFrontend.ajax_url, 724 method: 'POST', 725 timeout: 5000, 726 data: { action: 'klicktipp_refresh_nonce' } 727 }).done(function(refreshResponse) { 728 if (refreshResponse && refreshResponse.success && refreshResponse.data && refreshResponse.data.nonce) { 729 klicktippFrontend.nonce = refreshResponse.data.nonce; 730 data.nonce = refreshResponse.data.nonce; 731 doSubmit().done(handleSubmitDone).fail(handleSubmitFail).always(handleSubmitAlways); 732 } else { 733 handleSubmitAlways(); 734 showMessage($message, (klicktippFrontend.messages && klicktippFrontend.messages.security_cached) ? klicktippFrontend.messages.security_cached : klicktippFrontend.messages.error, 'error'); 735 } 736 }).fail(function() { 737 handleSubmitAlways(); 738 showMessage($message, (klicktippFrontend.messages && klicktippFrontend.messages.security_cached) ? klicktippFrontend.messages.security_cached : klicktippFrontend.messages.error, 'error'); 739 }); 740 return; 741 } 742 743 var fallback = (response && response.data && response.data.message) ? response.data.message : klicktippFrontend.messages.error; 744 if (code === 'security') { 745 fallback = (klicktippFrontend.messages && klicktippFrontend.messages.security_cached) ? klicktippFrontend.messages.security_cached : fallback; 746 } 747 showMessage($message, fallback, 'error'); 748 } 749 750 function handleSubmitFail(error) { 751 if (error && String(error.statusText || '').toLowerCase() === 'timeout') { 752 showMessage($message, (klicktippFrontend.messages && klicktippFrontend.messages.timeout) ? klicktippFrontend.messages.timeout : klicktippFrontend.messages.error, 'error'); 753 return; 754 } 755 if (error && error.responseJSON && error.responseJSON.data && error.responseJSON.data.message) { 756 showMessage($message, error.responseJSON.data.message, 'error'); 757 return; 758 } 759 showMessage($message, klicktippFrontend.messages.error, 'error'); 760 } 761 762 function handleSubmitAlways() { 763 window.clearTimeout(unlockTimer); 764 $button.prop('disabled', false); 765 $buttonLoading.hide(); 766 $buttonText.show(); 767 } 768 769 doSubmit().done(handleSubmitDone).fail(handleSubmitFail).always(handleSubmitAlways); 770 } 771 772 var $captchaGroup = $form.find('.kt-captcha-group'); 773 var captchaProvider = $captchaGroup.length ? String($captchaGroup.attr('data-captcha-provider') || '') : ''; 774 775 if (captchaProvider === 'captcha_eu' && window.KROT && typeof window.KROT.getSolution === 'function') { 776 window.KROT.getSolution().then(function(solution) { 777 // captcha.eu's getSolution() resolves to a rich object 778 // ({challenge, solutions[], expires, timings, ...}), not a 779 // plain string â confirmed against the real captcha.eu 780 // service. Naively coercing this object to a string 781 // previously produced the literal text "[object Object]", 782 // which their /validate endpoint always rejects, so every 783 // real submission with captcha.eu configured was silently 784 // failing captcha verification. JSON.stringify() is what 785 // their own PHP SDK's POST-body usage expects on the wire. 786 proceedWithSubmit(solution ? JSON.stringify(solution) : ''); 787 }, function() { 788 // INTWORD-17: if the solution can't be generated, still submit â 789 // the server-side guard_captcha() rejects a missing/invalid 790 // token with the standard captcha_missing/captcha_failed error, 791 // exactly like a visitor who never completed the widget. 792 proceedWithSubmit(''); 793 }); 794 } else { 795 proceedWithSubmit(''); 796 } 797 }); 798 799 // INTWORD-19: clear a field's inline error as soon as the visitor edits 800 // it, rather than only re-checking on the next submit attempt. 801 function clearGroupError($el) { 802 var $group = $el.closest('.kt-field-group, .kt-gdpr-group'); 803 if ($group.length) { 804 $group.removeClass('kt-field-invalid'); 805 $group.find('.kt-field-error').remove(); 806 } 807 $el.removeAttr('aria-invalid'); 808 } 809 810 $(document).on('input change', '.kt-optin-form input.kt-input, .kt-optin-form textarea.kt-input, .kt-optin-form input[name="kt_gdpr"]', function() { 811 clearGroupError($(this)); 812 }); 813 814 $(document).on('change', '.kt-multi-dropdown-option', function() { 815 var $dropdown = $(this).closest('.kt-multi-dropdown'); 816 updateMultiDropdownLabel($dropdown); 817 clearGroupError($dropdown); 818 }); 819 820 $(document).on('click', '.kt-single-dropdown-option', function(e) { 821 e.preventDefault(); 822 var $option = $(this); 823 var $dropdown = $option.closest('.kt-single-dropdown'); 824 if (!$dropdown.length) { 825 return; 826 }
827 var value = String($option.data('value') || '').trim(); 828 syncSingleDropdownSelection($dropdown, value); 829 clearGroupError($dropdown); 830 $dropdown.removeAttr('open'); 831 }); 832 833 $(document).on('click', '.kt-multi-dropdown-trigger', function(e) { 834 e.preventDefault(); 835 var $dropdown = $(this).closest('.kt-multi-dropdown'); 836 if (!$dropdown.length) { 837 return; 838 } 839 var shouldOpen = !$dropdown.prop('open'); 840 $('.kt-multi-dropdown[open]').not($dropdown).removeAttr('open'); 841 if (shouldOpen) { 842 $dropdown.attr('open', 'open'); 843 } else { 844 $dropdown.removeAttr('open'); 845 } 846 }); 847 848 $(document).on('click', function(e) { 849 var $target = $(e.target); 850 if ($target.closest('.kt-multi-dropdown').length) { 851 return; 852 } 853 $('.kt-multi-dropdown[open]').removeAttr('open'); 854 }); 855 856 $(document).on('keydown', function(e) { 857 if (e.key === 'Escape' || e.keyCode === 27) { 858 $('.kt-multi-dropdown[open]').removeAttr('open'); 859 } 860 }); 861 862 $(function() { 863 refreshStartedTimestamps(); 864 $('.kt-optin-form').each(function() { 865 ensureFormIdempotencyKey($(this)); 866 }); 867 $('.kt-multi-dropdown').each(function() { 868 updateMultiDropdownLabel($(this)); 869 }); 870 $('.kt-single-dropdown').each(function() { 871 var $dropdown = $(this); 872 var initialValue = String($dropdown.find('.kt-single-dropdown-input').val() || '').trim(); 873 syncSingleDropdownSelection($dropdown, initialValue); 874 }); 875 trackFormViews(); 876 installPreviewHeightSync(); 877 }); 878})(jQuery);
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.