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>
This commit is contained in:
parent
6039be6fb9
commit
2353d8a4be
4 changed files with 1614 additions and 68 deletions
407
assets/css/hvac-searchable-selectors.css
Normal file
407
assets/css/hvac-searchable-selectors.css
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
/**
|
||||
* HVAC Searchable Selectors Styling
|
||||
*
|
||||
* Styles for dynamic multi-select organizers, categories, and single-select venue
|
||||
* with autocomplete search and modal integration.
|
||||
*/
|
||||
|
||||
/* Main selector container */
|
||||
.hvac-searchable-selector {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Input wrapper with arrow */
|
||||
.selector-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
|
||||
.hvac-searchable-selector.dropdown-open .selector-input-wrapper {
|
||||
border-color: #0274be;
|
||||
box-shadow: 0 0 0 3px rgba(2, 116, 190, 0.1);
|
||||
}
|
||||
|
||||
.selector-search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.selector-search-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.selector-arrow {
|
||||
padding: 0 12px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.hvac-searchable-selector.dropdown-open .selector-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Selected items display */
|
||||
.selected-items {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: #0274be;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.selected-item-text {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.remove-item {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.remove-item:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* Dropdown */
|
||||
.selector-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: white;
|
||||
border: 2px solid #0274be;
|
||||
border-top: none;
|
||||
border-radius: 0 0 4px 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading-spinner {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* No results message */
|
||||
.no-results {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Dropdown items */
|
||||
.dropdown-items {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.dropdown-item.selected {
|
||||
background-color: #e6f3fb;
|
||||
color: #0274be;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.item-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.item-selected {
|
||||
color: #0274be;
|
||||
font-weight: bold;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Create new section */
|
||||
.create-new-section {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.create-new-btn {
|
||||
width: 100%;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
color: #0274be;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.create-new-btn:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #0274be;
|
||||
}
|
||||
|
||||
.create-new-btn .dashicons {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.create-new-disabled {
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
/* Hidden inputs container */
|
||||
.hidden-inputs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.selector-search-input {
|
||||
font-size: 16px; /* Prevent zoom on iOS */
|
||||
}
|
||||
|
||||
.selected-items {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
font-size: 13px;
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus states for accessibility */
|
||||
.selector-search-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown-item:focus {
|
||||
outline: 2px solid #0274be;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.create-new-btn:focus {
|
||||
outline: 2px solid #0274be;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.remove-item:focus {
|
||||
outline: 2px solid white;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* Error states */
|
||||
.hvac-searchable-selector.error .selector-input-wrapper {
|
||||
border-color: #d63638;
|
||||
}
|
||||
|
||||
.hvac-searchable-selector.error .selector-input-wrapper:focus-within {
|
||||
box-shadow: 0 0 0 3px rgba(214, 54, 56, 0.1);
|
||||
}
|
||||
|
||||
/* Disabled state */
|
||||
.hvac-searchable-selector.disabled {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Animation for dropdown */
|
||||
.selector-dropdown {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.hvac-searchable-selector.dropdown-open .selector-dropdown {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* High contrast mode support */
|
||||
@media (prefers-contrast: high) {
|
||||
.selector-input-wrapper {
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-bottom-width: 2px;
|
||||
}
|
||||
|
||||
.selected-item {
|
||||
border: 2px solid white;
|
||||
}
|
||||
}
|
||||
|
||||
/* Advanced Options Toggle */
|
||||
.hvac-advanced-options-toggle {
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toggle-advanced-options {
|
||||
background: #f8f9fa;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-advanced-options:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #0274be;
|
||||
color: #0274be;
|
||||
}
|
||||
|
||||
.toggle-advanced-options .dashicons {
|
||||
font-size: 16px;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toggle-description {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Advanced fields */
|
||||
.advanced-field {
|
||||
border-left: 4px solid #0274be;
|
||||
padding-left: 16px;
|
||||
margin-left: 8px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 0 4px 4px 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.advanced-field::before {
|
||||
content: "Advanced";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: #0274be;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Override advanced field styling for better integration */
|
||||
.advanced-field .form-row {
|
||||
background: transparent;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Focus states for advanced options toggle */
|
||||
.toggle-advanced-options:focus {
|
||||
outline: 2px solid #0274be;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for advanced options */
|
||||
@media (max-width: 768px) {
|
||||
.toggle-advanced-options {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.advanced-field {
|
||||
margin-left: 0;
|
||||
border-left-width: 3px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.advanced-field::before {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
722
assets/js/hvac-ai-assist.js
Normal file
722
assets/js/hvac-ai-assist.js
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
/**
|
||||
* HVAC AI Assist JavaScript
|
||||
*
|
||||
* Handles AI-powered event population modal interface and form integration
|
||||
*
|
||||
* @package HVAC_Community_Events
|
||||
* @since 3.2.0
|
||||
*/
|
||||
|
||||
jQuery(document).ready(function($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* AI Assist functionality object
|
||||
*/
|
||||
const HVACAIAssist = {
|
||||
// Properties
|
||||
modal: null,
|
||||
isProcessing: false,
|
||||
currentInput: '',
|
||||
currentInputType: 'auto',
|
||||
|
||||
// Initialize
|
||||
init: function() {
|
||||
this.createModal();
|
||||
this.bindEvents();
|
||||
this.enableAIButton();
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the AI modal interface
|
||||
*/
|
||||
createModal: function() {
|
||||
const modalHTML = `
|
||||
<div class="hvac-ai-modal" id="hvac-ai-modal" style="display: none;">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3><i class="dashicons dashicons-superhero-alt"></i> AI Event Assistant</h3>
|
||||
<button type="button" class="modal-close" id="ai-modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="ai-intro">
|
||||
<p>Let AI help you create your event! Provide any of the following:</p>
|
||||
<ul>
|
||||
<li><strong>URL:</strong> EventBrite, Facebook Events, or any event webpage</li>
|
||||
<li><strong>Text:</strong> Copy/paste from emails, documents, or flyers</li>
|
||||
<li><strong>Description:</strong> Brief description with key event details</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="ai-input-section">
|
||||
<div class="input-type-tabs">
|
||||
<button type="button" class="tab-btn active" data-type="auto">
|
||||
<i class="dashicons dashicons-admin-generic"></i> Auto-Detect
|
||||
</button>
|
||||
<button type="button" class="tab-btn" data-type="url">
|
||||
<i class="dashicons dashicons-admin-links"></i> URL
|
||||
</button>
|
||||
<button type="button" class="tab-btn" data-type="text">
|
||||
<i class="dashicons dashicons-media-document"></i> Text
|
||||
</button>
|
||||
<button type="button" class="tab-btn" data-type="description">
|
||||
<i class="dashicons dashicons-edit"></i> Description
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="input-area">
|
||||
<div class="input-tab-content active" data-type="auto">
|
||||
<label for="ai-input-auto">Paste URL, text, or type a description:</label>
|
||||
<textarea id="ai-input-auto" placeholder="Paste an EventBrite URL, copy/paste event details, or describe the event you want to create..." rows="6"></textarea>
|
||||
</div>
|
||||
<div class="input-tab-content" data-type="url">
|
||||
<label for="ai-input-url">Event webpage URL:</label>
|
||||
<input type="url" id="ai-input-url" placeholder="https://www.eventbrite.com/e/..." />
|
||||
<p class="input-help">Works with EventBrite, Facebook Events, Meetup, and most event websites</p>
|
||||
</div>
|
||||
<div class="input-tab-content" data-type="text">
|
||||
<label for="ai-input-text">Copy/paste event information:</label>
|
||||
<textarea id="ai-input-text" placeholder="Paste text from emails, flyers, documents..." rows="6"></textarea>
|
||||
<p class="input-help">AI will extract event details from any formatted or unformatted text</p>
|
||||
</div>
|
||||
<div class="input-tab-content" data-type="description">
|
||||
<label for="ai-input-description">Describe your event:</label>
|
||||
<textarea id="ai-input-description" placeholder="Example: HVAC troubleshooting workshop on March 15th at Johnson Community Center, 2-4 PM, $50 per person..." rows="4"></textarea>
|
||||
<p class="input-help">Provide as many details as you can - dates, times, location, cost, etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-processing" id="ai-processing" style="display: none;">
|
||||
<div class="processing-spinner">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
<div class="processing-status">
|
||||
<p class="status-message">Analyzing your input...</p>
|
||||
<div class="progress-steps">
|
||||
<div class="step active" data-step="1">Parsing content</div>
|
||||
<div class="step" data-step="2">Extracting details</div>
|
||||
<div class="step" data-step="3">Validating data</div>
|
||||
<div class="step" data-step="4">Preparing form</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-results" id="ai-results" style="display: none;">
|
||||
<div class="results-header">
|
||||
<h4><i class="dashicons dashicons-yes-alt"></i> Event Information Extracted</h4>
|
||||
<div class="confidence-indicator">
|
||||
<span class="confidence-label">Confidence:</span>
|
||||
<div class="confidence-bar">
|
||||
<div class="confidence-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span class="confidence-percent">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="results-content">
|
||||
<div class="result-fields"></div>
|
||||
<div class="result-warnings" style="display: none;">
|
||||
<h5><i class="dashicons dashicons-warning"></i> Please Review</h5>
|
||||
<ul class="warning-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" id="ai-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn-primary" id="ai-process-btn">
|
||||
<i class="dashicons dashicons-superhero-alt"></i> Process with AI
|
||||
</button>
|
||||
<button type="button" class="btn-primary" id="ai-apply-btn" style="display: none;">
|
||||
<i class="dashicons dashicons-yes"></i> Apply to Form
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('body').append(modalHTML);
|
||||
this.modal = $('#hvac-ai-modal');
|
||||
},
|
||||
|
||||
/**
|
||||
* Bind event handlers
|
||||
*/
|
||||
bindEvents: function() {
|
||||
const self = this;
|
||||
|
||||
// AI Assist button click
|
||||
$(document).on('click', '#ai-assist-btn', function(e) {
|
||||
e.preventDefault();
|
||||
if (!$(this).prop('disabled')) {
|
||||
self.openModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal close handlers
|
||||
$(document).on('click', '#ai-modal-close, #ai-modal-cancel, .modal-overlay', function() {
|
||||
self.closeModal();
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
$(document).on('click', '.tab-btn', function() {
|
||||
const type = $(this).data('type');
|
||||
self.switchTab(type);
|
||||
});
|
||||
|
||||
// Process button
|
||||
$(document).on('click', '#ai-process-btn', function() {
|
||||
self.processInput();
|
||||
});
|
||||
|
||||
// Apply button
|
||||
$(document).on('click', '#ai-apply-btn', function() {
|
||||
self.applyToForm();
|
||||
});
|
||||
|
||||
// Input change handlers for validation
|
||||
$(document).on('input', '#ai-input-auto, #ai-input-url, #ai-input-text, #ai-input-description', function() {
|
||||
self.validateInput();
|
||||
});
|
||||
|
||||
// ESC key handler
|
||||
$(document).on('keyup', function(e) {
|
||||
if (e.keyCode === 27 && self.modal.is(':visible')) {
|
||||
self.closeModal();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Enable the AI Assist button (remove placeholder status)
|
||||
*/
|
||||
enableAIButton: function() {
|
||||
const $aiBtn = $('#ai-assist-btn');
|
||||
$aiBtn.removeClass('placeholder-btn')
|
||||
.prop('disabled', false)
|
||||
.attr('title', 'AI-powered event creation assistant')
|
||||
.text('AI Assist');
|
||||
},
|
||||
|
||||
/**
|
||||
* Open the AI modal
|
||||
*/
|
||||
openModal: function() {
|
||||
this.modal.fadeIn(300);
|
||||
this.resetModal();
|
||||
$('#ai-input-auto').focus();
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the AI modal
|
||||
*/
|
||||
closeModal: function() {
|
||||
if (!this.isProcessing) {
|
||||
this.modal.fadeOut(300);
|
||||
this.resetModal();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reset modal to initial state
|
||||
*/
|
||||
resetModal: function() {
|
||||
// Reset tabs
|
||||
$('.tab-btn').removeClass('active');
|
||||
$('.tab-btn[data-type="auto"]').addClass('active');
|
||||
$('.input-tab-content').removeClass('active');
|
||||
$('.input-tab-content[data-type="auto"]').addClass('active');
|
||||
|
||||
// Clear inputs
|
||||
$('#ai-input-auto, #ai-input-url, #ai-input-text, #ai-input-description').val('');
|
||||
|
||||
// Hide sections
|
||||
$('#ai-processing, #ai-results').hide();
|
||||
$('.ai-input-section').show();
|
||||
|
||||
// Reset buttons
|
||||
$('#ai-process-btn').show().prop('disabled', true);
|
||||
$('#ai-apply-btn').hide();
|
||||
|
||||
// Reset progress steps
|
||||
$('.progress-steps .step').removeClass('active');
|
||||
$('.status-message').text('Analyzing your input...');
|
||||
|
||||
// Reset confidence indicator
|
||||
$('.confidence-fill').css('width', '0%');
|
||||
$('.confidence-percent').text('0%');
|
||||
$('.confidence-bar').removeClass('confidence-low confidence-medium confidence-high');
|
||||
|
||||
// Clear results content
|
||||
$('.result-fields').html('');
|
||||
$('.warning-list').html('');
|
||||
$('.result-warnings').hide();
|
||||
|
||||
// Clear stored data
|
||||
this.extractedData = null;
|
||||
|
||||
// Reset properties
|
||||
this.currentInput = '';
|
||||
this.currentInputType = 'auto';
|
||||
this.isProcessing = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch input tabs
|
||||
*/
|
||||
switchTab: function(type) {
|
||||
$('.tab-btn').removeClass('active');
|
||||
$(`.tab-btn[data-type="${type}"]`).addClass('active');
|
||||
|
||||
$('.input-tab-content').removeClass('active');
|
||||
$(`.input-tab-content[data-type="${type}"]`).addClass('active');
|
||||
|
||||
this.currentInputType = type;
|
||||
|
||||
// Focus on the input field
|
||||
$(`#ai-input-${type}`).focus();
|
||||
|
||||
this.validateInput();
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate current input
|
||||
*/
|
||||
validateInput: function() {
|
||||
const input = this.getCurrentInput();
|
||||
const $processBtn = $('#ai-process-btn');
|
||||
|
||||
if (input.length >= 10) {
|
||||
$processBtn.prop('disabled', false);
|
||||
} else {
|
||||
$processBtn.prop('disabled', true);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current input value
|
||||
*/
|
||||
getCurrentInput: function() {
|
||||
const activeTab = $('.input-tab-content.active');
|
||||
const inputElement = activeTab.find('input, textarea');
|
||||
return inputElement.val().trim();
|
||||
},
|
||||
|
||||
/**
|
||||
* Process input through AI
|
||||
*/
|
||||
processInput: function() {
|
||||
const input = this.getCurrentInput();
|
||||
|
||||
if (input.length < 10) {
|
||||
this.showError('Please provide at least 10 characters of event information.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing = true;
|
||||
this.currentInput = input;
|
||||
|
||||
// Show processing UI
|
||||
$('.ai-input-section').hide();
|
||||
$('#ai-processing').show();
|
||||
$('#ai-process-btn').hide();
|
||||
|
||||
// Start progress animation
|
||||
this.animateProgress();
|
||||
|
||||
// Make AJAX request
|
||||
this.makeAIRequest(input, this.currentInputType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Animate processing steps
|
||||
*/
|
||||
animateProgress: function() {
|
||||
const isUrl = this.currentInputType === 'url';
|
||||
const steps = isUrl ? [
|
||||
{ step: 1, message: 'Fetching webpage content...', delay: 500 },
|
||||
{ step: 2, message: 'Processing webpage data (this may take up to 40 seconds)...', delay: 3000 },
|
||||
{ step: 3, message: 'Extracting event details with AI...', delay: 15000 },
|
||||
{ step: 4, message: 'Preparing form data...', delay: 25000 }
|
||||
] : [
|
||||
{ step: 1, message: 'Analyzing your input...', delay: 500 },
|
||||
{ step: 2, message: 'Extracting event details...', delay: 2000 },
|
||||
{ step: 3, message: 'Validating information...', delay: 4000 },
|
||||
{ step: 4, message: 'Preparing form data...', delay: 6000 }
|
||||
];
|
||||
|
||||
steps.forEach(({ step, message, delay }) => {
|
||||
setTimeout(() => {
|
||||
if (this.isProcessing) {
|
||||
$(`.progress-steps .step[data-step="${step}"]`).addClass('active');
|
||||
$('.status-message').text(message);
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Make AJAX request to AI endpoint
|
||||
*/
|
||||
makeAIRequest: function(input, inputType) {
|
||||
const self = this;
|
||||
|
||||
const requestData = {
|
||||
action: 'hvac_ai_populate_event',
|
||||
input: input,
|
||||
input_type: inputType,
|
||||
nonce: hvacAjaxVars.nonce // Assuming nonce is available
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: hvacAjaxVars.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: requestData,
|
||||
timeout: inputType === 'url' ? 60000 : 35000, // 60 seconds for URLs, 35 for text
|
||||
success: function(response) {
|
||||
self.handleAISuccess(response);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
self.handleAIError(xhr, status, error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle successful AI response
|
||||
*/
|
||||
handleAISuccess: function(response) {
|
||||
this.isProcessing = false;
|
||||
|
||||
if (response.success && response.data && response.data.event_data) {
|
||||
this.displayResults(response.data.event_data);
|
||||
} else {
|
||||
const message = response.data && response.data.message
|
||||
? response.data.message
|
||||
: 'Unexpected response format from AI service.';
|
||||
this.showError(message);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle AI request error
|
||||
*/
|
||||
handleAIError: function(xhr, status, error) {
|
||||
this.isProcessing = false;
|
||||
|
||||
let message = 'AI service temporarily unavailable. Please try again later.';
|
||||
|
||||
if (status === 'timeout') {
|
||||
message = 'Request timed out. The AI might be processing a complex input. Please try with simpler content.';
|
||||
} else if (xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message) {
|
||||
message = xhr.responseJSON.data.message;
|
||||
}
|
||||
|
||||
this.showError(message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display AI extraction results
|
||||
*/
|
||||
displayResults: function(eventData) {
|
||||
$('#ai-processing').hide();
|
||||
$('#ai-results').show();
|
||||
|
||||
// Update confidence indicator
|
||||
const confidence = eventData.confidence && eventData.confidence.overall
|
||||
? eventData.confidence.overall
|
||||
: 0;
|
||||
const confidencePercent = Math.round(confidence * 100);
|
||||
|
||||
$('.confidence-fill').css('width', confidencePercent + '%');
|
||||
$('.confidence-percent').text(confidencePercent + '%');
|
||||
|
||||
// Color code confidence
|
||||
let confidenceClass = 'confidence-low';
|
||||
if (confidencePercent >= 80) confidenceClass = 'confidence-high';
|
||||
else if (confidencePercent >= 60) confidenceClass = 'confidence-medium';
|
||||
|
||||
$('.confidence-bar').removeClass('confidence-low confidence-medium confidence-high')
|
||||
.addClass(confidenceClass);
|
||||
|
||||
// Display extracted fields
|
||||
this.displayExtractedFields(eventData);
|
||||
|
||||
// Check for warnings
|
||||
this.checkAndDisplayWarnings(eventData);
|
||||
|
||||
// Show apply button
|
||||
$('#ai-apply-btn').show();
|
||||
|
||||
// Store data for form application
|
||||
this.extractedData = eventData;
|
||||
},
|
||||
|
||||
/**
|
||||
* Display extracted fields summary
|
||||
*/
|
||||
displayExtractedFields: function(eventData) {
|
||||
const fieldsHtml = [];
|
||||
|
||||
// Title
|
||||
if (eventData.title) {
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>Title:</label>
|
||||
<span>${this.escapeHtml(eventData.title)}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
// Description (truncated for display)
|
||||
if (eventData.description) {
|
||||
let descriptionPreview = eventData.description;
|
||||
// Truncate if too long for modal display
|
||||
if (descriptionPreview.length > 200) {
|
||||
descriptionPreview = descriptionPreview.substring(0, 200) + '...';
|
||||
}
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>Description:</label>
|
||||
<span>${this.escapeHtml(descriptionPreview)}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
// Date and time
|
||||
if (eventData.start_date) {
|
||||
let dateDisplay = eventData.start_date;
|
||||
if (eventData.start_time) {
|
||||
dateDisplay += ` at ${eventData.start_time}`;
|
||||
}
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>Start:</label>
|
||||
<span>${this.escapeHtml(dateDisplay)}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
if (eventData.end_date) {
|
||||
let dateDisplay = eventData.end_date;
|
||||
if (eventData.end_time) {
|
||||
dateDisplay += ` at ${eventData.end_time}`;
|
||||
}
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>End:</label>
|
||||
<span>${this.escapeHtml(dateDisplay)}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
// Venue
|
||||
if (eventData.venue_name) {
|
||||
let venueDisplay = eventData.venue_name;
|
||||
if (eventData.venue_address) {
|
||||
venueDisplay += ` (${eventData.venue_address})`;
|
||||
}
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>Venue:</label>
|
||||
<span>${this.escapeHtml(venueDisplay)}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
// Cost
|
||||
if (eventData.cost !== null && eventData.cost !== undefined) {
|
||||
fieldsHtml.push(`<div class="result-field">
|
||||
<label>Cost:</label>
|
||||
<span>$${eventData.cost}</span>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
$('.result-fields').html(fieldsHtml.join(''));
|
||||
},
|
||||
|
||||
/**
|
||||
* Check for and display warnings
|
||||
*/
|
||||
checkAndDisplayWarnings: function(eventData) {
|
||||
const warnings = [];
|
||||
|
||||
// Check confidence levels
|
||||
if (eventData.confidence && eventData.confidence.per_field) {
|
||||
Object.entries(eventData.confidence.per_field).forEach(([field, confidence]) => {
|
||||
if (confidence < 0.7) {
|
||||
warnings.push(`${field} information may need review (${Math.round(confidence * 100)}% confidence)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check for missing critical fields
|
||||
if (!eventData.title) warnings.push('Event title not found');
|
||||
if (!eventData.start_date) warnings.push('Event date not found');
|
||||
|
||||
if (warnings.length > 0) {
|
||||
const warningsHtml = warnings.map(warning => `<li>${this.escapeHtml(warning)}</li>`).join('');
|
||||
$('.warning-list').html(warningsHtml);
|
||||
$('.result-warnings').show();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply extracted data to the event form
|
||||
*/
|
||||
applyToForm: function() {
|
||||
if (!this.extractedData) return;
|
||||
|
||||
const data = this.extractedData;
|
||||
|
||||
try {
|
||||
// Apply title
|
||||
if (data.title) {
|
||||
$('#event_title, [name="event_title"]').val(data.title);
|
||||
}
|
||||
|
||||
// Apply description (handle TinyMCE, regular textarea, and rich text editor)
|
||||
if (data.description) {
|
||||
// Try TinyMCE first if available
|
||||
if (typeof tinyMCE !== 'undefined' && tinyMCE.get('event_description')) {
|
||||
tinyMCE.get('event_description').setContent(data.description);
|
||||
} else {
|
||||
// Update the hidden textarea
|
||||
$('#event_description, [name="event_description"]').val(data.description);
|
||||
|
||||
// Also update the visible rich text editor div if it exists
|
||||
const $richEditor = $('#event-description-editor');
|
||||
if ($richEditor.length && $richEditor.is('[contenteditable]')) {
|
||||
$richEditor.html(data.description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply start date and time (combine into datetime-local format)
|
||||
if (data.start_date) {
|
||||
let startDateTime = data.start_date;
|
||||
if (data.start_time) {
|
||||
startDateTime += 'T' + data.start_time;
|
||||
} else {
|
||||
// Default to 9:00 AM if no time specified
|
||||
startDateTime += 'T09:00';
|
||||
}
|
||||
$('#event_start_datetime, [name="event_start_datetime"]').val(startDateTime);
|
||||
}
|
||||
|
||||
// Apply end date and time (combine into datetime-local format)
|
||||
if (data.end_date) {
|
||||
let endDateTime = data.end_date;
|
||||
if (data.end_time) {
|
||||
endDateTime += 'T' + data.end_time;
|
||||
} else {
|
||||
// Default to 5:00 PM if no time specified
|
||||
endDateTime += 'T17:00';
|
||||
}
|
||||
$('#event_end_datetime, [name="event_end_datetime"]').val(endDateTime);
|
||||
}
|
||||
|
||||
// Apply cost
|
||||
if (data.cost !== null && data.cost !== undefined) {
|
||||
$('#event_cost, [name="event_cost"]').val(data.cost);
|
||||
}
|
||||
|
||||
// Apply capacity
|
||||
if (data.capacity) {
|
||||
$('#event_capacity, [name="event_capacity"]').val(data.capacity);
|
||||
}
|
||||
|
||||
// Apply URL
|
||||
if (data.url) {
|
||||
$('#event_url, [name="event_url"]').val(data.url);
|
||||
}
|
||||
|
||||
// Apply venue fields (flatter structure)
|
||||
if (data.venue_name) {
|
||||
$('#venue_name, [name="venue_name"]').val(data.venue_name);
|
||||
}
|
||||
if (data.venue_address) {
|
||||
$('#venue_address, [name="venue_address"]').val(data.venue_address);
|
||||
}
|
||||
if (data.venue_city) {
|
||||
$('#venue_city, [name="venue_city"]').val(data.venue_city);
|
||||
}
|
||||
if (data.venue_state) {
|
||||
$('#venue_state, [name="venue_state"]').val(data.venue_state);
|
||||
}
|
||||
if (data.venue_zip) {
|
||||
$('#venue_zip, [name="venue_zip"]').val(data.venue_zip);
|
||||
}
|
||||
|
||||
// Apply organizer fields (flatter structure)
|
||||
if (data.organizer_name) {
|
||||
$('#organizer_name, [name="organizer_name"]').val(data.organizer_name);
|
||||
}
|
||||
if (data.organizer_email) {
|
||||
$('#organizer_email, [name="organizer_email"]').val(data.organizer_email);
|
||||
}
|
||||
if (data.organizer_phone) {
|
||||
$('#organizer_phone, [name="organizer_phone"]').val(data.organizer_phone);
|
||||
}
|
||||
|
||||
// Apply website URL
|
||||
if (data.website) {
|
||||
$('#website, [name="website"]').val(data.website);
|
||||
}
|
||||
|
||||
// Apply event URL
|
||||
if (data.event_url) {
|
||||
$('#event_url, [name="event_url"]').val(data.event_url);
|
||||
}
|
||||
|
||||
// Apply timezone (if provided)
|
||||
if (data.timezone) {
|
||||
$('#event_timezone, [name="event_timezone"]').val(data.timezone);
|
||||
}
|
||||
|
||||
// Apply event image (only if >= 200x200px)
|
||||
if (data.event_image_url) {
|
||||
$('#event_image_url, [name="event_image_url"]').val(data.event_image_url);
|
||||
}
|
||||
|
||||
// Trigger autosave if available
|
||||
if (typeof performAutoSave === 'function') {
|
||||
setTimeout(performAutoSave, 1000);
|
||||
}
|
||||
|
||||
// Close modal and show success message
|
||||
this.closeModal();
|
||||
this.showSuccess('Event information applied successfully! Please review and adjust as needed before submitting.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error applying AI data to form:', error);
|
||||
this.showError('Error applying data to form. Please try filling the fields manually.');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show error message
|
||||
*/
|
||||
showError: function(message) {
|
||||
// Reset processing UI
|
||||
$('#ai-processing').hide();
|
||||
$('.ai-input-section').show();
|
||||
$('#ai-process-btn').show();
|
||||
this.isProcessing = false;
|
||||
|
||||
// Show error in modal or as alert
|
||||
alert('Error: ' + message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Show success message
|
||||
*/
|
||||
showSuccess: function(message) {
|
||||
// You might want to show this in a nicer way, like a toast notification
|
||||
alert(message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Escape HTML for safe display
|
||||
*/
|
||||
escapeHtml: function(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize when document is ready
|
||||
HVACAIAssist.init();
|
||||
});
|
||||
304
assets/js/hvac-searchable-selectors.js
Normal file
304
assets/js/hvac-searchable-selectors.js
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
/**
|
||||
* 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);
|
||||
|
|
@ -348,13 +348,7 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
'wrapper_class' => 'form-row event-description-field'
|
||||
];
|
||||
|
||||
// Original description field for fallback
|
||||
$description_field_fallback = array_merge($this->event_field_defaults['event-description'], [
|
||||
'name' => 'event_description',
|
||||
'label' => 'Event Description',
|
||||
'placeholder' => 'Describe your event...',
|
||||
'rows' => 6,
|
||||
]);
|
||||
// Description field uses rich text editor above
|
||||
|
||||
$this->add_field($title_field);
|
||||
$this->add_field($description_field);
|
||||
|
|
@ -421,20 +415,16 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
* Add venue selection and management fields
|
||||
*/
|
||||
public function add_venue_fields(): self {
|
||||
// Simplified venue selector using regular select field
|
||||
$venue_field = array_merge($this->event_field_defaults['venue-select'], [
|
||||
// Dynamic single-select venue selector with autocomplete and modal creation
|
||||
$venue_field = [
|
||||
'type' => 'custom',
|
||||
'name' => 'event_venue',
|
||||
'label' => 'Venue',
|
||||
'options' => $this->get_venue_options(),
|
||||
'custom_html' => $this->render_searchable_venue_selector(),
|
||||
'wrapper_class' => 'form-row venue-field',
|
||||
'description' => 'Select an existing venue or create a new one'
|
||||
]);
|
||||
];
|
||||
|
||||
$this->add_field($venue_field);
|
||||
|
||||
// Add venue creation fields (initially hidden)
|
||||
$this->add_venue_creation_fields();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -442,20 +432,16 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
* Add organizer selection and management fields
|
||||
*/
|
||||
public function add_organizer_fields(): self {
|
||||
// Simplified organizer selector using regular select field
|
||||
$organizer_field = array_merge($this->event_field_defaults['organizer-select'], [
|
||||
// Dynamic multi-select organizer selector with autocomplete
|
||||
$organizer_field = [
|
||||
'type' => 'custom',
|
||||
'name' => 'event_organizer',
|
||||
'label' => 'Organizer',
|
||||
'options' => $this->get_organizer_options(),
|
||||
'custom_html' => $this->render_searchable_organizer_selector(),
|
||||
'wrapper_class' => 'form-row organizer-field',
|
||||
'description' => 'Select an existing organizer or create a new one'
|
||||
]);
|
||||
];
|
||||
|
||||
$this->add_field($organizer_field);
|
||||
|
||||
// Add organizer creation fields (initially hidden)
|
||||
$this->add_organizer_creation_fields();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -463,42 +449,12 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
* Add categories field with multi-select search functionality
|
||||
*/
|
||||
public function add_categories_fields(): self {
|
||||
// Get event categories from TEC taxonomy
|
||||
$category_options = ['0' => '-- Select Category --'];
|
||||
|
||||
// Get TEC event categories
|
||||
$categories = get_terms([
|
||||
'taxonomy' => 'tribe_events_cat',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC'
|
||||
]);
|
||||
|
||||
if (!is_wp_error($categories) && !empty($categories)) {
|
||||
foreach ($categories as $category) {
|
||||
$category_options[$category->term_id] = $category->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add default categories if none exist
|
||||
if (count($category_options) === 1) {
|
||||
$category_options['general'] = 'General';
|
||||
$category_options['training'] = 'Training';
|
||||
$category_options['workshop'] = 'Workshop';
|
||||
$category_options['certification'] = 'Certification';
|
||||
}
|
||||
|
||||
// Simplified categories selector using regular select field
|
||||
// Dynamic multi-select category selector with autocomplete (limited for trainers)
|
||||
$categories_field = [
|
||||
'type' => 'select',
|
||||
'type' => 'custom',
|
||||
'name' => 'event_categories',
|
||||
'label' => 'Category',
|
||||
'options' => $category_options,
|
||||
'custom_html' => $this->render_searchable_category_selector(),
|
||||
'wrapper_class' => 'form-row categories-field',
|
||||
'description' => 'Select an event category',
|
||||
'class' => 'hvac-categories-select',
|
||||
'validate' => [],
|
||||
'sanitize' => 'int',
|
||||
];
|
||||
|
||||
$this->add_field($categories_field);
|
||||
|
|
@ -1229,17 +1185,11 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
return $field['custom_html'];
|
||||
}
|
||||
|
||||
// Fallback for custom fields without custom_html
|
||||
$output = sprintf('<div class="%s">', esc_attr($field['wrapper_class'] ?? 'form-row'));
|
||||
// Error: custom fields must have custom_html defined
|
||||
error_log("HVAC Event Form Builder: Custom field '{$field['name']}' missing required custom_html property");
|
||||
|
||||
if (isset($field['label'])) {
|
||||
$output .= sprintf('<label>%s</label>', esc_html($field['label']));
|
||||
}
|
||||
|
||||
$output .= sprintf('<p>Custom field "%s" missing custom_html</p>', esc_html($field['name']));
|
||||
$output .= '</div>';
|
||||
|
||||
return $output;
|
||||
// Return empty string to avoid breaking the form
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1504,4 +1454,167 @@ class HVAC_Event_Form_Builder extends HVAC_Form_Builder {
|
|||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render searchable organizer selector with multi-select and "Add New" functionality
|
||||
*/
|
||||
private function render_searchable_organizer_selector(): string {
|
||||
$current_user = wp_get_current_user();
|
||||
$can_create = in_array('hvac_trainer', $current_user->roles) || in_array('hvac_master_trainer', $current_user->roles);
|
||||
|
||||
return <<<HTML
|
||||
<div class="form-row organizer-selector-wrapper">
|
||||
<label for="organizer-search"><strong>Organizers</strong> <span class="form-required">*</span></label>
|
||||
<div class="organizer-selector hvac-searchable-selector" data-max-selections="3" data-type="organizer">
|
||||
<div class="selector-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="organizer-search"
|
||||
placeholder="Search organizers..."
|
||||
class="selector-search-input"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="selector-arrow">▼</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-items" id="selected-organizers">
|
||||
<!-- Selected organizers will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="selector-dropdown" style="display: none;">
|
||||
<div class="dropdown-content">
|
||||
<div class="loading-spinner" style="display: none;">Loading...</div>
|
||||
<div class="no-results" style="display: none;">No organizers found</div>
|
||||
<div class="dropdown-items">
|
||||
<!-- Dynamic organizer options will be loaded here -->
|
||||
</div>
|
||||
{$this->render_create_new_button('organizer', $can_create)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for form submission -->
|
||||
<div class="hidden-inputs">
|
||||
<!-- Will be populated with selected organizer IDs -->
|
||||
</div>
|
||||
</div>
|
||||
<small class="description">Select up to 3 organizers for this event. You can search by name or email.</small>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render searchable category selector with multi-select (no create for trainers)
|
||||
*/
|
||||
private function render_searchable_category_selector(): string {
|
||||
$current_user = wp_get_current_user();
|
||||
$can_create = in_array('hvac_master_trainer', $current_user->roles); // Only master trainers can create categories
|
||||
|
||||
return <<<HTML
|
||||
<div class="form-row category-selector-wrapper">
|
||||
<label for="category-search"><strong>Categories</strong></label>
|
||||
<div class="category-selector hvac-searchable-selector" data-max-selections="3" data-type="category">
|
||||
<div class="selector-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="category-search"
|
||||
placeholder="Search categories..."
|
||||
class="selector-search-input"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="selector-arrow">▼</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-items" id="selected-categories">
|
||||
<!-- Selected categories will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="selector-dropdown" style="display: none;">
|
||||
<div class="dropdown-content">
|
||||
<div class="loading-spinner" style="display: none;">Loading...</div>
|
||||
<div class="no-results" style="display: none;">No categories found</div>
|
||||
<div class="dropdown-items">
|
||||
<!-- Dynamic category options will be loaded here -->
|
||||
</div>
|
||||
{$this->render_create_new_button('category', $can_create)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for form submission -->
|
||||
<div class="hidden-inputs">
|
||||
<!-- Will be populated with selected category IDs -->
|
||||
</div>
|
||||
</div>
|
||||
<small class="description">Select up to 3 categories for this event.</small>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render searchable venue selector with single-select and "Add New" functionality
|
||||
*/
|
||||
private function render_searchable_venue_selector(): string {
|
||||
$current_user = wp_get_current_user();
|
||||
$can_create = in_array('hvac_trainer', $current_user->roles) || in_array('hvac_master_trainer', $current_user->roles);
|
||||
|
||||
return <<<HTML
|
||||
<div class="form-row venue-selector-wrapper">
|
||||
<label for="venue-search"><strong>Venue</strong> <span class="form-required">*</span></label>
|
||||
<div class="venue-selector hvac-searchable-selector" data-max-selections="1" data-type="venue">
|
||||
<div class="selector-input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
id="venue-search"
|
||||
placeholder="Search venues..."
|
||||
class="selector-search-input"
|
||||
autocomplete="off"
|
||||
>
|
||||
<div class="selector-arrow">▼</div>
|
||||
</div>
|
||||
|
||||
<div class="selected-items" id="selected-venues">
|
||||
<!-- Selected venue will appear here -->
|
||||
</div>
|
||||
|
||||
<div class="selector-dropdown" style="display: none;">
|
||||
<div class="dropdown-content">
|
||||
<div class="loading-spinner" style="display: none;">Loading...</div>
|
||||
<div class="no-results" style="display: none;">No venues found</div>
|
||||
<div class="dropdown-items">
|
||||
<!-- Dynamic venue options will be loaded here -->
|
||||
</div>
|
||||
{$this->render_create_new_button('venue', $can_create)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for form submission -->
|
||||
<div class="hidden-inputs">
|
||||
<!-- Will be populated with selected venue ID -->
|
||||
</div>
|
||||
</div>
|
||||
<small class="description">Select a venue for this event. You can search by name or address.</small>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render "Create New" button with role-based permissions
|
||||
*/
|
||||
private function render_create_new_button(string $type, bool $can_create): string {
|
||||
if (!$can_create) {
|
||||
if ($type === 'category') {
|
||||
return '<div class="create-new-disabled">Only Master Trainers can create new categories</div>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
$label = ucfirst($type);
|
||||
return <<<HTML
|
||||
<div class="create-new-section">
|
||||
<button type="button" class="create-new-btn" data-type="{$type}">
|
||||
<span class="dashicons dashicons-plus-alt"></span>
|
||||
Add New {$label}
|
||||
</button>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue