11d05cddcSAtari911/** 21d05cddcSAtari911 * DokuWiki Compact Calendar Plugin JavaScript 31d05cddcSAtari911 * Loaded independently to avoid DokuWiki concatenation issues 41d05cddcSAtari911 */ 51d05cddcSAtari911 61d05cddcSAtari911// Ensure DOKU_BASE is defined - check multiple sources 71d05cddcSAtari911if (typeof DOKU_BASE === 'undefined') { 81d05cddcSAtari911 // Try to get from global jsinfo object (DokuWiki standard) 91d05cddcSAtari911 if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) { 101d05cddcSAtari911 window.DOKU_BASE = window.jsinfo.dokubase; 111d05cddcSAtari911 } else { 121d05cddcSAtari911 // Fallback: extract from script source path 131d05cddcSAtari911 var scripts = document.getElementsByTagName('script'); 141d05cddcSAtari911 var pluginScriptPath = null; 151d05cddcSAtari911 for (var i = 0; i < scripts.length; i++) { 161d05cddcSAtari911 if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) { 171d05cddcSAtari911 pluginScriptPath = scripts[i].src; 181d05cddcSAtari911 break; 191d05cddcSAtari911 } 201d05cddcSAtari911 } 211d05cddcSAtari911 221d05cddcSAtari911 if (pluginScriptPath) { 231d05cddcSAtari911 // Extract base path from: .../lib/plugins/calendar/script.js 241d05cddcSAtari911 var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//); 251d05cddcSAtari911 window.DOKU_BASE = match ? match[1] : '/'; 261d05cddcSAtari911 } else { 271d05cddcSAtari911 // Last resort: use root 281d05cddcSAtari911 window.DOKU_BASE = '/'; 291d05cddcSAtari911 } 301d05cddcSAtari911 } 311d05cddcSAtari911} 321d05cddcSAtari911 331d05cddcSAtari911// Shorthand for convenience 341d05cddcSAtari911var DOKU_BASE = window.DOKU_BASE || '/'; 351d05cddcSAtari911 36*9ccd446eSAtari911// Helper: propagate CSS variables from a calendar container to a target element 37*9ccd446eSAtari911// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars 38*9ccd446eSAtari911// from DOM parents per spec, but some DokuWiki templates break this inheritance) 39*9ccd446eSAtari911function propagateThemeVars(calId, targetEl) { 40*9ccd446eSAtari911 if (!targetEl) return; 41*9ccd446eSAtari911 // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.) 42*9ccd446eSAtari911 const container = document.getElementById(calId) 43*9ccd446eSAtari911 || document.getElementById('sidebar-widget-' + calId) 44*9ccd446eSAtari911 || document.querySelector('[id$="' + calId + '"]'); 45*9ccd446eSAtari911 if (!container) return; 46*9ccd446eSAtari911 const cs = getComputedStyle(container); 47*9ccd446eSAtari911 const vars = [ 48*9ccd446eSAtari911 '--background-site', '--background-alt', '--background-header', 49*9ccd446eSAtari911 '--text-primary', '--text-bright', '--text-dim', 50*9ccd446eSAtari911 '--border-color', '--border-main', 51*9ccd446eSAtari911 '--cell-bg', '--cell-today-bg', '--grid-bg', 52*9ccd446eSAtari911 '--shadow-color', '--header-border', '--header-shadow', 53*9ccd446eSAtari911 '--btn-text' 54*9ccd446eSAtari911 ]; 55*9ccd446eSAtari911 vars.forEach(v => { 56*9ccd446eSAtari911 const val = cs.getPropertyValue(v).trim(); 57*9ccd446eSAtari911 if (val) targetEl.style.setProperty(v, val); 58*9ccd446eSAtari911 }); 59*9ccd446eSAtari911} 60*9ccd446eSAtari911 611d05cddcSAtari911// Filter calendar by namespace 621d05cddcSAtari911window.filterCalendarByNamespace = function(calId, namespace) { 631d05cddcSAtari911 // Get current year and month from calendar 641d05cddcSAtari911 const container = document.getElementById(calId); 651d05cddcSAtari911 if (!container) { 661d05cddcSAtari911 console.error('Calendar container not found:', calId); 671d05cddcSAtari911 return; 681d05cddcSAtari911 } 691d05cddcSAtari911 701d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 711d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 721d05cddcSAtari911 731d05cddcSAtari911 // Reload calendar with the filtered namespace 741d05cddcSAtari911 navCalendar(calId, year, month, namespace); 751d05cddcSAtari911}; 761d05cddcSAtari911 771d05cddcSAtari911// Navigate to different month 781d05cddcSAtari911window.navCalendar = function(calId, year, month, namespace) { 791d05cddcSAtari911 801d05cddcSAtari911 const params = new URLSearchParams({ 811d05cddcSAtari911 call: 'plugin_calendar', 821d05cddcSAtari911 action: 'load_month', 831d05cddcSAtari911 year: year, 841d05cddcSAtari911 month: month, 851d05cddcSAtari911 namespace: namespace, 861d05cddcSAtari911 _: new Date().getTime() // Cache buster 871d05cddcSAtari911 }); 881d05cddcSAtari911 891d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 901d05cddcSAtari911 method: 'POST', 911d05cddcSAtari911 headers: { 921d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 931d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 941d05cddcSAtari911 'Pragma': 'no-cache' 951d05cddcSAtari911 }, 961d05cddcSAtari911 body: params.toString() 971d05cddcSAtari911 }) 981d05cddcSAtari911 .then(r => r.json()) 991d05cddcSAtari911 .then(data => { 1001d05cddcSAtari911 if (data.success) { 1011d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 1021d05cddcSAtari911 } else { 1031d05cddcSAtari911 console.error('Failed to load month:', data.error); 1041d05cddcSAtari911 } 1051d05cddcSAtari911 }) 1061d05cddcSAtari911 .catch(err => { 1071d05cddcSAtari911 console.error('Error loading month:', err); 1081d05cddcSAtari911 }); 1091d05cddcSAtari911}; 1101d05cddcSAtari911 1111d05cddcSAtari911// Jump to current month 1121d05cddcSAtari911window.jumpToToday = function(calId, namespace) { 1131d05cddcSAtari911 const today = new Date(); 1141d05cddcSAtari911 const year = today.getFullYear(); 1151d05cddcSAtari911 const month = today.getMonth() + 1; // JavaScript months are 0-indexed 1161d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1171d05cddcSAtari911}; 1181d05cddcSAtari911 1191d05cddcSAtari911// Jump to today for event panel 1201d05cddcSAtari911window.jumpTodayPanel = function(calId, namespace) { 1211d05cddcSAtari911 const today = new Date(); 1221d05cddcSAtari911 const year = today.getFullYear(); 1231d05cddcSAtari911 const month = today.getMonth() + 1; 1241d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 1251d05cddcSAtari911}; 1261d05cddcSAtari911 1271d05cddcSAtari911// Open month picker dialog 1281d05cddcSAtari911window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) { 1291d05cddcSAtari911 1301d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 1311d05cddcSAtari911 1321d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 1331d05cddcSAtari911 1341d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 1351d05cddcSAtari911 1361d05cddcSAtari911 if (!overlay) { 1371d05cddcSAtari911 console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId); 1381d05cddcSAtari911 return; 1391d05cddcSAtari911 } 1401d05cddcSAtari911 1411d05cddcSAtari911 if (!monthSelect || !yearSelect) { 1421d05cddcSAtari911 console.error('Select elements not found!'); 1431d05cddcSAtari911 return; 1441d05cddcSAtari911 } 1451d05cddcSAtari911 1461d05cddcSAtari911 // Set current values 1471d05cddcSAtari911 monthSelect.value = currentMonth; 1481d05cddcSAtari911 yearSelect.value = currentYear; 1491d05cddcSAtari911 1501d05cddcSAtari911 // Show overlay 1511d05cddcSAtari911 overlay.style.display = 'flex'; 1521d05cddcSAtari911}; 1531d05cddcSAtari911 1541d05cddcSAtari911// Open month picker dialog for event panel 1551d05cddcSAtari911window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) { 1561d05cddcSAtari911 openMonthPicker(calId, currentYear, currentMonth, namespace); 1571d05cddcSAtari911}; 1581d05cddcSAtari911 1591d05cddcSAtari911// Close month picker dialog 1601d05cddcSAtari911window.closeMonthPicker = function(calId) { 1611d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 1621d05cddcSAtari911 overlay.style.display = 'none'; 1631d05cddcSAtari911}; 1641d05cddcSAtari911 1651d05cddcSAtari911// Jump to selected month 1661d05cddcSAtari911window.jumpToSelectedMonth = function(calId, namespace) { 1671d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 1681d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 1691d05cddcSAtari911 1701d05cddcSAtari911 const month = parseInt(monthSelect.value); 1711d05cddcSAtari911 const year = parseInt(yearSelect.value); 1721d05cddcSAtari911 1731d05cddcSAtari911 closeMonthPicker(calId); 1741d05cddcSAtari911 1751d05cddcSAtari911 // Check if this is a calendar or event panel 1761d05cddcSAtari911 const container = document.getElementById(calId); 1771d05cddcSAtari911 if (container && container.classList.contains('event-panel-standalone')) { 1781d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 1791d05cddcSAtari911 } else { 1801d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1811d05cddcSAtari911 } 1821d05cddcSAtari911}; 1831d05cddcSAtari911 1841d05cddcSAtari911// Rebuild calendar grid after navigation 1851d05cddcSAtari911window.rebuildCalendar = function(calId, year, month, events, namespace) { 1861d05cddcSAtari911 1871d05cddcSAtari911 const container = document.getElementById(calId); 1881d05cddcSAtari911 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 1891d05cddcSAtari911 'July', 'August', 'September', 'October', 'November', 'December']; 1901d05cddcSAtari911 191*9ccd446eSAtari911 // Get theme data from container 192*9ccd446eSAtari911 const theme = container.dataset.theme || 'matrix'; 193*9ccd446eSAtari911 let themeStyles = {}; 194*9ccd446eSAtari911 try { 195*9ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles || '{}'); 196*9ccd446eSAtari911 } catch (e) { 197*9ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 198*9ccd446eSAtari911 themeStyles = {}; 199*9ccd446eSAtari911 } 200*9ccd446eSAtari911 2011d05cddcSAtari911 // Preserve original namespace if not yet set 2021d05cddcSAtari911 if (!container.dataset.originalNamespace) { 2031d05cddcSAtari911 container.setAttribute('data-original-namespace', namespace || ''); 2041d05cddcSAtari911 } 2051d05cddcSAtari911 2061d05cddcSAtari911 // Update container data attributes for current month/year 2071d05cddcSAtari911 container.setAttribute('data-year', year); 2081d05cddcSAtari911 container.setAttribute('data-month', month); 2091d05cddcSAtari911 2101d05cddcSAtari911 // Update embedded events data 2111d05cddcSAtari911 let eventsDataEl = document.getElementById('events-data-' + calId); 2121d05cddcSAtari911 if (eventsDataEl) { 2131d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2141d05cddcSAtari911 } else { 2151d05cddcSAtari911 eventsDataEl = document.createElement('script'); 2161d05cddcSAtari911 eventsDataEl.type = 'application/json'; 2171d05cddcSAtari911 eventsDataEl.id = 'events-data-' + calId; 2181d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2191d05cddcSAtari911 container.appendChild(eventsDataEl); 2201d05cddcSAtari911 } 2211d05cddcSAtari911 2221d05cddcSAtari911 // Update header 2231d05cddcSAtari911 const header = container.querySelector('.calendar-compact-header h3'); 2241d05cddcSAtari911 header.textContent = monthNames[month - 1] + ' ' + year; 2251d05cddcSAtari911 2261d05cddcSAtari911 // Update or create namespace filter indicator 2271d05cddcSAtari911 let filterIndicator = container.querySelector('.calendar-namespace-filter'); 2281d05cddcSAtari911 const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' && 2291d05cddcSAtari911 namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1; 2301d05cddcSAtari911 2311d05cddcSAtari911 if (shouldShowFilter) { 2321d05cddcSAtari911 // Show/update filter indicator 2331d05cddcSAtari911 if (!filterIndicator) { 2341d05cddcSAtari911 // Create filter indicator if it doesn't exist 2351d05cddcSAtari911 const headerDiv = container.querySelector('.calendar-compact-header'); 236*9ccd446eSAtari911 if (headerDiv) { 2371d05cddcSAtari911 filterIndicator = document.createElement('div'); 2381d05cddcSAtari911 filterIndicator.className = 'calendar-namespace-filter'; 2391d05cddcSAtari911 filterIndicator.id = 'namespace-filter-' + calId; 2401d05cddcSAtari911 headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling); 2411d05cddcSAtari911 } 2421d05cddcSAtari911 } 2431d05cddcSAtari911 2441d05cddcSAtari911 if (filterIndicator) { 2451d05cddcSAtari911 filterIndicator.innerHTML = 2461d05cddcSAtari911 '<span class="namespace-filter-label">Filtering:</span>' + 2471d05cddcSAtari911 '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' + 2481d05cddcSAtari911 '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>'; 2491d05cddcSAtari911 filterIndicator.style.display = 'flex'; 2501d05cddcSAtari911 } 2511d05cddcSAtari911 } else { 2521d05cddcSAtari911 // Hide filter indicator 2531d05cddcSAtari911 if (filterIndicator) { 2541d05cddcSAtari911 filterIndicator.style.display = 'none'; 2551d05cddcSAtari911 } 2561d05cddcSAtari911 } 2571d05cddcSAtari911 2581d05cddcSAtari911 // Update container's namespace attribute 2591d05cddcSAtari911 container.setAttribute('data-namespace', namespace || ''); 2601d05cddcSAtari911 2611d05cddcSAtari911 // Update nav buttons 2621d05cddcSAtari911 let prevMonth = month - 1; 2631d05cddcSAtari911 let prevYear = year; 2641d05cddcSAtari911 if (prevMonth < 1) { 2651d05cddcSAtari911 prevMonth = 12; 2661d05cddcSAtari911 prevYear--; 2671d05cddcSAtari911 } 2681d05cddcSAtari911 2691d05cddcSAtari911 let nextMonth = month + 1; 2701d05cddcSAtari911 let nextYear = year; 2711d05cddcSAtari911 if (nextMonth > 12) { 2721d05cddcSAtari911 nextMonth = 1; 2731d05cddcSAtari911 nextYear++; 2741d05cddcSAtari911 } 2751d05cddcSAtari911 2761d05cddcSAtari911 const navBtns = container.querySelectorAll('.cal-nav-btn'); 2771d05cddcSAtari911 navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 2781d05cddcSAtari911 navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 2791d05cddcSAtari911 2801d05cddcSAtari911 // Rebuild calendar grid 2811d05cddcSAtari911 const tbody = container.querySelector('.calendar-compact-grid tbody'); 2821d05cddcSAtari911 const firstDay = new Date(year, month - 1, 1); 2831d05cddcSAtari911 const daysInMonth = new Date(year, month, 0).getDate(); 2841d05cddcSAtari911 const dayOfWeek = firstDay.getDay(); 2851d05cddcSAtari911 2861d05cddcSAtari911 // Calculate month boundaries 2871d05cddcSAtari911 const monthStart = new Date(year, month - 1, 1); 2881d05cddcSAtari911 const monthEnd = new Date(year, month - 1, daysInMonth); 2891d05cddcSAtari911 2901d05cddcSAtari911 // Build a map of all events with their date ranges 2911d05cddcSAtari911 const eventRanges = {}; 2921d05cddcSAtari911 for (const [dateKey, dayEvents] of Object.entries(events)) { 2931d05cddcSAtari911 // Defensive check: ensure dayEvents is an array 2941d05cddcSAtari911 if (!Array.isArray(dayEvents)) { 2951d05cddcSAtari911 console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents); 2961d05cddcSAtari911 continue; 2971d05cddcSAtari911 } 2981d05cddcSAtari911 2991d05cddcSAtari911 // Only process events that could possibly overlap with this month/year 3001d05cddcSAtari911 const dateYear = parseInt(dateKey.split('-')[0]); 3011d05cddcSAtari911 3021d05cddcSAtari911 // Skip events from completely different years (unless they're very long multi-day events) 3031d05cddcSAtari911 if (Math.abs(dateYear - year) > 1) { 3041d05cddcSAtari911 continue; 3051d05cddcSAtari911 } 3061d05cddcSAtari911 3071d05cddcSAtari911 for (const evt of dayEvents) { 3081d05cddcSAtari911 const startDate = dateKey; 3091d05cddcSAtari911 const endDate = evt.endDate || dateKey; 3101d05cddcSAtari911 3111d05cddcSAtari911 // Check if event overlaps with current month 3121d05cddcSAtari911 const eventStart = new Date(startDate + 'T00:00:00'); 3131d05cddcSAtari911 const eventEnd = new Date(endDate + 'T00:00:00'); 3141d05cddcSAtari911 3151d05cddcSAtari911 // Skip if event doesn't overlap with current month 3161d05cddcSAtari911 if (eventEnd < monthStart || eventStart > monthEnd) { 3171d05cddcSAtari911 continue; 3181d05cddcSAtari911 } 3191d05cddcSAtari911 3201d05cddcSAtari911 // Create entry for each day the event spans 3211d05cddcSAtari911 const start = new Date(startDate + 'T00:00:00'); 3221d05cddcSAtari911 const end = new Date(endDate + 'T00:00:00'); 3231d05cddcSAtari911 const current = new Date(start); 3241d05cddcSAtari911 3251d05cddcSAtari911 while (current <= end) { 3261d05cddcSAtari911 const currentKey = current.toISOString().split('T')[0]; 3271d05cddcSAtari911 3281d05cddcSAtari911 // Check if this date is in current month 3291d05cddcSAtari911 const currentDate = new Date(currentKey + 'T00:00:00'); 3301d05cddcSAtari911 if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) { 3311d05cddcSAtari911 if (!eventRanges[currentKey]) { 3321d05cddcSAtari911 eventRanges[currentKey] = []; 3331d05cddcSAtari911 } 3341d05cddcSAtari911 3351d05cddcSAtari911 // Add event with span information 3361d05cddcSAtari911 const eventCopy = {...evt}; 3371d05cddcSAtari911 eventCopy._span_start = startDate; 3381d05cddcSAtari911 eventCopy._span_end = endDate; 3391d05cddcSAtari911 eventCopy._is_first_day = (currentKey === startDate); 3401d05cddcSAtari911 eventCopy._is_last_day = (currentKey === endDate); 3411d05cddcSAtari911 eventCopy._original_date = dateKey; 3421d05cddcSAtari911 3431d05cddcSAtari911 // Check if event continues from previous month or to next month 3441d05cddcSAtari911 eventCopy._continues_from_prev = (eventStart < monthStart); 3451d05cddcSAtari911 eventCopy._continues_to_next = (eventEnd > monthEnd); 3461d05cddcSAtari911 3471d05cddcSAtari911 eventRanges[currentKey].push(eventCopy); 3481d05cddcSAtari911 } 3491d05cddcSAtari911 3501d05cddcSAtari911 current.setDate(current.getDate() + 1); 3511d05cddcSAtari911 } 3521d05cddcSAtari911 } 3531d05cddcSAtari911 } 3541d05cddcSAtari911 3551d05cddcSAtari911 let html = ''; 3561d05cddcSAtari911 let currentDay = 1; 3571d05cddcSAtari911 const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7); 3581d05cddcSAtari911 3591d05cddcSAtari911 for (let row = 0; row < rowCount; row++) { 3601d05cddcSAtari911 html += '<tr>'; 3611d05cddcSAtari911 for (let col = 0; col < 7; col++) { 3621d05cddcSAtari911 if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) { 363*9ccd446eSAtari911 html += `<td class="cal-empty"></td>`; 3641d05cddcSAtari911 } else { 3651d05cddcSAtari911 const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 3661d05cddcSAtari911 3671d05cddcSAtari911 // Get today's date in local timezone 3681d05cddcSAtari911 const todayObj = new Date(); 3691d05cddcSAtari911 const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`; 3701d05cddcSAtari911 3711d05cddcSAtari911 const isToday = dateKey === today; 3721d05cddcSAtari911 const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0; 3731d05cddcSAtari911 3741d05cddcSAtari911 let classes = 'cal-day'; 3751d05cddcSAtari911 if (isToday) classes += ' cal-today'; 3761d05cddcSAtari911 if (hasEvents) classes += ' cal-has-events'; 3771d05cddcSAtari911 378*9ccd446eSAtari911 const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num'; 379*9ccd446eSAtari911 3801d05cddcSAtari911 html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`; 381*9ccd446eSAtari911 html += `<span class="${dayNumClass}">${currentDay}</span>`; 3821d05cddcSAtari911 3831d05cddcSAtari911 if (hasEvents) { 3841d05cddcSAtari911 // Sort events by time (no time first, then by time) 3851d05cddcSAtari911 const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => { 3861d05cddcSAtari911 const timeA = a.time || ''; 3871d05cddcSAtari911 const timeB = b.time || ''; 3881d05cddcSAtari911 if (!timeA && timeB) return -1; 3891d05cddcSAtari911 if (timeA && !timeB) return 1; 3901d05cddcSAtari911 if (!timeA && !timeB) return 0; 3911d05cddcSAtari911 return timeA.localeCompare(timeB); 3921d05cddcSAtari911 }); 3931d05cddcSAtari911 3941d05cddcSAtari911 // Show colored stacked bars for each event 3951d05cddcSAtari911 html += '<div class="event-indicators">'; 3961d05cddcSAtari911 for (const evt of sortedEvents) { 3971d05cddcSAtari911 const eventId = evt.id || ''; 3981d05cddcSAtari911 const eventColor = evt.color || '#3498db'; 3991d05cddcSAtari911 const eventTitle = evt.title || 'Event'; 400*9ccd446eSAtari911 const eventTime = evt.time || ''; 4011d05cddcSAtari911 const originalDate = evt._original_date || dateKey; 4021d05cddcSAtari911 const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true; 4031d05cddcSAtari911 const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true; 4041d05cddcSAtari911 4051d05cddcSAtari911 let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed'; 4061d05cddcSAtari911 if (!isFirstDay) barClass += ' event-bar-continues'; 4071d05cddcSAtari911 if (!isLastDay) barClass += ' event-bar-continuing'; 4081d05cddcSAtari911 4091d05cddcSAtari911 html += `<span class="event-bar ${barClass}" `; 4101d05cddcSAtari911 html += `style="background: ${eventColor};" `; 4111d05cddcSAtari911 html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `; 4121d05cddcSAtari911 html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');"></span>`; 4131d05cddcSAtari911 } 4141d05cddcSAtari911 html += '</div>'; 4151d05cddcSAtari911 } 4161d05cddcSAtari911 4171d05cddcSAtari911 html += '</td>'; 4181d05cddcSAtari911 currentDay++; 4191d05cddcSAtari911 } 4201d05cddcSAtari911 } 4211d05cddcSAtari911 html += '</tr>'; 4221d05cddcSAtari911 } 4231d05cddcSAtari911 4241d05cddcSAtari911 tbody.innerHTML = html; 4251d05cddcSAtari911 4261d05cddcSAtari911 // Update Today button with current namespace 4271d05cddcSAtari911 const todayBtn = container.querySelector('.cal-today-btn'); 4281d05cddcSAtari911 if (todayBtn) { 4291d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`); 4301d05cddcSAtari911 } 4311d05cddcSAtari911 4321d05cddcSAtari911 // Update month picker with current namespace 4331d05cddcSAtari911 const monthPicker = container.querySelector('.calendar-month-picker'); 4341d05cddcSAtari911 if (monthPicker) { 4351d05cddcSAtari911 monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`); 4361d05cddcSAtari911 } 4371d05cddcSAtari911 4381d05cddcSAtari911 // Rebuild event list - server already filtered to current month 4391d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 4401d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 4411d05cddcSAtari911 4421d05cddcSAtari911 // Auto-scroll to first future event (past events will be above viewport) 4431d05cddcSAtari911 setTimeout(() => { 4441d05cddcSAtari911 const firstFuture = eventList.querySelector('[data-first-future="true"]'); 4451d05cddcSAtari911 if (firstFuture) { 4461d05cddcSAtari911 firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' }); 4471d05cddcSAtari911 } 4481d05cddcSAtari911 }, 100); 4491d05cddcSAtari911 4501d05cddcSAtari911 // Update title 4511d05cddcSAtari911 const title = container.querySelector('#eventlist-title-' + calId); 4521d05cddcSAtari911 title.textContent = 'Events'; 4531d05cddcSAtari911}; 4541d05cddcSAtari911 4551d05cddcSAtari911// Render event list from data 4561d05cddcSAtari911window.renderEventListFromData = function(events, calId, namespace, year, month) { 4571d05cddcSAtari911 if (!events || Object.keys(events).length === 0) { 4581d05cddcSAtari911 return '<p class="no-events-msg">No events this month</p>'; 4591d05cddcSAtari911 } 4601d05cddcSAtari911 461*9ccd446eSAtari911 // Get theme data from container 462*9ccd446eSAtari911 const container = document.getElementById(calId); 463*9ccd446eSAtari911 let themeStyles = {}; 464*9ccd446eSAtari911 if (container && container.dataset.themeStyles) { 465*9ccd446eSAtari911 try { 466*9ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 467*9ccd446eSAtari911 } catch (e) { 468*9ccd446eSAtari911 console.error('Failed to parse theme styles in renderEventListFromData:', e); 469*9ccd446eSAtari911 } 470*9ccd446eSAtari911 } 471*9ccd446eSAtari911 4721d05cddcSAtari911 // Check for time conflicts 4731d05cddcSAtari911 events = checkTimeConflicts(events, null); 4741d05cddcSAtari911 4751d05cddcSAtari911 let pastHtml = ''; 4761d05cddcSAtari911 let futureHtml = ''; 4771d05cddcSAtari911 let pastCount = 0; 4781d05cddcSAtari911 4791d05cddcSAtari911 const sortedDates = Object.keys(events).sort(); 4801d05cddcSAtari911 const today = new Date(); 4811d05cddcSAtari911 today.setHours(0, 0, 0, 0); 4821d05cddcSAtari911 const todayStr = today.toISOString().split('T')[0]; 4831d05cddcSAtari911 4841d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period) 4851d05cddcSAtari911 const isEventPast = function(dateKey, time) { 4861d05cddcSAtari911 // If event is on a past date, it's definitely past 4871d05cddcSAtari911 if (dateKey < todayStr) { 4881d05cddcSAtari911 return true; 4891d05cddcSAtari911 } 4901d05cddcSAtari911 4911d05cddcSAtari911 // If event is on a future date, it's definitely not past 4921d05cddcSAtari911 if (dateKey > todayStr) { 4931d05cddcSAtari911 return false; 4941d05cddcSAtari911 } 4951d05cddcSAtari911 4961d05cddcSAtari911 // Event is today - check time with grace period 4971d05cddcSAtari911 if (time && time.trim() !== '') { 4981d05cddcSAtari911 try { 4991d05cddcSAtari911 const now = new Date(); 5001d05cddcSAtari911 const eventDateTime = new Date(dateKey + 'T' + time); 5011d05cddcSAtari911 5021d05cddcSAtari911 // Add 15-minute grace period 5031d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 5041d05cddcSAtari911 5051d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 5061d05cddcSAtari911 return now > gracePeriodEnd; 5071d05cddcSAtari911 } catch (e) { 5081d05cddcSAtari911 // If time parsing fails, treat as future 5091d05cddcSAtari911 return false; 5101d05cddcSAtari911 } 5111d05cddcSAtari911 } 5121d05cddcSAtari911 5131d05cddcSAtari911 // No time specified for today's event, treat as future 5141d05cddcSAtari911 return false; 5151d05cddcSAtari911 }; 5161d05cddcSAtari911 5171d05cddcSAtari911 // Filter events to only current month if year/month provided 5181d05cddcSAtari911 const monthStart = year && month ? new Date(year, month - 1, 1) : null; 5191d05cddcSAtari911 const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null; 5201d05cddcSAtari911 5211d05cddcSAtari911 for (const dateKey of sortedDates) { 5221d05cddcSAtari911 // Skip events not in current month if filtering 5231d05cddcSAtari911 if (monthStart && monthEnd) { 5241d05cddcSAtari911 const eventDate = new Date(dateKey + 'T00:00:00'); 5251d05cddcSAtari911 5261d05cddcSAtari911 if (eventDate < monthStart || eventDate > monthEnd) { 5271d05cddcSAtari911 continue; 5281d05cddcSAtari911 } 5291d05cddcSAtari911 } 5301d05cddcSAtari911 5311d05cddcSAtari911 // Sort events within this day by time (all-day events at top) 5321d05cddcSAtari911 const dayEvents = events[dateKey]; 5331d05cddcSAtari911 dayEvents.sort((a, b) => { 5341d05cddcSAtari911 const timeA = a.time && a.time.trim() !== '' ? a.time : null; 5351d05cddcSAtari911 const timeB = b.time && b.time.trim() !== '' ? b.time : null; 5361d05cddcSAtari911 5371d05cddcSAtari911 // All-day events (no time) go to the TOP 5381d05cddcSAtari911 if (timeA === null && timeB !== null) return -1; // A before B 5391d05cddcSAtari911 if (timeA !== null && timeB === null) return 1; // A after B 5401d05cddcSAtari911 if (timeA === null && timeB === null) return 0; // Both all-day, equal 5411d05cddcSAtari911 5421d05cddcSAtari911 // Both have times, sort chronologically 5431d05cddcSAtari911 return timeA.localeCompare(timeB); 5441d05cddcSAtari911 }); 5451d05cddcSAtari911 5461d05cddcSAtari911 for (const event of dayEvents) { 5471d05cddcSAtari911 const isTask = event.isTask || false; 5481d05cddcSAtari911 const completed = event.completed || false; 5491d05cddcSAtari911 5501d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 5511d05cddcSAtari911 const isPast = isEventPast(dateKey, event.time); 5521d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 5531d05cddcSAtari911 5541d05cddcSAtari911 // Determine if this goes in past section 5551d05cddcSAtari911 const isPastOrCompleted = (isPast && (!isTask || completed)) || completed; 5561d05cddcSAtari911 5571d05cddcSAtari911 const eventHtml = renderEventItem(event, dateKey, calId, namespace); 5581d05cddcSAtari911 5591d05cddcSAtari911 if (isPastOrCompleted) { 5601d05cddcSAtari911 pastCount++; 5611d05cddcSAtari911 pastHtml += eventHtml; 5621d05cddcSAtari911 } else { 5631d05cddcSAtari911 futureHtml += eventHtml; 5641d05cddcSAtari911 } 5651d05cddcSAtari911 } 5661d05cddcSAtari911 } 5671d05cddcSAtari911 5681d05cddcSAtari911 let html = ''; 5691d05cddcSAtari911 5701d05cddcSAtari911 // Add collapsible past events section if any exist 5711d05cddcSAtari911 if (pastCount > 0) { 5721d05cddcSAtari911 html += '<div class="past-events-section">'; 5731d05cddcSAtari911 html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">'; 5741d05cddcSAtari911 html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> '; 5751d05cddcSAtari911 html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>'; 5761d05cddcSAtari911 html += '</div>'; 5771d05cddcSAtari911 html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">'; 5781d05cddcSAtari911 html += pastHtml; 5791d05cddcSAtari911 html += '</div>'; 5801d05cddcSAtari911 html += '</div>'; 5811d05cddcSAtari911 } else { 5821d05cddcSAtari911 } 5831d05cddcSAtari911 5841d05cddcSAtari911 // Add future events 5851d05cddcSAtari911 html += futureHtml; 5861d05cddcSAtari911 5871d05cddcSAtari911 5881d05cddcSAtari911 if (!html) { 5891d05cddcSAtari911 return '<p class="no-events-msg">No events this month</p>'; 5901d05cddcSAtari911 } 5911d05cddcSAtari911 5921d05cddcSAtari911 return html; 5931d05cddcSAtari911}; 5941d05cddcSAtari911 5951d05cddcSAtari911// Show day popup with events when clicking a date 5961d05cddcSAtari911window.showDayPopup = function(calId, date, namespace) { 5971d05cddcSAtari911 // Get events for this calendar 5981d05cddcSAtari911 const eventsDataEl = document.getElementById('events-data-' + calId); 5991d05cddcSAtari911 let events = {}; 6001d05cddcSAtari911 6011d05cddcSAtari911 if (eventsDataEl) { 6021d05cddcSAtari911 try { 6031d05cddcSAtari911 events = JSON.parse(eventsDataEl.textContent); 6041d05cddcSAtari911 } catch (e) { 6051d05cddcSAtari911 console.error('Failed to parse events data:', e); 6061d05cddcSAtari911 } 6071d05cddcSAtari911 } 6081d05cddcSAtari911 6091d05cddcSAtari911 const dayEvents = events[date] || []; 6101d05cddcSAtari911 6111d05cddcSAtari911 // Check for conflicts on this day 6121d05cddcSAtari911 const dayEventsObj = {[date]: dayEvents}; 6131d05cddcSAtari911 const checkedEvents = checkTimeConflicts(dayEventsObj, null); 6141d05cddcSAtari911 const dayEventsWithConflicts = checkedEvents[date] || dayEvents; 6151d05cddcSAtari911 6161d05cddcSAtari911 // Sort events: all-day at top, then chronological by time 6171d05cddcSAtari911 dayEventsWithConflicts.sort((a, b) => { 6181d05cddcSAtari911 const timeA = a.time && a.time.trim() !== '' ? a.time : null; 6191d05cddcSAtari911 const timeB = b.time && b.time.trim() !== '' ? b.time : null; 6201d05cddcSAtari911 6211d05cddcSAtari911 // All-day events (no time) go to the TOP 6221d05cddcSAtari911 if (timeA === null && timeB !== null) return -1; // A before B 6231d05cddcSAtari911 if (timeA !== null && timeB === null) return 1; // A after B 6241d05cddcSAtari911 if (timeA === null && timeB === null) return 0; // Both all-day, equal 6251d05cddcSAtari911 6261d05cddcSAtari911 // Both have times, sort chronologically 6271d05cddcSAtari911 return timeA.localeCompare(timeB); 6281d05cddcSAtari911 }); 6291d05cddcSAtari911 6301d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 6311d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 6321d05cddcSAtari911 weekday: 'long', 6331d05cddcSAtari911 month: 'long', 6341d05cddcSAtari911 day: 'numeric', 6351d05cddcSAtari911 year: 'numeric' 6361d05cddcSAtari911 }); 6371d05cddcSAtari911 6381d05cddcSAtari911 // Create popup 6391d05cddcSAtari911 let popup = document.getElementById('day-popup-' + calId); 6401d05cddcSAtari911 if (!popup) { 6411d05cddcSAtari911 popup = document.createElement('div'); 6421d05cddcSAtari911 popup.id = 'day-popup-' + calId; 6431d05cddcSAtari911 popup.className = 'day-popup'; 6441d05cddcSAtari911 document.body.appendChild(popup); 6451d05cddcSAtari911 } 6461d05cddcSAtari911 647*9ccd446eSAtari911 // Get theme styles 648*9ccd446eSAtari911 const container = document.getElementById(calId); 649*9ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 650*9ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 651*9ccd446eSAtari911 6521d05cddcSAtari911 let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>'; 6531d05cddcSAtari911 html += '<div class="day-popup-content">'; 6541d05cddcSAtari911 html += '<div class="day-popup-header">'; 6551d05cddcSAtari911 html += '<h4>' + displayDate + '</h4>'; 6561d05cddcSAtari911 html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>'; 6571d05cddcSAtari911 html += '</div>'; 6581d05cddcSAtari911 6591d05cddcSAtari911 html += '<div class="day-popup-body">'; 6601d05cddcSAtari911 6611d05cddcSAtari911 if (dayEventsWithConflicts.length === 0) { 6621d05cddcSAtari911 html += '<p class="no-events-msg">No events on this day</p>'; 6631d05cddcSAtari911 } else { 6641d05cddcSAtari911 html += '<div class="popup-events-list">'; 6651d05cddcSAtari911 dayEventsWithConflicts.forEach(event => { 6661d05cddcSAtari911 const color = event.color || '#3498db'; 6671d05cddcSAtari911 6681d05cddcSAtari911 // Use individual event namespace if available (for multi-namespace support) 6691d05cddcSAtari911 const eventNamespace = event._namespace !== undefined ? event._namespace : namespace; 6701d05cddcSAtari911 6711d05cddcSAtari911 // Check if this is a continuation (event started before this date) 6721d05cddcSAtari911 const originalStartDate = event.originalStartDate || event._dateKey || date; 6731d05cddcSAtari911 const isContinuation = originalStartDate < date; 6741d05cddcSAtari911 6751d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 6761d05cddcSAtari911 let displayTime = ''; 6771d05cddcSAtari911 if (event.time) { 6781d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 6791d05cddcSAtari911 } 6801d05cddcSAtari911 6811d05cddcSAtari911 // Multi-day indicator 6821d05cddcSAtari911 let multiDay = ''; 6831d05cddcSAtari911 if (event.endDate && event.endDate !== date) { 6841d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 6851d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 6861d05cddcSAtari911 month: 'short', 6871d05cddcSAtari911 day: 'numeric' 6881d05cddcSAtari911 }); 6891d05cddcSAtari911 } 6901d05cddcSAtari911 6911d05cddcSAtari911 // Continuation message 6921d05cddcSAtari911 if (isContinuation) { 6931d05cddcSAtari911 const startObj = new Date(originalStartDate + 'T00:00:00'); 6941d05cddcSAtari911 const startDisplay = startObj.toLocaleDateString('en-US', { 6951d05cddcSAtari911 weekday: 'short', 6961d05cddcSAtari911 month: 'short', 6971d05cddcSAtari911 day: 'numeric' 6981d05cddcSAtari911 }); 6991d05cddcSAtari911 html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>'; 7001d05cddcSAtari911 } 7011d05cddcSAtari911 7021d05cddcSAtari911 html += '<div class="popup-event-item">'; 7031d05cddcSAtari911 html += '<div class="event-color-bar" style="background: ' + color + ';"></div>'; 7041d05cddcSAtari911 html += '<div class="popup-event-content">'; 7051d05cddcSAtari911 7061d05cddcSAtari911 // Single line with title, time, date range, namespace, and actions 7071d05cddcSAtari911 html += '<div class="popup-event-main-row">'; 7081d05cddcSAtari911 html += '<div class="popup-event-info-inline">'; 7091d05cddcSAtari911 html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>'; 7101d05cddcSAtari911 if (displayTime) { 7111d05cddcSAtari911 html += '<span class="popup-event-time"> ' + displayTime + '</span>'; 7121d05cddcSAtari911 } 7131d05cddcSAtari911 if (multiDay) { 7141d05cddcSAtari911 html += '<span class="popup-event-multiday">' + multiDay + '</span>'; 7151d05cddcSAtari911 } 7161d05cddcSAtari911 if (eventNamespace) { 7171d05cddcSAtari911 html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>'; 7181d05cddcSAtari911 } 7191d05cddcSAtari911 7201d05cddcSAtari911 // Add conflict warning badge if event has conflicts 7211d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 7221d05cddcSAtari911 // Build conflict list for tooltip 7231d05cddcSAtari911 let conflictList = []; 7241d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 7251d05cddcSAtari911 let conflictText = conflict.title; 7261d05cddcSAtari911 if (conflict.time) { 7271d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 7281d05cddcSAtari911 } 7291d05cddcSAtari911 conflictList.push(conflictText); 7301d05cddcSAtari911 }); 7311d05cddcSAtari911 732*9ccd446eSAtari911 html += '<span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 7331d05cddcSAtari911 } 7341d05cddcSAtari911 7351d05cddcSAtari911 html += '</div>'; 7361d05cddcSAtari911 html += '<div class="popup-event-actions">'; 7371d05cddcSAtari911 html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>'; 7381d05cddcSAtari911 html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">️</button>'; 7391d05cddcSAtari911 html += '</div>'; 7401d05cddcSAtari911 html += '</div>'; 7411d05cddcSAtari911 7421d05cddcSAtari911 // Description on separate line if present 7431d05cddcSAtari911 if (event.description) { 7441d05cddcSAtari911 html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>'; 7451d05cddcSAtari911 } 7461d05cddcSAtari911 7471d05cddcSAtari911 html += '</div></div>'; 7481d05cddcSAtari911 }); 7491d05cddcSAtari911 html += '</div>'; 7501d05cddcSAtari911 } 7511d05cddcSAtari911 7521d05cddcSAtari911 html += '</div>'; 7531d05cddcSAtari911 7541d05cddcSAtari911 html += '<div class="day-popup-footer">'; 7551d05cddcSAtari911 html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>'; 7561d05cddcSAtari911 html += '</div>'; 7571d05cddcSAtari911 7581d05cddcSAtari911 html += '</div>'; 7591d05cddcSAtari911 7601d05cddcSAtari911 popup.innerHTML = html; 7611d05cddcSAtari911 popup.style.display = 'flex'; 762*9ccd446eSAtari911 763*9ccd446eSAtari911 // Propagate CSS vars from calendar container to popup (popup is outside container in DOM) 764*9ccd446eSAtari911 if (container) { 765*9ccd446eSAtari911 propagateThemeVars(calId, popup.querySelector('.day-popup-content')); 766*9ccd446eSAtari911 } 7671d05cddcSAtari911}; 7681d05cddcSAtari911 7691d05cddcSAtari911// Close day popup 7701d05cddcSAtari911window.closeDayPopup = function(calId) { 7711d05cddcSAtari911 const popup = document.getElementById('day-popup-' + calId); 7721d05cddcSAtari911 if (popup) { 7731d05cddcSAtari911 popup.style.display = 'none'; 7741d05cddcSAtari911 } 7751d05cddcSAtari911}; 7761d05cddcSAtari911 7771d05cddcSAtari911// Show events for a specific day (for event list panel) 7781d05cddcSAtari911window.showDayEvents = function(calId, date, namespace) { 7791d05cddcSAtari911 const params = new URLSearchParams({ 7801d05cddcSAtari911 call: 'plugin_calendar', 7811d05cddcSAtari911 action: 'load_month', 7821d05cddcSAtari911 year: date.split('-')[0], 7831d05cddcSAtari911 month: parseInt(date.split('-')[1]), 7841d05cddcSAtari911 namespace: namespace, 7851d05cddcSAtari911 _: new Date().getTime() // Cache buster 7861d05cddcSAtari911 }); 7871d05cddcSAtari911 7881d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 7891d05cddcSAtari911 method: 'POST', 7901d05cddcSAtari911 headers: { 7911d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 7921d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 7931d05cddcSAtari911 'Pragma': 'no-cache' 7941d05cddcSAtari911 }, 7951d05cddcSAtari911 body: params.toString() 7961d05cddcSAtari911 }) 7971d05cddcSAtari911 .then(r => r.json()) 7981d05cddcSAtari911 .then(data => { 7991d05cddcSAtari911 if (data.success) { 8001d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 8011d05cddcSAtari911 const events = data.events; 8021d05cddcSAtari911 const title = document.getElementById('eventlist-title-' + calId); 8031d05cddcSAtari911 8041d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 8051d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 8061d05cddcSAtari911 weekday: 'short', 8071d05cddcSAtari911 month: 'short', 8081d05cddcSAtari911 day: 'numeric' 8091d05cddcSAtari911 }); 8101d05cddcSAtari911 8111d05cddcSAtari911 title.textContent = 'Events - ' + displayDate; 8121d05cddcSAtari911 8131d05cddcSAtari911 // Filter events for this day 8141d05cddcSAtari911 const dayEvents = events[date] || []; 8151d05cddcSAtari911 8161d05cddcSAtari911 if (dayEvents.length === 0) { 8171d05cddcSAtari911 eventList.innerHTML = '<p class="no-events-msg">No events on this day<br><button class="add-event-compact" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\')">+ Add Event</button></p>'; 8181d05cddcSAtari911 } else { 8191d05cddcSAtari911 let html = ''; 8201d05cddcSAtari911 dayEvents.forEach(event => { 8211d05cddcSAtari911 html += renderEventItem(event, date, calId, namespace); 8221d05cddcSAtari911 }); 8231d05cddcSAtari911 eventList.innerHTML = html; 8241d05cddcSAtari911 } 8251d05cddcSAtari911 } 8261d05cddcSAtari911 }) 8271d05cddcSAtari911 .catch(err => console.error('Error:', err)); 8281d05cddcSAtari911}; 8291d05cddcSAtari911 8301d05cddcSAtari911// Render a single event item 8311d05cddcSAtari911window.renderEventItem = function(event, date, calId, namespace) { 832*9ccd446eSAtari911 // Get theme data from container 833*9ccd446eSAtari911 const container = document.getElementById(calId); 834*9ccd446eSAtari911 let themeStyles = {}; 835*9ccd446eSAtari911 if (container && container.dataset.themeStyles) { 836*9ccd446eSAtari911 try { 837*9ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 838*9ccd446eSAtari911 } catch (e) { 839*9ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 840*9ccd446eSAtari911 } 841*9ccd446eSAtari911 } 842*9ccd446eSAtari911 8431d05cddcSAtari911 // Check if this event is in the past or today (with 15-minute grace period) 8441d05cddcSAtari911 const today = new Date(); 8451d05cddcSAtari911 today.setHours(0, 0, 0, 0); 8461d05cddcSAtari911 const todayStr = today.toISOString().split('T')[0]; 8471d05cddcSAtari911 const eventDate = new Date(date + 'T00:00:00'); 8481d05cddcSAtari911 8491d05cddcSAtari911 // Helper to determine if event is past with grace period 8501d05cddcSAtari911 let isPast; 8511d05cddcSAtari911 if (date < todayStr) { 8521d05cddcSAtari911 isPast = true; // Past date 8531d05cddcSAtari911 } else if (date > todayStr) { 8541d05cddcSAtari911 isPast = false; // Future date 8551d05cddcSAtari911 } else { 8561d05cddcSAtari911 // Today - check time with grace period 8571d05cddcSAtari911 if (event.time && event.time.trim() !== '') { 8581d05cddcSAtari911 try { 8591d05cddcSAtari911 const now = new Date(); 8601d05cddcSAtari911 const eventDateTime = new Date(date + 'T' + event.time); 8611d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 8621d05cddcSAtari911 isPast = now > gracePeriodEnd; 8631d05cddcSAtari911 } catch (e) { 8641d05cddcSAtari911 isPast = false; 8651d05cddcSAtari911 } 8661d05cddcSAtari911 } else { 8671d05cddcSAtari911 isPast = false; // No time, treat as future 8681d05cddcSAtari911 } 8691d05cddcSAtari911 } 8701d05cddcSAtari911 8711d05cddcSAtari911 const isToday = eventDate.getTime() === today.getTime(); 8721d05cddcSAtari911 8731d05cddcSAtari911 // Format date display with day of week 8741d05cddcSAtari911 const displayDateKey = event.originalStartDate || date; 8751d05cddcSAtari911 const dateObj = new Date(displayDateKey + 'T00:00:00'); 8761d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 8771d05cddcSAtari911 weekday: 'short', 8781d05cddcSAtari911 month: 'short', 8791d05cddcSAtari911 day: 'numeric' 8801d05cddcSAtari911 }); 8811d05cddcSAtari911 8821d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 8831d05cddcSAtari911 let displayTime = ''; 8841d05cddcSAtari911 if (event.time) { 8851d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 8861d05cddcSAtari911 } 8871d05cddcSAtari911 8881d05cddcSAtari911 // Multi-day indicator 8891d05cddcSAtari911 let multiDay = ''; 8901d05cddcSAtari911 if (event.endDate && event.endDate !== displayDateKey) { 8911d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 8921d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 8931d05cddcSAtari911 weekday: 'short', 8941d05cddcSAtari911 month: 'short', 8951d05cddcSAtari911 day: 'numeric' 8961d05cddcSAtari911 }); 8971d05cddcSAtari911 } 8981d05cddcSAtari911 8991d05cddcSAtari911 const completedClass = event.completed ? ' event-completed' : ''; 9001d05cddcSAtari911 const isTask = event.isTask || false; 9011d05cddcSAtari911 const completed = event.completed || false; 9021d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 9031d05cddcSAtari911 const pastClass = (isPast && !isPastDue) ? ' event-past' : ''; 9041d05cddcSAtari911 const pastDueClass = isPastDue ? ' event-pastdue' : ''; 9051d05cddcSAtari911 const color = event.color || '#3498db'; 9061d05cddcSAtari911 907*9ccd446eSAtari911 // Only inline style needed: border-left-color for event color indicator 9081d05cddcSAtari911 let html = '<div class="event-compact-item' + completedClass + pastClass + pastDueClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';" onclick="' + (isPast && !isPastDue ? 'togglePastEventExpand(this)' : '') + '">'; 9091d05cddcSAtari911 9101d05cddcSAtari911 html += '<div class="event-info">'; 9111d05cddcSAtari911 html += '<div class="event-title-row">'; 9121d05cddcSAtari911 html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>'; 9131d05cddcSAtari911 html += '</div>'; 9141d05cddcSAtari911 9151d05cddcSAtari911 // Show meta and description for non-past events AND past due tasks 9161d05cddcSAtari911 if (!isPast || isPastDue) { 9171d05cddcSAtari911 html += '<div class="event-meta-compact">'; 9181d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 9191d05cddcSAtari911 if (displayTime) { 9201d05cddcSAtari911 html += ' • ' + displayTime; 9211d05cddcSAtari911 } 9221d05cddcSAtari911 // Add PAST DUE or TODAY badge 9231d05cddcSAtari911 if (isPastDue) { 9241d05cddcSAtari911 html += ' <span class="event-pastdue-badge">PAST DUE</span>'; 9251d05cddcSAtari911 } else if (isToday) { 9261d05cddcSAtari911 html += ' <span class="event-today-badge">TODAY</span>'; 9271d05cddcSAtari911 } 928*9ccd446eSAtari911 // Add namespace badge 9291d05cddcSAtari911 let eventNamespace = event.namespace || ''; 9301d05cddcSAtari911 if (!eventNamespace && event._namespace !== undefined) { 931*9ccd446eSAtari911 eventNamespace = event._namespace; 9321d05cddcSAtari911 } 9331d05cddcSAtari911 if (eventNamespace) { 934*9ccd446eSAtari911 html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>'; 9351d05cddcSAtari911 } 9361d05cddcSAtari911 // Add conflict warning if event has time conflicts 9371d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 9381d05cddcSAtari911 let conflictList = []; 9391d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 9401d05cddcSAtari911 let conflictText = conflict.title; 9411d05cddcSAtari911 if (conflict.time) { 9421d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 9431d05cddcSAtari911 } 9441d05cddcSAtari911 conflictList.push(conflictText); 9451d05cddcSAtari911 }); 9461d05cddcSAtari911 947*9ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 9481d05cddcSAtari911 } 9491d05cddcSAtari911 html += '</span>'; 9501d05cddcSAtari911 html += '</div>'; 9511d05cddcSAtari911 9521d05cddcSAtari911 if (event.description) { 9531d05cddcSAtari911 html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>'; 9541d05cddcSAtari911 } 9551d05cddcSAtari911 } else { 9561d05cddcSAtari911 // For past events (not past due), store data in hidden divs for expand/collapse 9571d05cddcSAtari911 html += '<div class="event-meta-compact" style="display: none;">'; 9581d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 9591d05cddcSAtari911 if (displayTime) { 9601d05cddcSAtari911 html += ' • ' + displayTime; 9611d05cddcSAtari911 } 9621d05cddcSAtari911 // Add namespace badge for past events too 9631d05cddcSAtari911 let eventNamespace = event.namespace || ''; 9641d05cddcSAtari911 if (!eventNamespace && event._namespace !== undefined) { 9651d05cddcSAtari911 eventNamespace = event._namespace; 9661d05cddcSAtari911 } 9671d05cddcSAtari911 if (eventNamespace) { 968*9ccd446eSAtari911 html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>'; 969*9ccd446eSAtari911 } 970*9ccd446eSAtari911 // Add conflict warning for past events too 971*9ccd446eSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 972*9ccd446eSAtari911 let conflictList = []; 973*9ccd446eSAtari911 event.conflictsWith.forEach(conflict => { 974*9ccd446eSAtari911 let conflictText = conflict.title; 975*9ccd446eSAtari911 if (conflict.time) { 976*9ccd446eSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 977*9ccd446eSAtari911 } 978*9ccd446eSAtari911 conflictList.push(conflictText); 979*9ccd446eSAtari911 }); 980*9ccd446eSAtari911 981*9ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 9821d05cddcSAtari911 } 9831d05cddcSAtari911 html += '</span>'; 9841d05cddcSAtari911 html += '</div>'; 9851d05cddcSAtari911 9861d05cddcSAtari911 if (event.description) { 9871d05cddcSAtari911 html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>'; 9881d05cddcSAtari911 } 9891d05cddcSAtari911 } 9901d05cddcSAtari911 9911d05cddcSAtari911 html += '</div>'; // event-info 9921d05cddcSAtari911 9931d05cddcSAtari911 // Use stored namespace from event, fallback to _namespace, then passed namespace 9941d05cddcSAtari911 let buttonNamespace = event.namespace || ''; 9951d05cddcSAtari911 if (!buttonNamespace && event._namespace !== undefined) { 9961d05cddcSAtari911 buttonNamespace = event._namespace; 9971d05cddcSAtari911 } 9981d05cddcSAtari911 if (!buttonNamespace) { 9991d05cddcSAtari911 buttonNamespace = namespace; 10001d05cddcSAtari911 } 10011d05cddcSAtari911 10021d05cddcSAtari911 html += '<div class="event-actions-compact">'; 10031d05cddcSAtari911 html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">️</button>'; 10041d05cddcSAtari911 html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>'; 10051d05cddcSAtari911 html += '</div>'; 10061d05cddcSAtari911 10071d05cddcSAtari911 // Checkbox for tasks - ON THE FAR RIGHT 10081d05cddcSAtari911 if (isTask) { 10091d05cddcSAtari911 const checked = completed ? 'checked' : ''; 10101d05cddcSAtari911 html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">'; 10111d05cddcSAtari911 } 10121d05cddcSAtari911 10131d05cddcSAtari911 html += '</div>'; 10141d05cddcSAtari911 10151d05cddcSAtari911 return html; 10161d05cddcSAtari911}; 10171d05cddcSAtari911 10181d05cddcSAtari911// Render description with rich content support 10191d05cddcSAtari911window.renderDescription = function(description) { 10201d05cddcSAtari911 if (!description) return ''; 10211d05cddcSAtari911 10221d05cddcSAtari911 // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping) 10231d05cddcSAtari911 // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00 10241d05cddcSAtari911 10251d05cddcSAtari911 let rendered = description; 10261d05cddcSAtari911 const tokens = []; 10271d05cddcSAtari911 let tokenIndex = 0; 10281d05cddcSAtari911 10291d05cddcSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 10301d05cddcSAtari911 rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) { 10311d05cddcSAtari911 imagePath = imagePath.trim(); 10321d05cddcSAtari911 alt = alt ? alt.trim() : ''; 10331d05cddcSAtari911 10341d05cddcSAtari911 let imageHtml; 10351d05cddcSAtari911 // Handle external URLs 10361d05cddcSAtari911 if (imagePath.match(/^https?:\/\//)) { 10371d05cddcSAtari911 imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 10381d05cddcSAtari911 } else { 10391d05cddcSAtari911 // Handle internal DokuWiki images 10401d05cddcSAtari911 const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath); 10411d05cddcSAtari911 imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 10421d05cddcSAtari911 } 10431d05cddcSAtari911 10441d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 10451d05cddcSAtari911 tokens[tokenIndex] = imageHtml; 10461d05cddcSAtari911 tokenIndex++; 10471d05cddcSAtari911 return token; 10481d05cddcSAtari911 }); 10491d05cddcSAtari911 10501d05cddcSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 10511d05cddcSAtari911 rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) { 10521d05cddcSAtari911 link = link.trim(); 10531d05cddcSAtari911 text = text ? text.trim() : link; 10541d05cddcSAtari911 10551d05cddcSAtari911 let linkHtml; 10561d05cddcSAtari911 // Handle external URLs 10571d05cddcSAtari911 if (link.match(/^https?:\/\//)) { 10581d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 10591d05cddcSAtari911 } else { 10601d05cddcSAtari911 // Handle internal DokuWiki links with section anchors 10611d05cddcSAtari911 const hashIndex = link.indexOf('#'); 10621d05cddcSAtari911 let pagePart = link; 10631d05cddcSAtari911 let sectionPart = ''; 10641d05cddcSAtari911 10651d05cddcSAtari911 if (hashIndex !== -1) { 10661d05cddcSAtari911 pagePart = link.substring(0, hashIndex); 10671d05cddcSAtari911 sectionPart = link.substring(hashIndex); // Includes the # 10681d05cddcSAtari911 } 10691d05cddcSAtari911 10701d05cddcSAtari911 const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart; 10711d05cddcSAtari911 linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>'; 10721d05cddcSAtari911 } 10731d05cddcSAtari911 10741d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 10751d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 10761d05cddcSAtari911 tokenIndex++; 10771d05cddcSAtari911 return token; 10781d05cddcSAtari911 }); 10791d05cddcSAtari911 10801d05cddcSAtari911 // Convert markdown-style links [text](url) to tokens 10811d05cddcSAtari911 rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) { 10821d05cddcSAtari911 text = text.trim(); 10831d05cddcSAtari911 url = url.trim(); 10841d05cddcSAtari911 10851d05cddcSAtari911 let linkHtml; 10861d05cddcSAtari911 if (url.match(/^https?:\/\//)) { 10871d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 10881d05cddcSAtari911 } else { 10891d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>'; 10901d05cddcSAtari911 } 10911d05cddcSAtari911 10921d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 10931d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 10941d05cddcSAtari911 tokenIndex++; 10951d05cddcSAtari911 return token; 10961d05cddcSAtari911 }); 10971d05cddcSAtari911 10981d05cddcSAtari911 // Convert plain URLs to tokens 10991d05cddcSAtari911 rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) { 11001d05cddcSAtari911 const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>'; 11011d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 11021d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 11031d05cddcSAtari911 tokenIndex++; 11041d05cddcSAtari911 return token; 11051d05cddcSAtari911 }); 11061d05cddcSAtari911 11071d05cddcSAtari911 // NOW escape the remaining text (tokens are protected with null bytes) 11081d05cddcSAtari911 rendered = escapeHtml(rendered); 11091d05cddcSAtari911 11101d05cddcSAtari911 // Convert newlines to <br> 11111d05cddcSAtari911 rendered = rendered.replace(/\n/g, '<br>'); 11121d05cddcSAtari911 11131d05cddcSAtari911 // DokuWiki text formatting (on escaped text) 11141d05cddcSAtari911 // Bold: **text** or __text__ 11151d05cddcSAtari911 rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); 11161d05cddcSAtari911 rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>'); 11171d05cddcSAtari911 11181d05cddcSAtari911 // Italic: //text// 11191d05cddcSAtari911 rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>'); 11201d05cddcSAtari911 11211d05cddcSAtari911 // Strikethrough: <del>text</del> 11221d05cddcSAtari911 rendered = rendered.replace(/<del>(.+?)<\/del>/g, '<del>$1</del>'); 11231d05cddcSAtari911 11241d05cddcSAtari911 // Monospace: ''text'' 11251d05cddcSAtari911 rendered = rendered.replace(/''(.+?)''/g, '<code>$1</code>'); 11261d05cddcSAtari911 11271d05cddcSAtari911 // Subscript: <sub>text</sub> 11281d05cddcSAtari911 rendered = rendered.replace(/<sub>(.+?)<\/sub>/g, '<sub>$1</sub>'); 11291d05cddcSAtari911 11301d05cddcSAtari911 // Superscript: <sup>text</sup> 11311d05cddcSAtari911 rendered = rendered.replace(/<sup>(.+?)<\/sup>/g, '<sup>$1</sup>'); 11321d05cddcSAtari911 11331d05cddcSAtari911 // Restore tokens (replace with actual HTML) 11341d05cddcSAtari911 for (let i = 0; i < tokens.length; i++) { 11351d05cddcSAtari911 const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g'); 11361d05cddcSAtari911 rendered = rendered.replace(tokenPattern, tokens[i]); 11371d05cddcSAtari911 } 11381d05cddcSAtari911 11391d05cddcSAtari911 return rendered; 11401d05cddcSAtari911} 11411d05cddcSAtari911 11421d05cddcSAtari911// Open add event dialog 11431d05cddcSAtari911window.openAddEvent = function(calId, namespace, date) { 11441d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 11451d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 11461d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 11471d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 11481d05cddcSAtari911 11491d05cddcSAtari911 if (!dateField) { 11501d05cddcSAtari911 console.error('Date field not found! ID: event-date-' + calId); 11511d05cddcSAtari911 return; 11521d05cddcSAtari911 } 11531d05cddcSAtari911 1154231d0edbSAtari911 // Check if there's a filtered namespace active (only for regular calendars) 11551d05cddcSAtari911 const calendar = document.getElementById(calId); 1156231d0edbSAtari911 const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null; 11571d05cddcSAtari911 11581d05cddcSAtari911 // Use filtered namespace if available, otherwise use the passed namespace 11591d05cddcSAtari911 const effectiveNamespace = filteredNamespace || namespace; 11601d05cddcSAtari911 11611d05cddcSAtari911 11621d05cddcSAtari911 // Reset form 11631d05cddcSAtari911 form.reset(); 11641d05cddcSAtari911 document.getElementById('event-id-' + calId).value = ''; 11651d05cddcSAtari911 11661d05cddcSAtari911 // Store the effective namespace in a hidden field or data attribute 11671d05cddcSAtari911 form.dataset.effectiveNamespace = effectiveNamespace; 11681d05cddcSAtari911 11691d05cddcSAtari911 // Set namespace dropdown to effective namespace 11701d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 11711d05cddcSAtari911 if (namespaceSelect) { 11721d05cddcSAtari911 if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) { 11731d05cddcSAtari911 // Set to specific namespace if not wildcard or multi-namespace 11741d05cddcSAtari911 namespaceSelect.value = effectiveNamespace; 11751d05cddcSAtari911 } else { 11761d05cddcSAtari911 // Default to empty (default namespace) for wildcard/multi views 11771d05cddcSAtari911 namespaceSelect.value = ''; 11781d05cddcSAtari911 } 11791d05cddcSAtari911 } 11801d05cddcSAtari911 11811d05cddcSAtari911 // Clear event namespace from previous edits 11821d05cddcSAtari911 delete form.dataset.eventNamespace; 11831d05cddcSAtari911 11841d05cddcSAtari911 // Set date - use local date, not UTC 11851d05cddcSAtari911 let defaultDate = date; 11861d05cddcSAtari911 if (!defaultDate) { 11871d05cddcSAtari911 // Get the currently displayed month from the calendar container 11881d05cddcSAtari911 const container = document.getElementById(calId); 11891d05cddcSAtari911 const displayedYear = parseInt(container.getAttribute('data-year')); 11901d05cddcSAtari911 const displayedMonth = parseInt(container.getAttribute('data-month')); 11911d05cddcSAtari911 11921d05cddcSAtari911 11931d05cddcSAtari911 if (displayedYear && displayedMonth) { 11941d05cddcSAtari911 // Use first day of the displayed month 11951d05cddcSAtari911 const year = displayedYear; 11961d05cddcSAtari911 const month = String(displayedMonth).padStart(2, '0'); 11971d05cddcSAtari911 defaultDate = `${year}-${month}-01`; 11981d05cddcSAtari911 } else { 11991d05cddcSAtari911 // Fallback to today if attributes not found 12001d05cddcSAtari911 const today = new Date(); 12011d05cddcSAtari911 const year = today.getFullYear(); 12021d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 12031d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 12041d05cddcSAtari911 defaultDate = `${year}-${month}-${day}`; 12051d05cddcSAtari911 } 12061d05cddcSAtari911 } 12071d05cddcSAtari911 dateField.value = defaultDate; 12081d05cddcSAtari911 dateField.removeAttribute('data-original-date'); 12091d05cddcSAtari911 12101d05cddcSAtari911 // Also set the end date field to the same default (user can change it) 12111d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 12121d05cddcSAtari911 if (endDateField) { 12131d05cddcSAtari911 endDateField.value = ''; // Empty by default (single-day event) 12141d05cddcSAtari911 // Set min attribute to help the date picker open on the right month 12151d05cddcSAtari911 endDateField.setAttribute('min', defaultDate); 12161d05cddcSAtari911 } 12171d05cddcSAtari911 12181d05cddcSAtari911 // Set default color 12191d05cddcSAtari911 document.getElementById('event-color-' + calId).value = '#3498db'; 12201d05cddcSAtari911 12211d05cddcSAtari911 // Initialize end time dropdown (disabled by default since no start time set) 12221d05cddcSAtari911 const endTimeField = document.getElementById('event-end-time-' + calId); 12231d05cddcSAtari911 if (endTimeField) { 12241d05cddcSAtari911 endTimeField.disabled = true; 12251d05cddcSAtari911 endTimeField.value = ''; 12261d05cddcSAtari911 } 12271d05cddcSAtari911 12281d05cddcSAtari911 // Initialize namespace search 12291d05cddcSAtari911 initNamespaceSearch(calId); 12301d05cddcSAtari911 12311d05cddcSAtari911 // Set title 12321d05cddcSAtari911 title.textContent = 'Add Event'; 12331d05cddcSAtari911 12341d05cddcSAtari911 // Show dialog 12351d05cddcSAtari911 dialog.style.display = 'flex'; 12361d05cddcSAtari911 1237*9ccd446eSAtari911 // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates) 1238*9ccd446eSAtari911 propagateThemeVars(calId, dialog); 1239*9ccd446eSAtari911 12401d05cddcSAtari911 // Focus title field 12411d05cddcSAtari911 setTimeout(() => { 12421d05cddcSAtari911 const titleField = document.getElementById('event-title-' + calId); 12431d05cddcSAtari911 if (titleField) titleField.focus(); 12441d05cddcSAtari911 }, 100); 12451d05cddcSAtari911}; 12461d05cddcSAtari911 12471d05cddcSAtari911// Edit event 12481d05cddcSAtari911window.editEvent = function(calId, eventId, date, namespace) { 12491d05cddcSAtari911 const params = new URLSearchParams({ 12501d05cddcSAtari911 call: 'plugin_calendar', 12511d05cddcSAtari911 action: 'get_event', 12521d05cddcSAtari911 namespace: namespace, 12531d05cddcSAtari911 date: date, 12541d05cddcSAtari911 eventId: eventId 12551d05cddcSAtari911 }); 12561d05cddcSAtari911 12571d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 12581d05cddcSAtari911 method: 'POST', 12591d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 12601d05cddcSAtari911 body: params.toString() 12611d05cddcSAtari911 }) 12621d05cddcSAtari911 .then(r => r.json()) 12631d05cddcSAtari911 .then(data => { 12641d05cddcSAtari911 if (data.success && data.event) { 12651d05cddcSAtari911 const event = data.event; 12661d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 12671d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 12681d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 12691d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 12701d05cddcSAtari911 12711d05cddcSAtari911 if (!dateField) { 12721d05cddcSAtari911 console.error('Date field not found when editing!'); 12731d05cddcSAtari911 return; 12741d05cddcSAtari911 } 12751d05cddcSAtari911 12761d05cddcSAtari911 // Store the event's actual namespace for saving (important for namespace=* views) 12771d05cddcSAtari911 if (event.namespace !== undefined) { 12781d05cddcSAtari911 form.dataset.eventNamespace = event.namespace; 12791d05cddcSAtari911 } 12801d05cddcSAtari911 12811d05cddcSAtari911 // Populate form 12821d05cddcSAtari911 document.getElementById('event-id-' + calId).value = event.id; 12831d05cddcSAtari911 dateField.value = date; 12841d05cddcSAtari911 dateField.setAttribute('data-original-date', date); 12851d05cddcSAtari911 12861d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 12871d05cddcSAtari911 endDateField.value = event.endDate || ''; 12881d05cddcSAtari911 // Set min attribute to help date picker open on the start date's month 12891d05cddcSAtari911 endDateField.setAttribute('min', date); 12901d05cddcSAtari911 12911d05cddcSAtari911 document.getElementById('event-title-' + calId).value = event.title; 12921d05cddcSAtari911 document.getElementById('event-time-' + calId).value = event.time || ''; 12931d05cddcSAtari911 document.getElementById('event-end-time-' + calId).value = event.endTime || ''; 12941d05cddcSAtari911 document.getElementById('event-color-' + calId).value = event.color || '#3498db'; 12951d05cddcSAtari911 document.getElementById('event-desc-' + calId).value = event.description || ''; 12961d05cddcSAtari911 document.getElementById('event-is-task-' + calId).checked = event.isTask || false; 12971d05cddcSAtari911 12981d05cddcSAtari911 // Update end time options based on start time 12991d05cddcSAtari911 if (event.time) { 13001d05cddcSAtari911 updateEndTimeOptions(calId); 13011d05cddcSAtari911 } 13021d05cddcSAtari911 13031d05cddcSAtari911 // Initialize namespace search 13041d05cddcSAtari911 initNamespaceSearch(calId); 13051d05cddcSAtari911 13061d05cddcSAtari911 // Set namespace fields if available 13071d05cddcSAtari911 const namespaceHidden = document.getElementById('event-namespace-' + calId); 13081d05cddcSAtari911 const namespaceSearch = document.getElementById('event-namespace-search-' + calId); 13091d05cddcSAtari911 if (namespaceHidden && event.namespace !== undefined) { 1310*9ccd446eSAtari911 // Set the hidden input (this is what gets submitted) 1311*9ccd446eSAtari911 namespaceHidden.value = event.namespace || ''; 1312*9ccd446eSAtari911 // Set the search input to display the namespace 13131d05cddcSAtari911 if (namespaceSearch) { 13141d05cddcSAtari911 namespaceSearch.value = event.namespace || '(default)'; 13151d05cddcSAtari911 } 1316*9ccd446eSAtari911 console.log('Set namespace for editing:', event.namespace, 'Hidden value:', namespaceHidden.value); 1317*9ccd446eSAtari911 } else { 1318*9ccd446eSAtari911 // No namespace on event, set to default 1319*9ccd446eSAtari911 if (namespaceHidden) { 1320*9ccd446eSAtari911 namespaceHidden.value = ''; 1321*9ccd446eSAtari911 } 1322*9ccd446eSAtari911 if (namespaceSearch) { 1323*9ccd446eSAtari911 namespaceSearch.value = '(default)'; 1324*9ccd446eSAtari911 } 1325*9ccd446eSAtari911 console.log('No namespace on event, using default'); 13261d05cddcSAtari911 } 13271d05cddcSAtari911 13281d05cddcSAtari911 title.textContent = 'Edit Event'; 13291d05cddcSAtari911 dialog.style.display = 'flex'; 1330*9ccd446eSAtari911 1331*9ccd446eSAtari911 // Propagate CSS vars to dialog 1332*9ccd446eSAtari911 propagateThemeVars(calId, dialog); 13331d05cddcSAtari911 } 13341d05cddcSAtari911 }) 13351d05cddcSAtari911 .catch(err => console.error('Error editing event:', err)); 13361d05cddcSAtari911}; 13371d05cddcSAtari911 13381d05cddcSAtari911// Delete event 13391d05cddcSAtari911window.deleteEvent = function(calId, eventId, date, namespace) { 13401d05cddcSAtari911 if (!confirm('Delete this event?')) return; 13411d05cddcSAtari911 13421d05cddcSAtari911 const params = new URLSearchParams({ 13431d05cddcSAtari911 call: 'plugin_calendar', 13441d05cddcSAtari911 action: 'delete_event', 13451d05cddcSAtari911 namespace: namespace, 13461d05cddcSAtari911 date: date, 13471d05cddcSAtari911 eventId: eventId 13481d05cddcSAtari911 }); 13491d05cddcSAtari911 13501d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 13511d05cddcSAtari911 method: 'POST', 13521d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 13531d05cddcSAtari911 body: params.toString() 13541d05cddcSAtari911 }) 13551d05cddcSAtari911 .then(r => r.json()) 13561d05cddcSAtari911 .then(data => { 13571d05cddcSAtari911 if (data.success) { 13581d05cddcSAtari911 // Extract year and month from date 13591d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 13601d05cddcSAtari911 13611d05cddcSAtari911 // Reload calendar data via AJAX 13621d05cddcSAtari911 reloadCalendarData(calId, year, month, namespace); 13631d05cddcSAtari911 } 13641d05cddcSAtari911 }) 13651d05cddcSAtari911 .catch(err => console.error('Error:', err)); 13661d05cddcSAtari911}; 13671d05cddcSAtari911 13681d05cddcSAtari911// Save event (add or edit) 13691d05cddcSAtari911window.saveEventCompact = function(calId, namespace) { 13701d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 13711d05cddcSAtari911 13721d05cddcSAtari911 // Get namespace from dropdown - this is what the user selected 13731d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 13741d05cddcSAtari911 const selectedNamespace = namespaceSelect ? namespaceSelect.value : ''; 13751d05cddcSAtari911 13761d05cddcSAtari911 // ALWAYS use what the user selected in the dropdown 13771d05cddcSAtari911 // This allows changing namespace when editing 13781d05cddcSAtari911 const finalNamespace = selectedNamespace; 13791d05cddcSAtari911 13801d05cddcSAtari911 const eventId = document.getElementById('event-id-' + calId).value; 13811d05cddcSAtari911 13821d05cddcSAtari911 // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event) 13831d05cddcSAtari911 const originalNamespace = form.dataset.eventNamespace; 13841d05cddcSAtari911 13851d05cddcSAtari911 13861d05cddcSAtari911 const dateInput = document.getElementById('event-date-' + calId); 13871d05cddcSAtari911 const date = dateInput.value; 13881d05cddcSAtari911 const oldDate = dateInput.getAttribute('data-original-date') || date; 13891d05cddcSAtari911 const endDate = document.getElementById('event-end-date-' + calId).value; 13901d05cddcSAtari911 const title = document.getElementById('event-title-' + calId).value; 13911d05cddcSAtari911 const time = document.getElementById('event-time-' + calId).value; 13921d05cddcSAtari911 const endTime = document.getElementById('event-end-time-' + calId).value; 13931d05cddcSAtari911 const colorSelect = document.getElementById('event-color-' + calId); 13941d05cddcSAtari911 let color = colorSelect.value; 13951d05cddcSAtari911 13961d05cddcSAtari911 // Handle custom color 13971d05cddcSAtari911 if (color === 'custom') { 13981d05cddcSAtari911 color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value; 13991d05cddcSAtari911 } 14001d05cddcSAtari911 14011d05cddcSAtari911 const description = document.getElementById('event-desc-' + calId).value; 14021d05cddcSAtari911 const isTask = document.getElementById('event-is-task-' + calId).checked; 14031d05cddcSAtari911 const completed = false; // New tasks are not completed 14041d05cddcSAtari911 const isRecurring = document.getElementById('event-recurring-' + calId).checked; 14051d05cddcSAtari911 const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value; 14061d05cddcSAtari911 const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value; 14071d05cddcSAtari911 14081d05cddcSAtari911 if (!title) { 14091d05cddcSAtari911 alert('Please enter a title'); 14101d05cddcSAtari911 return; 14111d05cddcSAtari911 } 14121d05cddcSAtari911 14131d05cddcSAtari911 if (!date) { 14141d05cddcSAtari911 alert('Please select a date'); 14151d05cddcSAtari911 return; 14161d05cddcSAtari911 } 14171d05cddcSAtari911 14181d05cddcSAtari911 const params = new URLSearchParams({ 14191d05cddcSAtari911 call: 'plugin_calendar', 14201d05cddcSAtari911 action: 'save_event', 14211d05cddcSAtari911 namespace: finalNamespace, 14221d05cddcSAtari911 eventId: eventId, 14231d05cddcSAtari911 date: date, 14241d05cddcSAtari911 oldDate: oldDate, 14251d05cddcSAtari911 endDate: endDate, 14261d05cddcSAtari911 title: title, 14271d05cddcSAtari911 time: time, 14281d05cddcSAtari911 endTime: endTime, 14291d05cddcSAtari911 color: color, 14301d05cddcSAtari911 description: description, 14311d05cddcSAtari911 isTask: isTask ? '1' : '0', 14321d05cddcSAtari911 completed: completed ? '1' : '0', 14331d05cddcSAtari911 isRecurring: isRecurring ? '1' : '0', 14341d05cddcSAtari911 recurrenceType: recurrenceType, 14351d05cddcSAtari911 recurrenceEnd: recurrenceEnd 14361d05cddcSAtari911 }); 14371d05cddcSAtari911 14381d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 14391d05cddcSAtari911 method: 'POST', 14401d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 14411d05cddcSAtari911 body: params.toString() 14421d05cddcSAtari911 }) 14431d05cddcSAtari911 .then(r => r.json()) 14441d05cddcSAtari911 .then(data => { 14451d05cddcSAtari911 if (data.success) { 14461d05cddcSAtari911 closeEventDialog(calId); 14471d05cddcSAtari911 14481d05cddcSAtari911 // For recurring events, do a full page reload to show all occurrences 14491d05cddcSAtari911 if (isRecurring) { 14501d05cddcSAtari911 location.reload(); 14511d05cddcSAtari911 return; 14521d05cddcSAtari911 } 14531d05cddcSAtari911 14541d05cddcSAtari911 // Extract year and month from the NEW date (in case date was changed) 14551d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 14561d05cddcSAtari911 14571d05cddcSAtari911 // Reload calendar data via AJAX to the month of the event 14581d05cddcSAtari911 reloadCalendarData(calId, year, month, namespace); 14591d05cddcSAtari911 } else { 14601d05cddcSAtari911 alert('Error: ' + (data.error || 'Unknown error')); 14611d05cddcSAtari911 } 14621d05cddcSAtari911 }) 14631d05cddcSAtari911 .catch(err => { 14641d05cddcSAtari911 console.error('Error:', err); 14651d05cddcSAtari911 alert('Error saving event'); 14661d05cddcSAtari911 }); 14671d05cddcSAtari911}; 14681d05cddcSAtari911 14691d05cddcSAtari911// Reload calendar data without page refresh 14701d05cddcSAtari911window.reloadCalendarData = function(calId, year, month, namespace) { 14711d05cddcSAtari911 const params = new URLSearchParams({ 14721d05cddcSAtari911 call: 'plugin_calendar', 14731d05cddcSAtari911 action: 'load_month', 14741d05cddcSAtari911 year: year, 14751d05cddcSAtari911 month: month, 14761d05cddcSAtari911 namespace: namespace, 14771d05cddcSAtari911 _: new Date().getTime() // Cache buster 14781d05cddcSAtari911 }); 14791d05cddcSAtari911 14801d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 14811d05cddcSAtari911 method: 'POST', 14821d05cddcSAtari911 headers: { 14831d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 14841d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 14851d05cddcSAtari911 'Pragma': 'no-cache' 14861d05cddcSAtari911 }, 14871d05cddcSAtari911 body: params.toString() 14881d05cddcSAtari911 }) 14891d05cddcSAtari911 .then(r => r.json()) 14901d05cddcSAtari911 .then(data => { 14911d05cddcSAtari911 if (data.success) { 14921d05cddcSAtari911 const container = document.getElementById(calId); 14931d05cddcSAtari911 14941d05cddcSAtari911 // Check if this is a full calendar or just event panel 14951d05cddcSAtari911 if (container.classList.contains('calendar-compact-container')) { 14961d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 14971d05cddcSAtari911 } else if (container.classList.contains('event-panel-standalone')) { 14981d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 14991d05cddcSAtari911 } 15001d05cddcSAtari911 } 15011d05cddcSAtari911 }) 15021d05cddcSAtari911 .catch(err => console.error('Error:', err)); 15031d05cddcSAtari911}; 15041d05cddcSAtari911 15051d05cddcSAtari911// Close event dialog 15061d05cddcSAtari911window.closeEventDialog = function(calId) { 15071d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 15081d05cddcSAtari911 dialog.style.display = 'none'; 15091d05cddcSAtari911}; 15101d05cddcSAtari911 15111d05cddcSAtari911// Escape HTML 15121d05cddcSAtari911window.escapeHtml = function(text) { 15131d05cddcSAtari911 const div = document.createElement('div'); 15141d05cddcSAtari911 div.textContent = text; 15151d05cddcSAtari911 return div.innerHTML; 15161d05cddcSAtari911}; 15171d05cddcSAtari911 15181d05cddcSAtari911// Highlight event when clicking on bar in calendar 15191d05cddcSAtari911window.highlightEvent = function(calId, eventId, date) { 1520*9ccd446eSAtari911 console.log('Highlighting event:', calId, eventId, date); 1521*9ccd446eSAtari911 15221d05cddcSAtari911 // Find the event item in the event list 15231d05cddcSAtari911 const eventList = document.querySelector('#' + calId + ' .event-list-compact'); 1524*9ccd446eSAtari911 if (!eventList) { 1525*9ccd446eSAtari911 console.log('Event list not found'); 1526*9ccd446eSAtari911 return; 1527*9ccd446eSAtari911 } 15281d05cddcSAtari911 15291d05cddcSAtari911 const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]'); 1530*9ccd446eSAtari911 if (!eventItem) { 1531*9ccd446eSAtari911 console.log('Event item not found'); 1532*9ccd446eSAtari911 return; 1533*9ccd446eSAtari911 } 15341d05cddcSAtari911 1535*9ccd446eSAtari911 console.log('Found event item:', eventItem); 1536*9ccd446eSAtari911 1537*9ccd446eSAtari911 // Get theme 1538*9ccd446eSAtari911 const container = document.getElementById(calId); 1539*9ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 1540*9ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 1541*9ccd446eSAtari911 1542*9ccd446eSAtari911 console.log('Theme:', theme); 1543*9ccd446eSAtari911 1544*9ccd446eSAtari911 // Theme-specific highlight colors 1545*9ccd446eSAtari911 let highlightBg, highlightShadow; 1546*9ccd446eSAtari911 if (theme === 'matrix') { 1547*9ccd446eSAtari911 highlightBg = '#1a3d1a'; // Darker green 1548*9ccd446eSAtari911 highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)'; 1549*9ccd446eSAtari911 } else if (theme === 'purple') { 1550*9ccd446eSAtari911 highlightBg = '#3d2b4d'; // Darker purple 1551*9ccd446eSAtari911 highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)'; 1552*9ccd446eSAtari911 } else if (theme === 'professional') { 1553*9ccd446eSAtari911 highlightBg = '#e3f2fd'; // Light blue 1554*9ccd446eSAtari911 highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)'; 1555*9ccd446eSAtari911 } else if (theme === 'pink') { 1556*9ccd446eSAtari911 highlightBg = '#3d2030'; // Darker pink 1557*9ccd446eSAtari911 highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)'; 1558*9ccd446eSAtari911 } else if (theme === 'wiki') { 1559*9ccd446eSAtari911 highlightBg = '#dce9f5'; // Light blue highlight 1560*9ccd446eSAtari911 highlightShadow = '0 0 20px rgba(43, 115, 183, 0.4)'; 1561*9ccd446eSAtari911 } 1562*9ccd446eSAtari911 1563*9ccd446eSAtari911 console.log('Highlight colors:', highlightBg, highlightShadow); 1564*9ccd446eSAtari911 1565*9ccd446eSAtari911 // Store original styles 1566*9ccd446eSAtari911 const originalBg = eventItem.style.background; 1567*9ccd446eSAtari911 const originalShadow = eventItem.style.boxShadow; 1568*9ccd446eSAtari911 1569*9ccd446eSAtari911 // Remove previous highlights (restore their original styles) 15701d05cddcSAtari911 const previousHighlights = eventList.querySelectorAll('.event-highlighted'); 1571*9ccd446eSAtari911 previousHighlights.forEach(el => { 1572*9ccd446eSAtari911 el.classList.remove('event-highlighted'); 1573*9ccd446eSAtari911 }); 15741d05cddcSAtari911 1575*9ccd446eSAtari911 // Add highlight class and apply theme-aware glow 15761d05cddcSAtari911 eventItem.classList.add('event-highlighted'); 15771d05cddcSAtari911 1578*9ccd446eSAtari911 // Set CSS properties directly 1579*9ccd446eSAtari911 eventItem.style.setProperty('background', highlightBg, 'important'); 1580*9ccd446eSAtari911 eventItem.style.setProperty('box-shadow', highlightShadow, 'important'); 1581*9ccd446eSAtari911 eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important'); 1582*9ccd446eSAtari911 1583*9ccd446eSAtari911 console.log('Applied highlight styles'); 1584*9ccd446eSAtari911 15851d05cddcSAtari911 // Scroll to event 15861d05cddcSAtari911 eventItem.scrollIntoView({ 15871d05cddcSAtari911 behavior: 'smooth', 15881d05cddcSAtari911 block: 'nearest', 15891d05cddcSAtari911 inline: 'nearest' 15901d05cddcSAtari911 }); 15911d05cddcSAtari911 1592*9ccd446eSAtari911 // Remove highlight after 3 seconds and restore original styles 15931d05cddcSAtari911 setTimeout(() => { 1594*9ccd446eSAtari911 console.log('Removing highlight'); 15951d05cddcSAtari911 eventItem.classList.remove('event-highlighted'); 1596*9ccd446eSAtari911 eventItem.style.setProperty('background', originalBg); 1597*9ccd446eSAtari911 eventItem.style.setProperty('box-shadow', originalShadow); 1598*9ccd446eSAtari911 eventItem.style.setProperty('transition', ''); 15991d05cddcSAtari911 }, 3000); 16001d05cddcSAtari911}; 16011d05cddcSAtari911 16021d05cddcSAtari911// Toggle recurring event options 16031d05cddcSAtari911window.toggleRecurringOptions = function(calId) { 16041d05cddcSAtari911 const checkbox = document.getElementById('event-recurring-' + calId); 16051d05cddcSAtari911 const options = document.getElementById('recurring-options-' + calId); 16061d05cddcSAtari911 16071d05cddcSAtari911 if (checkbox && options) { 16081d05cddcSAtari911 options.style.display = checkbox.checked ? 'block' : 'none'; 16091d05cddcSAtari911 } 16101d05cddcSAtari911}; 16111d05cddcSAtari911 1612*9ccd446eSAtari911// ============================================================ 1613*9ccd446eSAtari911// Document-level event delegation (guarded - only attach once) 1614*9ccd446eSAtari911// These use event delegation so they work for AJAX-rebuilt content. 1615*9ccd446eSAtari911// ============================================================ 1616*9ccd446eSAtari911if (!window._calendarDelegationInit) { 1617*9ccd446eSAtari911 window._calendarDelegationInit = true; 1618*9ccd446eSAtari911 1619*9ccd446eSAtari911 // ESC closes dialogs, popups, tooltips 16201d05cddcSAtari911 document.addEventListener('keydown', function(e) { 16211d05cddcSAtari911 if (e.key === 'Escape') { 1622*9ccd446eSAtari911 document.querySelectorAll('.event-dialog-compact').forEach(function(d) { 1623*9ccd446eSAtari911 if (d.style.display === 'flex') d.style.display = 'none'; 1624*9ccd446eSAtari911 }); 1625*9ccd446eSAtari911 document.querySelectorAll('.day-popup').forEach(function(p) { 1626*9ccd446eSAtari911 p.style.display = 'none'; 1627*9ccd446eSAtari911 }); 1628*9ccd446eSAtari911 hideConflictTooltip(); 16291d05cddcSAtari911 } 16301d05cddcSAtari911 }); 1631*9ccd446eSAtari911 1632*9ccd446eSAtari911 // Conflict tooltip delegation (capture phase for mouseenter/leave) 1633*9ccd446eSAtari911 document.addEventListener('mouseenter', function(e) { 1634*9ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 1635*9ccd446eSAtari911 showConflictTooltip(e.target); 16361d05cddcSAtari911 } 1637*9ccd446eSAtari911 }, true); 1638*9ccd446eSAtari911 1639*9ccd446eSAtari911 document.addEventListener('mouseleave', function(e) { 1640*9ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 1641*9ccd446eSAtari911 hideConflictTooltip(); 1642*9ccd446eSAtari911 } 1643*9ccd446eSAtari911 }, true); 1644*9ccd446eSAtari911} // end delegation guard 16451d05cddcSAtari911 16461d05cddcSAtari911// Event panel navigation 16471d05cddcSAtari911window.navEventPanel = function(calId, year, month, namespace) { 16481d05cddcSAtari911 const params = new URLSearchParams({ 16491d05cddcSAtari911 call: 'plugin_calendar', 16501d05cddcSAtari911 action: 'load_month', 16511d05cddcSAtari911 year: year, 16521d05cddcSAtari911 month: month, 16531d05cddcSAtari911 namespace: namespace, 16541d05cddcSAtari911 _: new Date().getTime() // Cache buster 16551d05cddcSAtari911 }); 16561d05cddcSAtari911 16571d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 16581d05cddcSAtari911 method: 'POST', 16591d05cddcSAtari911 headers: { 16601d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 16611d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 16621d05cddcSAtari911 'Pragma': 'no-cache' 16631d05cddcSAtari911 }, 16641d05cddcSAtari911 body: params.toString() 16651d05cddcSAtari911 }) 16661d05cddcSAtari911 .then(r => r.json()) 16671d05cddcSAtari911 .then(data => { 16681d05cddcSAtari911 if (data.success) { 16691d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 16701d05cddcSAtari911 } 16711d05cddcSAtari911 }) 16721d05cddcSAtari911 .catch(err => console.error('Error:', err)); 16731d05cddcSAtari911}; 16741d05cddcSAtari911 16751d05cddcSAtari911// Rebuild event panel only 16761d05cddcSAtari911window.rebuildEventPanel = function(calId, year, month, events, namespace) { 16771d05cddcSAtari911 const container = document.getElementById(calId); 16781d05cddcSAtari911 const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 16791d05cddcSAtari911 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 16801d05cddcSAtari911 16811d05cddcSAtari911 // Update month title in new compact header 16821d05cddcSAtari911 const monthTitle = container.querySelector('.panel-month-title'); 16831d05cddcSAtari911 if (monthTitle) { 16841d05cddcSAtari911 monthTitle.textContent = monthNames[month - 1] + ' ' + year; 16851d05cddcSAtari911 monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 16861d05cddcSAtari911 monthTitle.setAttribute('title', 'Click to jump to month'); 16871d05cddcSAtari911 } 16881d05cddcSAtari911 16891d05cddcSAtari911 // Fallback: Update old header format if exists 16901d05cddcSAtari911 const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker'); 16911d05cddcSAtari911 if (oldHeader && !monthTitle) { 16921d05cddcSAtari911 oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events'; 16931d05cddcSAtari911 oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 16941d05cddcSAtari911 } 16951d05cddcSAtari911 16961d05cddcSAtari911 // Update nav buttons 16971d05cddcSAtari911 let prevMonth = month - 1; 16981d05cddcSAtari911 let prevYear = year; 16991d05cddcSAtari911 if (prevMonth < 1) { 17001d05cddcSAtari911 prevMonth = 12; 17011d05cddcSAtari911 prevYear--; 17021d05cddcSAtari911 } 17031d05cddcSAtari911 17041d05cddcSAtari911 let nextMonth = month + 1; 17051d05cddcSAtari911 let nextYear = year; 17061d05cddcSAtari911 if (nextMonth > 12) { 17071d05cddcSAtari911 nextMonth = 1; 17081d05cddcSAtari911 nextYear++; 17091d05cddcSAtari911 } 17101d05cddcSAtari911 17111d05cddcSAtari911 // Update new compact nav buttons 17121d05cddcSAtari911 const navBtns = container.querySelectorAll('.panel-nav-btn'); 17131d05cddcSAtari911 if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 17141d05cddcSAtari911 if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 17151d05cddcSAtari911 17161d05cddcSAtari911 // Fallback for old nav buttons 17171d05cddcSAtari911 const oldNavBtns = container.querySelectorAll('.cal-nav-btn'); 17181d05cddcSAtari911 if (oldNavBtns.length > 0 && navBtns.length === 0) { 17191d05cddcSAtari911 if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 17201d05cddcSAtari911 if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 17211d05cddcSAtari911 } 17221d05cddcSAtari911 17231d05cddcSAtari911 // Update Today button (works for both old and new) 17241d05cddcSAtari911 const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact'); 17251d05cddcSAtari911 if (todayBtn) { 17261d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`); 17271d05cddcSAtari911 } 17281d05cddcSAtari911 17291d05cddcSAtari911 // Rebuild event list 17301d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 17311d05cddcSAtari911 if (eventList) { 17321d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 17331d05cddcSAtari911 } 17341d05cddcSAtari911}; 17351d05cddcSAtari911 17361d05cddcSAtari911// Open add event for panel 17371d05cddcSAtari911window.openAddEventPanel = function(calId, namespace) { 17381d05cddcSAtari911 const today = new Date(); 17391d05cddcSAtari911 const year = today.getFullYear(); 17401d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 17411d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 17421d05cddcSAtari911 const localDate = `${year}-${month}-${day}`; 17431d05cddcSAtari911 openAddEvent(calId, namespace, localDate); 17441d05cddcSAtari911}; 17451d05cddcSAtari911 17461d05cddcSAtari911// Toggle task completion 17471d05cddcSAtari911window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) { 17481d05cddcSAtari911 const params = new URLSearchParams({ 17491d05cddcSAtari911 call: 'plugin_calendar', 17501d05cddcSAtari911 action: 'toggle_task', 17511d05cddcSAtari911 namespace: namespace, 17521d05cddcSAtari911 date: date, 17531d05cddcSAtari911 eventId: eventId, 17541d05cddcSAtari911 completed: completed ? '1' : '0' 17551d05cddcSAtari911 }); 17561d05cddcSAtari911 17571d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 17581d05cddcSAtari911 method: 'POST', 17591d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 17601d05cddcSAtari911 body: params.toString() 17611d05cddcSAtari911 }) 17621d05cddcSAtari911 .then(r => r.json()) 17631d05cddcSAtari911 .then(data => { 17641d05cddcSAtari911 if (data.success) { 17651d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 17661d05cddcSAtari911 reloadCalendarData(calId, year, month, namespace); 17671d05cddcSAtari911 } 17681d05cddcSAtari911 }) 17691d05cddcSAtari911 .catch(err => console.error('Error toggling task:', err)); 17701d05cddcSAtari911}; 17711d05cddcSAtari911 17721d05cddcSAtari911// Make dialog draggable 17731d05cddcSAtari911window.makeDialogDraggable = function(calId) { 17741d05cddcSAtari911 const dialog = document.getElementById('dialog-content-' + calId); 17751d05cddcSAtari911 const handle = document.getElementById('drag-handle-' + calId); 17761d05cddcSAtari911 17771d05cddcSAtari911 if (!dialog || !handle) return; 17781d05cddcSAtari911 17791d05cddcSAtari911 let isDragging = false; 17801d05cddcSAtari911 let currentX; 17811d05cddcSAtari911 let currentY; 17821d05cddcSAtari911 let initialX; 17831d05cddcSAtari911 let initialY; 17841d05cddcSAtari911 let xOffset = 0; 17851d05cddcSAtari911 let yOffset = 0; 17861d05cddcSAtari911 17871d05cddcSAtari911 handle.addEventListener('mousedown', dragStart); 17881d05cddcSAtari911 document.addEventListener('mousemove', drag); 17891d05cddcSAtari911 document.addEventListener('mouseup', dragEnd); 17901d05cddcSAtari911 17911d05cddcSAtari911 function dragStart(e) { 17921d05cddcSAtari911 initialX = e.clientX - xOffset; 17931d05cddcSAtari911 initialY = e.clientY - yOffset; 17941d05cddcSAtari911 isDragging = true; 17951d05cddcSAtari911 } 17961d05cddcSAtari911 17971d05cddcSAtari911 function drag(e) { 17981d05cddcSAtari911 if (isDragging) { 17991d05cddcSAtari911 e.preventDefault(); 18001d05cddcSAtari911 currentX = e.clientX - initialX; 18011d05cddcSAtari911 currentY = e.clientY - initialY; 18021d05cddcSAtari911 xOffset = currentX; 18031d05cddcSAtari911 yOffset = currentY; 18041d05cddcSAtari911 setTranslate(currentX, currentY, dialog); 18051d05cddcSAtari911 } 18061d05cddcSAtari911 } 18071d05cddcSAtari911 18081d05cddcSAtari911 function dragEnd(e) { 18091d05cddcSAtari911 initialX = currentX; 18101d05cddcSAtari911 initialY = currentY; 18111d05cddcSAtari911 isDragging = false; 18121d05cddcSAtari911 } 18131d05cddcSAtari911 18141d05cddcSAtari911 function setTranslate(xPos, yPos, el) { 18151d05cddcSAtari911 el.style.transform = `translate(${xPos}px, ${yPos}px)`; 18161d05cddcSAtari911 } 18171d05cddcSAtari911}; 18181d05cddcSAtari911 18191d05cddcSAtari911// Initialize dialog draggability when opened (avoid duplicate declaration) 18201d05cddcSAtari911if (!window.calendarDraggabilityPatched) { 18211d05cddcSAtari911 window.calendarDraggabilityPatched = true; 18221d05cddcSAtari911 18231d05cddcSAtari911 const originalOpenAddEvent = openAddEvent; 18241d05cddcSAtari911 openAddEvent = function(calId, namespace, date) { 18251d05cddcSAtari911 originalOpenAddEvent(calId, namespace, date); 18261d05cddcSAtari911 setTimeout(() => makeDialogDraggable(calId), 100); 18271d05cddcSAtari911 }; 18281d05cddcSAtari911 18291d05cddcSAtari911 const originalEditEvent = editEvent; 18301d05cddcSAtari911 editEvent = function(calId, eventId, date, namespace) { 18311d05cddcSAtari911 originalEditEvent(calId, eventId, date, namespace); 18321d05cddcSAtari911 setTimeout(() => makeDialogDraggable(calId), 100); 18331d05cddcSAtari911 }; 18341d05cddcSAtari911} 18351d05cddcSAtari911 18361d05cddcSAtari911// Toggle expand/collapse for past events 18371d05cddcSAtari911window.togglePastEventExpand = function(element) { 18381d05cddcSAtari911 // Stop propagation to prevent any parent click handlers 18391d05cddcSAtari911 event.stopPropagation(); 18401d05cddcSAtari911 18411d05cddcSAtari911 const meta = element.querySelector(".event-meta-compact"); 18421d05cddcSAtari911 const desc = element.querySelector(".event-desc-compact"); 18431d05cddcSAtari911 18441d05cddcSAtari911 // Toggle visibility 18451d05cddcSAtari911 if (meta.style.display === "none") { 18461d05cddcSAtari911 // Expand 18471d05cddcSAtari911 meta.style.display = "block"; 18481d05cddcSAtari911 if (desc) desc.style.display = "block"; 18491d05cddcSAtari911 element.classList.add("event-past-expanded"); 18501d05cddcSAtari911 } else { 18511d05cddcSAtari911 // Collapse 18521d05cddcSAtari911 meta.style.display = "none"; 18531d05cddcSAtari911 if (desc) desc.style.display = "none"; 18541d05cddcSAtari911 element.classList.remove("event-past-expanded"); 18551d05cddcSAtari911 } 18561d05cddcSAtari911}; 18571d05cddcSAtari911 1858*9ccd446eSAtari911// Filter calendar by namespace when clicking namespace badge (guarded) 1859*9ccd446eSAtari911if (!window._calendarClickDelegationInit) { 1860*9ccd446eSAtari911 window._calendarClickDelegationInit = true; 18611d05cddcSAtari911 document.addEventListener('click', function(e) { 18621d05cddcSAtari911 if (e.target.classList.contains('event-namespace-badge')) { 18631d05cddcSAtari911 const namespace = e.target.textContent; 18641d05cddcSAtari911 const eventItem = e.target.closest('.event-compact-item'); 18651d05cddcSAtari911 const eventList = e.target.closest('.event-list-compact'); 18661d05cddcSAtari911 const calendar = e.target.closest('.calendar-compact-container'); 18671d05cddcSAtari911 18681d05cddcSAtari911 if (!eventList || !calendar) return; 18691d05cddcSAtari911 18701d05cddcSAtari911 const calId = calendar.id; 18711d05cddcSAtari911 18721d05cddcSAtari911 // Check if already filtered 18731d05cddcSAtari911 const isFiltered = eventList.classList.contains('namespace-filtered'); 18741d05cddcSAtari911 18751d05cddcSAtari911 if (isFiltered && eventList.dataset.filterNamespace === namespace) { 18761d05cddcSAtari911 // Unfilter - show all 18771d05cddcSAtari911 eventList.classList.remove('namespace-filtered'); 18781d05cddcSAtari911 delete eventList.dataset.filterNamespace; 18791d05cddcSAtari911 delete calendar.dataset.filteredNamespace; 18801d05cddcSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 18811d05cddcSAtari911 item.style.display = ''; 18821d05cddcSAtari911 }); 18831d05cddcSAtari911 18841d05cddcSAtari911 // Update header to show "all namespaces" 18851d05cddcSAtari911 updateFilteredNamespaceDisplay(calId, null); 18861d05cddcSAtari911 } else { 18871d05cddcSAtari911 // Filter by this namespace 18881d05cddcSAtari911 eventList.classList.add('namespace-filtered'); 18891d05cddcSAtari911 eventList.dataset.filterNamespace = namespace; 18901d05cddcSAtari911 calendar.dataset.filteredNamespace = namespace; 18911d05cddcSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 18921d05cddcSAtari911 const itemBadge = item.querySelector('.event-namespace-badge'); 18931d05cddcSAtari911 if (itemBadge && itemBadge.textContent === namespace) { 18941d05cddcSAtari911 item.style.display = ''; 18951d05cddcSAtari911 } else { 18961d05cddcSAtari911 item.style.display = 'none'; 18971d05cddcSAtari911 } 18981d05cddcSAtari911 }); 18991d05cddcSAtari911 19001d05cddcSAtari911 // Update header to show filtered namespace 19011d05cddcSAtari911 updateFilteredNamespaceDisplay(calId, namespace); 19021d05cddcSAtari911 } 19031d05cddcSAtari911 } 19041d05cddcSAtari911 }); 1905*9ccd446eSAtari911} // end click delegation guard 19061d05cddcSAtari911 19071d05cddcSAtari911// Update the displayed filtered namespace in event list header 19081d05cddcSAtari911window.updateFilteredNamespaceDisplay = function(calId, namespace) { 19091d05cddcSAtari911 const calendar = document.getElementById(calId); 19101d05cddcSAtari911 if (!calendar) return; 19111d05cddcSAtari911 19121d05cddcSAtari911 const headerContent = calendar.querySelector('.event-list-header-content'); 19131d05cddcSAtari911 if (!headerContent) return; 19141d05cddcSAtari911 19151d05cddcSAtari911 // Remove existing filter badge 19161d05cddcSAtari911 let filterBadge = headerContent.querySelector('.namespace-filter-badge'); 19171d05cddcSAtari911 if (filterBadge) { 19181d05cddcSAtari911 filterBadge.remove(); 19191d05cddcSAtari911 } 19201d05cddcSAtari911 19211d05cddcSAtari911 // Add new filter badge if filtering 19221d05cddcSAtari911 if (namespace) { 19231d05cddcSAtari911 filterBadge = document.createElement('span'); 19241d05cddcSAtari911 filterBadge.className = 'namespace-badge namespace-filter-badge'; 19251d05cddcSAtari911 filterBadge.innerHTML = escapeHtml(namespace) + ' <button class="filter-clear-inline" onclick="clearNamespaceFilter(\'' + calId + '\'); event.stopPropagation();">✕</button>'; 19261d05cddcSAtari911 headerContent.appendChild(filterBadge); 19271d05cddcSAtari911 } 19281d05cddcSAtari911}; 19291d05cddcSAtari911 19301d05cddcSAtari911// Clear namespace filter 19311d05cddcSAtari911window.clearNamespaceFilter = function(calId) { 19321d05cddcSAtari911 19331d05cddcSAtari911 const container = document.getElementById(calId); 19341d05cddcSAtari911 if (!container) { 19351d05cddcSAtari911 console.error('Calendar container not found:', calId); 19361d05cddcSAtari911 return; 19371d05cddcSAtari911 } 19381d05cddcSAtari911 1939*9ccd446eSAtari911 // Immediately hide/remove the filter badge 1940*9ccd446eSAtari911 const filterBadge = container.querySelector('.calendar-namespace-filter'); 1941*9ccd446eSAtari911 if (filterBadge) { 1942*9ccd446eSAtari911 filterBadge.style.display = 'none'; 1943*9ccd446eSAtari911 filterBadge.remove(); 1944*9ccd446eSAtari911 } 1945*9ccd446eSAtari911 19461d05cddcSAtari911 // Get current year and month 19471d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 19481d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 19491d05cddcSAtari911 19501d05cddcSAtari911 // Get original namespace (what the calendar was initialized with) 19511d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 19521d05cddcSAtari911 1953*9ccd446eSAtari911 // Also check for sidebar widget 1954*9ccd446eSAtari911 const sidebarContainer = document.getElementById('sidebar-widget-' + calId); 1955*9ccd446eSAtari911 if (sidebarContainer) { 1956*9ccd446eSAtari911 // For sidebar widget, just reload the page without namespace filter 1957*9ccd446eSAtari911 // Remove the namespace from the URL and reload 1958*9ccd446eSAtari911 const url = new URL(window.location.href); 1959*9ccd446eSAtari911 url.searchParams.delete('namespace'); 1960*9ccd446eSAtari911 window.location.href = url.toString(); 1961*9ccd446eSAtari911 return; 1962*9ccd446eSAtari911 } 19631d05cddcSAtari911 1964*9ccd446eSAtari911 // For regular calendar, reload calendar with original namespace 19651d05cddcSAtari911 navCalendar(calId, year, month, originalNamespace); 19661d05cddcSAtari911}; 19671d05cddcSAtari911 19681d05cddcSAtari911window.clearNamespaceFilterPanel = function(calId) { 19691d05cddcSAtari911 19701d05cddcSAtari911 const container = document.getElementById(calId); 19711d05cddcSAtari911 if (!container) { 19721d05cddcSAtari911 console.error('Event panel container not found:', calId); 19731d05cddcSAtari911 return; 19741d05cddcSAtari911 } 19751d05cddcSAtari911 19761d05cddcSAtari911 // Get current year and month from URL params or container 19771d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 19781d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 19791d05cddcSAtari911 19801d05cddcSAtari911 // Get original namespace (what the panel was initialized with) 19811d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 19821d05cddcSAtari911 19831d05cddcSAtari911 19841d05cddcSAtari911 // Reload event panel with original namespace 19851d05cddcSAtari911 navEventPanel(calId, year, month, originalNamespace); 19861d05cddcSAtari911}; 19871d05cddcSAtari911 19881d05cddcSAtari911// Color picker functions 19891d05cddcSAtari911window.updateCustomColorPicker = function(calId) { 19901d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 19911d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 19921d05cddcSAtari911 19931d05cddcSAtari911 if (select.value === 'custom') { 19941d05cddcSAtari911 // Show color picker 19951d05cddcSAtari911 picker.style.display = 'inline-block'; 19961d05cddcSAtari911 picker.click(); // Open color picker 19971d05cddcSAtari911 } else { 19981d05cddcSAtari911 // Hide color picker and sync value 19991d05cddcSAtari911 picker.style.display = 'none'; 20001d05cddcSAtari911 picker.value = select.value; 20011d05cddcSAtari911 } 20021d05cddcSAtari911}; 20031d05cddcSAtari911 20041d05cddcSAtari911function updateColorFromPicker(calId) { 20051d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 20061d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 20071d05cddcSAtari911 20081d05cddcSAtari911 // Set select to custom and update its underlying value 20091d05cddcSAtari911 select.value = 'custom'; 20101d05cddcSAtari911 // Store the actual color value in a data attribute 20111d05cddcSAtari911 select.dataset.customColor = picker.value; 20121d05cddcSAtari911} 20131d05cddcSAtari911 20141d05cddcSAtari911// Toggle past events visibility 20151d05cddcSAtari911window.togglePastEvents = function(calId) { 20161d05cddcSAtari911 const content = document.getElementById('past-events-' + calId); 20171d05cddcSAtari911 const arrow = document.getElementById('past-arrow-' + calId); 20181d05cddcSAtari911 20191d05cddcSAtari911 if (!content || !arrow) { 20201d05cddcSAtari911 console.error('Past events elements not found for:', calId); 20211d05cddcSAtari911 return; 20221d05cddcSAtari911 } 20231d05cddcSAtari911 20241d05cddcSAtari911 // Check computed style instead of inline style 20251d05cddcSAtari911 const isHidden = window.getComputedStyle(content).display === 'none'; 20261d05cddcSAtari911 20271d05cddcSAtari911 if (isHidden) { 20281d05cddcSAtari911 content.style.display = 'block'; 20291d05cddcSAtari911 arrow.textContent = '▼'; 20301d05cddcSAtari911 } else { 20311d05cddcSAtari911 content.style.display = 'none'; 20321d05cddcSAtari911 arrow.textContent = '▶'; 20331d05cddcSAtari911 } 20341d05cddcSAtari911}; 20351d05cddcSAtari911 20361d05cddcSAtari911// Fuzzy match scoring function 20371d05cddcSAtari911window.fuzzyMatch = function(pattern, str) { 20381d05cddcSAtari911 pattern = pattern.toLowerCase(); 20391d05cddcSAtari911 str = str.toLowerCase(); 20401d05cddcSAtari911 20411d05cddcSAtari911 let patternIdx = 0; 20421d05cddcSAtari911 let score = 0; 20431d05cddcSAtari911 let consecutiveMatches = 0; 20441d05cddcSAtari911 20451d05cddcSAtari911 for (let i = 0; i < str.length; i++) { 20461d05cddcSAtari911 if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) { 20471d05cddcSAtari911 score += 1 + consecutiveMatches; 20481d05cddcSAtari911 consecutiveMatches++; 20491d05cddcSAtari911 patternIdx++; 20501d05cddcSAtari911 } else { 20511d05cddcSAtari911 consecutiveMatches = 0; 20521d05cddcSAtari911 } 20531d05cddcSAtari911 } 20541d05cddcSAtari911 20551d05cddcSAtari911 // Return null if not all characters matched 20561d05cddcSAtari911 if (patternIdx !== pattern.length) { 20571d05cddcSAtari911 return null; 20581d05cddcSAtari911 } 20591d05cddcSAtari911 20601d05cddcSAtari911 // Bonus for exact match 20611d05cddcSAtari911 if (str === pattern) { 20621d05cddcSAtari911 score += 100; 20631d05cddcSAtari911 } 20641d05cddcSAtari911 20651d05cddcSAtari911 // Bonus for starts with 20661d05cddcSAtari911 if (str.startsWith(pattern)) { 20671d05cddcSAtari911 score += 50; 20681d05cddcSAtari911 } 20691d05cddcSAtari911 20701d05cddcSAtari911 return score; 20711d05cddcSAtari911}; 20721d05cddcSAtari911 20731d05cddcSAtari911// Initialize namespace search for a calendar 20741d05cddcSAtari911window.initNamespaceSearch = function(calId) { 20751d05cddcSAtari911 const searchInput = document.getElementById('event-namespace-search-' + calId); 20761d05cddcSAtari911 const hiddenInput = document.getElementById('event-namespace-' + calId); 20771d05cddcSAtari911 const dropdown = document.getElementById('event-namespace-dropdown-' + calId); 20781d05cddcSAtari911 const dataElement = document.getElementById('namespaces-data-' + calId); 20791d05cddcSAtari911 20801d05cddcSAtari911 if (!searchInput || !hiddenInput || !dropdown || !dataElement) { 20811d05cddcSAtari911 return; // Elements not found 20821d05cddcSAtari911 } 20831d05cddcSAtari911 20841d05cddcSAtari911 let namespaces = []; 20851d05cddcSAtari911 try { 20861d05cddcSAtari911 namespaces = JSON.parse(dataElement.textContent); 20871d05cddcSAtari911 } catch (e) { 20881d05cddcSAtari911 console.error('Failed to parse namespaces data:', e); 20891d05cddcSAtari911 return; 20901d05cddcSAtari911 } 20911d05cddcSAtari911 20921d05cddcSAtari911 let selectedIndex = -1; 20931d05cddcSAtari911 20941d05cddcSAtari911 // Filter and show dropdown 20951d05cddcSAtari911 function filterNamespaces(query) { 20961d05cddcSAtari911 if (!query || query.trim() === '') { 20971d05cddcSAtari911 // Show all namespaces when empty 20981d05cddcSAtari911 hiddenInput.value = ''; 20991d05cddcSAtari911 const results = namespaces.slice(0, 20); // Limit to 20 21001d05cddcSAtari911 showDropdown(results); 21011d05cddcSAtari911 return; 21021d05cddcSAtari911 } 21031d05cddcSAtari911 21041d05cddcSAtari911 // Fuzzy match and score 21051d05cddcSAtari911 const matches = []; 21061d05cddcSAtari911 for (let i = 0; i < namespaces.length; i++) { 21071d05cddcSAtari911 const score = fuzzyMatch(query, namespaces[i]); 21081d05cddcSAtari911 if (score !== null) { 21091d05cddcSAtari911 matches.push({ namespace: namespaces[i], score: score }); 21101d05cddcSAtari911 } 21111d05cddcSAtari911 } 21121d05cddcSAtari911 21131d05cddcSAtari911 // Sort by score (descending) 21141d05cddcSAtari911 matches.sort((a, b) => b.score - a.score); 21151d05cddcSAtari911 21161d05cddcSAtari911 // Take top 20 results 21171d05cddcSAtari911 const results = matches.slice(0, 20).map(m => m.namespace); 21181d05cddcSAtari911 showDropdown(results); 21191d05cddcSAtari911 } 21201d05cddcSAtari911 21211d05cddcSAtari911 function showDropdown(results) { 21221d05cddcSAtari911 dropdown.innerHTML = ''; 21231d05cddcSAtari911 selectedIndex = -1; 21241d05cddcSAtari911 21251d05cddcSAtari911 if (results.length === 0) { 21261d05cddcSAtari911 dropdown.style.display = 'none'; 21271d05cddcSAtari911 return; 21281d05cddcSAtari911 } 21291d05cddcSAtari911 21301d05cddcSAtari911 // Add (default) option 21311d05cddcSAtari911 const defaultOption = document.createElement('div'); 21321d05cddcSAtari911 defaultOption.className = 'namespace-option'; 21331d05cddcSAtari911 defaultOption.textContent = '(default)'; 21341d05cddcSAtari911 defaultOption.dataset.value = ''; 21351d05cddcSAtari911 dropdown.appendChild(defaultOption); 21361d05cddcSAtari911 21371d05cddcSAtari911 results.forEach(ns => { 21381d05cddcSAtari911 const option = document.createElement('div'); 21391d05cddcSAtari911 option.className = 'namespace-option'; 21401d05cddcSAtari911 option.textContent = ns; 21411d05cddcSAtari911 option.dataset.value = ns; 21421d05cddcSAtari911 dropdown.appendChild(option); 21431d05cddcSAtari911 }); 21441d05cddcSAtari911 21451d05cddcSAtari911 dropdown.style.display = 'block'; 21461d05cddcSAtari911 } 21471d05cddcSAtari911 21481d05cddcSAtari911 function hideDropdown() { 21491d05cddcSAtari911 dropdown.style.display = 'none'; 21501d05cddcSAtari911 selectedIndex = -1; 21511d05cddcSAtari911 } 21521d05cddcSAtari911 21531d05cddcSAtari911 function selectOption(namespace) { 21541d05cddcSAtari911 hiddenInput.value = namespace; 21551d05cddcSAtari911 searchInput.value = namespace || '(default)'; 21561d05cddcSAtari911 hideDropdown(); 21571d05cddcSAtari911 } 21581d05cddcSAtari911 21591d05cddcSAtari911 // Event listeners 21601d05cddcSAtari911 searchInput.addEventListener('input', function(e) { 21611d05cddcSAtari911 filterNamespaces(e.target.value); 21621d05cddcSAtari911 }); 21631d05cddcSAtari911 21641d05cddcSAtari911 searchInput.addEventListener('focus', function(e) { 21651d05cddcSAtari911 filterNamespaces(e.target.value); 21661d05cddcSAtari911 }); 21671d05cddcSAtari911 21681d05cddcSAtari911 searchInput.addEventListener('blur', function(e) { 21691d05cddcSAtari911 // Delay to allow click on dropdown 21701d05cddcSAtari911 setTimeout(hideDropdown, 200); 21711d05cddcSAtari911 }); 21721d05cddcSAtari911 21731d05cddcSAtari911 searchInput.addEventListener('keydown', function(e) { 21741d05cddcSAtari911 const options = dropdown.querySelectorAll('.namespace-option'); 21751d05cddcSAtari911 21761d05cddcSAtari911 if (e.key === 'ArrowDown') { 21771d05cddcSAtari911 e.preventDefault(); 21781d05cddcSAtari911 selectedIndex = Math.min(selectedIndex + 1, options.length - 1); 21791d05cddcSAtari911 updateSelection(options); 21801d05cddcSAtari911 } else if (e.key === 'ArrowUp') { 21811d05cddcSAtari911 e.preventDefault(); 21821d05cddcSAtari911 selectedIndex = Math.max(selectedIndex - 1, -1); 21831d05cddcSAtari911 updateSelection(options); 21841d05cddcSAtari911 } else if (e.key === 'Enter') { 21851d05cddcSAtari911 e.preventDefault(); 21861d05cddcSAtari911 if (selectedIndex >= 0 && options[selectedIndex]) { 21871d05cddcSAtari911 selectOption(options[selectedIndex].dataset.value); 21881d05cddcSAtari911 } 21891d05cddcSAtari911 } else if (e.key === 'Escape') { 21901d05cddcSAtari911 hideDropdown(); 21911d05cddcSAtari911 } 21921d05cddcSAtari911 }); 21931d05cddcSAtari911 21941d05cddcSAtari911 function updateSelection(options) { 21951d05cddcSAtari911 options.forEach((opt, idx) => { 21961d05cddcSAtari911 if (idx === selectedIndex) { 21971d05cddcSAtari911 opt.classList.add('selected'); 21981d05cddcSAtari911 opt.scrollIntoView({ block: 'nearest' }); 21991d05cddcSAtari911 } else { 22001d05cddcSAtari911 opt.classList.remove('selected'); 22011d05cddcSAtari911 } 22021d05cddcSAtari911 }); 22031d05cddcSAtari911 } 22041d05cddcSAtari911 22051d05cddcSAtari911 // Click on dropdown option 22061d05cddcSAtari911 dropdown.addEventListener('mousedown', function(e) { 22071d05cddcSAtari911 if (e.target.classList.contains('namespace-option')) { 22081d05cddcSAtari911 selectOption(e.target.dataset.value); 22091d05cddcSAtari911 } 22101d05cddcSAtari911 }); 22111d05cddcSAtari911}; 22121d05cddcSAtari911 22131d05cddcSAtari911// Update end time options based on start time selection 22141d05cddcSAtari911window.updateEndTimeOptions = function(calId) { 22151d05cddcSAtari911 const startTimeSelect = document.getElementById('event-time-' + calId); 22161d05cddcSAtari911 const endTimeSelect = document.getElementById('event-end-time-' + calId); 22171d05cddcSAtari911 22181d05cddcSAtari911 if (!startTimeSelect || !endTimeSelect) return; 22191d05cddcSAtari911 22201d05cddcSAtari911 const startTime = startTimeSelect.value; 22211d05cddcSAtari911 22221d05cddcSAtari911 // If start time is empty (all day), disable end time 22231d05cddcSAtari911 if (!startTime) { 22241d05cddcSAtari911 endTimeSelect.disabled = true; 22251d05cddcSAtari911 endTimeSelect.value = ''; 22261d05cddcSAtari911 return; 22271d05cddcSAtari911 } 22281d05cddcSAtari911 22291d05cddcSAtari911 // Enable end time select 22301d05cddcSAtari911 endTimeSelect.disabled = false; 22311d05cddcSAtari911 22321d05cddcSAtari911 // Convert start time to minutes 22331d05cddcSAtari911 const startMinutes = timeToMinutes(startTime); 22341d05cddcSAtari911 22351d05cddcSAtari911 // Get current end time value (to preserve if valid) 22361d05cddcSAtari911 const currentEndTime = endTimeSelect.value; 22371d05cddcSAtari911 const currentEndMinutes = currentEndTime ? timeToMinutes(currentEndTime) : 0; 22381d05cddcSAtari911 22391d05cddcSAtari911 // Filter options - show only times after start time 22401d05cddcSAtari911 const options = endTimeSelect.options; 22411d05cddcSAtari911 let firstValidOption = null; 22421d05cddcSAtari911 let currentStillValid = false; 22431d05cddcSAtari911 22441d05cddcSAtari911 for (let i = 0; i < options.length; i++) { 22451d05cddcSAtari911 const option = options[i]; 22461d05cddcSAtari911 const optionValue = option.value; 22471d05cddcSAtari911 22481d05cddcSAtari911 if (optionValue === '') { 22491d05cddcSAtari911 // Keep "Same as start" option visible 22501d05cddcSAtari911 option.style.display = ''; 22511d05cddcSAtari911 continue; 22521d05cddcSAtari911 } 22531d05cddcSAtari911 22541d05cddcSAtari911 const optionMinutes = timeToMinutes(optionValue); 22551d05cddcSAtari911 22561d05cddcSAtari911 if (optionMinutes > startMinutes) { 22571d05cddcSAtari911 // Show options after start time 22581d05cddcSAtari911 option.style.display = ''; 22591d05cddcSAtari911 if (!firstValidOption) { 22601d05cddcSAtari911 firstValidOption = optionValue; 22611d05cddcSAtari911 } 22621d05cddcSAtari911 if (optionValue === currentEndTime) { 22631d05cddcSAtari911 currentStillValid = true; 22641d05cddcSAtari911 } 22651d05cddcSAtari911 } else { 22661d05cddcSAtari911 // Hide options before or equal to start time 22671d05cddcSAtari911 option.style.display = 'none'; 22681d05cddcSAtari911 } 22691d05cddcSAtari911 } 22701d05cddcSAtari911 22711d05cddcSAtari911 // If current end time is now invalid, set a new one 22721d05cddcSAtari911 if (!currentStillValid || currentEndMinutes <= startMinutes) { 22731d05cddcSAtari911 // Try to set to 1 hour after start 22741d05cddcSAtari911 const [startHour, startMinute] = startTime.split(':').map(Number); 22751d05cddcSAtari911 let endHour = startHour + 1; 22761d05cddcSAtari911 let endMinute = startMinute; 22771d05cddcSAtari911 22781d05cddcSAtari911 if (endHour >= 24) { 22791d05cddcSAtari911 endHour = 23; 22801d05cddcSAtari911 endMinute = 45; 22811d05cddcSAtari911 } 22821d05cddcSAtari911 22831d05cddcSAtari911 const suggestedEndTime = String(endHour).padStart(2, '0') + ':' + String(endMinute).padStart(2, '0'); 22841d05cddcSAtari911 22851d05cddcSAtari911 // Check if suggested time is in the list 22861d05cddcSAtari911 const suggestedExists = Array.from(options).some(opt => opt.value === suggestedEndTime); 22871d05cddcSAtari911 22881d05cddcSAtari911 if (suggestedExists) { 22891d05cddcSAtari911 endTimeSelect.value = suggestedEndTime; 22901d05cddcSAtari911 } else if (firstValidOption) { 22911d05cddcSAtari911 // Use first valid option 22921d05cddcSAtari911 endTimeSelect.value = firstValidOption; 22931d05cddcSAtari911 } else { 22941d05cddcSAtari911 // No valid options (shouldn't happen, but just in case) 22951d05cddcSAtari911 endTimeSelect.value = ''; 22961d05cddcSAtari911 } 22971d05cddcSAtari911 } 22981d05cddcSAtari911}; 22991d05cddcSAtari911 23001d05cddcSAtari911// Check for time conflicts between events on the same date 23011d05cddcSAtari911window.checkTimeConflicts = function(events, currentEventId) { 23021d05cddcSAtari911 const conflicts = []; 23031d05cddcSAtari911 23041d05cddcSAtari911 // Group events by date 23051d05cddcSAtari911 const eventsByDate = {}; 23061d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(events)) { 23071d05cddcSAtari911 if (!Array.isArray(dateEvents)) continue; 23081d05cddcSAtari911 23091d05cddcSAtari911 dateEvents.forEach(evt => { 23101d05cddcSAtari911 if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event 23111d05cddcSAtari911 23121d05cddcSAtari911 if (!eventsByDate[date]) eventsByDate[date] = []; 23131d05cddcSAtari911 eventsByDate[date].push(evt); 23141d05cddcSAtari911 }); 23151d05cddcSAtari911 } 23161d05cddcSAtari911 23171d05cddcSAtari911 // Check for overlaps on each date 23181d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(eventsByDate)) { 23191d05cddcSAtari911 for (let i = 0; i < dateEvents.length; i++) { 23201d05cddcSAtari911 for (let j = i + 1; j < dateEvents.length; j++) { 23211d05cddcSAtari911 const evt1 = dateEvents[i]; 23221d05cddcSAtari911 const evt2 = dateEvents[j]; 23231d05cddcSAtari911 23241d05cddcSAtari911 if (eventsOverlap(evt1, evt2)) { 23251d05cddcSAtari911 // Mark both events as conflicting 23261d05cddcSAtari911 if (!evt1.hasConflict) evt1.hasConflict = true; 23271d05cddcSAtari911 if (!evt2.hasConflict) evt2.hasConflict = true; 23281d05cddcSAtari911 23291d05cddcSAtari911 // Store conflict info 23301d05cddcSAtari911 if (!evt1.conflictsWith) evt1.conflictsWith = []; 23311d05cddcSAtari911 if (!evt2.conflictsWith) evt2.conflictsWith = []; 23321d05cddcSAtari911 23331d05cddcSAtari911 evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime}); 23341d05cddcSAtari911 evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime}); 23351d05cddcSAtari911 } 23361d05cddcSAtari911 } 23371d05cddcSAtari911 } 23381d05cddcSAtari911 } 23391d05cddcSAtari911 23401d05cddcSAtari911 return events; 23411d05cddcSAtari911}; 23421d05cddcSAtari911 23431d05cddcSAtari911// Check if two events overlap in time 23441d05cddcSAtari911function eventsOverlap(evt1, evt2) { 23451d05cddcSAtari911 if (!evt1.time || !evt2.time) return false; // All-day events don't conflict 23461d05cddcSAtari911 23471d05cddcSAtari911 const start1 = evt1.time; 23481d05cddcSAtari911 const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start 23491d05cddcSAtari911 23501d05cddcSAtari911 const start2 = evt2.time; 23511d05cddcSAtari911 const end2 = evt2.endTime || evt2.time; 23521d05cddcSAtari911 23531d05cddcSAtari911 // Convert to minutes for easier comparison 23541d05cddcSAtari911 const start1Mins = timeToMinutes(start1); 23551d05cddcSAtari911 const end1Mins = timeToMinutes(end1); 23561d05cddcSAtari911 const start2Mins = timeToMinutes(start2); 23571d05cddcSAtari911 const end2Mins = timeToMinutes(end2); 23581d05cddcSAtari911 23591d05cddcSAtari911 // Check for overlap 23601d05cddcSAtari911 // Events overlap if: start1 < end2 AND start2 < end1 23611d05cddcSAtari911 return start1Mins < end2Mins && start2Mins < end1Mins; 23621d05cddcSAtari911} 23631d05cddcSAtari911 23641d05cddcSAtari911// Convert HH:MM time to minutes since midnight 23651d05cddcSAtari911function timeToMinutes(timeStr) { 23661d05cddcSAtari911 const [hours, minutes] = timeStr.split(':').map(Number); 23671d05cddcSAtari911 return hours * 60 + minutes; 23681d05cddcSAtari911} 23691d05cddcSAtari911 23701d05cddcSAtari911// Format time range for display 23711d05cddcSAtari911window.formatTimeRange = function(startTime, endTime) { 23721d05cddcSAtari911 if (!startTime) return ''; 23731d05cddcSAtari911 23741d05cddcSAtari911 const formatTime = (timeStr) => { 23751d05cddcSAtari911 const [hour24, minute] = timeStr.split(':').map(Number); 23761d05cddcSAtari911 const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24); 23771d05cddcSAtari911 const ampm = hour24 < 12 ? 'AM' : 'PM'; 23781d05cddcSAtari911 return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 23791d05cddcSAtari911 }; 23801d05cddcSAtari911 23811d05cddcSAtari911 if (!endTime || endTime === startTime) { 23821d05cddcSAtari911 return formatTime(startTime); 23831d05cddcSAtari911 } 23841d05cddcSAtari911 23851d05cddcSAtari911 return formatTime(startTime) + ' - ' + formatTime(endTime); 23861d05cddcSAtari911}; 23871d05cddcSAtari911 2388*9ccd446eSAtari911// Track last known mouse position for tooltip positioning fallback 2389*9ccd446eSAtari911var _lastMouseX = 0, _lastMouseY = 0; 2390*9ccd446eSAtari911document.addEventListener('mousemove', function(e) { 2391*9ccd446eSAtari911 _lastMouseX = e.clientX; 2392*9ccd446eSAtari911 _lastMouseY = e.clientY; 2393*9ccd446eSAtari911}); 2394*9ccd446eSAtari911 23951d05cddcSAtari911// Show custom conflict tooltip 23961d05cddcSAtari911window.showConflictTooltip = function(badgeElement) { 23971d05cddcSAtari911 // Remove any existing tooltip 23981d05cddcSAtari911 hideConflictTooltip(); 23991d05cddcSAtari911 2400*9ccd446eSAtari911 // Get conflict data (base64-encoded JSON to avoid attribute quote issues) 2401*9ccd446eSAtari911 const conflictsRaw = badgeElement.getAttribute('data-conflicts'); 2402*9ccd446eSAtari911 if (!conflictsRaw) return; 24031d05cddcSAtari911 24041d05cddcSAtari911 let conflicts; 24051d05cddcSAtari911 try { 2406*9ccd446eSAtari911 conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw)))); 24071d05cddcSAtari911 } catch (e) { 2408*9ccd446eSAtari911 // Fallback: try parsing as plain JSON (for PHP-rendered badges) 2409*9ccd446eSAtari911 try { 2410*9ccd446eSAtari911 conflicts = JSON.parse(conflictsRaw); 2411*9ccd446eSAtari911 } catch (e2) { 2412*9ccd446eSAtari911 console.error('Failed to parse conflicts:', e2); 24131d05cddcSAtari911 return; 24141d05cddcSAtari911 } 2415*9ccd446eSAtari911 } 2416*9ccd446eSAtari911 2417*9ccd446eSAtari911 // Get theme from the calendar container via CSS variables 2418*9ccd446eSAtari911 // Try closest ancestor first, then fall back to any calendar on the page 2419*9ccd446eSAtari911 let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone'); 2420*9ccd446eSAtari911 if (!containerEl) { 2421*9ccd446eSAtari911 // Badge might be inside a day popup (appended to body) - find any calendar container 2422*9ccd446eSAtari911 containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]'); 2423*9ccd446eSAtari911 } 2424*9ccd446eSAtari911 const cs = containerEl ? getComputedStyle(containerEl) : null; 2425*9ccd446eSAtari911 2426*9ccd446eSAtari911 const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424'; 2427*9ccd446eSAtari911 const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07'; 2428*9ccd446eSAtari911 const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07'; 2429*9ccd446eSAtari911 const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00'; 2430*9ccd446eSAtari911 const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)'; 24311d05cddcSAtari911 24321d05cddcSAtari911 // Create tooltip 24331d05cddcSAtari911 const tooltip = document.createElement('div'); 24341d05cddcSAtari911 tooltip.id = 'conflict-tooltip'; 24351d05cddcSAtari911 tooltip.className = 'conflict-tooltip'; 24361d05cddcSAtari911 2437*9ccd446eSAtari911 // Apply theme styles 2438*9ccd446eSAtari911 tooltip.style.background = bg; 2439*9ccd446eSAtari911 tooltip.style.borderColor = border; 2440*9ccd446eSAtari911 tooltip.style.color = textPrimary; 2441*9ccd446eSAtari911 tooltip.style.boxShadow = '0 4px 12px ' + shadow; 2442*9ccd446eSAtari911 2443*9ccd446eSAtari911 // Build content with themed colors 2444*9ccd446eSAtari911 let html = '<div class="conflict-tooltip-header" style="color: ' + textPrimary + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>'; 24451d05cddcSAtari911 html += '<div class="conflict-tooltip-body">'; 24461d05cddcSAtari911 conflicts.forEach(conflict => { 2447*9ccd446eSAtari911 html += '<div class="conflict-item" style="color: ' + textDim + ';">• ' + escapeHtml(conflict) + '</div>'; 24481d05cddcSAtari911 }); 24491d05cddcSAtari911 html += '</div>'; 24501d05cddcSAtari911 24511d05cddcSAtari911 tooltip.innerHTML = html; 24521d05cddcSAtari911 document.body.appendChild(tooltip); 24531d05cddcSAtari911 24541d05cddcSAtari911 // Position tooltip 24551d05cddcSAtari911 const rect = badgeElement.getBoundingClientRect(); 24561d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 24571d05cddcSAtari911 24581d05cddcSAtari911 // Position above the badge, centered 24591d05cddcSAtari911 let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); 24601d05cddcSAtari911 let top = rect.top - tooltipRect.height - 8; 24611d05cddcSAtari911 24621d05cddcSAtari911 // Keep tooltip within viewport 24631d05cddcSAtari911 if (left < 10) left = 10; 24641d05cddcSAtari911 if (left + tooltipRect.width > window.innerWidth - 10) { 24651d05cddcSAtari911 left = window.innerWidth - tooltipRect.width - 10; 24661d05cddcSAtari911 } 24671d05cddcSAtari911 if (top < 10) { 24681d05cddcSAtari911 // If not enough room above, show below 24691d05cddcSAtari911 top = rect.bottom + 8; 24701d05cddcSAtari911 } 24711d05cddcSAtari911 24721d05cddcSAtari911 tooltip.style.left = left + 'px'; 24731d05cddcSAtari911 tooltip.style.top = top + 'px'; 24741d05cddcSAtari911 tooltip.style.opacity = '1'; 24751d05cddcSAtari911}; 24761d05cddcSAtari911 24771d05cddcSAtari911// Hide conflict tooltip 24781d05cddcSAtari911window.hideConflictTooltip = function() { 24791d05cddcSAtari911 const tooltip = document.getElementById('conflict-tooltip'); 24801d05cddcSAtari911 if (tooltip) { 24811d05cddcSAtari911 tooltip.remove(); 24821d05cddcSAtari911 } 24831d05cddcSAtari911}; 24841d05cddcSAtari911 24851d05cddcSAtari911// Filter events by search term 24861d05cddcSAtari911window.filterEvents = function(calId, searchTerm) { 24871d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 24881d05cddcSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 24891d05cddcSAtari911 24901d05cddcSAtari911 if (!eventList) return; 24911d05cddcSAtari911 24921d05cddcSAtari911 // Show/hide clear button 24931d05cddcSAtari911 if (searchClear) { 24941d05cddcSAtari911 searchClear.style.display = searchTerm ? 'block' : 'none'; 24951d05cddcSAtari911 } 24961d05cddcSAtari911 24971d05cddcSAtari911 searchTerm = searchTerm.toLowerCase().trim(); 24981d05cddcSAtari911 24991d05cddcSAtari911 // Get all event items 25001d05cddcSAtari911 const eventItems = eventList.querySelectorAll('.event-compact-item'); 25011d05cddcSAtari911 let visibleCount = 0; 25021d05cddcSAtari911 let hiddenPastCount = 0; 25031d05cddcSAtari911 25041d05cddcSAtari911 eventItems.forEach(item => { 25051d05cddcSAtari911 const title = item.querySelector('.event-title-compact'); 25061d05cddcSAtari911 const description = item.querySelector('.event-desc-compact'); 25071d05cddcSAtari911 const dateTime = item.querySelector('.event-date-time'); 25081d05cddcSAtari911 25091d05cddcSAtari911 // Build searchable text 25101d05cddcSAtari911 let searchableText = ''; 25111d05cddcSAtari911 if (title) searchableText += title.textContent.toLowerCase() + ' '; 25121d05cddcSAtari911 if (description) searchableText += description.textContent.toLowerCase() + ' '; 25131d05cddcSAtari911 if (dateTime) searchableText += dateTime.textContent.toLowerCase() + ' '; 25141d05cddcSAtari911 25151d05cddcSAtari911 // Check if matches search 25161d05cddcSAtari911 const matches = !searchTerm || searchableText.includes(searchTerm); 25171d05cddcSAtari911 25181d05cddcSAtari911 if (matches) { 25191d05cddcSAtari911 item.style.display = ''; 25201d05cddcSAtari911 visibleCount++; 25211d05cddcSAtari911 } else { 25221d05cddcSAtari911 item.style.display = 'none'; 25231d05cddcSAtari911 // Check if this is a past event 25241d05cddcSAtari911 if (item.classList.contains('event-past') || item.classList.contains('event-completed')) { 25251d05cddcSAtari911 hiddenPastCount++; 25261d05cddcSAtari911 } 25271d05cddcSAtari911 } 25281d05cddcSAtari911 }); 25291d05cddcSAtari911 25301d05cddcSAtari911 // Update past events toggle if it exists 25311d05cddcSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 25321d05cddcSAtari911 const pastLabel = eventList.querySelector('.past-events-label'); 25331d05cddcSAtari911 const pastContent = document.getElementById('past-events-' + calId); 25341d05cddcSAtari911 25351d05cddcSAtari911 if (pastToggle && pastLabel && pastContent) { 25361d05cddcSAtari911 const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])'); 25371d05cddcSAtari911 const totalPastVisible = visiblePastEvents.length; 25381d05cddcSAtari911 25391d05cddcSAtari911 if (totalPastVisible > 0) { 25401d05cddcSAtari911 pastLabel.textContent = `Past Events (${totalPastVisible})`; 25411d05cddcSAtari911 pastToggle.style.display = ''; 25421d05cddcSAtari911 } else { 25431d05cddcSAtari911 pastToggle.style.display = 'none'; 25441d05cddcSAtari911 } 25451d05cddcSAtari911 } 25461d05cddcSAtari911 25471d05cddcSAtari911 // Show "no results" message if nothing visible 25481d05cddcSAtari911 let noResultsMsg = eventList.querySelector('.no-search-results'); 25491d05cddcSAtari911 if (visibleCount === 0 && searchTerm) { 25501d05cddcSAtari911 if (!noResultsMsg) { 25511d05cddcSAtari911 noResultsMsg = document.createElement('p'); 25521d05cddcSAtari911 noResultsMsg.className = 'no-search-results no-events-msg'; 25531d05cddcSAtari911 noResultsMsg.textContent = 'No events match your search'; 25541d05cddcSAtari911 eventList.appendChild(noResultsMsg); 25551d05cddcSAtari911 } 25561d05cddcSAtari911 noResultsMsg.style.display = 'block'; 25571d05cddcSAtari911 } else if (noResultsMsg) { 25581d05cddcSAtari911 noResultsMsg.style.display = 'none'; 25591d05cddcSAtari911 } 25601d05cddcSAtari911}; 25611d05cddcSAtari911 25621d05cddcSAtari911// Clear event search 25631d05cddcSAtari911window.clearEventSearch = function(calId) { 25641d05cddcSAtari911 const searchInput = document.getElementById('event-search-' + calId); 25651d05cddcSAtari911 if (searchInput) { 25661d05cddcSAtari911 searchInput.value = ''; 25671d05cddcSAtari911 filterEvents(calId, ''); 25681d05cddcSAtari911 searchInput.focus(); 25691d05cddcSAtari911 } 25701d05cddcSAtari911}; 25711d05cddcSAtari911 2572*9ccd446eSAtari911// ============================================ 2573*9ccd446eSAtari911// PINK THEME - GLOWING PARTICLE EFFECTS 2574*9ccd446eSAtari911// ============================================ 2575*9ccd446eSAtari911 2576*9ccd446eSAtari911// Create glowing pink particle effects for pink theme 2577*9ccd446eSAtari911(function() { 2578*9ccd446eSAtari911 let pinkThemeActive = false; 2579*9ccd446eSAtari911 let trailTimer = null; 2580*9ccd446eSAtari911 let pixelTimer = null; 2581*9ccd446eSAtari911 2582*9ccd446eSAtari911 // Check if pink theme is active 2583*9ccd446eSAtari911 function checkPinkTheme() { 2584*9ccd446eSAtari911 const pinkCalendars = document.querySelectorAll('.calendar-theme-pink'); 2585*9ccd446eSAtari911 pinkThemeActive = pinkCalendars.length > 0; 2586*9ccd446eSAtari911 return pinkThemeActive; 2587*9ccd446eSAtari911 } 2588*9ccd446eSAtari911 2589*9ccd446eSAtari911 // Create trail particle 2590*9ccd446eSAtari911 function createTrailParticle(clientX, clientY) { 2591*9ccd446eSAtari911 if (!pinkThemeActive) return; 2592*9ccd446eSAtari911 2593*9ccd446eSAtari911 const trail = document.createElement('div'); 2594*9ccd446eSAtari911 trail.className = 'pink-cursor-trail'; 2595*9ccd446eSAtari911 trail.style.left = clientX + 'px'; 2596*9ccd446eSAtari911 trail.style.top = clientY + 'px'; 2597*9ccd446eSAtari911 trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards'; 2598*9ccd446eSAtari911 2599*9ccd446eSAtari911 document.body.appendChild(trail); 2600*9ccd446eSAtari911 2601*9ccd446eSAtari911 setTimeout(function() { 2602*9ccd446eSAtari911 trail.remove(); 2603*9ccd446eSAtari911 }, 500); 2604*9ccd446eSAtari911 } 2605*9ccd446eSAtari911 2606*9ccd446eSAtari911 // Create pixel sparkles 2607*9ccd446eSAtari911 function createPixelSparkles(clientX, clientY) { 2608*9ccd446eSAtari911 if (!pinkThemeActive || pixelTimer) return; 2609*9ccd446eSAtari911 2610*9ccd446eSAtari911 const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels 2611*9ccd446eSAtari911 2612*9ccd446eSAtari911 for (let i = 0; i < pixelCount; i++) { 2613*9ccd446eSAtari911 const pixel = document.createElement('div'); 2614*9ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 2615*9ccd446eSAtari911 2616*9ccd446eSAtari911 // Random offset from cursor 2617*9ccd446eSAtari911 const offsetX = (Math.random() - 0.5) * 30; 2618*9ccd446eSAtari911 const offsetY = (Math.random() - 0.5) * 30; 2619*9ccd446eSAtari911 2620*9ccd446eSAtari911 pixel.style.left = (clientX + offsetX) + 'px'; 2621*9ccd446eSAtari911 pixel.style.top = (clientY + offsetY) + 'px'; 2622*9ccd446eSAtari911 2623*9ccd446eSAtari911 // Random color - bright neon pinks and whites 2624*9ccd446eSAtari911 const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 2625*9ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 2626*9ccd446eSAtari911 pixel.style.background = color; 2627*9ccd446eSAtari911 pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff'; 2628*9ccd446eSAtari911 2629*9ccd446eSAtari911 // Random animation 2630*9ccd446eSAtari911 if (Math.random() > 0.5) { 2631*9ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards'; 2632*9ccd446eSAtari911 } else { 2633*9ccd446eSAtari911 pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards'; 2634*9ccd446eSAtari911 } 2635*9ccd446eSAtari911 2636*9ccd446eSAtari911 document.body.appendChild(pixel); 2637*9ccd446eSAtari911 2638*9ccd446eSAtari911 setTimeout(function() { 2639*9ccd446eSAtari911 pixel.remove(); 2640*9ccd446eSAtari911 }, 800); 2641*9ccd446eSAtari911 } 2642*9ccd446eSAtari911 2643*9ccd446eSAtari911 pixelTimer = setTimeout(function() { 2644*9ccd446eSAtari911 pixelTimer = null; 2645*9ccd446eSAtari911 }, 40); 2646*9ccd446eSAtari911 } 2647*9ccd446eSAtari911 2648*9ccd446eSAtari911 // Create explosion 2649*9ccd446eSAtari911 function createExplosion(clientX, clientY) { 2650*9ccd446eSAtari911 if (!pinkThemeActive) return; 2651*9ccd446eSAtari911 2652*9ccd446eSAtari911 const particleCount = 25; 2653*9ccd446eSAtari911 const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff']; 2654*9ccd446eSAtari911 2655*9ccd446eSAtari911 // Add hearts to explosion (8-12 hearts) 2656*9ccd446eSAtari911 const heartCount = 8 + Math.floor(Math.random() * 5); 2657*9ccd446eSAtari911 for (let i = 0; i < heartCount; i++) { 2658*9ccd446eSAtari911 const heart = document.createElement('div'); 2659*9ccd446eSAtari911 heart.textContent = ''; 2660*9ccd446eSAtari911 heart.style.position = 'fixed'; 2661*9ccd446eSAtari911 heart.style.left = clientX + 'px'; 2662*9ccd446eSAtari911 heart.style.top = clientY + 'px'; 2663*9ccd446eSAtari911 heart.style.pointerEvents = 'none'; 2664*9ccd446eSAtari911 heart.style.zIndex = '9999999'; 2665*9ccd446eSAtari911 heart.style.fontSize = (12 + Math.random() * 16) + 'px'; 2666*9ccd446eSAtari911 2667*9ccd446eSAtari911 // Random direction 2668*9ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 2669*9ccd446eSAtari911 const velocity = 60 + Math.random() * 80; 2670*9ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 2671*9ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 2672*9ccd446eSAtari911 2673*9ccd446eSAtari911 heart.style.setProperty('--tx', tx + 'px'); 2674*9ccd446eSAtari911 heart.style.setProperty('--ty', ty + 'px'); 2675*9ccd446eSAtari911 2676*9ccd446eSAtari911 const duration = 0.8 + Math.random() * 0.4; 2677*9ccd446eSAtari911 heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 2678*9ccd446eSAtari911 2679*9ccd446eSAtari911 document.body.appendChild(heart); 2680*9ccd446eSAtari911 2681*9ccd446eSAtari911 setTimeout(function() { 2682*9ccd446eSAtari911 heart.remove(); 2683*9ccd446eSAtari911 }, duration * 1000); 2684*9ccd446eSAtari911 } 2685*9ccd446eSAtari911 2686*9ccd446eSAtari911 // Main explosion particles 2687*9ccd446eSAtari911 for (let i = 0; i < particleCount; i++) { 2688*9ccd446eSAtari911 const particle = document.createElement('div'); 2689*9ccd446eSAtari911 particle.className = 'pink-particle'; 2690*9ccd446eSAtari911 2691*9ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 2692*9ccd446eSAtari911 particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)'; 2693*9ccd446eSAtari911 particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color; 2694*9ccd446eSAtari911 2695*9ccd446eSAtari911 particle.style.left = clientX + 'px'; 2696*9ccd446eSAtari911 particle.style.top = clientY + 'px'; 2697*9ccd446eSAtari911 2698*9ccd446eSAtari911 const angle = (Math.PI * 2 * i) / particleCount; 2699*9ccd446eSAtari911 const velocity = 50 + Math.random() * 100; 2700*9ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 2701*9ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 2702*9ccd446eSAtari911 2703*9ccd446eSAtari911 particle.style.setProperty('--tx', tx + 'px'); 2704*9ccd446eSAtari911 particle.style.setProperty('--ty', ty + 'px'); 2705*9ccd446eSAtari911 2706*9ccd446eSAtari911 const size = 4 + Math.random() * 6; 2707*9ccd446eSAtari911 particle.style.width = size + 'px'; 2708*9ccd446eSAtari911 particle.style.height = size + 'px'; 2709*9ccd446eSAtari911 2710*9ccd446eSAtari911 const duration = 0.6 + Math.random() * 0.4; 2711*9ccd446eSAtari911 particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 2712*9ccd446eSAtari911 2713*9ccd446eSAtari911 document.body.appendChild(particle); 2714*9ccd446eSAtari911 2715*9ccd446eSAtari911 setTimeout(function() { 2716*9ccd446eSAtari911 particle.remove(); 2717*9ccd446eSAtari911 }, duration * 1000); 2718*9ccd446eSAtari911 } 2719*9ccd446eSAtari911 2720*9ccd446eSAtari911 // Pixel sparkles 2721*9ccd446eSAtari911 const pixelSparkleCount = 40; 2722*9ccd446eSAtari911 2723*9ccd446eSAtari911 for (let i = 0; i < pixelSparkleCount; i++) { 2724*9ccd446eSAtari911 const pixel = document.createElement('div'); 2725*9ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 2726*9ccd446eSAtari911 2727*9ccd446eSAtari911 const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 2728*9ccd446eSAtari911 const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)]; 2729*9ccd446eSAtari911 pixel.style.background = pixelColor; 2730*9ccd446eSAtari911 pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff'; 2731*9ccd446eSAtari911 2732*9ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 2733*9ccd446eSAtari911 const distance = 30 + Math.random() * 80; 2734*9ccd446eSAtari911 const offsetX = Math.cos(angle) * distance; 2735*9ccd446eSAtari911 const offsetY = Math.sin(angle) * distance; 2736*9ccd446eSAtari911 2737*9ccd446eSAtari911 pixel.style.left = clientX + 'px'; 2738*9ccd446eSAtari911 pixel.style.top = clientY + 'px'; 2739*9ccd446eSAtari911 pixel.style.setProperty('--tx', offsetX + 'px'); 2740*9ccd446eSAtari911 pixel.style.setProperty('--ty', offsetY + 'px'); 2741*9ccd446eSAtari911 2742*9ccd446eSAtari911 const pixelSize = 1 + Math.random() * 2; 2743*9ccd446eSAtari911 pixel.style.width = pixelSize + 'px'; 2744*9ccd446eSAtari911 pixel.style.height = pixelSize + 'px'; 2745*9ccd446eSAtari911 2746*9ccd446eSAtari911 const duration = 0.4 + Math.random() * 0.4; 2747*9ccd446eSAtari911 if (Math.random() > 0.5) { 2748*9ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards'; 2749*9ccd446eSAtari911 } else { 2750*9ccd446eSAtari911 pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 2751*9ccd446eSAtari911 } 2752*9ccd446eSAtari911 2753*9ccd446eSAtari911 document.body.appendChild(pixel); 2754*9ccd446eSAtari911 2755*9ccd446eSAtari911 setTimeout(function() { 2756*9ccd446eSAtari911 pixel.remove(); 2757*9ccd446eSAtari911 }, duration * 1000); 2758*9ccd446eSAtari911 } 2759*9ccd446eSAtari911 2760*9ccd446eSAtari911 // Flash 2761*9ccd446eSAtari911 const flash = document.createElement('div'); 2762*9ccd446eSAtari911 flash.style.position = 'fixed'; 2763*9ccd446eSAtari911 flash.style.left = clientX + 'px'; 2764*9ccd446eSAtari911 flash.style.top = clientY + 'px'; 2765*9ccd446eSAtari911 flash.style.width = '40px'; 2766*9ccd446eSAtari911 flash.style.height = '40px'; 2767*9ccd446eSAtari911 flash.style.borderRadius = '50%'; 2768*9ccd446eSAtari911 flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)'; 2769*9ccd446eSAtari911 flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4'; 2770*9ccd446eSAtari911 flash.style.pointerEvents = 'none'; 2771*9ccd446eSAtari911 flash.style.zIndex = '9999999'; // Above everything including dialogs 2772*9ccd446eSAtari911 flash.style.transform = 'translate(-50%, -50%)'; 2773*9ccd446eSAtari911 flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards'; 2774*9ccd446eSAtari911 2775*9ccd446eSAtari911 document.body.appendChild(flash); 2776*9ccd446eSAtari911 2777*9ccd446eSAtari911 setTimeout(function() { 2778*9ccd446eSAtari911 flash.remove(); 2779*9ccd446eSAtari911 }, 300); 2780*9ccd446eSAtari911 } 2781*9ccd446eSAtari911 2782*9ccd446eSAtari911 function initPinkParticles() { 2783*9ccd446eSAtari911 if (!checkPinkTheme()) return; 2784*9ccd446eSAtari911 2785*9ccd446eSAtari911 // Use capture phase to catch events before stopPropagation 2786*9ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 2787*9ccd446eSAtari911 if (!pinkThemeActive) return; 2788*9ccd446eSAtari911 2789*9ccd446eSAtari911 createTrailParticle(e.clientX, e.clientY); 2790*9ccd446eSAtari911 createPixelSparkles(e.clientX, e.clientY); 2791*9ccd446eSAtari911 }, true); // Capture phase! 2792*9ccd446eSAtari911 2793*9ccd446eSAtari911 // Throttle main trail 2794*9ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 2795*9ccd446eSAtari911 if (!pinkThemeActive || trailTimer) return; 2796*9ccd446eSAtari911 2797*9ccd446eSAtari911 trailTimer = setTimeout(function() { 2798*9ccd446eSAtari911 trailTimer = null; 2799*9ccd446eSAtari911 }, 30); 2800*9ccd446eSAtari911 }, true); // Capture phase! 2801*9ccd446eSAtari911 2802*9ccd446eSAtari911 // Click explosion - use capture phase 2803*9ccd446eSAtari911 document.addEventListener('click', function(e) { 2804*9ccd446eSAtari911 if (!pinkThemeActive) return; 2805*9ccd446eSAtari911 2806*9ccd446eSAtari911 createExplosion(e.clientX, e.clientY); 2807*9ccd446eSAtari911 }, true); // Capture phase! 2808*9ccd446eSAtari911 } 2809*9ccd446eSAtari911 2810*9ccd446eSAtari911 // Initialize on load 2811*9ccd446eSAtari911 if (document.readyState === 'loading') { 2812*9ccd446eSAtari911 document.addEventListener('DOMContentLoaded', initPinkParticles); 2813*9ccd446eSAtari911 } else { 2814*9ccd446eSAtari911 initPinkParticles(); 2815*9ccd446eSAtari911 } 2816*9ccd446eSAtari911 2817*9ccd446eSAtari911 // Re-check theme if calendar is dynamically added 2818*9ccd446eSAtari911 if (typeof MutationObserver !== 'undefined') { 2819*9ccd446eSAtari911 const observer = new MutationObserver(function(mutations) { 2820*9ccd446eSAtari911 mutations.forEach(function(mutation) { 2821*9ccd446eSAtari911 if (mutation.addedNodes.length > 0) { 2822*9ccd446eSAtari911 mutation.addedNodes.forEach(function(node) { 2823*9ccd446eSAtari911 if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) { 2824*9ccd446eSAtari911 checkPinkTheme(); 2825*9ccd446eSAtari911 initPinkParticles(); 2826*9ccd446eSAtari911 } 2827*9ccd446eSAtari911 }); 2828*9ccd446eSAtari911 } 2829*9ccd446eSAtari911 }); 2830*9ccd446eSAtari911 }); 2831*9ccd446eSAtari911 2832*9ccd446eSAtari911 observer.observe(document.body, { 2833*9ccd446eSAtari911 childList: true, 2834*9ccd446eSAtari911 subtree: true 2835*9ccd446eSAtari911 }); 2836*9ccd446eSAtari911 } 2837*9ccd446eSAtari911})(); 2838*9ccd446eSAtari911 28391d05cddcSAtari911// End of calendar plugin JavaScript 2840