Files
thrillwiki_django_no_react/static/js/moderation.js

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