mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:51:09 -05:00
223 lines
7.4 KiB
JavaScript
223 lines
7.4 KiB
JavaScript
// 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 = '<option value="">Select area</option>';
|
|
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 = `
|
|
<i class="fas fa-exclamation-circle mr-2"></i>
|
|
<span>${evt.detail.error || 'An error occurred'}</span>
|
|
<button class="ml-4 hover:text-red-200" onclick="this.parentElement.remove()">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
`;
|
|
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 = '';
|
|
}
|
|
} |