/**
* Meal Selection Injector – Robust Version
* This script handles rendering meal selection dropdowns based on ticket quantity
* and enforces validation before adding to cart.
* * NOTE: This file contains raw JavaScript (no
// Global variables
var bodyObserver;
var TICKET_QUANTITY_INPUT_CLASS = 'tribe-tickets-quantity';
var MEAL_STORAGE_KEY = 'meal_choices_per_ticket';
var MEAL_ERROR_ID = 'meal-selection-error';
// --- STEP 1: EVENT PAGE INJECTION AND LOCAL STORAGE SAVE (Per-Ticket Logic) ---
/**
* Renders the correct number of meal selector dropdowns based on current ticket quantity.
* @param {HTMLElement} targetContainer - The main ticket form container (e.g., .tribe-tickets).
*/
function renderMealSelectors(targetContainer) {
var totalQuantity = 0;
// 1. Calculate the total number of tickets across all ticket types
var quantityInputs = targetContainer.querySelectorAll('.' + TICKET_QUANTITY_INPUT_CLASS);
quantityInputs.forEach(function(input) {
totalQuantity += parseInt(input.value) || 0;
});
var selectionsContainer = document.getElementById('individual-meal-selections');
var existingDropdowns = selectionsContainer ? selectionsContainer.querySelectorAll('.meal-choice-dropdown') : [];
// Check if total quantity is 0. If so, clear and remove the container.
if (totalQuantity === 0) {
if (selectionsContainer) {
selectionsContainer.remove();
var errorBox = document.getElementById(MEAL_ERROR_ID);
if (errorBox) errorBox.remove();
}
localStorage.removeItem(MEAL_STORAGE_KEY);
console.log('Ticket quantity is zero. Meal selector removed.');
return;
}
// If the count hasn't changed, and the container exists, just return to prevent unnecessary DOM updates
if (totalQuantity === existingDropdowns.length && selectionsContainer) {
// Even if we don't re-render, we need to ensure the error box is cleared if tickets were added/removed
var errorBox = document.getElementById(MEAL_ERROR_ID);
if (errorBox && errorBox.textContent.length > 0) {
validateMealSelections(); // Re-validate to potentially clear the error
}
return;
}
// Initialize or clear the main meal selections container
if (!selectionsContainer) {
selectionsContainer = document.createElement('div');
selectionsContainer.id = 'individual-meal-selections';
// Applying styles to match the dark theme and highlight the section
selectionsContainer.style.cssText = 'border: 2px solid #FF8C00; background-color: #1a1a1a; padding: 15px; margin-top: 20px; border-radius: 8px;';
// Inject the new container before the ticket form
targetContainer.insertAdjacentElement('beforebegin', selectionsContainer);
}
// Clear previous selectors inside the container
selectionsContainer.innerHTML = '';
// Header for the selection area
selectionsContainer.innerHTML = `
Dinner Selection Details (${totalQuantity} Tickets)
Please select the meal choice for each ticket below.
`;
// Retrieve previous choices from localStorage
var storedChoices = [];
try {
var storedJson = localStorage.getItem(MEAL_STORAGE_KEY);
// We ensure we only take as many choices as there are tickets now
var parsedChoices = storedJson ? JSON.parse(storedJson) : [];
storedChoices = parsedChoices.slice(0, totalQuantity);
} catch(e) {
console.error("Error parsing stored meal choices:", e);
}
// 2. Loop and render a dropdown for each ticket
for (var i = 1; i <= totalQuantity; i++) {
var ticketId = 'ticket_' + i;
// Use the sequential index for retrieval
var savedChoice = storedChoices[i - 1];
var savedValue = savedChoice ? savedChoice.meal : '';
var dropdownHTML = `
`;
selectionsContainer.insertAdjacentHTML('beforeend', dropdownHTML);
}
// 3. Attach a single listener to the parent container for delegation
selectionsContainer.removeEventListener('change', saveAllMealSelections);
selectionsContainer.addEventListener('change', saveAllMealSelections);
console.log('Successfully rendered', totalQuantity, 'meal selectors.');
saveAllMealSelections(); // Save initial or retrieved state
}
/**
* Reads all current meal selections and saves them as JSON to localStorage.
*/
function saveAllMealSelections() {
var selectionsContainer = document.getElementById('individual-meal-selections');
if (!selectionsContainer) return;
var allChoices = [];
selectionsContainer.querySelectorAll('.meal-choice-dropdown').forEach(function(selectElement) {
allChoices.push({
id: selectElement.dataset.ticketId,
meal: selectElement.value
});
// Clear red border immediately on making a selection
if (selectElement.value !== "") {
selectElement.style.border = '1px solid #FF8C00';
}
});
// Save the array as a JSON string
localStorage.setItem(MEAL_STORAGE_KEY, JSON.stringify(allChoices));
// After saving, re-validate to clear the global error box if the issue was just fixed
if (document.getElementById(MEAL_ERROR_ID)) {
validateMealSelections();
}
}
/**
* Checks all rendered meal selector dropdowns for a selected value.
* Prevents form submission and displays an error message if any meal is missing.
* @returns {boolean} True if all meals are selected, false otherwise.
*/
function validateMealSelections() {
var selectionsContainer = document.getElementById('individual-meal-selections');
var errorBox = document.getElementById(MEAL_ERROR_ID);
// If the meal selector area doesn't exist (i.e., no tickets selected), validation passes
if (!selectionsContainer) {
if (errorBox) errorBox.remove();
return true;
}
var dropdowns = selectionsContainer.querySelectorAll('.meal-choice-dropdown');
var isValid = true;
var unselectedCount = 0;
dropdowns.forEach(function(selectElement) {
// Check if the selected value is the empty string from the disabled option
if (selectElement.value === "") {
isValid = false;
unselectedCount++;
// Highlight the invalid dropdown in red
selectElement.style.border = '2px solid red';
} else {
// Reset styling if valid
selectElement.style.border = '1px solid #FF8C00';
}
});
if (!isValid) {
var message = "ERROR: Please select a meal choice for all " + unselectedCount + " required ticket(s) before continuing. Missing selections are highlighted in red.";
// Create or update the error message box
if (!errorBox) {
errorBox = document.createElement('div');
errorBox.id = MEAL_ERROR_ID;
// Style this strongly to contrast with the dark theme
errorBox.style.cssText = 'padding: 15px; background-color: #FFE6E6; color: #CC0000; border: 2px solid #CC0000; border-radius: 6px; margin-bottom: 15px; font-weight: 700; text-align: center;';
selectionsContainer.insertAdjacentElement('beforebegin', errorBox);
}
errorBox.textContent = message;
// Scroll to the error box to ensure the user sees it
errorBox.scrollIntoView({ behavior: 'smooth', block: 'start' });
} else {
// Remove error message if validation passes
if (errorBox) {
errorBox.remove();
}
}
return isValid;
}
/**
* Sets up listeners on the ticket form, quantity inputs, and the "Add to Cart" button.
*/
function setupTicketObservers() {
// IMPORTANT TARGETS: Find the main ticket form/container
var targetContainer = document.querySelector('.tribe-tickets') ||
document.querySelector('#tribe-tickets__tickets-form');
if (!targetContainer) {
return;
}
// Initial render of meal selectors based on default quantities
renderMealSelectors(targetContainer);
// Find the closest parent form and the submit button
var mainForm = targetContainer.closest('form');
var addToCartButton = mainForm ? mainForm.querySelector('button[type="submit"], input[type="submit"], .tribe-tickets-button') : null;
if (mainForm && !mainForm.dataset.mealValidatorSet) {
// Hook into the primary Add to Cart button click (most critical guard against AJAX)
if (addToCartButton) {
addToCartButton.addEventListener('click', function(event) {
if (!validateMealSelections()) {
event.preventDefault();
event.stopImmediatePropagation();
console.warn('Form submission prevented by meal selector (Button Click Guard).');
return false;
}
}, true); // Use 'true' for capturing phase to ensure it runs before the plugin's listener
console.log('Validation attached to Add to Cart button (Click Listener).');
}
// Also keep the submit listener as a fallback for standard form submission
mainForm.addEventListener('submit', function(event) {
if (!validateMealSelections()) {
event.preventDefault();
event.stopImmediatePropagation();
console.warn('Form submission prevented by meal selector (Form Submit Guard).');
return false;
}
});
mainForm.dataset.mealValidatorSet = 'true';
}
// 1. Set up listeners on ALL quantity inputs for direct typing/change events
var quantityInputs = targetContainer.querySelectorAll('.' + TICKET_QUANTITY_INPUT_CLASS);
quantityInputs.forEach(function(input) {
var updateHandler = function() {
renderMealSelectors(targetContainer);
};
// Listen for standard change and input events
input.addEventListener('change', updateHandler);
input.addEventListener('input', updateHandler);
});
// 2. Set up a single, robust Mutation Observer on the main container
if (!targetContainer.dataset.mealObserverSet) {
var ticketListObserver = new MutationObserver(function(mutationsList) {
// Check if any mutation involves a quantity input's 'value' attribute change
var shouldRender = mutationsList.some(function(mutation) {
return mutation.type === 'attributes' &&
(mutation.attributeName === 'value' || mutation.target.classList.contains(TICKET_QUANTITY_INPUT_CLASS));
});
if (shouldRender || mutationsList.some(m => m.type === 'childList')) {
// Use a short delay (debounce) to wait for all DOM updates from the plugin logic to finish
clearTimeout(window.mealSelectorTimeout);
window.mealSelectorTimeout = setTimeout(function() {
renderMealSelectors(targetContainer);
}, 100);
}
});
// Observe the container for attribute changes (to input values) and subtree changes
ticketListObserver.observe(targetContainer, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['value']
});
targetContainer.dataset.mealObserverSet = 'true'; // Mark as observed
// Disconnect the initial body observer now that we are observing the specific target
if (bodyObserver) {
bodyObserver.disconnect();
console.log('Initial Body MutationObserver disconnected.');
}
console.log('Specific MutationObserver set up on ticket container for quantity changes.');
}
}
// --- STEP 2: CHECKOUT PAGE DATA RETRIEVAL ---
function applyMealChoiceToCheckout() {
// Check if we are likely on a checkout page (WooCommerce standard ID)
var orderNotesField = document.getElementById('order_comments');
var storedJson = localStorage.getItem(MEAL_STORAGE_KEY);
// Proceed only if we have stored data AND the notes field is available
if (storedJson && orderNotesField) {
try {
var storedChoices = JSON.parse(storedJson);
var existingNotes = orderNotesField.value.trim();
var mealNoteHeader = "--- INDIVIDUAL MEAL CHOICES ---n";
var mealNoteDetails = storedChoices
.filter(function(choice) {
// Only include choices that have actually been made
return choice.meal && choice.meal !== '';
})
.map(function(choice, index) {
// Use index + 1 to show proper ticket numbering (Ticket #1, #2, etc.)
return 'Ticket #' + (index + 1) + ": " + choice.meal;
}).join('n');
if (mealNoteDetails.length === 0) {
localStorage.removeItem(MEAL_STORAGE_KEY);
return;
}
var finalNote = mealNoteHeader + mealNoteDetails + "n---";
// Append the meal note to the existing notes, if any
orderNotesField.value = existingNotes + (existingNotes ? "nn" : "") + finalNote;
// Clean up localStorage to prevent data leakage in future orders
localStorage.removeItem(MEAL_STORAGE_KEY);
console.log('Meal choices retrieved and successfully added to Order Notes. Storage key (' + MEAL_STORAGE_KEY + ') cleared.');
} catch(e) {
console.error('Failed to parse meal choices JSON:', e);
}
}
}
// --- PRIMARY EXECUTION METHOD ---
function initializeInjection() {
// 1. Always attempt to handle checkout data immediately
applyMealChoiceToCheckout();
// 2. Attempt initial injection setup immediately
setupTicketObservers();
// 3. If the main ticket container is not immediately present,
// set up a MutationObserver on the body to watch for its eventual appearance via AJAX.
var targetExists = document.querySelector('.tribe-tickets') || document.querySelector('#tribe-tickets__tickets-form');
if (!targetExists && document.body) {
bodyObserver = new MutationObserver(function(mutationsList) {
var newTarget = document.querySelector('.tribe-tickets') || document.querySelector('#tribe-tickets__tickets-form');
if (newTarget) {
// If the target finally appears, set up the specific ticket observers and stop the body observer
setupTicketObservers();
// bodyObserver will be disconnected inside setupTicketObservers
}
});
// Watch for the ticket form being injected anywhere in the body
bodyObserver.observe(document.body, { childList: true, subtree: true });
console.log('Initial Body MutationObserver started to wait for ticket form.');
}
}
// Use DOMContentLoaded to ensure the script runs after the document structure is loaded
document.addEventListener('DOMContentLoaded', initializeInjection);
console.log('Meal Selection Injector script setup complete.');
