PageSourceSearch

https://www.lotte.de/typo3temp/assets/compressed/Powermail-5e0d840bff15212d96b1d717233799d7.js?1789174820

js lotte.de collected 2026-10-02 18:59:08 UTC 23,837 bytes, 672 lines download raw bytes

1/**
2 * minCore Powermail Integration
3 * =============================
4 * AJAX Form Submission, Enhanced Validation, and Accessibility
5 * WCAG 2.2 AA konform
6 *
7 * @version 1.0.0
8 * @author made in nature Projektagentur
9 */
10
11(function () {
12    'use strict';
13
14    // Default configuration
15    const defaultConfig = {
16        ajaxSubmit: true,
17        nativeValidation: true,
18        focusOnError: true,
19        scrollToError: true,
20        scrollOffset: 100,
21        animateDuration: 300,
22        messages: {
23            sending: 'Formular wird gesendet...',
24            success: 'Vielen Dank! Ihre Nachricht wurde erfolgreich gesendet.',
25            error: 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.',
26            validation: 'Bitte korrigieren Sie die markierten Felder.'
27        },
28        classes: {
29            form: 'min-form',
30            field: 'min-form__field',
31            label: 'min-form__label',
32            input: 'min-form__input',
33            error: 'min-form__field--error',
34            success: 'min-form__field--success',
35            loading: 'min-form--loading',
36            submitted: 'min-form--submitted'
37        }
38    };
39
40    // Merge with global config if available
41    const config = Object.assign({}, defaultConfig, window.MINCORE_POWERMAIL_CONFIG || {});
42
43    /**
44     * MincorePowermail Class
45     * Main controller for Powermail form enhancements
46     */
47    class MincorePowermail {
48        constructor(form) {
49            this.form = form;
50            this.submitButton = form.querySelector('[type="submit"]');
51            this.messageContainer = null;
52            this.isSubmitting = false;
53            this.announcer = null;
54
55            this.init();
56        }
57
58        /**
59         * Initialize the form enhancements
60         */
61        init() {
62            this.createMessageContainer();
63            this.createAnnouncer();
64            this.bindEvents();
65            this.enhanceAccessibility();
66        }
67
68        /**
69         * Create message container for success/error messages
70         */
71        createMessageContainer() {
72            this.messageContainer = document.createElement('div');
73            this.messageContainer.className = 'min-form__messages';
74            this.messageContainer.setAttribute('role', 'alert');
75            this.messageContainer.setAttribute('aria-live', 'polite');
76            this.messageContainer.setAttribute('aria-atomic', 'true');
77            this.messageContainer.style.display = 'none';
78            this.form.insertBefore(this.messageContainer, this.form.firstChild);
79        }
80
81        /**
82         * Create ARIA announcer for screen readers
83         */
84        createAnnouncer() {
85            this.announcer = document.createElement('div');
86            this.announcer.className = 'sr-only';
87            this.announcer.setAttribute('role', 'status');
88            this.announcer.setAttribute('aria-live', 'assertive');
89            this.announcer.setAttribute('aria-atomic', 'true');
90            this.announcer.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
91            document.body.appendChild(this.announcer);
92        }
93
94        /**
95         * Announce message to screen readers
96         * @param {string} message - Message to announce
97         */
98        announce(message) {
99            if (this.announcer) {
100                this.announcer.textContent = '';
101                setTimeout(() => {
102                    this.announcer.textContent = message;
103                }, 100);
104            }
105        }
106
107        /**
108         * Bind form events
109         */
110        bindEvents() {
111            // Form submission
112            if (config.ajaxSubmit) {
113                this.form.addEventListener('submit', this.handleSubmit.bind(this));
114            }
115
116            // Real-time validation
117            const inputs = this.form.querySelectorAll('input, textarea, select');
118            inputs.forEach(input => {
119                input.addEventListener('blur', this.validateField.bind(this));
120                input.addEventListener('input', this.clearFieldError.bind(this));
121            });
122
123            // Enhanced keyboard navigation for radio/checkbox groups
124            const groups = this.form.querySelectorAll('.min-form__radio-group, .min-form__checkbox-group');
125            groups.forEach(group => {
126                group.addEventListener('keydown', this.handleGroupKeydown.bind(this));
127            });
128        }
129
130        /**
131         * Enhance accessibility features
132         */
133        enhanceAccessibility() {
134            // Add aria-required to required fields
135            const requiredFields = this.form.querySelectorAll('[required]');
136            requiredFields.forEach(field => {
137                field.setAttribute('aria-required', 'true');
138
139                // Find and update label
140                const label = this.findLabel(field);
141                if (label && !label.querySelector('.sr-only')) {
142                    const srText = document.createElement('span');
143                    srText.className = 'sr-only';
144                    srText.textContent = ' (Pflichtfeld)';
145                    label.appendChild(srText);
146                }
147            });
148
149            // Connect error messages with aria-describedby
150            const errorMessages = this.form.querySelectorAll('.min-form__error-message');
151            errorMessages.forEach(msg => {
152                const field = msg.closest('.min-form__field');
153                if (field) {
154                    const input = field.querySelector('input, textarea, select');
155                    if (input && !msg.id) {
156                        msg.id = 'error-' + (input.id || input.name || Math.random().toString(36).substr(2, 9));
157                        input.setAttribute('aria-describedby', msg.id);
158                    }
159                }
160            });
161        }
162
163        /**
164         * Find label for a field
165         * @param {HTMLElement} field - Input field
166         * @returns {HTMLElement|null} Label element
167         */
168        findLabel(field) {
169            // Try for attribute
170            if (field.id) {
171                const label = document.querySelector(`label[for="${field.id}"]`);
172                if (label) return label;
173            }
174
175            // Try parent label
176            const parentLabel = field.closest('label');
177            if (parentLabel) return parentLabel;
178
179            // Try sibling label
180            const wrapper = field.closest('.min-form__field');
181            if (wrapper) {
182                return wrapper.querySelector('.min-form__label');
183            }
184
185            return null;
186        }
187
188        /**
189         * Handle form submission
190         * @param {Event} e - Submit event
191         */
192        async handleSubmit(e) {
193            e.preventDefault();
194
195            if (this.isSubmitting) return;
196
197            // Validate form
198            if (!this.validateForm()) {
199                this.showMessage(config.messages.validation, 'error');
200                this.announce(config.messages.validation);
201                return;
202            }
203
204            this.isSubmitting = true;
205            this.setLoadingState(true);
206            this.announce(config.messages.sending);
207
208            try {
209                const formData = new FormData(this.form);
210                const response = await fetch(this.form.action, {
211                    method: 'POST',
212                    body: formData,
213                    headers: {
214                        'X-Requested-With': 'XMLHttpRequest'
215                    }
216                });
217
218                if (!response.ok) {
219                    throw new Error('Network response was not ok');
220                }
221
222                const result = await response.text();
223
224                // Check if response contains success indicators
225                if (this.isSuccessResponse(result)) {
226                    this.handleSuccess(result);
227                } else {
228                    // Response might contain validation errors
229                    this.handleValidationErrors(result);
230                }
231            } catch (error) {
232                console.error('Form submission error:', error);
233                this.handleError(error);
234            } finally {
235                this.isSubmitting = false;
236                this.setLoadingState(false);
237            }
238        }
239
240        /**
241         * Check if response indicates success
242         * @param {string} response - Server response
243         * @returns {boolean}
244         */
245        isSuccessResponse(response) {
246            // Look for typical powermail success indicators
247            return response.includes('min-form__success-message') ||
248                   response.includes('powermail_confirmation') ||
249                   response.includes('tx-powermail-pi1-thx') ||
250                   !response.includes('error');
251        }
252
253        /**
254         * Handle successful submission
255         * @param {string} response - Server response
256         */
257        handleSuccess(response) {
258            // Try to extract success message from response
259            const tempDiv = document.createElement('div');
260            tempDiv.innerHTML = response;
261            const successMsg = tempDiv.querySelector('.min-form__success-message, .powermail_confirmation');
262
263            if (successMsg) {
264                this.showMessage(successMsg.innerHTML, 'success', true);
265            } else {
266                this.showMessage(config.messages.success, 'success');
267            }
268
269            this.form.classList.add(config.classes.submitted);
270            this.announce(config.messages.success);
271
272            // Optionally hide the form
273            const formContent = this.form.querySelector('.min-form__content');
274            if (formContent) {
275                formContent.style.display = 'none';
276            }
277
278            // Reset form
279            this.form.reset();
280            this.clearAllErrors();
281        }
282
283        /**
284         * Handle validation errors from server
285         * @param {string} response - Server response with errors
286         */
287        handleValidationErrors(response) {
288            const tempDiv = document.createElement('div');
289            tempDiv.innerHTML = response;
290
291            // Find error messages in response
292            const errors = tempDiv.querySelectorAll('.min-form__error-message, .powermail_error');
293
294            if (errors.length > 0) {
295                // Update form with error messages
296                this.form.innerHTML = response;
297                this.bindEvents();
298                this.enhanceAccessibility();
299
300                // Focus first error
301                const firstError = this.form.querySelector('.min-form__field--error input, .min-form__field--error textarea, .min-form__field--error select');
302                if (firstError && config.focusOnError) {
303                    firstError.focus();
304                }
305
306                this.announce(config.messages.validation);
307            } else {
308                this.handleError(new Error('Unknown validation error'));
309            }
310        }
311
312        /**
313         * Handle submission error
314         * @param {Error} error - Error object
315         */
316        handleError(error) {
317            this.showMessage(config.messages.error, 'error');
318            this.announce(config.messages.error);
319            console.error('Powermail error:', error);
320        }
321
322        /**
323         * Set loading state
324         * @param {boolean} loading - Loading state
325         */
326        setLoadingState(loading) {
327            if (loading) {
328                this.form.classList.add(config.classes.loading);
329                if (this.submitButton) {
330                    this.submitButton.disabled = true;
331                    this.submitButton.setAttribute('aria-busy', 'true');
332                }
333            } else {
334                this.form.classList.remove(config.classes.loading);
335                if (this.submitButton) {
336                    this.submitButton.disabled = false;
337                    this.submitButton.removeAttribute('aria-busy');
338                }
339            }
340        }
341
342        /**
343         * Show message
344         * @param {string} message - Message text
345         * @param {string} type - Message type (success, error)
346         * @param {boolean} isHtml - Whether message is HTML
347         */
348        showMessage(message, type, isHtml = false) {
349            if (isHtml) {
350                this.messageContainer.innerHTML = message;
351            } else {
352                this.messageContainer.innerHTML = `
353                    <div class="min-form__${type}-message">
354                        ${this.escapeHtml(message)}
355                    </div>
356                `;
357            }
358
359            this.messageContainer.style.display = 'block';
360
361            if (config.scrollToError && type === 'error') {
362                this.scrollToElement(this.messageContainer);
363            }
364        }
365
366        /**
367         * Escape HTML characters
368         * @param {string} text - Text to escape
369         * @returns {string} Escaped text
370         */
371        escapeHtml(text) {
372            const div = document.createElement('div');
373            div.textContent = text;
374            return div.innerHTML;
375        }
376
377        /**
378         * Validate entire form
379         * @returns {boolean} Form validity
380         */
381        validateForm() {
382            let isValid = true;
383            const fields = this.form.querySelectorAll('input, textarea, select');
384
385            fields.forEach(field => {
386                if (!this.validateField({ target: field }, false)) {
387                    isValid = false;
388                }
389            });
390
391            // Focus first invalid field
392            if (!isValid && config.focusOnError) {
393                const firstInvalid = this.form.querySelector('.min-form__field--error input, .min-form__field--error textarea, .min-form__field--error select');
394                if (firstInvalid) {
395                    if (config.scrollToError) {
396                        this.scrollToElement(firstInvalid);
397                    }
398                    firstInvalid.focus();
399                }
400            }
401
402            return isValid;
403        }
404
405        /**
406         * Validate single field
407         * @param {Event} e - Blur event
408         * @param {boolean} showMessage - Whether to show error message
409         * @returns {boolean} Field validity
410         */
411        validateField(e, showMessage = true) {
412            const field = e.target;
413            const wrapper = field.closest('.min-form__field');
414
415            if (!wrapper) return true;
416
417            // Skip hidden fields
418            if (field.type === 'hidden' || field.offsetParent === null) {
419                return true;
420            }
421
422            let isValid = true;
423            let errorMessage = '';
424
425            // Check required
426            if (field.required && !field.value.trim()) {
427                isValid = false;
428                errorMessage = 'Dieses Feld ist erforderlich.';
429            }
430
431            // Check pattern
432            if (isValid && field.pattern && field.value) {
433                const regex = new RegExp(field.pattern);
434                if (!regex.test(field.value)) {
435                    isValid = false;
436                    errorMessage = field.title || 'Bitte geben Sie einen gueltigen Wert ein.';
437                }
438            }
439
440            // Check email
441            if (isValid && field.type === 'email' && field.value) {
442                const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
443                if (!emailRegex.test(field.value)) {
444                    isValid = false;
445                    errorMessage = 'Bitte geben Sie eine gueltige E-Mail-Adresse ein.';
446                }
447            }
448
449            // Check min/max length
450            if (isValid && field.minLength && field.value.length < field.minLength) {
451                isValid = false;
452                errorMessage = `Mindestens ${field.minLength} Zeichen erforderlich.`;
453            }
454
455            if (isValid && field.maxLength && field.value.length > field.maxLength) {
456                isValid = false;
457                errorMessage = `Maximal ${field.maxLength} Zeichen erlaubt.`;
458            }
459
460            // Update UI
461            if (!isValid) {
462                this.setFieldError(wrapper, field, errorMessage, showMessage);
463            } else {
464                this.clearFieldErrorElement(wrapper, field);
465            }
466
467            return isValid;
468        }
469
470        /**
471         * Set field error state
472         * @param {HTMLElement} wrapper - Field wrapper
473         * @param {HTMLElement} field - Input field
474         * @param {string} message - Error message
475         * @param {boolean} showMessage - Whether to show error message
476         */
477        setFieldError(wrapper, field, message, showMessage = true) {
478            wrapper.classList.add(config.classes.error);
479            wrapper.classList.remove(config.classes.success);
480
481            field.setAttribute('aria-invalid', 'true');
482
483            if (showMessage) {
484                let errorEl = wrapper.querySelector('.min-form__error-message');
485                if (!errorEl) {
486                    errorEl = document.createElement('div');
487                    errorEl.className = 'min-form__error-message';
488                    errorEl.id = 'error-' + (field.id || field.name || Math.random().toString(36).substr(2, 9));
489                    wrapper.appendChild(errorEl);
490                }
491
492                errorEl.textContent = message;
493                field.setAttribute('aria-describedby', errorEl.id);
494            }
495        }
496
497        /**
498         * Clear error on input
499         * @param {Event} e - Input event
500         */
501        clearFieldError(e) {
502            const field = e.target;
503            const wrapper = field.closest('.min-form__field');
504
505            if (wrapper && wrapper.classList.contains(config.classes.error)) {
506                // Re-validate on input
507                if (field.value.trim()) {
508                    this.validateField(e, false);
509                }
510            }
511        }
512
513        /**
514         * Clear field error element
515         * @param {HTMLElement} wrapper - Field wrapper
516         * @param {HTMLElement} field - Input field
517         */
518        clearFieldErrorElement(wrapper, field) {
519            wrapper.classList.remove(config.classes.error);
520            wrapper.classList.add(config.classes.success);
521
522            field.removeAttribute('aria-invalid');
523
524            const errorEl = wrapper.querySelector('.min-form__error-message');
525            if (errorEl) {
526                field.removeAttribute('aria-describedby');
527                errorEl.remove();
528            }
529        }
530
531        /**
532         * Clear all errors
533         */
534        clearAllErrors() {
535            const errorWrappers = this.form.querySelectorAll(`.${config.classes.error}`);
536            errorWrappers.forEach(wrapper => {
537                wrapper.classList.remove(config.classes.error);
538                const errorEl = wrapper.querySelector('.min-form__error-message');
539                if (errorEl) errorEl.remove();
540            });
541
542            const invalidFields = this.form.querySelectorAll('[aria-invalid="true"]');
543            invalidFields.forEach(field => {
544                field.removeAttribute('aria-invalid');
545                field.removeAttribute('aria-describedby');
546            });
547        }
548
549        /**
550         * Handle keyboard navigation in radio/checkbox groups
551         * @param {KeyboardEvent} e - Keydown event
552         */
553        handleGroupKeydown(e) {
554            const inputs = Array.from(e.currentTarget.querySelectorAll('input[type="radio"], input[type="checkbox"]'));
555            const currentIndex = inputs.indexOf(document.activeElement);
556
557            if (currentIndex === -1) return;
558
559            let newIndex;
560
561            switch (e.key) {
562                case 'ArrowDown':
563                case 'ArrowRight':
564                    e.preventDefault();
565                    newIndex = (currentIndex + 1) % inputs.length;
566                    inputs[newIndex].focus();
567                    if (inputs[newIndex].type === 'radio') {
568                        inputs[newIndex].checked = true;
569                    }
570                    break;
571
572                case 'ArrowUp':
573                case 'ArrowLeft':
574                    e.preventDefault();
575                    newIndex = (currentIndex - 1 + inputs.length) % inputs.length;
576                    inputs[newIndex].focus();
577                    if (inputs[newIndex].type === 'radio') {
578                        inputs[newIndex].checked = true;
579                    }
580                    break;
581            }
582        }
583
584        /**
585         * Scroll to element
586         * @param {HTMLElement} element - Element to scroll to
587         */
588        scrollToElement(element) {
589            const rect = element.getBoundingClientRect();
590            const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
591            const top = rect.top + scrollTop - config.scrollOffset;
592
593            window.scrollTo({
594                top: top,
595                behavior: 'smooth'
596            });
597        }
598    }
599
600    /**
601     * PowermailManager - Handles multiple forms on page
602     */
603    class PowermailManager {
604        constructor() {
605            this.forms = [];
606            this.init();
607        }
608
609        init() {
610            // Initialize existing forms
611            this.initForms();
612
613            // Watch for dynamically added forms
614            this.observeMutations();
615        }
616
617        initForms() {
618            const forms = document.querySelectorAll('.min-form, .tx-powermail form');
619            forms.forEach(form => {
620                if (!form.dataset.mincoreInitialized) {
621                    this.forms.push(new MincorePowermail(form));
622                    form.dataset.mincoreInitialized = 'true';
623                }
624            });
625        }
626
627        observeMutations() {
628            const observer = new MutationObserver((mutations) => {
629                mutations.forEach((mutation) => {
630                    mutation.addedNodes.forEach((node) => {
631                        if (node.nodeType === 1) {
632                            if (node.matches && (node.matches('.min-form') || node.matches('.tx-powermail form'))) {
633                                if (!node.dataset.mincoreInitialized) {
634                                    this.forms.push(new MincorePowermail(node));
635                                    node.dataset.mincoreInitialized = 'true';
636                                }
637                            }
638
639                            // Check descendants
640                            const nestedForms = node.querySelectorAll && node.querySelectorAll('.min-form, .tx-powermail form');
641                            if (nestedForms) {
642                                nestedForms.forEach(form => {
643                                    if (!form.dataset.mincoreInitialized) {
644                                        this.forms.push(new MincorePowermail(form));
645                                        form.dataset.mincoreInitialized = 'true';
646                                    }
647                                });
648                            }
649                        }
650                    });
651                });
652            });
653
654            observer.observe(document.body, {
655                childList: true,
656                subtree: true
657            });
658        }
659    }
660
661    // Initialize when DOM is ready
662    if (document.readyState === 'loading') {
663        document.addEventListener('DOMContentLoaded', () => new PowermailManager());
664    } else {
665        new PowermailManager();
666    }
667
668    // Export for external access
669    window.MincorePowermail = MincorePowermail;
670    window.PowermailManager = PowermailManager;
671
672})();

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.