Replace single trainer list with a tabbed sidebar interface: - Three tabs with dynamic counts for each category - Venue cards with icon, name, location, and upcoming events count - Event cards with date badge, title, venue, and cost - Visibility toggles moved from map overlay to sidebar header - Context-aware search placeholder based on active tab - Client-side filtering for instant search results - Info button and modal with usage instructions and map legend - Keyboard navigation for tabs (arrow keys) - All lists sync with map viewport on pan/zoom Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
594 lines
21 KiB
JavaScript
594 lines
21 KiB
JavaScript
/**
|
|
* Find Training Filters
|
|
*
|
|
* Handles filtering, searching, and geolocation functionality
|
|
* for the Find Training page.
|
|
*
|
|
* @package HVAC_Community_Events
|
|
* @since 2.2.0
|
|
*/
|
|
|
|
(function($) {
|
|
'use strict';
|
|
|
|
// Namespace for filters module
|
|
window.HVACTrainingFilters = {
|
|
|
|
// Debounce timer for search
|
|
searchTimer: null,
|
|
|
|
// Current AJAX request (for aborting)
|
|
currentRequest: null,
|
|
|
|
// Active filters
|
|
activeFilters: {
|
|
state: '',
|
|
certification: '',
|
|
training_format: '',
|
|
search: '',
|
|
include_past: false
|
|
},
|
|
|
|
// User location (if obtained)
|
|
userLocation: null,
|
|
|
|
/**
|
|
* Initialize filters
|
|
*/
|
|
init: function() {
|
|
this.bindEvents();
|
|
this.initMobileFilterToggle();
|
|
},
|
|
|
|
/**
|
|
* Bind event handlers
|
|
*/
|
|
bindEvents: function() {
|
|
const self = this;
|
|
|
|
// Search input with debounce - client-side filtering for instant results
|
|
$('#hvac-training-search').on('input', function() {
|
|
clearTimeout(self.searchTimer);
|
|
const value = $(this).val().toLowerCase().trim();
|
|
|
|
self.searchTimer = setTimeout(function() {
|
|
self.activeFilters.search = value;
|
|
|
|
// For empty search or server-side filters, use AJAX
|
|
if (!value || self.hasActiveServerFilters()) {
|
|
self.applyFilters();
|
|
} else {
|
|
// Client-side filtering for instant results
|
|
self.filterActiveTabList(value);
|
|
}
|
|
}, 150); // Faster for client-side
|
|
});
|
|
|
|
// State filter
|
|
$('#hvac-filter-state').on('change', function() {
|
|
self.activeFilters.state = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Certification filter
|
|
$('#hvac-filter-certification').on('change', function() {
|
|
self.activeFilters.certification = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Training format filter
|
|
$('#hvac-filter-format').on('change', function() {
|
|
self.activeFilters.training_format = $(this).val();
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Include past events checkbox
|
|
$('#hvac-include-past').on('change', function() {
|
|
self.activeFilters.include_past = $(this).is(':checked');
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Mobile include past events checkbox
|
|
$('#hvac-include-past-mobile').on('change', function() {
|
|
const checked = $(this).is(':checked');
|
|
$('#hvac-include-past').prop('checked', checked);
|
|
self.activeFilters.include_past = checked;
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Near Me button
|
|
$('#hvac-near-me-btn').on('click', function() {
|
|
self.handleNearMeClick($(this));
|
|
});
|
|
|
|
// Clear all filters
|
|
$('.hvac-clear-filters').on('click', function() {
|
|
self.clearAllFilters();
|
|
});
|
|
|
|
// Remove individual filter
|
|
$(document).on('click', '.hvac-active-filter button', function() {
|
|
const filterType = $(this).parent().data('filter');
|
|
self.removeFilter(filterType);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Apply current filters
|
|
*/
|
|
applyFilters: function() {
|
|
const self = this;
|
|
|
|
// Abort any pending request to prevent race conditions
|
|
if (this.currentRequest && this.currentRequest.readyState !== 4) {
|
|
this.currentRequest.abort();
|
|
}
|
|
|
|
// Build filter data
|
|
const filterData = {
|
|
action: 'hvac_filter_training_map',
|
|
nonce: hvacFindTraining.nonce,
|
|
state: this.activeFilters.state,
|
|
certification: this.activeFilters.certification,
|
|
training_format: this.activeFilters.training_format,
|
|
search: this.activeFilters.search,
|
|
show_trainers: $('#hvac-show-trainers').is(':checked'),
|
|
show_venues: $('#hvac-show-venues').is(':checked'),
|
|
show_events: $('#hvac-show-events').is(':checked'),
|
|
include_past: this.activeFilters.include_past
|
|
};
|
|
|
|
// Add user location if available
|
|
if (this.userLocation) {
|
|
filterData.lat = this.userLocation.lat;
|
|
filterData.lng = this.userLocation.lng;
|
|
filterData.radius = 100; // km
|
|
}
|
|
|
|
// Send filter request and store reference
|
|
this.currentRequest = $.ajax({
|
|
url: hvacFindTraining.ajax_url,
|
|
type: 'POST',
|
|
data: filterData,
|
|
success: function(response) {
|
|
if (response.success) {
|
|
// Update map data
|
|
HVACTrainingMap.trainers = response.data.trainers || [];
|
|
HVACTrainingMap.venues = response.data.venues || [];
|
|
HVACTrainingMap.events = response.data.events || [];
|
|
|
|
// Reset visible arrays to all items
|
|
HVACTrainingMap.visibleTrainers = HVACTrainingMap.trainers.slice();
|
|
HVACTrainingMap.visibleVenues = HVACTrainingMap.venues.slice();
|
|
HVACTrainingMap.visibleEvents = HVACTrainingMap.events.slice();
|
|
|
|
// Reset displayed counts
|
|
HVACTrainingMap.displayedCounts = { trainers: 0, venues: 0, events: 0 };
|
|
|
|
// Update map markers
|
|
HVACTrainingMap.updateMarkers();
|
|
|
|
// Update all counts
|
|
HVACTrainingMap.updateAllCounts();
|
|
|
|
// Render the active tab
|
|
HVACTrainingMap.renderActiveTabList();
|
|
|
|
// Note: syncSidebarWithViewport will be called by map 'idle' event
|
|
}
|
|
},
|
|
complete: function() {
|
|
self.currentRequest = null;
|
|
}
|
|
});
|
|
|
|
// Show/hide clear button
|
|
this.updateClearButtonVisibility();
|
|
},
|
|
|
|
/**
|
|
* Handle Near Me button click
|
|
*/
|
|
handleNearMeClick: function($button) {
|
|
const self = this;
|
|
|
|
// Show loading state
|
|
$button.prop('disabled', true);
|
|
$button.html('<span class="dashicons dashicons-update-alt hvac-spin"></span> Locating...');
|
|
|
|
// Clear any previous error message
|
|
this.clearLocationError();
|
|
|
|
// Get user location
|
|
HVACTrainingMap.getUserLocation(function(location, error) {
|
|
if (location) {
|
|
self.userLocation = location;
|
|
|
|
// Center map on user location
|
|
HVACTrainingMap.centerOnLocation(location.lat, location.lng, 9);
|
|
|
|
// Apply filters with location
|
|
self.applyFilters();
|
|
|
|
// Update button state
|
|
$button.html('<span class="dashicons dashicons-yes-alt"></span> Near Me');
|
|
$button.addClass('active');
|
|
|
|
// Add to active filters display
|
|
self.addActiveFilter('location', 'Near Me');
|
|
} else {
|
|
// Show inline error instead of alert
|
|
self.showLocationError(error || 'Unable to get your location. Please check browser permissions.');
|
|
|
|
// Reset button
|
|
$button.html('<span class="dashicons dashicons-location-alt"></span> Near Me');
|
|
$button.prop('disabled', false);
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Show location error message inline
|
|
*/
|
|
showLocationError: function(message) {
|
|
// Remove any existing error
|
|
this.clearLocationError();
|
|
|
|
// Create error element
|
|
const $error = $('<div class="hvac-location-error">' +
|
|
'<span class="dashicons dashicons-warning"></span> ' +
|
|
this.escapeHtml(message) +
|
|
'<button type="button" class="hvac-dismiss-error" aria-label="Dismiss">×</button>' +
|
|
'</div>');
|
|
|
|
// Insert after Near Me button
|
|
$('#hvac-near-me-btn').after($error);
|
|
|
|
// Auto-dismiss after 5 seconds
|
|
setTimeout(function() {
|
|
$error.fadeOut(300, function() { $(this).remove(); });
|
|
}, 5000);
|
|
|
|
// Click to dismiss
|
|
$error.find('.hvac-dismiss-error').on('click', function() {
|
|
$error.remove();
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Clear location error message
|
|
*/
|
|
clearLocationError: function() {
|
|
$('.hvac-location-error').remove();
|
|
},
|
|
|
|
/**
|
|
* Clear all filters
|
|
*/
|
|
clearAllFilters: function() {
|
|
// Reset filter values
|
|
this.activeFilters = {
|
|
state: '',
|
|
certification: '',
|
|
training_format: '',
|
|
search: '',
|
|
include_past: false
|
|
};
|
|
|
|
// Reset user location
|
|
this.userLocation = null;
|
|
|
|
// Reset form elements
|
|
$('#hvac-filter-state').val('');
|
|
$('#hvac-filter-certification').val('');
|
|
$('#hvac-filter-format').val('');
|
|
$('#hvac-training-search').val('');
|
|
$('#hvac-include-past').prop('checked', false);
|
|
$('#hvac-include-past-mobile').prop('checked', false);
|
|
|
|
// Reset Near Me button
|
|
$('#hvac-near-me-btn')
|
|
.removeClass('active')
|
|
.html('<span class="dashicons dashicons-location-alt"></span> Near Me')
|
|
.prop('disabled', false);
|
|
|
|
// Clear active filters display
|
|
$('.hvac-active-filters').empty();
|
|
|
|
// Hide clear button
|
|
$('.hvac-clear-filters').hide();
|
|
|
|
// Reset map to default view
|
|
HVACTrainingMap.map.setCenter(HVACTrainingMap.config.defaultCenter);
|
|
HVACTrainingMap.map.setZoom(HVACTrainingMap.config.defaultZoom);
|
|
|
|
// Reload data without filters
|
|
HVACTrainingMap.loadMapData();
|
|
},
|
|
|
|
/**
|
|
* Remove a specific filter
|
|
*/
|
|
removeFilter: function(filterType) {
|
|
switch (filterType) {
|
|
case 'state':
|
|
this.activeFilters.state = '';
|
|
$('#hvac-filter-state').val('');
|
|
break;
|
|
case 'certification':
|
|
this.activeFilters.certification = '';
|
|
$('#hvac-filter-certification').val('');
|
|
break;
|
|
case 'training_format':
|
|
this.activeFilters.training_format = '';
|
|
$('#hvac-filter-format').val('');
|
|
break;
|
|
case 'search':
|
|
this.activeFilters.search = '';
|
|
$('#hvac-training-search').val('');
|
|
break;
|
|
case 'location':
|
|
this.userLocation = null;
|
|
$('#hvac-near-me-btn')
|
|
.removeClass('active')
|
|
.html('<span class="dashicons dashicons-location-alt"></span> Near Me')
|
|
.prop('disabled', false);
|
|
break;
|
|
case 'include_past':
|
|
this.activeFilters.include_past = false;
|
|
$('#hvac-include-past').prop('checked', false);
|
|
$('#hvac-include-past-mobile').prop('checked', false);
|
|
break;
|
|
}
|
|
|
|
this.applyFilters();
|
|
this.updateActiveFiltersDisplay();
|
|
},
|
|
|
|
/**
|
|
* Update active filters display
|
|
*/
|
|
updateActiveFiltersDisplay: function() {
|
|
const $container = $('.hvac-active-filters');
|
|
$container.empty();
|
|
|
|
// State filter
|
|
if (this.activeFilters.state) {
|
|
this.addActiveFilter('state', `State: ${this.activeFilters.state}`);
|
|
}
|
|
|
|
// Certification filter
|
|
if (this.activeFilters.certification) {
|
|
this.addActiveFilter('certification', this.activeFilters.certification);
|
|
}
|
|
|
|
// Training format filter
|
|
if (this.activeFilters.training_format) {
|
|
this.addActiveFilter('training_format', this.activeFilters.training_format);
|
|
}
|
|
|
|
// Search filter
|
|
if (this.activeFilters.search) {
|
|
this.addActiveFilter('search', `"${this.activeFilters.search}"`);
|
|
}
|
|
|
|
// Location filter
|
|
if (this.userLocation) {
|
|
this.addActiveFilter('location', 'Near Me');
|
|
}
|
|
|
|
// Include past events filter
|
|
if (this.activeFilters.include_past) {
|
|
this.addActiveFilter('include_past', 'Including Past Events');
|
|
}
|
|
|
|
this.updateClearButtonVisibility();
|
|
},
|
|
|
|
/**
|
|
* Add an active filter chip
|
|
*/
|
|
addActiveFilter: function(type, label) {
|
|
const $container = $('.hvac-active-filters');
|
|
const $chip = $(`
|
|
<span class="hvac-active-filter" data-filter="${type}">
|
|
${this.escapeHtml(label)}
|
|
<button type="button" aria-label="Remove filter">×</button>
|
|
</span>
|
|
`);
|
|
$container.append($chip);
|
|
},
|
|
|
|
/**
|
|
* Update clear button visibility
|
|
*/
|
|
updateClearButtonVisibility: function() {
|
|
const hasFilters = this.activeFilters.state ||
|
|
this.activeFilters.certification ||
|
|
this.activeFilters.training_format ||
|
|
this.activeFilters.search ||
|
|
this.activeFilters.include_past ||
|
|
this.userLocation;
|
|
|
|
if (hasFilters) {
|
|
$('.hvac-clear-filters').show();
|
|
} else {
|
|
$('.hvac-clear-filters').hide();
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Check if any server-side filters are active
|
|
*/
|
|
hasActiveServerFilters: function() {
|
|
return this.activeFilters.state ||
|
|
this.activeFilters.certification ||
|
|
this.activeFilters.training_format ||
|
|
this.activeFilters.include_past ||
|
|
this.userLocation;
|
|
},
|
|
|
|
/**
|
|
* Filter the active tab's list client-side for instant results
|
|
*/
|
|
filterActiveTabList: function(searchTerm) {
|
|
const activeTab = HVACTrainingMap.activeTab;
|
|
let items, filterFn;
|
|
|
|
switch (activeTab) {
|
|
case 'trainers':
|
|
items = HVACTrainingMap.visibleTrainers.length > 0
|
|
? HVACTrainingMap.trainers
|
|
: HVACTrainingMap.trainers;
|
|
filterFn = (trainer) => {
|
|
const searchFields = [
|
|
trainer.name,
|
|
trainer.city,
|
|
trainer.state,
|
|
trainer.company,
|
|
...(trainer.certifications || [])
|
|
].filter(Boolean).join(' ').toLowerCase();
|
|
return searchFields.includes(searchTerm);
|
|
};
|
|
break;
|
|
|
|
case 'venues':
|
|
items = HVACTrainingMap.venues;
|
|
filterFn = (venue) => {
|
|
const searchFields = [
|
|
venue.name,
|
|
venue.city,
|
|
venue.state,
|
|
venue.address
|
|
].filter(Boolean).join(' ').toLowerCase();
|
|
return searchFields.includes(searchTerm);
|
|
};
|
|
break;
|
|
|
|
case 'events':
|
|
items = HVACTrainingMap.events;
|
|
filterFn = (event) => {
|
|
const searchFields = [
|
|
event.title,
|
|
event.venue_name,
|
|
event.venue_city,
|
|
event.venue_state
|
|
].filter(Boolean).join(' ').toLowerCase();
|
|
return searchFields.includes(searchTerm);
|
|
};
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
// Apply filter
|
|
const filteredItems = searchTerm ? items.filter(filterFn) : items;
|
|
|
|
// Update the visible items for the active tab
|
|
switch (activeTab) {
|
|
case 'trainers':
|
|
HVACTrainingMap.visibleTrainers = filteredItems;
|
|
HVACTrainingMap.displayedCounts.trainers = 0;
|
|
HVACTrainingMap.updateTrainerGrid();
|
|
break;
|
|
case 'venues':
|
|
HVACTrainingMap.visibleVenues = filteredItems;
|
|
HVACTrainingMap.displayedCounts.venues = 0;
|
|
HVACTrainingMap.updateVenueGrid();
|
|
break;
|
|
case 'events':
|
|
HVACTrainingMap.visibleEvents = filteredItems;
|
|
HVACTrainingMap.displayedCounts.events = 0;
|
|
HVACTrainingMap.updateEventGrid();
|
|
break;
|
|
}
|
|
|
|
// Update all counts
|
|
HVACTrainingMap.updateAllCounts();
|
|
},
|
|
|
|
/**
|
|
* Escape HTML for safe output
|
|
*/
|
|
escapeHtml: function(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
},
|
|
|
|
/**
|
|
* Initialize mobile filter toggle
|
|
*/
|
|
initMobileFilterToggle: function() {
|
|
const self = this;
|
|
|
|
// Mobile filter panel toggle
|
|
$(document).on('click', '.hvac-mobile-filter-toggle', function() {
|
|
const $toggle = $(this);
|
|
const $panel = $('#hvac-mobile-filter-panel');
|
|
const isExpanded = $toggle.attr('aria-expanded') === 'true';
|
|
|
|
if (isExpanded) {
|
|
$panel.attr('hidden', '');
|
|
$toggle.attr('aria-expanded', 'false');
|
|
} else {
|
|
$panel.removeAttr('hidden');
|
|
$toggle.attr('aria-expanded', 'true');
|
|
}
|
|
});
|
|
|
|
// Sync mobile filter selects with desktop selects
|
|
$('#hvac-filter-state-mobile').on('change', function() {
|
|
const value = $(this).val();
|
|
$('#hvac-filter-state').val(value);
|
|
self.activeFilters.state = value;
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
$('#hvac-filter-certification-mobile').on('change', function() {
|
|
const value = $(this).val();
|
|
$('#hvac-filter-certification').val(value);
|
|
self.activeFilters.certification = value;
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
$('#hvac-filter-format-mobile').on('change', function() {
|
|
const value = $(this).val();
|
|
$('#hvac-filter-format').val(value);
|
|
self.activeFilters.training_format = value;
|
|
self.applyFilters();
|
|
self.updateActiveFiltersDisplay();
|
|
});
|
|
|
|
// Also sync desktop to mobile when desktop changes
|
|
$('#hvac-filter-state').on('change', function() {
|
|
$('#hvac-filter-state-mobile').val($(this).val());
|
|
});
|
|
|
|
$('#hvac-filter-certification').on('change', function() {
|
|
$('#hvac-filter-certification-mobile').val($(this).val());
|
|
});
|
|
|
|
$('#hvac-filter-format').on('change', function() {
|
|
$('#hvac-filter-format-mobile').val($(this).val());
|
|
});
|
|
}
|
|
};
|
|
|
|
// Initialize when document is ready
|
|
$(document).ready(function() {
|
|
if ($('#hvac-training-map').length) {
|
|
HVACTrainingFilters.init();
|
|
}
|
|
});
|
|
|
|
})(jQuery);
|