// Validation Helpers const ValidationRules = { date: { validate: (value, input) => { if (!value) return true; const date = new Date(value); const now = new Date(); const min = new Date('1800-01-01'); if (date > now) { return 'Date cannot be in the future'; } if (date < min) { return 'Date cannot be before 1800'; } return true; } }, numeric: { validate: (value, input) => { if (!value) return true; const num = parseFloat(value); const min = parseFloat(input.getAttribute('min') || '-Infinity'); const max = parseFloat(input.getAttribute('max') || 'Infinity'); if (isNaN(num)) { return 'Please enter a valid number'; } if (num < min) { return `Value must be at least ${min}`; } if (num > max) { return `Value must be no more than ${max}`; } return true; } } }; // Form Validation and Error Handling document.addEventListener('DOMContentLoaded', function() { // Form Validation document.querySelectorAll('form[hx-post]').forEach(form => { // Add validation on field change form.addEventListener('input', function(e) { const input = e.target; if (input.hasAttribute('data-validate')) { validateField(input); } }); form.addEventListener('htmx:beforeRequest', function(event) { let isValid = true; // Validate all fields form.querySelectorAll('[data-validate]').forEach(input => { if (!validateField(input)) { isValid = false; } }); // Check required notes field const notesField = form.querySelector('textarea[name="notes"]'); if (notesField && !notesField.value.trim()) { showError(notesField, 'Notes are required'); isValid = false; } if (!isValid) { event.preventDefault(); // Focus first invalid field form.querySelector('.border-red-500')?.focus(); } }); // Clear error states on input form.addEventListener('input', function(e) { if (e.target.classList.contains('border-red-500')) { e.target.classList.remove('border-red-500'); } }); }); // Form State Management document.querySelectorAll('form[hx-post]').forEach(form => { const formId = form.getAttribute('id'); if (!formId) return; // Save form state before submission form.addEventListener('htmx:beforeRequest', function() { const formData = new FormData(form); const state = {}; formData.forEach((value, key) => { state[key] = value; }); sessionStorage.setItem('formState-' + formId, JSON.stringify(state)); }); // Restore form state if available const savedState = sessionStorage.getItem('formState-' + formId); if (savedState) { const state = JSON.parse(savedState); Object.entries(state).forEach(([key, value]) => { const input = form.querySelector(`[name="${key}"]`); if (input) { input.value = value; } }); } }); // Park Area Sync with Park Selection document.querySelectorAll('[id^="park-input-"]').forEach(parkInput => { const submissionId = parkInput.id.replace('park-input-', ''); const areaSelect = document.querySelector(`#park-area-select-${submissionId}`); if (parkInput && areaSelect) { parkInput.addEventListener('change', function() { const parkId = this.value; if (!parkId) { areaSelect.innerHTML = ''; return; } htmx.ajax('GET', `/parks/${parkId}/areas/`, { target: areaSelect, swap: 'innerHTML' }); }); } }); // Improved Error Handling document.body.addEventListener('htmx:responseError', function(evt) { const errorToast = document.createElement('div'); errorToast.className = 'fixed bottom-4 right-4 bg-red-500 text-white px-6 py-3 rounded-lg shadow-lg z-50 flex items-center'; errorToast.innerHTML = ` ${evt.detail.error || 'An error occurred'} `; document.body.appendChild(errorToast); setTimeout(() => { errorToast.remove(); }, 5000); }); // Accessibility Improvements document.addEventListener('htmx:afterSettle', function(evt) { // Focus management const target = evt.detail.target; const focusableElement = target.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (focusableElement) { focusableElement.focus(); } // Announce state changes if (target.hasAttribute('aria-live')) { const announcement = target.getAttribute('aria-label') || target.textContent; const announcer = document.getElementById('a11y-announcer') || createAnnouncer(); announcer.textContent = announcement; } }); }); // Helper function to create accessibility announcer function createAnnouncer() { const announcer = document.createElement('div'); announcer.id = 'a11y-announcer'; announcer.className = 'sr-only'; announcer.setAttribute('aria-live', 'polite'); document.body.appendChild(announcer); return announcer; } // Validation Helper Functions function validateField(input) { const validationType = input.getAttribute('data-validate'); if (!validationType || !ValidationRules[validationType]) return true; const result = ValidationRules[validationType].validate(input.value, input); if (result === true) { clearError(input); return true; } else { showError(input, result); return false; } } function showError(input, message) { const errorId = input.getAttribute('aria-describedby'); const errorElement = document.getElementById(errorId); input.classList.add('border-red-500', 'error-shake'); if (errorElement) { errorElement.textContent = message; errorElement.classList.remove('hidden'); } // Announce error to screen readers const announcer = document.getElementById('a11y-announcer'); if (announcer) { announcer.textContent = `Error: ${message}`; } setTimeout(() => { input.classList.remove('error-shake'); }, 820); } function clearError(input) { const errorId = input.getAttribute('aria-describedby'); const errorElement = document.getElementById(errorId); input.classList.remove('border-red-500'); if (errorElement) { errorElement.classList.add('hidden'); errorElement.textContent = ''; } }