import { Logger } from '../../core/logger.js'; export class FormValidator { constructor(form) { this.form = form; this.errors = new Map(); } static create(form) { return new FormValidator(form); } validate() { this.errors.clear(); const fields = this.form.querySelectorAll('input, textarea, select'); for (const field of fields) { if (field.type === 'hidden' || field.disabled) continue; this.validateField(field); } return this.errors.size === 0; } validateField(field) { const value = field.value; const fieldName = field.name; // HTML5 required attribute if (field.hasAttribute('required') && (!value || value.trim() === '')) { this.errors.set(fieldName, this.getErrorMessage(field, 'valueMissing') || `${this.getFieldLabel(field)} ist erforderlich`); return; } // Skip further validation if field is empty and not required if (!value || value.trim() === '') return; // HTML5 type validation if (field.type === 'email' && !this.isValidEmail(value)) { this.errors.set(fieldName, this.getErrorMessage(field, 'typeMismatch') || 'Bitte geben Sie eine gültige E-Mail-Adresse ein'); return; } if (field.type === 'url' && !this.isValidUrl(value)) { this.errors.set(fieldName, this.getErrorMessage(field, 'typeMismatch') || 'Bitte geben Sie eine gültige URL ein'); return; } // HTML5 minlength attribute const minLength = field.getAttribute('minlength'); if (minLength && value.length < parseInt(minLength)) { this.errors.set(fieldName, this.getErrorMessage(field, 'tooShort') || `Mindestens ${minLength} Zeichen erforderlich`); return; } // HTML5 maxlength attribute (usually enforced by browser, but for safety) const maxLength = field.getAttribute('maxlength'); if (maxLength && value.length > parseInt(maxLength)) { this.errors.set(fieldName, this.getErrorMessage(field, 'tooLong') || `Maximal ${maxLength} Zeichen erlaubt`); return; } // HTML5 min/max for number inputs if (field.type === 'number') { const min = field.getAttribute('min'); const max = field.getAttribute('max'); const numValue = parseFloat(value); if (min && numValue < parseFloat(min)) { this.errors.set(fieldName, this.getErrorMessage(field, 'rangeUnderflow') || `Wert muss mindestens ${min} sein`); return; } if (max && numValue > parseFloat(max)) { this.errors.set(fieldName, this.getErrorMessage(field, 'rangeOverflow') || `Wert darf maximal ${max} sein`); return; } } // HTML5 pattern attribute const pattern = field.getAttribute('pattern'); if (pattern) { const regex = new RegExp(pattern); if (!regex.test(value)) { this.errors.set(fieldName, this.getErrorMessage(field, 'patternMismatch') || 'Ungültiges Format'); return; } } // Custom validation via data-validate attribute const customValidation = field.getAttribute('data-validate'); if (customValidation) { const result = this.runCustomValidation(customValidation, value, field); if (!result.valid) { this.errors.set(fieldName, result.message); return; } } } runCustomValidation(validationType, value, field) { switch (validationType) { case 'phone': const phoneRegex = /^[\+]?[0-9\s\-\(\)]{10,}$/; return { valid: phoneRegex.test(value), message: 'Bitte geben Sie eine gültige Telefonnummer ein' }; case 'postal-code-de': const postalRegex = /^[0-9]{5}$/; return { valid: postalRegex.test(value), message: 'Bitte geben Sie eine gültige Postleitzahl ein' }; case 'no-html': const hasHtml = /<[^>]*>/g.test(value); return { valid: !hasHtml, message: 'HTML-Code ist nicht erlaubt' }; default: Logger.warn(`Unknown custom validation: ${validationType}`); return { valid: true, message: '' }; } } isValidEmail(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); } isValidUrl(url) { try { new URL(url); return true; } catch { return false; } } getFieldLabel(field) { const label = this.form.querySelector(`label[for="${field.id}"]`) || this.form.querySelector(`label:has([name="${field.name}"])`); return label ? label.textContent.trim().replace(':', '') : field.name; } getErrorMessage(field, validityType) { // Check for custom error messages via data-error-* attributes const customMessage = field.getAttribute(`data-error-${validityType}`) || field.getAttribute('data-error'); return customMessage; } getErrors() { return Object.fromEntries(this.errors); } getFieldError(fieldName) { return this.errors.get(fieldName); } hasErrors() { return this.errors.size > 0; } clearErrors() { this.errors.clear(); } // Check HTML5 validity API as fallback validateWithHTML5() { const isValid = this.form.checkValidity(); if (!isValid) { const fields = this.form.querySelectorAll('input, textarea, select'); for (const field of fields) { if (!field.validity.valid) { this.errors.set(field.name, field.validationMessage); } } } return isValid; } }