1/** 2 * Agenda.js - Handles agenda page functionality 3 * 4 * This script provides: 5 * 1. Tab functionality for day switching 6 * 2. Current time slot highlighting 7 * 3. Animations for content 8 */ 9document.addEventListener('DOMContentLoaded', () => { 10 // Get tab elements 11 const tabs = document.querySelectorAll('.agenda-tab'); 12 const panels = document.querySelectorAll('.agenda-day'); 13 14 // Add subtle animation to agenda slots 15 const slots = document.querySelectorAll('.agenda-slot'); 16 addAnimationToSlots(slots); 17 18 // Enhance current time slot highlighting 19 highlightCurrentTimeSlot(); 20 21 // Setup tabs functionality 22 setupTabs(); 23 24 /** 25 * Sets up tab functionality 26 */ 27 function setupTabs() { 28 // Check for hash in URL to set initial active tab 29 const initialTabId = location.hash.replace('#day-', ''); 30 if (initialTabId) { 31 const tabToActivate = document.getElementById(`day-${initialTabId}`); 32 if (tabToActivate) { 33 activateTab(tabToActivate); 34 } 35 } 36 37 // Add click event to each tab
38 tabs.forEach(tab => { 39 tab.addEventListener('click', function () { 40 activateTab(this); 41 // Update URL hash without scrolling 42 const id = this.id.replace('day-', ''); 43 history.replaceState(null, '', `#day-${id}`); 44 }); 45 46 // Add keyboard accessibility 47 tab.addEventListener('keydown', e => { 48 if (e.key === 'Enter' || e.key === ' ') { 49 e.preventDefault(); 50 activateTab(tab); 51 } 52 }); 53 }); 54 } 55 56 /** 57 * Activates a specific tab and its corresponding panel 58 */ 59 function activateTab(tabToActivate) { 60 // Deactivate all tabs and panels 61 tabs.forEach(tab => { 62 tab.classList.remove('active'); 63 tab.setAttribute('aria-selected', 'false'); 64 }); 65 66 panels.forEach(panel => { 67 panel.classList.remove('active'); 68 }); 69 70 // Activate the selected tab and panel 71 tabToActivate.classList.add('active'); 72 tabToActivate.setAttribute('aria-selected', 'true'); 73 74 const panelId = tabToActivate.getAttribute('aria-controls'); 75 const panel = document.getElementById(panelId); 76 if (panel) { 77 panel.classList.add('active'); 78 79 // Re-apply animations to visible slots 80 resetSlotAnimations(panel); 81 } 82 } 83 84 /** 85 * Resets animations for slots when a tab is activated 86 */ 87 function resetSlotAnimations(panel) { 88 const visibleSlots = panel.querySelectorAll('.agenda-slot'); 89 visibleSlots.forEach((slot, index) => { 90 // Reset animation 91 slot.classList.remove('visible'); 92 93 // Force reflow to restart animation 94 void slot.offsetWidth; 95 96 // Add visible class with staggered timing 97 setTimeout(() => { 98 slot.classList.add('visible'); 99 }, 50 + index * 100); 100 }); 101 } 102 103 /** 104 * Adds subtle entrance animation to agenda slots 105 */ 106 function addAnimationToSlots(slots) { 107 if (window.IntersectionObserver) { 108 const observer = new IntersectionObserver( 109 entries => { 110 entries.forEach(entry => { 111 if (entry.isIntersecting) { 112 const delay = parseInt(entry.target.dataset.delay || 0); 113 setTimeout(() => { 114 entry.target.classList.add('visible'); 115 }, delay); 116 observer.unobserve(entry.target); 117 } 118 }); 119 }, 120 { 121 root: null, 122 rootMargin: '0px', 123 threshold: 0.1, 124 } 125 ); 126 127 slots.forEach(slot => { 128 observer.observe(slot); 129 }); 130 } else { 131 // Fallback for browsers without Intersection Observer 132 slots.forEach((slot, index) => { 133 setTimeout(() => { 134 slot.classList.add('visible'); 135 }, 100 + index * 100); 136 }); 137 } 138 } 139 140 /** 141 * Finds and highlights the current timeslot if viewing today's agenda 142 */ 143 function highlightCurrentTimeSlot() { 144 const today = new Date(); 145 146 // Process each day section 147 panels.forEach(panel => { 148 const dateText = getDateFromElement(panel); 149 if (!dateText) return; 150 151 // Compare with today's date 152 const eventDate = new Date(dateText); 153 if (isNaN(eventDate.getTime())) return; 154 155 // If this is today's panel, highlight it and also activate its tab 156 if (isSameDay(eventDate, today)) { 157 highlightActiveSlots(panel, today); 158 159 // Find the tab that controls this panel and activate it 160 const tabId = panel.getAttribute('aria-labelledby'); 161 const todayTab = document.getElementById(tabId); 162 if (todayTab && !todayTab.classList.contains('active')) { 163 activateTab(todayTab); 164 } 165 } 166 }); 167 } 168 169 /** 170 * Highlights time slots that are currently active 171 */ 172 function highlightActiveSlots(panelElement, now) { 173 const slots = panelElement.querySelectorAll('.agenda-slot'); 174 175 slots.forEach(slot => { 176 const timeElement = slot.querySelector('.slot-time'); 177 if (!timeElement) return; 178 179 // Get all spans but exclude those with icon class 180 const timeNodes = Array.from(timeElement.childNodes).filter( 181 node =>
181 node.nodeType === 1 && !node.classList.contains('time-icon') 182 ); 183 184 const timeSpans = timeNodes.length ? timeNodes : timeElement.querySelectorAll('span'); 185 if (timeSpans.length === 0) return; 186 187 const startText = timeSpans[0]?.textContent?.trim(); 188 const endText = 189 timeSpans.length > 1 ? timeSpans[timeSpans.length - 1]?.textContent?.trim() : null; 190 191 if (!startText) return; 192 193 const start = parseTime(startText, now); 194 // Default to 1 hour duration if no end time 195 const end = endText ? parseTime(endText, now) : new Date(start.getTime() + 60 * 60 * 1000); 196 197 if (now >= start && now <= end) { 198 slot.classList.add('slot-now'); 199 } 200 }); 201 } 202 203 /** 204 * Utility function to extract date text from a day element 205 */ 206 function getDateFromElement(element) { 207 if (!element) return null; 208 const dateElement = element.querySelector('.agenda-day-date'); 209 return dateElement ? dateElement.textContent.trim() : null; 210 } 211 212 /** 213 * Utility function to check if two dates are the same day 214 */ 215 function isSameDay(date1, date2) { 216 return ( 217 date1.getFullYear() === date2.getFullYear() && 218 date1.getMonth() === date2.getMonth() && 219 date1.getDate() === date2.getDate() 220 ); 221 } 222 223 /** 224 * Utility function to parse time string into Date object 225 */ 226 function parseTime(timeStr, referenceDate) { 227 // Handle various time formats 228 let time = timeStr; 229 let period = ''; 230 231 if (timeStr.includes(' ')) { 232 [time, period] = timeStr.split(' '); 233 } else if (timeStr.includes('AM') || timeStr.includes('PM')) { 234 period = timeStr.slice(-2); 235 time = timeStr.slice(0, -2); 236 } 237 238 const [h, m] = time.split(':').map(Number); 239 240 // Clone reference date to avoid modifying the original 241 const date = new Date(referenceDate.getTime()); 242 243 let hours = h || 0; 244 245 if (period) { 246 if (period.toUpperCase() === 'PM' && hours < 12) hours += 12; 247 if (period.toUpperCase() === 'AM' && hours === 12) hours = 0; 248 } 249 250 date.setHours(hours, m || 0, 0, 0); 251 return date; 252 } 253});
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.