upskill-event-manager/assets/js/hvac-searchable-selectors.js
ben 2353d8a4be feat: implement dynamic searchable selectors and fix AI Assistant description population
## AI Assistant Fixes
- Fix description population for rich text editor by syncing contenteditable div
- AI now properly populates both hidden textarea and visible rich text editor

## Dynamic Searchable Selectors
- Convert organizer field to multi-select with autocomplete (max 3 selections)
- Convert category field to multi-select with role-based permissions
- Convert venue field to single-select with autocomplete and modal creation
- Add comprehensive search, filtering, and selection management

## Advanced Options Toggle
- Fix invisible timezone selector by implementing progressive disclosure
- Add functional "Advanced Options" toggle with proper JavaScript and CSS
- Advanced fields now properly show/hide with smooth animations

## Technical Implementation
- Create reusable HVACSearchableSelector JavaScript class
- Implement comprehensive styling with accessibility features
- Add role-based permissions (trainers vs master trainers)
- Include responsive design and high contrast mode support

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 15:09:25 -03:00

304 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 = '') {
const params = new URLSearchParams({
action: `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);