1document.addEventListener('DOMContentLoaded', function () { 2 /** 3 * Fetches ticket data for a collection of event rows in a single bulk AJAX request 4 * and updates the 'BUY TICKETS' buttons with the retrieved URLs and status tags. 5 * @param {NodeListOf<Element>} eventRows - A collection of event row elements. 6 */ 7 function fetchAndUpdateTicketData(eventRows) { 8 if (!eventRows || eventRows.length === 0) { 9 return; 10 } 11 12 const eventIds = []; 13 eventRows.forEach(row => { 14 const button = row.querySelector('.buy-tickets-button[data-post-id]'); 15 if (button) { 16 const postId = button.getAttribute('data-post-id'); 17 if (postId) { 18 eventIds.push(postId); 19 } 20 } 21 }); 22 23 if (eventIds.length === 0) { 24 return; // No IDs to fetch 25 } 26 27 // Use the 'dakotaAjax' object passed from PHP 28 const { ajaxUrl, nonce } = window.dakotaAjax; 29 30 const formData = new URLSearchParams(); 31 formData.append('action', 'get_bulk_etix_data'); 32 formData.append('nonce', nonce); 33 formData.append('event_ids', JSON.stringify(eventIds)); 34 fetch(ajaxUrl, { 35 method: 'POST', 36 body: formData, 37 }) 38 .then(response => { 39 if (!response.ok) { 40 throw new Error(`HTTP error! status: ${response.status}`); 41 } 42 return response.json(); 43 }) 44 .then(response => { 45 if (response.success) { 46 const ticketData = response.data; 47 // Loop through the returned data (key is the event ID) 48 for (const eventId in ticketData) { 49 if (!ticketData.hasOwnProperty(eventId)) continue; // Security check 50 51 const data = ticketData[eventId]; 52 // Find all matching buttons for this event ID (handles both views) 53 const buttons = document.querySelectorAll(`.buy-tickets-button[data-post-id="${eventId}"]`); 54 55 buttons.forEach(button => { 56 // Check if a valid ticket link was returned 57 if (data.link && data.link !== '') { 58 // Security: Basic URL validation - ensure it's a proper URL 59 try { 60 new URL(data.link); // This will throw if URL is invalid 61 button.href = data.link; 62 } catch (e) { 63 //console.warn('Invalid URL detected, hiding button:', data.link); 64 button.style.display = 'none'; 65 return; 66 } 67 68 // Add status tags as CSS classes (already sanitized server-side) 69 if (data.status_tags && Array.isArray(data.status_tags)) { 70 data.status_tags.forEach(tag => { 71 // Additional validation: ensure tag is a string and safe for CSS 72 if (typeof tag === 'string' && tag.length > 0) { 73 button.classList.add(`tag-${tag}`); 74 } 75 }); 76 } 77 } else { 78 // If no link, hide the 'BUY TICKETS' button for a clean UI 79 button.style.display = 'none'; 80 } 81 }); 82 } 83 } 84 }) 85 .catch(error => console.error('Error fetching ticket data:', error)); 86 } 87 88 /** 89 * Creates and inserts the 'EVENT DETAILS' and 'BUY TICKETS' buttons into an event row's header. 90 * @param {Element} eventRow - The event row element. 91 */ 92 function addButtonsToEvent(eventRow) {
93 // Support both list and day view headers 94 const header = eventRow.querySelector('.tribe-events-calendar-list__event-header, .tribe-events-calendar-day__event-header'); 95 // Support both list and day view title links 96 const eventLinkElement = eventRow.querySelector('.tribe-events-calendar-list__event-title-link, .tribe-events-calendar-day__event-title-link'); 97 98 if (!header || !eventLinkElement || header.querySelector('.event-buttons-container')) { 99 return; // Abort if elements are missing or buttons already exist 100 } 101 102 const eventUrl = eventLinkElement.getAttribute('href'); 103 const buttonContainer = document.createElement('div'); 104 buttonContainer.className = 'event-buttons-container'; 105 106 const detailsButton = document.createElement('a'); 107 detailsButton.className = 'event-button event-details-button'; 108 detailsButton.href = eventUrl; 109 detailsButton.textContent = 'EVENT DETAILS'; 110 detailsButton.setAttribute('role', 'button'); 111 112 const buyTicketsButton = document.createElement('a'); 113 buyTicketsButton.className = 'event-button buy-tickets-button'; 114 buyTicketsButton.href = '#'; // Placeholder 115 buyTicketsButton.textContent = 'BUY TICKETS'; 116 buyTicketsButton.setAttribute('role', 'button'); 117 118 // Try to find the article element more robustly (search up the tree if needed) 119 let articleElement = eventRow.querySelector('article[class*="post-"]'); 120 if (!articleElement) { 121 // Try searching ancestors (in case markup differs in day view) 122 articleElement = eventRow.closest('article[class*="post-"]'); 123 } 124 let postId = null; 125 if (articleElement) { 126 const postIdClass = Array.from(articleElement.classList).find(cls => cls.startsWith('post-')); 127 if (postIdClass) { 128 postId = postIdClass.replace('post-', ''); 129 // Security: Validate the post ID is numeric before using it 130 if (/^\d+$/.test(postId)) { 131 buyTicketsButton.setAttribute('data-post-id', postId); 132 // Debug: Log for troubleshooting 133 // console.log('Set data-post-id:', postId, 'for', buyTicketsButton, 'in', eventRow); 134 } else { 135 // console.warn('Non-numeric post ID found:', postIdClass); 136 } 137 } else { 138 // console.warn('No post-* class found on article:', articleElement); 139 } 140 } else { 141 // console.warn('No article[class*="post-"] found for eventRow:', eventRow); 142 } 143 144 buttonContainer.appendChild(detailsButton); 145 buttonContainer.appendChild(buyTicketsButton); 146 header.appendChild(buttonContainer); 147 } 148 149 // --- NEW, SEPARATE FUNCTION --- 150 /** 151 * Finds the event date/time string, splits it, and wraps each part in its own span. 152 * This function is self-contained and does not interact with button/data logic. 153 * @param {Element} eventRow - The event row element. 154 */ 155 function formatEventDateTime(eventRow) { 156 // Support both list and day view datetime containers 157 const timeElement = eventRow.querySelector('.tribe-events-calendar-list__event-datetime, .tribe-events-calendar-day__event-datetime'); 158 if (!timeElement) return; 159 160 const originalSpan = timeElement.querySelector('.tribe-event-date-start'); 161 if (!originalSpan || !originalSpan.textContent.includes('|')) return; 162 163 const parts = originalSpan.textContent.split('|').map(part => part.trim()); 164 if (parts.length < 2) return; 165 166 const dateStr = parts[0]; 167 const timeStr = parts[1]; 168 169 const dateSpan = document.createElement('span'); 170 dateSpan.className = 'event-list-date'; 171 dateSpan.textContent = dateStr; 172 173 const timeSpan = document.createElement('span'); 174 timeSpan.className = 'event-list-time'; 175 timeSpan.textContent = timeStr; 176 177 timeElement.innerHTML = ''; 178 timeElement.appendChild(dateSpan); 179 timeElement.appendChild(timeSpan); 180 } 181 182 /**
183 * Processes a collection of newly added nodes, adds buttons, and then triggers the data fetch. 184 * @param {NodeListOf<Node>} addedNodes - Nodes added to the DOM. 185 */ 186 function processNewNodes(addedNodes) { 187 const newEventRows = []; 188 addedNodes.forEach(node => { 189 if (node.nodeType === 1) { // Is an element 190 // Support both list and day view event rows 191 if (node.matches && node.matches('.tribe-events-calendar-list__event-row, .tribe-events-calendar-day__event-row')) { 192 addButtonsToEvent(node); 193 formatEventDateTime(node); 194 newEventRows.push(node); 195 } else { 196 const foundRows = node.querySelectorAll('.tribe-events-calendar-list__event-row, .tribe-events-calendar-day__event'); 197 if (foundRows.length > 0) { 198 foundRows.forEach(row => { 199 addButtonsToEvent(row); 200 formatEventDateTime(row); 201 newEventRows.push(row); 202 }); 203 } 204 } 205 } 206 }); 207 208 if (newEventRows.length > 0) { 209 fetchAndUpdateTicketData(newEventRows); 210 } 211 } 212 213 /** 214 * Wraps unlinked featured images in anchor tags so they navigate to the event. 215 * Covers Month view and List view image wrappers. 216 */ 217 function linkEventImages() { 218 var selectors = [ 219 { 220 wrapper: '.tribe-events-calendar-month__calendar-event-featured-image-wrapper', 221 link: '.tribe-events-calendar-month__calendar-event-title-link' 222 }, 223 { 224 wrapper: '.tribe-events-calendar-list__event-featured-image-wrapper', 225 link: '.tribe-events-calendar-list__event-title-link' 226 }, 227 { 228 wrapper: '.tribe-events-calendar-month-mobile-events__mobile-event-featured-image-wrapper', 229 link: '.tribe-events-calendar-month-mobile-events__mobile-event-title-link' 230 } 231 ]; 232 233 selectors.forEach(function (sel) { 234 document.querySelectorAll(sel.wrapper).forEach(function (imgWrapper) { 235 if (imgWrapper.querySelector('a')) return; 236 var event = imgWrapper.closest('article') || imgWrapper.closest('[class*="calendar-event"]'); 237 if (!event) return; 238 var titleLink = event.querySelector(sel.link); 239 if (!titleLink) return; 240 241 var anchor = document.createElement('a'); 242 anchor.href = titleLink.href; 243 anchor.setAttribute('aria-hidden', 'true'); 244 anchor.setAttribute('tabindex', '-1'); 245 while (imgWrapper.firstChild) { 246 anchor.appendChild(imgWrapper.firstChild); 247 } 248 imgWrapper.appendChild(anchor); 249 }); 250 }); 251 } 252 253 // --- Initial page load processing --- 254 // Support both list and day view event rows 255 const initialEventRows = document.querySelectorAll('.tribe-events-calendar-list__event-row, .tribe-events-calendar-day__event'); 256 if (initialEventRows.length > 0) { 257 initialEventRows.forEach(row => { 258 addButtonsToEvent(row); 259 formatEventDateTime(row); 260 }); 261 fetchAndUpdateTicketData(initialEventRows); 262 } 263 linkEventImages(); 264 265 // --- Set up MutationObserver to handle events loaded via AJAX --- 266 const observer = new MutationObserver(mutationsList => { 267 for (const mutation of mutationsList) { 268 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 269 processNewNodes(mutation.addedNodes); 270 linkEventImages(); 271 // We only process the first mutation with added nodes to avoid redundant checks 272 break; 273 } 274 } 275 }); 276 277 observer.observe(document.body, { 278 childList: true, 279 subtree: true 280 });
vendor: 3 bytes, line 281
281});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.