- Fix AI Assistant timeout issue (frontend: 35s → 50s) - Fix AJAX action name mismatch for categories (categorys → categories) - Fix nonce mismatch (hvac_general_nonce → hvac_ajax_nonce) - Add modal forms for creating new organizers, categories, and venues - Add comprehensive AJAX endpoints with security validation - Implement role-based permissions for category creation - Fix searchable selectors action mapping 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
311 lines
No EOL
10 KiB
JavaScript
311 lines
No EOL
10 KiB
JavaScript
/**
|
||
* HVAC Searchable Selectors
|
||
*
|
||
* Handles dynamic multi-select organizers, categories, and single-select venue
|
||
* with autocomplete search, "Add New" modal integration, and role-based permissions.
|
||
*/
|
||
|
||
(function($) {
|
||
'use strict';
|
||
|
||
class HVACSearchableSelector {
|
||
constructor(element) {
|
||
this.$element = $(element);
|
||
this.type = this.$element.data('type');
|
||
this.maxSelections = this.$element.data('max-selections') || 1;
|
||
this.selectedItems = [];
|
||
|
||
this.init();
|
||
}
|
||
|
||
init() {
|
||
this.bindEvents();
|
||
this.loadInitialData();
|
||
}
|
||
|
||
bindEvents() {
|
||
const $input = this.$element.find('.selector-search-input');
|
||
const $dropdown = this.$element.find('.selector-dropdown');
|
||
|
||
// Input focus/blur events
|
||
$input.on('focus', () => this.showDropdown());
|
||
$input.on('blur', (e) => {
|
||
// Delay hiding to allow clicking on dropdown items
|
||
setTimeout(() => {
|
||
if (!this.$element.find(':hover').length) {
|
||
this.hideDropdown();
|
||
}
|
||
}, 150);
|
||
});
|
||
|
||
// Search input
|
||
$input.on('input', (e) => this.handleSearch(e.target.value));
|
||
|
||
// Arrow click
|
||
this.$element.find('.selector-arrow').on('click', () => {
|
||
if ($dropdown.is(':visible')) {
|
||
this.hideDropdown();
|
||
} else {
|
||
$input.focus();
|
||
}
|
||
});
|
||
|
||
// Create new button
|
||
this.$element.find('.create-new-btn').on('click', (e) => {
|
||
e.preventDefault();
|
||
this.showCreateModal();
|
||
});
|
||
|
||
// Document click to close dropdown
|
||
$(document).on('click', (e) => {
|
||
if (!this.$element.has(e.target).length) {
|
||
this.hideDropdown();
|
||
}
|
||
});
|
||
}
|
||
|
||
async loadInitialData() {
|
||
try {
|
||
this.showLoading();
|
||
const data = await this.fetchData();
|
||
this.renderDropdownItems(data);
|
||
} catch (error) {
|
||
console.error(`Error loading ${this.type} data:`, error);
|
||
this.showError('Failed to load data');
|
||
} finally {
|
||
this.hideLoading();
|
||
}
|
||
}
|
||
|
||
async handleSearch(query) {
|
||
if (query.length < 2) {
|
||
await this.loadInitialData();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
this.showLoading();
|
||
const data = await this.fetchData(query);
|
||
this.renderDropdownItems(data);
|
||
} catch (error) {
|
||
console.error(`Error searching ${this.type}:`, error);
|
||
this.showError('Search failed');
|
||
} finally {
|
||
this.hideLoading();
|
||
}
|
||
}
|
||
|
||
async fetchData(search = '') {
|
||
// Map types to correct action names
|
||
const actionMap = {
|
||
'organizer': 'hvac_search_organizers',
|
||
'category': 'hvac_search_categories',
|
||
'venue': 'hvac_search_venues'
|
||
};
|
||
|
||
const params = new URLSearchParams({
|
||
action: actionMap[this.type] || `hvac_search_${this.type}s`,
|
||
nonce: hvacSelectors.nonce,
|
||
search: search
|
||
});
|
||
|
||
const response = await fetch(hvacSelectors.ajaxUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: params
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
if (!result.success) {
|
||
throw new Error(result.data || 'Request failed');
|
||
}
|
||
|
||
return result.data;
|
||
}
|
||
|
||
renderDropdownItems(items) {
|
||
const $container = this.$element.find('.dropdown-items');
|
||
$container.empty();
|
||
|
||
if (!items || items.length === 0) {
|
||
this.showNoResults();
|
||
return;
|
||
}
|
||
|
||
this.hideNoResults();
|
||
|
||
items.forEach(item => {
|
||
const isSelected = this.selectedItems.some(selected => selected.id === item.id);
|
||
const $item = $(`
|
||
<div class="dropdown-item ${isSelected ? 'selected' : ''}" data-id="${item.id}">
|
||
<div class="item-content">
|
||
<div class="item-title">${this.escapeHtml(item.title)}</div>
|
||
${item.subtitle ? `<div class="item-subtitle">${this.escapeHtml(item.subtitle)}</div>` : ''}
|
||
</div>
|
||
${isSelected ? '<span class="item-selected">✓</span>' : ''}
|
||
</div>
|
||
`);
|
||
|
||
$item.on('click', () => this.selectItem(item));
|
||
$container.append($item);
|
||
});
|
||
}
|
||
|
||
selectItem(item) {
|
||
// Check if already selected
|
||
if (this.selectedItems.some(selected => selected.id === item.id)) {
|
||
return;
|
||
}
|
||
|
||
// Check selection limit
|
||
if (this.selectedItems.length >= this.maxSelections) {
|
||
alert(`You can only select up to ${this.maxSelections} ${this.type}(s).`);
|
||
return;
|
||
}
|
||
|
||
// Add to selected items
|
||
this.selectedItems.push(item);
|
||
this.renderSelectedItems();
|
||
this.updateHiddenInputs();
|
||
this.hideDropdown();
|
||
this.clearSearch();
|
||
|
||
// Mark item as selected in dropdown
|
||
this.$element.find(`.dropdown-item[data-id="${item.id}"]`).addClass('selected').append('<span class="item-selected">✓</span>');
|
||
}
|
||
|
||
removeItem(itemId) {
|
||
this.selectedItems = this.selectedItems.filter(item => item.id !== itemId);
|
||
this.renderSelectedItems();
|
||
this.updateHiddenInputs();
|
||
|
||
// Unmark item in dropdown
|
||
this.$element.find(`.dropdown-item[data-id="${itemId}"]`).removeClass('selected').find('.item-selected').remove();
|
||
}
|
||
|
||
renderSelectedItems() {
|
||
const $container = this.$element.find('.selected-items');
|
||
$container.empty();
|
||
|
||
this.selectedItems.forEach(item => {
|
||
const $selectedItem = $(`
|
||
<div class="selected-item" data-id="${item.id}">
|
||
<span class="selected-item-text">${this.escapeHtml(item.title)}</span>
|
||
<button type="button" class="remove-item" title="Remove">×</button>
|
||
</div>
|
||
`);
|
||
|
||
$selectedItem.find('.remove-item').on('click', () => this.removeItem(item.id));
|
||
$container.append($selectedItem);
|
||
});
|
||
}
|
||
|
||
updateHiddenInputs() {
|
||
const $container = this.$element.find('.hidden-inputs');
|
||
$container.empty();
|
||
|
||
this.selectedItems.forEach((item, index) => {
|
||
const $input = $(`<input type="hidden" name="${this.type}_ids[]" value="${item.id}">`);
|
||
$container.append($input);
|
||
});
|
||
}
|
||
|
||
showDropdown() {
|
||
this.$element.find('.selector-dropdown').show();
|
||
this.$element.addClass('dropdown-open');
|
||
}
|
||
|
||
hideDropdown() {
|
||
this.$element.find('.selector-dropdown').hide();
|
||
this.$element.removeClass('dropdown-open');
|
||
}
|
||
|
||
clearSearch() {
|
||
this.$element.find('.selector-search-input').val('');
|
||
}
|
||
|
||
showLoading() {
|
||
this.$element.find('.loading-spinner').show();
|
||
this.$element.find('.dropdown-items, .no-results').hide();
|
||
}
|
||
|
||
hideLoading() {
|
||
this.$element.find('.loading-spinner').hide();
|
||
this.$element.find('.dropdown-items').show();
|
||
}
|
||
|
||
showNoResults() {
|
||
this.$element.find('.no-results').show();
|
||
this.$element.find('.dropdown-items').hide();
|
||
}
|
||
|
||
hideNoResults() {
|
||
this.$element.find('.no-results').hide();
|
||
}
|
||
|
||
showError(message) {
|
||
this.$element.find('.no-results').text(message).show();
|
||
}
|
||
|
||
showCreateModal() {
|
||
// Check permissions
|
||
if (!this.$element.find('.create-new-btn').length) {
|
||
return;
|
||
}
|
||
|
||
// Trigger create modal event
|
||
$(document).trigger('hvac:create-new-modal', {
|
||
type: this.type,
|
||
callback: (newItem) => {
|
||
if (newItem) {
|
||
this.selectItem(newItem);
|
||
this.loadInitialData(); // Refresh the list
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|
||
|
||
// Advanced Options Toggle Function
|
||
window.hvacToggleAdvancedOptions = function() {
|
||
const $toggle = $('.toggle-advanced-options');
|
||
const $icon = $toggle.find('.toggle-icon');
|
||
const $text = $toggle.find('.toggle-text');
|
||
const $advancedFields = $('.advanced-field');
|
||
|
||
if ($advancedFields.is(':visible')) {
|
||
// Hide advanced fields
|
||
$advancedFields.slideUp(300);
|
||
$icon.removeClass('dashicons-arrow-up-alt2').addClass('dashicons-arrow-down-alt2');
|
||
$text.text('Show Advanced Options');
|
||
} else {
|
||
// Show advanced fields
|
||
$advancedFields.slideDown(300);
|
||
$icon.removeClass('dashicons-arrow-down-alt2').addClass('dashicons-arrow-up-alt2');
|
||
$text.text('Hide Advanced Options');
|
||
}
|
||
};
|
||
|
||
// Initialize searchable selectors when document is ready
|
||
$(document).ready(function() {
|
||
$('.hvac-searchable-selector').each(function() {
|
||
new HVACSearchableSelector(this);
|
||
});
|
||
|
||
// Hide advanced fields by default
|
||
$('.advanced-field').hide();
|
||
});
|
||
|
||
})(jQuery); |