/** * HVAC AJAX Optimizer * * Provides optimized AJAX handling with debouncing, caching, and rate limiting * for the native WordPress event management system. * * @package HVAC_Community_Events * @since 3.0.0 */ (function($) { 'use strict'; // Configuration from localized script const config = window.hvacAjax || {}; const ajaxUrl = config.ajaxurl; const nonce = config.nonce; const debounceDelay = config.debounceDelay || 300; const cacheEnabled = config.cacheEnabled !== false; const rateLimits = config.rateLimits || {}; // Local cache for AJAX responses const cache = new Map(); // Rate limiting tracking const rateLimitTracking = new Map(); /** * HVAC AJAX Optimizer Class */ class HVACAjaxOptimizer { constructor() { this.debounceTimers = new Map(); this.pendingRequests = new Map(); this.init(); } /** * Initialize the optimizer */ init() { this.bindEvents(); this.warmCache(); } /** * Bind event handlers */ bindEvents() { // Venue search with debouncing $(document).on('input', '.hvac-venue-search', (e) => { this.debouncedVenueSearch($(e.target)); }); // Organizer search with debouncing $(document).on('input', '.hvac-organizer-search', (e) => { this.debouncedOrganizerSearch($(e.target)); }); // Form validation on change $(document).on('change input', '.hvac-event-form input, .hvac-event-form select, .hvac-event-form textarea', (e) => { this.debouncedFormValidation($(e.target).closest('form')); }); // Auto-save draft $(document).on('change', '.hvac-event-form input, .hvac-event-form select, .hvac-event-form textarea', (e) => { this.debouncedSaveDraft($(e.target).closest('form')); }); // Image upload with progress $(document).on('change', '.hvac-featured-image', (e) => { this.handleImageUpload($(e.target)); }); // Cache management (admin only) $(document).on('click', '.hvac-clear-cache', () => { this.clearCache(); }); $(document).on('click', '.hvac-warm-cache', () => { this.warmCache(); }); } /** * Debounced venue search */ debouncedVenueSearch($input) { this.debounce('venue-search', () => { this.searchVenues($input); }, debounceDelay); } /** * Debounced organizer search */ debouncedOrganizerSearch($input) { this.debounce('organizer-search', () => { this.searchOrganizers($input); }, debounceDelay); } /** * Debounced form validation */ debouncedFormValidation($form) { this.debounce('form-validation', () => { this.validateForm($form); }, debounceDelay); } /** * Debounced draft saving */ debouncedSaveDraft($form) { this.debounce('save-draft', () => { this.saveDraft($form); }, 2000); // 2 second delay for auto-save } /** * Generic debounce function */ debounce(key, func, delay) { if (this.debounceTimers.has(key)) { clearTimeout(this.debounceTimers.get(key)); } const timer = setTimeout(() => { func(); this.debounceTimers.delete(key); }, delay); this.debounceTimers.set(key, timer); } /** * Check rate limits before making requests */ checkRateLimit(action) { if (!rateLimits[action]) { return true; } const now = Date.now(); const windowStart = now - 60000; // 1 minute window const limit = rateLimits[action]; if (!rateLimitTracking.has(action)) { rateLimitTracking.set(action, []); } const requests = rateLimitTracking.get(action); // Remove old requests outside the window const recentRequests = requests.filter(timestamp => timestamp > windowStart); if (recentRequests.length >= limit) { this.showError('Rate limit exceeded. Please slow down your requests.'); return false; } // Add current request recentRequests.push(now); rateLimitTracking.set(action, recentRequests); return true; } /** * Make optimized AJAX request */ async makeRequest(action, data, options = {}) { const { method = 'GET', useCache = cacheEnabled, showLoader = true, timeout = 10000 } = options; // Check rate limits if (!this.checkRateLimit(action)) { return Promise.reject(new Error('Rate limit exceeded')); } // Generate cache key const cacheKey = this.generateCacheKey(action, data); // Check cache first for GET requests if (method === 'GET' && useCache && cache.has(cacheKey)) { const cachedData = cache.get(cacheKey); if (Date.now() - cachedData.timestamp < 300000) { // 5 minute cache return Promise.resolve(cachedData.data); } else { cache.delete(cacheKey); } } // Cancel any pending identical request if (this.pendingRequests.has(cacheKey)) { this.pendingRequests.get(cacheKey).abort(); } // Show loader if (showLoader) { this.showLoader(action); } // Prepare request data const requestData = { action: action, nonce: nonce, ...data }; // Create AJAX request const xhr = $.ajax({ url: ajaxUrl, type: method, data: requestData, timeout: timeout, dataType: 'json' }); // Track pending request this.pendingRequests.set(cacheKey, xhr); try { const response = await xhr; // Remove from pending requests this.pendingRequests.delete(cacheKey); // Hide loader if (showLoader) { this.hideLoader(action); } if (response.success) { // Cache successful GET responses if (method === 'GET' && useCache) { cache.set(cacheKey, { data: response, timestamp: Date.now() }); } return response.data; } else { throw new Error(response.data?.message || 'Request failed'); } } catch (error) { // Remove from pending requests this.pendingRequests.delete(cacheKey); // Hide loader if (showLoader) { this.hideLoader(action); } if (error.statusText !== 'abort') { this.showError(error.message || 'Request failed'); } throw error; } } /** * Generate cache key from action and data */ generateCacheKey(action, data) { return `${action}_${JSON.stringify(data)}`; } /** * Search venues with AJAX */ async searchVenues($input) { const query = $input.val().trim(); if (query.length < 2) { this.hideVenueResults(); return; } try { const data = await this.makeRequest('hvac_search_venues', { q: query }); this.displayVenueResults(data.venues, $input); } catch (error) { console.error('Venue search failed:', error); } } /** * Search organizers with AJAX */ async searchOrganizers($input) { const query = $input.val().trim(); if (query.length < 2) { this.hideOrganizerResults(); return; } try { const data = await this.makeRequest('hvac_search_organizers', { q: query }); this.displayOrganizerResults(data.organizers, $input); } catch (error) { console.error('Organizer search failed:', error); } } /** * Validate form with AJAX */ async validateForm($form) { const formData = this.serializeForm($form); try { const data = await this.makeRequest('hvac_validate_form', { form_data: formData }, { method: 'POST', showLoader: false } ); this.displayValidationResults(data, $form); } catch (error) { console.error('Form validation failed:', error); } } /** * Save draft with AJAX */ async saveDraft($form) { const formData = this.serializeForm($form); try { const data = await this.makeRequest('hvac_save_draft', { form_data: formData }, { method: 'POST', showLoader: false, useCache: false } ); this.showSuccess('Draft saved automatically'); // Store draft ID for later use $form.data('draft-id', data.draft_id); } catch (error) { console.error('Draft save failed:', error); } } /** * Handle image upload */ async handleImageUpload($input) { const file = $input[0].files[0]; if (!file) { return; } // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']; if (!allowedTypes.includes(file.type)) { this.showError('Please select a valid image file (JPG, PNG, or WebP)'); $input.val(''); return; } // Validate file size (5MB limit) if (file.size > 5 * 1024 * 1024) { this.showError('Image file must be smaller than 5MB'); $input.val(''); return; } const formData = new FormData(); formData.append('action', 'hvac_upload_image'); formData.append('nonce', nonce); formData.append('image', file); // Show upload progress const $progress = this.showUploadProgress($input); try { const response = await $.ajax({ url: ajaxUrl, type: 'POST', data: formData, processData: false, contentType: false, xhr: () => { const xhr = new window.XMLHttpRequest(); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = (e.loaded / e.total) * 100; $progress.find('.progress-bar').css('width', percentComplete + '%'); } }); return xhr; } }); this.hideUploadProgress($progress); if (response.success) { this.displayUploadedImage(response.data, $input); this.showSuccess('Image uploaded successfully'); } else { throw new Error(response.data?.message || 'Upload failed'); } } catch (error) { this.hideUploadProgress($progress); this.showError(error.message || 'Image upload failed'); $input.val(''); } } /** * Warm cache with frequently used data */ async warmCache() { try { // Load timezone list await this.makeRequest('hvac_get_timezone_list', {}); console.log('Cache warmed successfully'); } catch (error) { console.error('Cache warming failed:', error); } } /** * Clear all caches */ async clearCache() { cache.clear(); rateLimitTracking.clear(); try { await this.makeRequest('hvac_clear_cache', { cache_type: 'all' }, { method: 'POST' }); this.showSuccess('Cache cleared successfully'); } catch (error) { console.error('Cache clear failed:', error); } } /** * Serialize form data to object */ serializeForm($form) { const formData = {}; $form.find('input, select, textarea').each(function() { const $field = $(this); const name = $field.attr('name'); if (name && !$field.is(':disabled')) { if ($field.is(':checkbox')) { if ($field.is(':checked')) { formData[name] = $field.val(); } } else if ($field.is(':radio')) { if ($field.is(':checked')) { formData[name] = $field.val(); } } else { formData[name] = $field.val(); } } }); return formData; } /** * Display venue search results */ displayVenueResults(venues, $input) { let $results = $input.siblings('.venue-results'); if ($results.length === 0) { $results = $('
'); $input.after($results); } if (venues.length === 0) { $results.html('
No venues found
'); return; } const html = venues.map(venue => `
${venue.title}
${venue.address}, ${venue.city}, ${venue.state}
`).join(''); $results.html(html); // Handle venue selection $results.find('.venue-item').on('click', function() { const venueId = $(this).data('venue-id'); const venueName = $(this).find('strong').text(); $input.val(venueName); $input.data('venue-id', venueId); $results.hide(); }); } /** * Hide venue results */ hideVenueResults() { $('.venue-results').hide(); } /** * Display organizer search results */ displayOrganizerResults(organizers, $input) { let $results = $input.siblings('.organizer-results'); if ($results.length === 0) { $results = $('
'); $input.after($results); } if (organizers.length === 0) { $results.html('
No organizers found
'); return; } const html = organizers.map(organizer => `
${organizer.title}
${organizer.email}
`).join(''); $results.html(html); // Handle organizer selection $results.find('.organizer-item').on('click', function() { const organizerId = $(this).data('organizer-id'); const organizerName = $(this).find('strong').text(); $input.val(organizerName); $input.data('organizer-id', organizerId); $results.hide(); }); } /** * Hide organizer results */ hideOrganizerResults() { $('.organizer-results').hide(); } /** * Display validation results */ displayValidationResults(data, $form) { // Clear existing errors $form.find('.field-error').remove(); $form.find('.error').removeClass('error'); if (!data.valid && data.errors) { Object.keys(data.errors).forEach(fieldName => { const $field = $form.find(`[name="${fieldName}"]`); if ($field.length) { $field.addClass('error'); $field.after(`${data.errors[fieldName]}`); } }); } } /** * Show upload progress */ showUploadProgress($input) { const $progress = $('
'); $input.after($progress); return $progress; } /** * Hide upload progress */ hideUploadProgress($progress) { $progress.remove(); } /** * Display uploaded image */ displayUploadedImage(imageData, $input) { let $preview = $input.siblings('.image-preview'); if ($preview.length === 0) { $preview = $('
'); $input.after($preview); } $preview.html(` Uploaded image `); } /** * Show loader */ showLoader(action) { const $loader = $(`
Loading...
`); $('body').append($loader); } /** * Hide loader */ hideLoader(action) { $(`.hvac-loader[data-action="${action}"]`).remove(); } /** * Show success message */ showSuccess(message) { this.showNotification(message, 'success'); } /** * Show error message */ showError(message) { this.showNotification(message, 'error'); } /** * Show notification */ showNotification(message, type) { const $notification = $(`
${message}
`); $('body').append($notification); // Auto-hide after 5 seconds setTimeout(() => { $notification.fadeOut(() => $notification.remove()); }, 5000); // Manual close $notification.find('.close').on('click', () => { $notification.fadeOut(() => $notification.remove()); }); } } // Initialize when DOM is ready $(document).ready(() => { window.hvacAjaxOptimizer = new HVACAjaxOptimizer(); }); // Hide results when clicking outside $(document).on('click', (e) => { if (!$(e.target).closest('.ajax-results').length && !$(e.target).hasClass('hvac-venue-search') && !$(e.target).hasClass('hvac-organizer-search')) { $('.ajax-results').hide(); } }); })(jQuery);