PageSourceSearch

https://moin-gesundheit.de/wp-content/plugins/klicktipp/assets/js/klicktipp-frontend.js?ver=1790079854

js moin-gesundheit.de collected 2026-10-07 04:59:00 UTC 37,203 bytes, 878 lines download raw bytes

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.