upskill-event-manager/assets/js/hvac-searchable-selectors.js
ben 00f88070b8 fix: resolve trainer event creation page issues and implement modal forms
- 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>
2025-09-26 16:07:56 -03:00

311 lines
No EOL
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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);