Files
michaelschiemer/resources/js/modules/form-handling/FormValidator.js
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

190 lines
6.3 KiB
JavaScript

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;
}
}