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 369ccd446eSAtari911// Helper: propagate CSS variables from a calendar container to a target element 379ccd446eSAtari911// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars 389ccd446eSAtari911// from DOM parents per spec, but some DokuWiki templates break this inheritance) 399ccd446eSAtari911function propagateThemeVars(calId, targetEl) { 409ccd446eSAtari911 if (!targetEl) return; 419ccd446eSAtari911 // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.) 429ccd446eSAtari911 const container = document.getElementById(calId) 439ccd446eSAtari911 || document.getElementById('sidebar-widget-' + calId) 449ccd446eSAtari911 || document.querySelector('[id$="' + calId + '"]'); 459ccd446eSAtari911 if (!container) return; 469ccd446eSAtari911 const cs = getComputedStyle(container); 479ccd446eSAtari911 const vars = [ 489ccd446eSAtari911 '--background-site', '--background-alt', '--background-header', 499ccd446eSAtari911 '--text-primary', '--text-bright', '--text-dim', 509ccd446eSAtari911 '--border-color', '--border-main', 519ccd446eSAtari911 '--cell-bg', '--cell-today-bg', '--grid-bg', 529ccd446eSAtari911 '--shadow-color', '--header-border', '--header-shadow', 539ccd446eSAtari911 '--btn-text' 549ccd446eSAtari911 ]; 559ccd446eSAtari911 vars.forEach(v => { 569ccd446eSAtari911 const val = cs.getPropertyValue(v).trim(); 579ccd446eSAtari911 if (val) targetEl.style.setProperty(v, val); 589ccd446eSAtari911 }); 599ccd446eSAtari911} 609ccd446eSAtari911 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 1919ccd446eSAtari911 // Get theme data from container 1929ccd446eSAtari911 const theme = container.dataset.theme || 'matrix'; 1939ccd446eSAtari911 let themeStyles = {}; 1949ccd446eSAtari911 try { 1959ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles || '{}'); 1969ccd446eSAtari911 } catch (e) { 1979ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 1989ccd446eSAtari911 themeStyles = {}; 1999ccd446eSAtari911 } 2009ccd446eSAtari911 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'); 2369ccd446eSAtari911 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) { 3639ccd446eSAtari911 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 3789ccd446eSAtari911 const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num'; 3799ccd446eSAtari911 3801d05cddcSAtari911 html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`; 3819ccd446eSAtari911 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'; 4009ccd446eSAtari911 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 4619ccd446eSAtari911 // Get theme data from container 4629ccd446eSAtari911 const container = document.getElementById(calId); 4639ccd446eSAtari911 let themeStyles = {}; 4649ccd446eSAtari911 if (container && container.dataset.themeStyles) { 4659ccd446eSAtari911 try { 4669ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 4679ccd446eSAtari911 } catch (e) { 4689ccd446eSAtari911 console.error('Failed to parse theme styles in renderEventListFromData:', e); 4699ccd446eSAtari911 } 4709ccd446eSAtari911 } 4719ccd446eSAtari911 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 6479ccd446eSAtari911 // Get theme styles 6489ccd446eSAtari911 const container = document.getElementById(calId); 6499ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 6509ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 6519ccd446eSAtari911 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 7329ccd446eSAtari911 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'; 7629ccd446eSAtari911 7639ccd446eSAtari911 // Propagate CSS vars from calendar container to popup (popup is outside container in DOM) 7649ccd446eSAtari911 if (container) { 7659ccd446eSAtari911 propagateThemeVars(calId, popup.querySelector('.day-popup-content')); 7669ccd446eSAtari911 } 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) { 8329ccd446eSAtari911 // Get theme data from container 8339ccd446eSAtari911 const container = document.getElementById(calId); 8349ccd446eSAtari911 let themeStyles = {}; 8359ccd446eSAtari911 if (container && container.dataset.themeStyles) { 8369ccd446eSAtari911 try { 8379ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 8389ccd446eSAtari911 } catch (e) { 8399ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 8409ccd446eSAtari911 } 8419ccd446eSAtari911 } 8429ccd446eSAtari911 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 9079ccd446eSAtari911 // Only inline style needed: border-left-color for event color indicator 908*7e8ea635SAtari911 let html = '<div class="event-compact-item' + completedClass + pastClass + pastDueClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ' !important;" 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) { 924*7e8ea635SAtari911 html += ' <span class="event-pastdue-badge" style="background:var(--pastdue-color, #e74c3c) !important; color:white !important; -webkit-text-fill-color:white !important;">PAST DUE</span>'; 9251d05cddcSAtari911 } else if (isToday) { 926*7e8ea635SAtari911 html += ' <span class="event-today-badge" style="background:var(--border-main, #9b59b6) !important; color:var(--background-site, white) !important; -webkit-text-fill-color:var(--background-site, white) !important;">TODAY</span>'; 9271d05cddcSAtari911 } 9289ccd446eSAtari911 // Add namespace badge 9291d05cddcSAtari911 let eventNamespace = event.namespace || ''; 9301d05cddcSAtari911 if (!eventNamespace && event._namespace !== undefined) { 9319ccd446eSAtari911 eventNamespace = event._namespace; 9321d05cddcSAtari911 } 9331d05cddcSAtari911 if (eventNamespace) { 934*7e8ea635SAtari911 html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" style="background:var(--text-bright, #008800) !important; color:var(--background-site, white) !important; -webkit-text-fill-color:var(--background-site, white) !important;" 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 9479ccd446eSAtari911 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*7e8ea635SAtari911 html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" style="background:var(--text-bright, #008800) !important; color:var(--background-site, white) !important; -webkit-text-fill-color:var(--background-site, white) !important;" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>'; 9699ccd446eSAtari911 } 9709ccd446eSAtari911 // Add conflict warning for past events too 9719ccd446eSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 9729ccd446eSAtari911 let conflictList = []; 9739ccd446eSAtari911 event.conflictsWith.forEach(conflict => { 9749ccd446eSAtari911 let conflictText = conflict.title; 9759ccd446eSAtari911 if (conflict.time) { 9769ccd446eSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 9779ccd446eSAtari911 } 9789ccd446eSAtari911 conflictList.push(conflictText); 9799ccd446eSAtari911 }); 9809ccd446eSAtari911 9819ccd446eSAtari911 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 12379ccd446eSAtari911 // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates) 12389ccd446eSAtari911 propagateThemeVars(calId, dialog); 12399ccd446eSAtari911 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) { 13109ccd446eSAtari911 // Set the hidden input (this is what gets submitted) 13119ccd446eSAtari911 namespaceHidden.value = event.namespace || ''; 13129ccd446eSAtari911 // Set the search input to display the namespace 13131d05cddcSAtari911 if (namespaceSearch) { 13141d05cddcSAtari911 namespaceSearch.value = event.namespace || '(default)'; 13151d05cddcSAtari911 } 13169ccd446eSAtari911 } else { 13179ccd446eSAtari911 // No namespace on event, set to default 13189ccd446eSAtari911 if (namespaceHidden) { 13199ccd446eSAtari911 namespaceHidden.value = ''; 13209ccd446eSAtari911 } 13219ccd446eSAtari911 if (namespaceSearch) { 13229ccd446eSAtari911 namespaceSearch.value = '(default)'; 13239ccd446eSAtari911 } 13241d05cddcSAtari911 } 13251d05cddcSAtari911 13261d05cddcSAtari911 title.textContent = 'Edit Event'; 13271d05cddcSAtari911 dialog.style.display = 'flex'; 13289ccd446eSAtari911 13299ccd446eSAtari911 // Propagate CSS vars to dialog 13309ccd446eSAtari911 propagateThemeVars(calId, dialog); 13311d05cddcSAtari911 } 13321d05cddcSAtari911 }) 13331d05cddcSAtari911 .catch(err => console.error('Error editing event:', err)); 13341d05cddcSAtari911}; 13351d05cddcSAtari911 13361d05cddcSAtari911// Delete event 13371d05cddcSAtari911window.deleteEvent = function(calId, eventId, date, namespace) { 13381d05cddcSAtari911 if (!confirm('Delete this event?')) return; 13391d05cddcSAtari911 13401d05cddcSAtari911 const params = new URLSearchParams({ 13411d05cddcSAtari911 call: 'plugin_calendar', 13421d05cddcSAtari911 action: 'delete_event', 13431d05cddcSAtari911 namespace: namespace, 13441d05cddcSAtari911 date: date, 1345*7e8ea635SAtari911 eventId: eventId, 1346*7e8ea635SAtari911 sectok: typeof JSINFO !== 'undefined' ? JSINFO.sectok : '' 13471d05cddcSAtari911 }); 13481d05cddcSAtari911 13491d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 13501d05cddcSAtari911 method: 'POST', 13511d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 13521d05cddcSAtari911 body: params.toString() 13531d05cddcSAtari911 }) 13541d05cddcSAtari911 .then(r => r.json()) 13551d05cddcSAtari911 .then(data => { 13561d05cddcSAtari911 if (data.success) { 13571d05cddcSAtari911 // Extract year and month from date 13581d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 13591d05cddcSAtari911 13601d05cddcSAtari911 // Reload calendar data via AJAX 13611d05cddcSAtari911 reloadCalendarData(calId, year, month, namespace); 13621d05cddcSAtari911 } 13631d05cddcSAtari911 }) 13641d05cddcSAtari911 .catch(err => console.error('Error:', err)); 13651d05cddcSAtari911}; 13661d05cddcSAtari911 13671d05cddcSAtari911// Save event (add or edit) 13681d05cddcSAtari911window.saveEventCompact = function(calId, namespace) { 13691d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 13701d05cddcSAtari911 13711d05cddcSAtari911 // Get namespace from dropdown - this is what the user selected 13721d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 13731d05cddcSAtari911 const selectedNamespace = namespaceSelect ? namespaceSelect.value : ''; 13741d05cddcSAtari911 13751d05cddcSAtari911 // ALWAYS use what the user selected in the dropdown 13761d05cddcSAtari911 // This allows changing namespace when editing 13771d05cddcSAtari911 const finalNamespace = selectedNamespace; 13781d05cddcSAtari911 13791d05cddcSAtari911 const eventId = document.getElementById('event-id-' + calId).value; 13801d05cddcSAtari911 13811d05cddcSAtari911 // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event) 13821d05cddcSAtari911 const originalNamespace = form.dataset.eventNamespace; 13831d05cddcSAtari911 13841d05cddcSAtari911 13851d05cddcSAtari911 const dateInput = document.getElementById('event-date-' + calId); 13861d05cddcSAtari911 const date = dateInput.value; 13871d05cddcSAtari911 const oldDate = dateInput.getAttribute('data-original-date') || date; 13881d05cddcSAtari911 const endDate = document.getElementById('event-end-date-' + calId).value; 13891d05cddcSAtari911 const title = document.getElementById('event-title-' + calId).value; 13901d05cddcSAtari911 const time = document.getElementById('event-time-' + calId).value; 13911d05cddcSAtari911 const endTime = document.getElementById('event-end-time-' + calId).value; 13921d05cddcSAtari911 const colorSelect = document.getElementById('event-color-' + calId); 13931d05cddcSAtari911 let color = colorSelect.value; 13941d05cddcSAtari911 13951d05cddcSAtari911 // Handle custom color 13961d05cddcSAtari911 if (color === 'custom') { 13971d05cddcSAtari911 color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value; 13981d05cddcSAtari911 } 13991d05cddcSAtari911 14001d05cddcSAtari911 const description = document.getElementById('event-desc-' + calId).value; 14011d05cddcSAtari911 const isTask = document.getElementById('event-is-task-' + calId).checked; 14021d05cddcSAtari911 const completed = false; // New tasks are not completed 14031d05cddcSAtari911 const isRecurring = document.getElementById('event-recurring-' + calId).checked; 14041d05cddcSAtari911 const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value; 14051d05cddcSAtari911 const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value; 14061d05cddcSAtari911 14071d05cddcSAtari911 if (!title) { 14081d05cddcSAtari911 alert('Please enter a title'); 14091d05cddcSAtari911 return; 14101d05cddcSAtari911 } 14111d05cddcSAtari911 14121d05cddcSAtari911 if (!date) { 14131d05cddcSAtari911 alert('Please select a date'); 14141d05cddcSAtari911 return; 14151d05cddcSAtari911 } 14161d05cddcSAtari911 14171d05cddcSAtari911 const params = new URLSearchParams({ 14181d05cddcSAtari911 call: 'plugin_calendar', 14191d05cddcSAtari911 action: 'save_event', 14201d05cddcSAtari911 namespace: finalNamespace, 14211d05cddcSAtari911 eventId: eventId, 14221d05cddcSAtari911 date: date, 14231d05cddcSAtari911 oldDate: oldDate, 14241d05cddcSAtari911 endDate: endDate, 14251d05cddcSAtari911 title: title, 14261d05cddcSAtari911 time: time, 14271d05cddcSAtari911 endTime: endTime, 14281d05cddcSAtari911 color: color, 14291d05cddcSAtari911 description: description, 14301d05cddcSAtari911 isTask: isTask ? '1' : '0', 14311d05cddcSAtari911 completed: completed ? '1' : '0', 14321d05cddcSAtari911 isRecurring: isRecurring ? '1' : '0', 14331d05cddcSAtari911 recurrenceType: recurrenceType, 1434*7e8ea635SAtari911 recurrenceEnd: recurrenceEnd, 1435*7e8ea635SAtari911 sectok: typeof JSINFO !== 'undefined' ? JSINFO.sectok : '' 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) { 15209ccd446eSAtari911 15211d05cddcSAtari911 // Find the event item in the event list 15221d05cddcSAtari911 const eventList = document.querySelector('#' + calId + ' .event-list-compact'); 15239ccd446eSAtari911 if (!eventList) { 15249ccd446eSAtari911 return; 15259ccd446eSAtari911 } 15261d05cddcSAtari911 15271d05cddcSAtari911 const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]'); 15289ccd446eSAtari911 if (!eventItem) { 15299ccd446eSAtari911 return; 15309ccd446eSAtari911 } 15311d05cddcSAtari911 15329ccd446eSAtari911 15339ccd446eSAtari911 // Get theme 15349ccd446eSAtari911 const container = document.getElementById(calId); 15359ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 15369ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 15379ccd446eSAtari911 15389ccd446eSAtari911 15399ccd446eSAtari911 // Theme-specific highlight colors 15409ccd446eSAtari911 let highlightBg, highlightShadow; 15419ccd446eSAtari911 if (theme === 'matrix') { 15429ccd446eSAtari911 highlightBg = '#1a3d1a'; // Darker green 15439ccd446eSAtari911 highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)'; 15449ccd446eSAtari911 } else if (theme === 'purple') { 15459ccd446eSAtari911 highlightBg = '#3d2b4d'; // Darker purple 15469ccd446eSAtari911 highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)'; 15479ccd446eSAtari911 } else if (theme === 'professional') { 15489ccd446eSAtari911 highlightBg = '#e3f2fd'; // Light blue 15499ccd446eSAtari911 highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)'; 15509ccd446eSAtari911 } else if (theme === 'pink') { 15519ccd446eSAtari911 highlightBg = '#3d2030'; // Darker pink 15529ccd446eSAtari911 highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)'; 15539ccd446eSAtari911 } else if (theme === 'wiki') { 1554*7e8ea635SAtari911 highlightBg = themeStyles.header_bg || '#e8e8e8'; // __background_alt__ 1555*7e8ea635SAtari911 highlightShadow = '0 0 10px rgba(0, 0, 0, 0.15)'; 15569ccd446eSAtari911 } 15579ccd446eSAtari911 15589ccd446eSAtari911 15599ccd446eSAtari911 // Store original styles 15609ccd446eSAtari911 const originalBg = eventItem.style.background; 15619ccd446eSAtari911 const originalShadow = eventItem.style.boxShadow; 15629ccd446eSAtari911 15639ccd446eSAtari911 // Remove previous highlights (restore their original styles) 15641d05cddcSAtari911 const previousHighlights = eventList.querySelectorAll('.event-highlighted'); 15659ccd446eSAtari911 previousHighlights.forEach(el => { 15669ccd446eSAtari911 el.classList.remove('event-highlighted'); 15679ccd446eSAtari911 }); 15681d05cddcSAtari911 15699ccd446eSAtari911 // Add highlight class and apply theme-aware glow 15701d05cddcSAtari911 eventItem.classList.add('event-highlighted'); 15711d05cddcSAtari911 15729ccd446eSAtari911 // Set CSS properties directly 15739ccd446eSAtari911 eventItem.style.setProperty('background', highlightBg, 'important'); 15749ccd446eSAtari911 eventItem.style.setProperty('box-shadow', highlightShadow, 'important'); 15759ccd446eSAtari911 eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important'); 15769ccd446eSAtari911 15779ccd446eSAtari911 15781d05cddcSAtari911 // Scroll to event 15791d05cddcSAtari911 eventItem.scrollIntoView({ 15801d05cddcSAtari911 behavior: 'smooth', 15811d05cddcSAtari911 block: 'nearest', 15821d05cddcSAtari911 inline: 'nearest' 15831d05cddcSAtari911 }); 15841d05cddcSAtari911 15859ccd446eSAtari911 // Remove highlight after 3 seconds and restore original styles 15861d05cddcSAtari911 setTimeout(() => { 15871d05cddcSAtari911 eventItem.classList.remove('event-highlighted'); 15889ccd446eSAtari911 eventItem.style.setProperty('background', originalBg); 15899ccd446eSAtari911 eventItem.style.setProperty('box-shadow', originalShadow); 15909ccd446eSAtari911 eventItem.style.setProperty('transition', ''); 15911d05cddcSAtari911 }, 3000); 15921d05cddcSAtari911}; 15931d05cddcSAtari911 15941d05cddcSAtari911// Toggle recurring event options 15951d05cddcSAtari911window.toggleRecurringOptions = function(calId) { 15961d05cddcSAtari911 const checkbox = document.getElementById('event-recurring-' + calId); 15971d05cddcSAtari911 const options = document.getElementById('recurring-options-' + calId); 15981d05cddcSAtari911 15991d05cddcSAtari911 if (checkbox && options) { 16001d05cddcSAtari911 options.style.display = checkbox.checked ? 'block' : 'none'; 16011d05cddcSAtari911 } 16021d05cddcSAtari911}; 16031d05cddcSAtari911 16049ccd446eSAtari911// ============================================================ 16059ccd446eSAtari911// Document-level event delegation (guarded - only attach once) 16069ccd446eSAtari911// These use event delegation so they work for AJAX-rebuilt content. 16079ccd446eSAtari911// ============================================================ 16089ccd446eSAtari911if (!window._calendarDelegationInit) { 16099ccd446eSAtari911 window._calendarDelegationInit = true; 16109ccd446eSAtari911 16119ccd446eSAtari911 // ESC closes dialogs, popups, tooltips 16121d05cddcSAtari911 document.addEventListener('keydown', function(e) { 16131d05cddcSAtari911 if (e.key === 'Escape') { 16149ccd446eSAtari911 document.querySelectorAll('.event-dialog-compact').forEach(function(d) { 16159ccd446eSAtari911 if (d.style.display === 'flex') d.style.display = 'none'; 16169ccd446eSAtari911 }); 16179ccd446eSAtari911 document.querySelectorAll('.day-popup').forEach(function(p) { 16189ccd446eSAtari911 p.style.display = 'none'; 16199ccd446eSAtari911 }); 16209ccd446eSAtari911 hideConflictTooltip(); 16211d05cddcSAtari911 } 16221d05cddcSAtari911 }); 16239ccd446eSAtari911 16249ccd446eSAtari911 // Conflict tooltip delegation (capture phase for mouseenter/leave) 16259ccd446eSAtari911 document.addEventListener('mouseenter', function(e) { 16269ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 16279ccd446eSAtari911 showConflictTooltip(e.target); 16281d05cddcSAtari911 } 16299ccd446eSAtari911 }, true); 16309ccd446eSAtari911 16319ccd446eSAtari911 document.addEventListener('mouseleave', function(e) { 16329ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 16339ccd446eSAtari911 hideConflictTooltip(); 16349ccd446eSAtari911 } 16359ccd446eSAtari911 }, true); 16369ccd446eSAtari911} // end delegation guard 16371d05cddcSAtari911 16381d05cddcSAtari911// Event panel navigation 16391d05cddcSAtari911window.navEventPanel = function(calId, year, month, namespace) { 16401d05cddcSAtari911 const params = new URLSearchParams({ 16411d05cddcSAtari911 call: 'plugin_calendar', 16421d05cddcSAtari911 action: 'load_month', 16431d05cddcSAtari911 year: year, 16441d05cddcSAtari911 month: month, 16451d05cddcSAtari911 namespace: namespace, 16461d05cddcSAtari911 _: new Date().getTime() // Cache buster 16471d05cddcSAtari911 }); 16481d05cddcSAtari911 16491d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 16501d05cddcSAtari911 method: 'POST', 16511d05cddcSAtari911 headers: { 16521d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 16531d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 16541d05cddcSAtari911 'Pragma': 'no-cache' 16551d05cddcSAtari911 }, 16561d05cddcSAtari911 body: params.toString() 16571d05cddcSAtari911 }) 16581d05cddcSAtari911 .then(r => r.json()) 16591d05cddcSAtari911 .then(data => { 16601d05cddcSAtari911 if (data.success) { 16611d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 16621d05cddcSAtari911 } 16631d05cddcSAtari911 }) 16641d05cddcSAtari911 .catch(err => console.error('Error:', err)); 16651d05cddcSAtari911}; 16661d05cddcSAtari911 16671d05cddcSAtari911// Rebuild event panel only 16681d05cddcSAtari911window.rebuildEventPanel = function(calId, year, month, events, namespace) { 16691d05cddcSAtari911 const container = document.getElementById(calId); 16701d05cddcSAtari911 const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 16711d05cddcSAtari911 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 16721d05cddcSAtari911 16731d05cddcSAtari911 // Update month title in new compact header 16741d05cddcSAtari911 const monthTitle = container.querySelector('.panel-month-title'); 16751d05cddcSAtari911 if (monthTitle) { 16761d05cddcSAtari911 monthTitle.textContent = monthNames[month - 1] + ' ' + year; 16771d05cddcSAtari911 monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 16781d05cddcSAtari911 monthTitle.setAttribute('title', 'Click to jump to month'); 16791d05cddcSAtari911 } 16801d05cddcSAtari911 16811d05cddcSAtari911 // Fallback: Update old header format if exists 16821d05cddcSAtari911 const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker'); 16831d05cddcSAtari911 if (oldHeader && !monthTitle) { 16841d05cddcSAtari911 oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events'; 16851d05cddcSAtari911 oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 16861d05cddcSAtari911 } 16871d05cddcSAtari911 16881d05cddcSAtari911 // Update nav buttons 16891d05cddcSAtari911 let prevMonth = month - 1; 16901d05cddcSAtari911 let prevYear = year; 16911d05cddcSAtari911 if (prevMonth < 1) { 16921d05cddcSAtari911 prevMonth = 12; 16931d05cddcSAtari911 prevYear--; 16941d05cddcSAtari911 } 16951d05cddcSAtari911 16961d05cddcSAtari911 let nextMonth = month + 1; 16971d05cddcSAtari911 let nextYear = year; 16981d05cddcSAtari911 if (nextMonth > 12) { 16991d05cddcSAtari911 nextMonth = 1; 17001d05cddcSAtari911 nextYear++; 17011d05cddcSAtari911 } 17021d05cddcSAtari911 17031d05cddcSAtari911 // Update new compact nav buttons 17041d05cddcSAtari911 const navBtns = container.querySelectorAll('.panel-nav-btn'); 17051d05cddcSAtari911 if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 17061d05cddcSAtari911 if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 17071d05cddcSAtari911 17081d05cddcSAtari911 // Fallback for old nav buttons 17091d05cddcSAtari911 const oldNavBtns = container.querySelectorAll('.cal-nav-btn'); 17101d05cddcSAtari911 if (oldNavBtns.length > 0 && navBtns.length === 0) { 17111d05cddcSAtari911 if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 17121d05cddcSAtari911 if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 17131d05cddcSAtari911 } 17141d05cddcSAtari911 17151d05cddcSAtari911 // Update Today button (works for both old and new) 17161d05cddcSAtari911 const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact'); 17171d05cddcSAtari911 if (todayBtn) { 17181d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`); 17191d05cddcSAtari911 } 17201d05cddcSAtari911 17211d05cddcSAtari911 // Rebuild event list 17221d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 17231d05cddcSAtari911 if (eventList) { 17241d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 17251d05cddcSAtari911 } 17261d05cddcSAtari911}; 17271d05cddcSAtari911 17281d05cddcSAtari911// Open add event for panel 17291d05cddcSAtari911window.openAddEventPanel = function(calId, namespace) { 17301d05cddcSAtari911 const today = new Date(); 17311d05cddcSAtari911 const year = today.getFullYear(); 17321d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 17331d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 17341d05cddcSAtari911 const localDate = `${year}-${month}-${day}`; 17351d05cddcSAtari911 openAddEvent(calId, namespace, localDate); 17361d05cddcSAtari911}; 17371d05cddcSAtari911 17381d05cddcSAtari911// Toggle task completion 17391d05cddcSAtari911window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) { 17401d05cddcSAtari911 const params = new URLSearchParams({ 17411d05cddcSAtari911 call: 'plugin_calendar', 17421d05cddcSAtari911 action: 'toggle_task', 17431d05cddcSAtari911 namespace: namespace, 17441d05cddcSAtari911 date: date, 17451d05cddcSAtari911 eventId: eventId, 1746*7e8ea635SAtari911 completed: completed ? '1' : '0', 1747*7e8ea635SAtari911 sectok: typeof JSINFO !== 'undefined' ? JSINFO.sectok : '' 17481d05cddcSAtari911 }); 17491d05cddcSAtari911 17501d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 17511d05cddcSAtari911 method: 'POST', 17521d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 17531d05cddcSAtari911 body: params.toString() 17541d05cddcSAtari911 }) 17551d05cddcSAtari911 .then(r => r.json()) 17561d05cddcSAtari911 .then(data => { 17571d05cddcSAtari911 if (data.success) { 17581d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 17591d05cddcSAtari911 reloadCalendarData(calId, year, month, namespace); 17601d05cddcSAtari911 } 17611d05cddcSAtari911 }) 17621d05cddcSAtari911 .catch(err => console.error('Error toggling task:', err)); 17631d05cddcSAtari911}; 17641d05cddcSAtari911 17651d05cddcSAtari911// Make dialog draggable 17661d05cddcSAtari911window.makeDialogDraggable = function(calId) { 17671d05cddcSAtari911 const dialog = document.getElementById('dialog-content-' + calId); 17681d05cddcSAtari911 const handle = document.getElementById('drag-handle-' + calId); 17691d05cddcSAtari911 17701d05cddcSAtari911 if (!dialog || !handle) return; 17711d05cddcSAtari911 17721d05cddcSAtari911 let isDragging = false; 17731d05cddcSAtari911 let currentX; 17741d05cddcSAtari911 let currentY; 17751d05cddcSAtari911 let initialX; 17761d05cddcSAtari911 let initialY; 17771d05cddcSAtari911 let xOffset = 0; 17781d05cddcSAtari911 let yOffset = 0; 17791d05cddcSAtari911 17801d05cddcSAtari911 handle.addEventListener('mousedown', dragStart); 17811d05cddcSAtari911 document.addEventListener('mousemove', drag); 17821d05cddcSAtari911 document.addEventListener('mouseup', dragEnd); 17831d05cddcSAtari911 17841d05cddcSAtari911 function dragStart(e) { 17851d05cddcSAtari911 initialX = e.clientX - xOffset; 17861d05cddcSAtari911 initialY = e.clientY - yOffset; 17871d05cddcSAtari911 isDragging = true; 17881d05cddcSAtari911 } 17891d05cddcSAtari911 17901d05cddcSAtari911 function drag(e) { 17911d05cddcSAtari911 if (isDragging) { 17921d05cddcSAtari911 e.preventDefault(); 17931d05cddcSAtari911 currentX = e.clientX - initialX; 17941d05cddcSAtari911 currentY = e.clientY - initialY; 17951d05cddcSAtari911 xOffset = currentX; 17961d05cddcSAtari911 yOffset = currentY; 17971d05cddcSAtari911 setTranslate(currentX, currentY, dialog); 17981d05cddcSAtari911 } 17991d05cddcSAtari911 } 18001d05cddcSAtari911 18011d05cddcSAtari911 function dragEnd(e) { 18021d05cddcSAtari911 initialX = currentX; 18031d05cddcSAtari911 initialY = currentY; 18041d05cddcSAtari911 isDragging = false; 18051d05cddcSAtari911 } 18061d05cddcSAtari911 18071d05cddcSAtari911 function setTranslate(xPos, yPos, el) { 18081d05cddcSAtari911 el.style.transform = `translate(${xPos}px, ${yPos}px)`; 18091d05cddcSAtari911 } 18101d05cddcSAtari911}; 18111d05cddcSAtari911 18121d05cddcSAtari911// Initialize dialog draggability when opened (avoid duplicate declaration) 18131d05cddcSAtari911if (!window.calendarDraggabilityPatched) { 18141d05cddcSAtari911 window.calendarDraggabilityPatched = true; 18151d05cddcSAtari911 18161d05cddcSAtari911 const originalOpenAddEvent = openAddEvent; 18171d05cddcSAtari911 openAddEvent = function(calId, namespace, date) { 18181d05cddcSAtari911 originalOpenAddEvent(calId, namespace, date); 18191d05cddcSAtari911 setTimeout(() => makeDialogDraggable(calId), 100); 18201d05cddcSAtari911 }; 18211d05cddcSAtari911 18221d05cddcSAtari911 const originalEditEvent = editEvent; 18231d05cddcSAtari911 editEvent = function(calId, eventId, date, namespace) { 18241d05cddcSAtari911 originalEditEvent(calId, eventId, date, namespace); 18251d05cddcSAtari911 setTimeout(() => makeDialogDraggable(calId), 100); 18261d05cddcSAtari911 }; 18271d05cddcSAtari911} 18281d05cddcSAtari911 18291d05cddcSAtari911// Toggle expand/collapse for past events 18301d05cddcSAtari911window.togglePastEventExpand = function(element) { 18311d05cddcSAtari911 // Stop propagation to prevent any parent click handlers 18321d05cddcSAtari911 event.stopPropagation(); 18331d05cddcSAtari911 18341d05cddcSAtari911 const meta = element.querySelector(".event-meta-compact"); 18351d05cddcSAtari911 const desc = element.querySelector(".event-desc-compact"); 18361d05cddcSAtari911 18371d05cddcSAtari911 // Toggle visibility 18381d05cddcSAtari911 if (meta.style.display === "none") { 18391d05cddcSAtari911 // Expand 18401d05cddcSAtari911 meta.style.display = "block"; 18411d05cddcSAtari911 if (desc) desc.style.display = "block"; 18421d05cddcSAtari911 element.classList.add("event-past-expanded"); 18431d05cddcSAtari911 } else { 18441d05cddcSAtari911 // Collapse 18451d05cddcSAtari911 meta.style.display = "none"; 18461d05cddcSAtari911 if (desc) desc.style.display = "none"; 18471d05cddcSAtari911 element.classList.remove("event-past-expanded"); 18481d05cddcSAtari911 } 18491d05cddcSAtari911}; 18501d05cddcSAtari911 18519ccd446eSAtari911// Filter calendar by namespace when clicking namespace badge (guarded) 18529ccd446eSAtari911if (!window._calendarClickDelegationInit) { 18539ccd446eSAtari911 window._calendarClickDelegationInit = true; 18541d05cddcSAtari911 document.addEventListener('click', function(e) { 18551d05cddcSAtari911 if (e.target.classList.contains('event-namespace-badge')) { 18561d05cddcSAtari911 const namespace = e.target.textContent; 18571d05cddcSAtari911 const calendar = e.target.closest('.calendar-compact-container'); 18581d05cddcSAtari911 1859*7e8ea635SAtari911 if (!calendar) return; 18601d05cddcSAtari911 18611d05cddcSAtari911 const calId = calendar.id; 18621d05cddcSAtari911 1863*7e8ea635SAtari911 // Use AJAX reload to filter both calendar grid and event list 1864*7e8ea635SAtari911 filterCalendarByNamespace(calId, namespace); 18651d05cddcSAtari911 } 18661d05cddcSAtari911 }); 18679ccd446eSAtari911} // end click delegation guard 18681d05cddcSAtari911 18691d05cddcSAtari911// Update the displayed filtered namespace in event list header 1870*7e8ea635SAtari911// Legacy badge removed - namespace filtering still works but badge no longer shown 18711d05cddcSAtari911window.updateFilteredNamespaceDisplay = function(calId, namespace) { 18721d05cddcSAtari911 const calendar = document.getElementById(calId); 18731d05cddcSAtari911 if (!calendar) return; 18741d05cddcSAtari911 18751d05cddcSAtari911 const headerContent = calendar.querySelector('.event-list-header-content'); 18761d05cddcSAtari911 if (!headerContent) return; 18771d05cddcSAtari911 1878*7e8ea635SAtari911 // Remove any existing filter badge (cleanup) 18791d05cddcSAtari911 let filterBadge = headerContent.querySelector('.namespace-filter-badge'); 18801d05cddcSAtari911 if (filterBadge) { 18811d05cddcSAtari911 filterBadge.remove(); 18821d05cddcSAtari911 } 18831d05cddcSAtari911}; 18841d05cddcSAtari911 18851d05cddcSAtari911// Clear namespace filter 18861d05cddcSAtari911window.clearNamespaceFilter = function(calId) { 18871d05cddcSAtari911 18881d05cddcSAtari911 const container = document.getElementById(calId); 18891d05cddcSAtari911 if (!container) { 18901d05cddcSAtari911 console.error('Calendar container not found:', calId); 18911d05cddcSAtari911 return; 18921d05cddcSAtari911 } 18931d05cddcSAtari911 18949ccd446eSAtari911 // Immediately hide/remove the filter badge 18959ccd446eSAtari911 const filterBadge = container.querySelector('.calendar-namespace-filter'); 18969ccd446eSAtari911 if (filterBadge) { 18979ccd446eSAtari911 filterBadge.style.display = 'none'; 18989ccd446eSAtari911 filterBadge.remove(); 18999ccd446eSAtari911 } 19009ccd446eSAtari911 19011d05cddcSAtari911 // Get current year and month 19021d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 19031d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 19041d05cddcSAtari911 19051d05cddcSAtari911 // Get original namespace (what the calendar was initialized with) 19061d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 19071d05cddcSAtari911 19089ccd446eSAtari911 // Also check for sidebar widget 19099ccd446eSAtari911 const sidebarContainer = document.getElementById('sidebar-widget-' + calId); 19109ccd446eSAtari911 if (sidebarContainer) { 19119ccd446eSAtari911 // For sidebar widget, just reload the page without namespace filter 19129ccd446eSAtari911 // Remove the namespace from the URL and reload 19139ccd446eSAtari911 const url = new URL(window.location.href); 19149ccd446eSAtari911 url.searchParams.delete('namespace'); 19159ccd446eSAtari911 window.location.href = url.toString(); 19169ccd446eSAtari911 return; 19179ccd446eSAtari911 } 19181d05cddcSAtari911 19199ccd446eSAtari911 // For regular calendar, reload calendar with original namespace 19201d05cddcSAtari911 navCalendar(calId, year, month, originalNamespace); 19211d05cddcSAtari911}; 19221d05cddcSAtari911 19231d05cddcSAtari911window.clearNamespaceFilterPanel = function(calId) { 19241d05cddcSAtari911 19251d05cddcSAtari911 const container = document.getElementById(calId); 19261d05cddcSAtari911 if (!container) { 19271d05cddcSAtari911 console.error('Event panel container not found:', calId); 19281d05cddcSAtari911 return; 19291d05cddcSAtari911 } 19301d05cddcSAtari911 19311d05cddcSAtari911 // Get current year and month from URL params or container 19321d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 19331d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 19341d05cddcSAtari911 19351d05cddcSAtari911 // Get original namespace (what the panel was initialized with) 19361d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 19371d05cddcSAtari911 19381d05cddcSAtari911 19391d05cddcSAtari911 // Reload event panel with original namespace 19401d05cddcSAtari911 navEventPanel(calId, year, month, originalNamespace); 19411d05cddcSAtari911}; 19421d05cddcSAtari911 19431d05cddcSAtari911// Color picker functions 19441d05cddcSAtari911window.updateCustomColorPicker = function(calId) { 19451d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 19461d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 19471d05cddcSAtari911 19481d05cddcSAtari911 if (select.value === 'custom') { 19491d05cddcSAtari911 // Show color picker 19501d05cddcSAtari911 picker.style.display = 'inline-block'; 19511d05cddcSAtari911 picker.click(); // Open color picker 19521d05cddcSAtari911 } else { 19531d05cddcSAtari911 // Hide color picker and sync value 19541d05cddcSAtari911 picker.style.display = 'none'; 19551d05cddcSAtari911 picker.value = select.value; 19561d05cddcSAtari911 } 19571d05cddcSAtari911}; 19581d05cddcSAtari911 19591d05cddcSAtari911function updateColorFromPicker(calId) { 19601d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 19611d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 19621d05cddcSAtari911 19631d05cddcSAtari911 // Set select to custom and update its underlying value 19641d05cddcSAtari911 select.value = 'custom'; 19651d05cddcSAtari911 // Store the actual color value in a data attribute 19661d05cddcSAtari911 select.dataset.customColor = picker.value; 19671d05cddcSAtari911} 19681d05cddcSAtari911 19691d05cddcSAtari911// Toggle past events visibility 19701d05cddcSAtari911window.togglePastEvents = function(calId) { 19711d05cddcSAtari911 const content = document.getElementById('past-events-' + calId); 19721d05cddcSAtari911 const arrow = document.getElementById('past-arrow-' + calId); 19731d05cddcSAtari911 19741d05cddcSAtari911 if (!content || !arrow) { 19751d05cddcSAtari911 console.error('Past events elements not found for:', calId); 19761d05cddcSAtari911 return; 19771d05cddcSAtari911 } 19781d05cddcSAtari911 19791d05cddcSAtari911 // Check computed style instead of inline style 19801d05cddcSAtari911 const isHidden = window.getComputedStyle(content).display === 'none'; 19811d05cddcSAtari911 19821d05cddcSAtari911 if (isHidden) { 19831d05cddcSAtari911 content.style.display = 'block'; 19841d05cddcSAtari911 arrow.textContent = '▼'; 19851d05cddcSAtari911 } else { 19861d05cddcSAtari911 content.style.display = 'none'; 19871d05cddcSAtari911 arrow.textContent = '▶'; 19881d05cddcSAtari911 } 19891d05cddcSAtari911}; 19901d05cddcSAtari911 19911d05cddcSAtari911// Fuzzy match scoring function 19921d05cddcSAtari911window.fuzzyMatch = function(pattern, str) { 19931d05cddcSAtari911 pattern = pattern.toLowerCase(); 19941d05cddcSAtari911 str = str.toLowerCase(); 19951d05cddcSAtari911 19961d05cddcSAtari911 let patternIdx = 0; 19971d05cddcSAtari911 let score = 0; 19981d05cddcSAtari911 let consecutiveMatches = 0; 19991d05cddcSAtari911 20001d05cddcSAtari911 for (let i = 0; i < str.length; i++) { 20011d05cddcSAtari911 if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) { 20021d05cddcSAtari911 score += 1 + consecutiveMatches; 20031d05cddcSAtari911 consecutiveMatches++; 20041d05cddcSAtari911 patternIdx++; 20051d05cddcSAtari911 } else { 20061d05cddcSAtari911 consecutiveMatches = 0; 20071d05cddcSAtari911 } 20081d05cddcSAtari911 } 20091d05cddcSAtari911 20101d05cddcSAtari911 // Return null if not all characters matched 20111d05cddcSAtari911 if (patternIdx !== pattern.length) { 20121d05cddcSAtari911 return null; 20131d05cddcSAtari911 } 20141d05cddcSAtari911 20151d05cddcSAtari911 // Bonus for exact match 20161d05cddcSAtari911 if (str === pattern) { 20171d05cddcSAtari911 score += 100; 20181d05cddcSAtari911 } 20191d05cddcSAtari911 20201d05cddcSAtari911 // Bonus for starts with 20211d05cddcSAtari911 if (str.startsWith(pattern)) { 20221d05cddcSAtari911 score += 50; 20231d05cddcSAtari911 } 20241d05cddcSAtari911 20251d05cddcSAtari911 return score; 20261d05cddcSAtari911}; 20271d05cddcSAtari911 20281d05cddcSAtari911// Initialize namespace search for a calendar 20291d05cddcSAtari911window.initNamespaceSearch = function(calId) { 20301d05cddcSAtari911 const searchInput = document.getElementById('event-namespace-search-' + calId); 20311d05cddcSAtari911 const hiddenInput = document.getElementById('event-namespace-' + calId); 20321d05cddcSAtari911 const dropdown = document.getElementById('event-namespace-dropdown-' + calId); 20331d05cddcSAtari911 const dataElement = document.getElementById('namespaces-data-' + calId); 20341d05cddcSAtari911 20351d05cddcSAtari911 if (!searchInput || !hiddenInput || !dropdown || !dataElement) { 20361d05cddcSAtari911 return; // Elements not found 20371d05cddcSAtari911 } 20381d05cddcSAtari911 20391d05cddcSAtari911 let namespaces = []; 20401d05cddcSAtari911 try { 20411d05cddcSAtari911 namespaces = JSON.parse(dataElement.textContent); 20421d05cddcSAtari911 } catch (e) { 20431d05cddcSAtari911 console.error('Failed to parse namespaces data:', e); 20441d05cddcSAtari911 return; 20451d05cddcSAtari911 } 20461d05cddcSAtari911 20471d05cddcSAtari911 let selectedIndex = -1; 20481d05cddcSAtari911 20491d05cddcSAtari911 // Filter and show dropdown 20501d05cddcSAtari911 function filterNamespaces(query) { 20511d05cddcSAtari911 if (!query || query.trim() === '') { 20521d05cddcSAtari911 // Show all namespaces when empty 20531d05cddcSAtari911 hiddenInput.value = ''; 20541d05cddcSAtari911 const results = namespaces.slice(0, 20); // Limit to 20 20551d05cddcSAtari911 showDropdown(results); 20561d05cddcSAtari911 return; 20571d05cddcSAtari911 } 20581d05cddcSAtari911 20591d05cddcSAtari911 // Fuzzy match and score 20601d05cddcSAtari911 const matches = []; 20611d05cddcSAtari911 for (let i = 0; i < namespaces.length; i++) { 20621d05cddcSAtari911 const score = fuzzyMatch(query, namespaces[i]); 20631d05cddcSAtari911 if (score !== null) { 20641d05cddcSAtari911 matches.push({ namespace: namespaces[i], score: score }); 20651d05cddcSAtari911 } 20661d05cddcSAtari911 } 20671d05cddcSAtari911 20681d05cddcSAtari911 // Sort by score (descending) 20691d05cddcSAtari911 matches.sort((a, b) => b.score - a.score); 20701d05cddcSAtari911 20711d05cddcSAtari911 // Take top 20 results 20721d05cddcSAtari911 const results = matches.slice(0, 20).map(m => m.namespace); 20731d05cddcSAtari911 showDropdown(results); 20741d05cddcSAtari911 } 20751d05cddcSAtari911 20761d05cddcSAtari911 function showDropdown(results) { 20771d05cddcSAtari911 dropdown.innerHTML = ''; 20781d05cddcSAtari911 selectedIndex = -1; 20791d05cddcSAtari911 20801d05cddcSAtari911 if (results.length === 0) { 20811d05cddcSAtari911 dropdown.style.display = 'none'; 20821d05cddcSAtari911 return; 20831d05cddcSAtari911 } 20841d05cddcSAtari911 20851d05cddcSAtari911 // Add (default) option 20861d05cddcSAtari911 const defaultOption = document.createElement('div'); 20871d05cddcSAtari911 defaultOption.className = 'namespace-option'; 20881d05cddcSAtari911 defaultOption.textContent = '(default)'; 20891d05cddcSAtari911 defaultOption.dataset.value = ''; 20901d05cddcSAtari911 dropdown.appendChild(defaultOption); 20911d05cddcSAtari911 20921d05cddcSAtari911 results.forEach(ns => { 20931d05cddcSAtari911 const option = document.createElement('div'); 20941d05cddcSAtari911 option.className = 'namespace-option'; 20951d05cddcSAtari911 option.textContent = ns; 20961d05cddcSAtari911 option.dataset.value = ns; 20971d05cddcSAtari911 dropdown.appendChild(option); 20981d05cddcSAtari911 }); 20991d05cddcSAtari911 21001d05cddcSAtari911 dropdown.style.display = 'block'; 21011d05cddcSAtari911 } 21021d05cddcSAtari911 21031d05cddcSAtari911 function hideDropdown() { 21041d05cddcSAtari911 dropdown.style.display = 'none'; 21051d05cddcSAtari911 selectedIndex = -1; 21061d05cddcSAtari911 } 21071d05cddcSAtari911 21081d05cddcSAtari911 function selectOption(namespace) { 21091d05cddcSAtari911 hiddenInput.value = namespace; 21101d05cddcSAtari911 searchInput.value = namespace || '(default)'; 21111d05cddcSAtari911 hideDropdown(); 21121d05cddcSAtari911 } 21131d05cddcSAtari911 21141d05cddcSAtari911 // Event listeners 21151d05cddcSAtari911 searchInput.addEventListener('input', function(e) { 21161d05cddcSAtari911 filterNamespaces(e.target.value); 21171d05cddcSAtari911 }); 21181d05cddcSAtari911 21191d05cddcSAtari911 searchInput.addEventListener('focus', function(e) { 21201d05cddcSAtari911 filterNamespaces(e.target.value); 21211d05cddcSAtari911 }); 21221d05cddcSAtari911 21231d05cddcSAtari911 searchInput.addEventListener('blur', function(e) { 21241d05cddcSAtari911 // Delay to allow click on dropdown 21251d05cddcSAtari911 setTimeout(hideDropdown, 200); 21261d05cddcSAtari911 }); 21271d05cddcSAtari911 21281d05cddcSAtari911 searchInput.addEventListener('keydown', function(e) { 21291d05cddcSAtari911 const options = dropdown.querySelectorAll('.namespace-option'); 21301d05cddcSAtari911 21311d05cddcSAtari911 if (e.key === 'ArrowDown') { 21321d05cddcSAtari911 e.preventDefault(); 21331d05cddcSAtari911 selectedIndex = Math.min(selectedIndex + 1, options.length - 1); 21341d05cddcSAtari911 updateSelection(options); 21351d05cddcSAtari911 } else if (e.key === 'ArrowUp') { 21361d05cddcSAtari911 e.preventDefault(); 21371d05cddcSAtari911 selectedIndex = Math.max(selectedIndex - 1, -1); 21381d05cddcSAtari911 updateSelection(options); 21391d05cddcSAtari911 } else if (e.key === 'Enter') { 21401d05cddcSAtari911 e.preventDefault(); 21411d05cddcSAtari911 if (selectedIndex >= 0 && options[selectedIndex]) { 21421d05cddcSAtari911 selectOption(options[selectedIndex].dataset.value); 21431d05cddcSAtari911 } 21441d05cddcSAtari911 } else if (e.key === 'Escape') { 21451d05cddcSAtari911 hideDropdown(); 21461d05cddcSAtari911 } 21471d05cddcSAtari911 }); 21481d05cddcSAtari911 21491d05cddcSAtari911 function updateSelection(options) { 21501d05cddcSAtari911 options.forEach((opt, idx) => { 21511d05cddcSAtari911 if (idx === selectedIndex) { 21521d05cddcSAtari911 opt.classList.add('selected'); 21531d05cddcSAtari911 opt.scrollIntoView({ block: 'nearest' }); 21541d05cddcSAtari911 } else { 21551d05cddcSAtari911 opt.classList.remove('selected'); 21561d05cddcSAtari911 } 21571d05cddcSAtari911 }); 21581d05cddcSAtari911 } 21591d05cddcSAtari911 21601d05cddcSAtari911 // Click on dropdown option 21611d05cddcSAtari911 dropdown.addEventListener('mousedown', function(e) { 21621d05cddcSAtari911 if (e.target.classList.contains('namespace-option')) { 21631d05cddcSAtari911 selectOption(e.target.dataset.value); 21641d05cddcSAtari911 } 21651d05cddcSAtari911 }); 21661d05cddcSAtari911}; 21671d05cddcSAtari911 21681d05cddcSAtari911// Update end time options based on start time selection 21691d05cddcSAtari911window.updateEndTimeOptions = function(calId) { 21701d05cddcSAtari911 const startTimeSelect = document.getElementById('event-time-' + calId); 21711d05cddcSAtari911 const endTimeSelect = document.getElementById('event-end-time-' + calId); 21721d05cddcSAtari911 21731d05cddcSAtari911 if (!startTimeSelect || !endTimeSelect) return; 21741d05cddcSAtari911 21751d05cddcSAtari911 const startTime = startTimeSelect.value; 21761d05cddcSAtari911 21771d05cddcSAtari911 // If start time is empty (all day), disable end time 21781d05cddcSAtari911 if (!startTime) { 21791d05cddcSAtari911 endTimeSelect.disabled = true; 21801d05cddcSAtari911 endTimeSelect.value = ''; 21811d05cddcSAtari911 return; 21821d05cddcSAtari911 } 21831d05cddcSAtari911 21841d05cddcSAtari911 // Enable end time select 21851d05cddcSAtari911 endTimeSelect.disabled = false; 21861d05cddcSAtari911 21871d05cddcSAtari911 // Convert start time to minutes 21881d05cddcSAtari911 const startMinutes = timeToMinutes(startTime); 21891d05cddcSAtari911 21901d05cddcSAtari911 // Get current end time value (to preserve if valid) 21911d05cddcSAtari911 const currentEndTime = endTimeSelect.value; 21921d05cddcSAtari911 const currentEndMinutes = currentEndTime ? timeToMinutes(currentEndTime) : 0; 21931d05cddcSAtari911 21941d05cddcSAtari911 // Filter options - show only times after start time 21951d05cddcSAtari911 const options = endTimeSelect.options; 21961d05cddcSAtari911 let firstValidOption = null; 21971d05cddcSAtari911 let currentStillValid = false; 21981d05cddcSAtari911 21991d05cddcSAtari911 for (let i = 0; i < options.length; i++) { 22001d05cddcSAtari911 const option = options[i]; 22011d05cddcSAtari911 const optionValue = option.value; 22021d05cddcSAtari911 22031d05cddcSAtari911 if (optionValue === '') { 22041d05cddcSAtari911 // Keep "Same as start" option visible 22051d05cddcSAtari911 option.style.display = ''; 22061d05cddcSAtari911 continue; 22071d05cddcSAtari911 } 22081d05cddcSAtari911 22091d05cddcSAtari911 const optionMinutes = timeToMinutes(optionValue); 22101d05cddcSAtari911 22111d05cddcSAtari911 if (optionMinutes > startMinutes) { 22121d05cddcSAtari911 // Show options after start time 22131d05cddcSAtari911 option.style.display = ''; 22141d05cddcSAtari911 if (!firstValidOption) { 22151d05cddcSAtari911 firstValidOption = optionValue; 22161d05cddcSAtari911 } 22171d05cddcSAtari911 if (optionValue === currentEndTime) { 22181d05cddcSAtari911 currentStillValid = true; 22191d05cddcSAtari911 } 22201d05cddcSAtari911 } else { 22211d05cddcSAtari911 // Hide options before or equal to start time 22221d05cddcSAtari911 option.style.display = 'none'; 22231d05cddcSAtari911 } 22241d05cddcSAtari911 } 22251d05cddcSAtari911 22261d05cddcSAtari911 // If current end time is now invalid, set a new one 22271d05cddcSAtari911 if (!currentStillValid || currentEndMinutes <= startMinutes) { 22281d05cddcSAtari911 // Try to set to 1 hour after start 22291d05cddcSAtari911 const [startHour, startMinute] = startTime.split(':').map(Number); 22301d05cddcSAtari911 let endHour = startHour + 1; 22311d05cddcSAtari911 let endMinute = startMinute; 22321d05cddcSAtari911 22331d05cddcSAtari911 if (endHour >= 24) { 22341d05cddcSAtari911 endHour = 23; 22351d05cddcSAtari911 endMinute = 45; 22361d05cddcSAtari911 } 22371d05cddcSAtari911 22381d05cddcSAtari911 const suggestedEndTime = String(endHour).padStart(2, '0') + ':' + String(endMinute).padStart(2, '0'); 22391d05cddcSAtari911 22401d05cddcSAtari911 // Check if suggested time is in the list 22411d05cddcSAtari911 const suggestedExists = Array.from(options).some(opt => opt.value === suggestedEndTime); 22421d05cddcSAtari911 22431d05cddcSAtari911 if (suggestedExists) { 22441d05cddcSAtari911 endTimeSelect.value = suggestedEndTime; 22451d05cddcSAtari911 } else if (firstValidOption) { 22461d05cddcSAtari911 // Use first valid option 22471d05cddcSAtari911 endTimeSelect.value = firstValidOption; 22481d05cddcSAtari911 } else { 22491d05cddcSAtari911 // No valid options (shouldn't happen, but just in case) 22501d05cddcSAtari911 endTimeSelect.value = ''; 22511d05cddcSAtari911 } 22521d05cddcSAtari911 } 22531d05cddcSAtari911}; 22541d05cddcSAtari911 22551d05cddcSAtari911// Check for time conflicts between events on the same date 22561d05cddcSAtari911window.checkTimeConflicts = function(events, currentEventId) { 22571d05cddcSAtari911 const conflicts = []; 22581d05cddcSAtari911 22591d05cddcSAtari911 // Group events by date 22601d05cddcSAtari911 const eventsByDate = {}; 22611d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(events)) { 22621d05cddcSAtari911 if (!Array.isArray(dateEvents)) continue; 22631d05cddcSAtari911 22641d05cddcSAtari911 dateEvents.forEach(evt => { 22651d05cddcSAtari911 if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event 22661d05cddcSAtari911 22671d05cddcSAtari911 if (!eventsByDate[date]) eventsByDate[date] = []; 22681d05cddcSAtari911 eventsByDate[date].push(evt); 22691d05cddcSAtari911 }); 22701d05cddcSAtari911 } 22711d05cddcSAtari911 22721d05cddcSAtari911 // Check for overlaps on each date 22731d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(eventsByDate)) { 22741d05cddcSAtari911 for (let i = 0; i < dateEvents.length; i++) { 22751d05cddcSAtari911 for (let j = i + 1; j < dateEvents.length; j++) { 22761d05cddcSAtari911 const evt1 = dateEvents[i]; 22771d05cddcSAtari911 const evt2 = dateEvents[j]; 22781d05cddcSAtari911 22791d05cddcSAtari911 if (eventsOverlap(evt1, evt2)) { 22801d05cddcSAtari911 // Mark both events as conflicting 22811d05cddcSAtari911 if (!evt1.hasConflict) evt1.hasConflict = true; 22821d05cddcSAtari911 if (!evt2.hasConflict) evt2.hasConflict = true; 22831d05cddcSAtari911 22841d05cddcSAtari911 // Store conflict info 22851d05cddcSAtari911 if (!evt1.conflictsWith) evt1.conflictsWith = []; 22861d05cddcSAtari911 if (!evt2.conflictsWith) evt2.conflictsWith = []; 22871d05cddcSAtari911 22881d05cddcSAtari911 evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime}); 22891d05cddcSAtari911 evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime}); 22901d05cddcSAtari911 } 22911d05cddcSAtari911 } 22921d05cddcSAtari911 } 22931d05cddcSAtari911 } 22941d05cddcSAtari911 22951d05cddcSAtari911 return events; 22961d05cddcSAtari911}; 22971d05cddcSAtari911 22981d05cddcSAtari911// Check if two events overlap in time 22991d05cddcSAtari911function eventsOverlap(evt1, evt2) { 23001d05cddcSAtari911 if (!evt1.time || !evt2.time) return false; // All-day events don't conflict 23011d05cddcSAtari911 23021d05cddcSAtari911 const start1 = evt1.time; 23031d05cddcSAtari911 const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start 23041d05cddcSAtari911 23051d05cddcSAtari911 const start2 = evt2.time; 23061d05cddcSAtari911 const end2 = evt2.endTime || evt2.time; 23071d05cddcSAtari911 23081d05cddcSAtari911 // Convert to minutes for easier comparison 23091d05cddcSAtari911 const start1Mins = timeToMinutes(start1); 23101d05cddcSAtari911 const end1Mins = timeToMinutes(end1); 23111d05cddcSAtari911 const start2Mins = timeToMinutes(start2); 23121d05cddcSAtari911 const end2Mins = timeToMinutes(end2); 23131d05cddcSAtari911 23141d05cddcSAtari911 // Check for overlap 23151d05cddcSAtari911 // Events overlap if: start1 < end2 AND start2 < end1 23161d05cddcSAtari911 return start1Mins < end2Mins && start2Mins < end1Mins; 23171d05cddcSAtari911} 23181d05cddcSAtari911 23191d05cddcSAtari911// Convert HH:MM time to minutes since midnight 23201d05cddcSAtari911function timeToMinutes(timeStr) { 23211d05cddcSAtari911 const [hours, minutes] = timeStr.split(':').map(Number); 23221d05cddcSAtari911 return hours * 60 + minutes; 23231d05cddcSAtari911} 23241d05cddcSAtari911 23251d05cddcSAtari911// Format time range for display 23261d05cddcSAtari911window.formatTimeRange = function(startTime, endTime) { 23271d05cddcSAtari911 if (!startTime) return ''; 23281d05cddcSAtari911 23291d05cddcSAtari911 const formatTime = (timeStr) => { 23301d05cddcSAtari911 const [hour24, minute] = timeStr.split(':').map(Number); 23311d05cddcSAtari911 const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24); 23321d05cddcSAtari911 const ampm = hour24 < 12 ? 'AM' : 'PM'; 23331d05cddcSAtari911 return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 23341d05cddcSAtari911 }; 23351d05cddcSAtari911 23361d05cddcSAtari911 if (!endTime || endTime === startTime) { 23371d05cddcSAtari911 return formatTime(startTime); 23381d05cddcSAtari911 } 23391d05cddcSAtari911 23401d05cddcSAtari911 return formatTime(startTime) + ' - ' + formatTime(endTime); 23411d05cddcSAtari911}; 23421d05cddcSAtari911 23439ccd446eSAtari911// Track last known mouse position for tooltip positioning fallback 23449ccd446eSAtari911var _lastMouseX = 0, _lastMouseY = 0; 23459ccd446eSAtari911document.addEventListener('mousemove', function(e) { 23469ccd446eSAtari911 _lastMouseX = e.clientX; 23479ccd446eSAtari911 _lastMouseY = e.clientY; 23489ccd446eSAtari911}); 23499ccd446eSAtari911 23501d05cddcSAtari911// Show custom conflict tooltip 23511d05cddcSAtari911window.showConflictTooltip = function(badgeElement) { 23521d05cddcSAtari911 // Remove any existing tooltip 23531d05cddcSAtari911 hideConflictTooltip(); 23541d05cddcSAtari911 23559ccd446eSAtari911 // Get conflict data (base64-encoded JSON to avoid attribute quote issues) 23569ccd446eSAtari911 const conflictsRaw = badgeElement.getAttribute('data-conflicts'); 23579ccd446eSAtari911 if (!conflictsRaw) return; 23581d05cddcSAtari911 23591d05cddcSAtari911 let conflicts; 23601d05cddcSAtari911 try { 23619ccd446eSAtari911 conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw)))); 23621d05cddcSAtari911 } catch (e) { 23639ccd446eSAtari911 // Fallback: try parsing as plain JSON (for PHP-rendered badges) 23649ccd446eSAtari911 try { 23659ccd446eSAtari911 conflicts = JSON.parse(conflictsRaw); 23669ccd446eSAtari911 } catch (e2) { 23679ccd446eSAtari911 console.error('Failed to parse conflicts:', e2); 23681d05cddcSAtari911 return; 23691d05cddcSAtari911 } 23709ccd446eSAtari911 } 23719ccd446eSAtari911 23729ccd446eSAtari911 // Get theme from the calendar container via CSS variables 23739ccd446eSAtari911 // Try closest ancestor first, then fall back to any calendar on the page 23749ccd446eSAtari911 let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone'); 23759ccd446eSAtari911 if (!containerEl) { 23769ccd446eSAtari911 // Badge might be inside a day popup (appended to body) - find any calendar container 23779ccd446eSAtari911 containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]'); 23789ccd446eSAtari911 } 23799ccd446eSAtari911 const cs = containerEl ? getComputedStyle(containerEl) : null; 23809ccd446eSAtari911 23819ccd446eSAtari911 const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424'; 23829ccd446eSAtari911 const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07'; 23839ccd446eSAtari911 const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07'; 23849ccd446eSAtari911 const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00'; 23859ccd446eSAtari911 const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)'; 23861d05cddcSAtari911 23871d05cddcSAtari911 // Create tooltip 23881d05cddcSAtari911 const tooltip = document.createElement('div'); 23891d05cddcSAtari911 tooltip.id = 'conflict-tooltip'; 23901d05cddcSAtari911 tooltip.className = 'conflict-tooltip'; 23911d05cddcSAtari911 23929ccd446eSAtari911 // Apply theme styles 23939ccd446eSAtari911 tooltip.style.background = bg; 23949ccd446eSAtari911 tooltip.style.borderColor = border; 23959ccd446eSAtari911 tooltip.style.color = textPrimary; 23969ccd446eSAtari911 tooltip.style.boxShadow = '0 4px 12px ' + shadow; 23979ccd446eSAtari911 23989ccd446eSAtari911 // Build content with themed colors 2399*7e8ea635SAtari911 let html = '<div class="conflict-tooltip-header" style="background: ' + border + '; color: ' + bg + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>'; 24001d05cddcSAtari911 html += '<div class="conflict-tooltip-body">'; 24011d05cddcSAtari911 conflicts.forEach(conflict => { 2402*7e8ea635SAtari911 html += '<div class="conflict-item" style="color: ' + textDim + '; border-bottom-color: ' + border + ';">• ' + escapeHtml(conflict) + '</div>'; 24031d05cddcSAtari911 }); 24041d05cddcSAtari911 html += '</div>'; 24051d05cddcSAtari911 24061d05cddcSAtari911 tooltip.innerHTML = html; 24071d05cddcSAtari911 document.body.appendChild(tooltip); 24081d05cddcSAtari911 24091d05cddcSAtari911 // Position tooltip 24101d05cddcSAtari911 const rect = badgeElement.getBoundingClientRect(); 24111d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 24121d05cddcSAtari911 24131d05cddcSAtari911 // Position above the badge, centered 24141d05cddcSAtari911 let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); 24151d05cddcSAtari911 let top = rect.top - tooltipRect.height - 8; 24161d05cddcSAtari911 24171d05cddcSAtari911 // Keep tooltip within viewport 24181d05cddcSAtari911 if (left < 10) left = 10; 24191d05cddcSAtari911 if (left + tooltipRect.width > window.innerWidth - 10) { 24201d05cddcSAtari911 left = window.innerWidth - tooltipRect.width - 10; 24211d05cddcSAtari911 } 24221d05cddcSAtari911 if (top < 10) { 24231d05cddcSAtari911 // If not enough room above, show below 24241d05cddcSAtari911 top = rect.bottom + 8; 24251d05cddcSAtari911 } 24261d05cddcSAtari911 24271d05cddcSAtari911 tooltip.style.left = left + 'px'; 24281d05cddcSAtari911 tooltip.style.top = top + 'px'; 24291d05cddcSAtari911 tooltip.style.opacity = '1'; 24301d05cddcSAtari911}; 24311d05cddcSAtari911 24321d05cddcSAtari911// Hide conflict tooltip 24331d05cddcSAtari911window.hideConflictTooltip = function() { 24341d05cddcSAtari911 const tooltip = document.getElementById('conflict-tooltip'); 24351d05cddcSAtari911 if (tooltip) { 24361d05cddcSAtari911 tooltip.remove(); 24371d05cddcSAtari911 } 24381d05cddcSAtari911}; 24391d05cddcSAtari911 24401d05cddcSAtari911// Filter events by search term 24411d05cddcSAtari911window.filterEvents = function(calId, searchTerm) { 24421d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 24431d05cddcSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 24441d05cddcSAtari911 24451d05cddcSAtari911 if (!eventList) return; 24461d05cddcSAtari911 24471d05cddcSAtari911 // Show/hide clear button 24481d05cddcSAtari911 if (searchClear) { 24491d05cddcSAtari911 searchClear.style.display = searchTerm ? 'block' : 'none'; 24501d05cddcSAtari911 } 24511d05cddcSAtari911 24521d05cddcSAtari911 searchTerm = searchTerm.toLowerCase().trim(); 24531d05cddcSAtari911 24541d05cddcSAtari911 // Get all event items 24551d05cddcSAtari911 const eventItems = eventList.querySelectorAll('.event-compact-item'); 24561d05cddcSAtari911 let visibleCount = 0; 24571d05cddcSAtari911 let hiddenPastCount = 0; 24581d05cddcSAtari911 24591d05cddcSAtari911 eventItems.forEach(item => { 24601d05cddcSAtari911 const title = item.querySelector('.event-title-compact'); 24611d05cddcSAtari911 const description = item.querySelector('.event-desc-compact'); 24621d05cddcSAtari911 const dateTime = item.querySelector('.event-date-time'); 24631d05cddcSAtari911 24641d05cddcSAtari911 // Build searchable text 24651d05cddcSAtari911 let searchableText = ''; 24661d05cddcSAtari911 if (title) searchableText += title.textContent.toLowerCase() + ' '; 24671d05cddcSAtari911 if (description) searchableText += description.textContent.toLowerCase() + ' '; 24681d05cddcSAtari911 if (dateTime) searchableText += dateTime.textContent.toLowerCase() + ' '; 24691d05cddcSAtari911 24701d05cddcSAtari911 // Check if matches search 24711d05cddcSAtari911 const matches = !searchTerm || searchableText.includes(searchTerm); 24721d05cddcSAtari911 24731d05cddcSAtari911 if (matches) { 24741d05cddcSAtari911 item.style.display = ''; 24751d05cddcSAtari911 visibleCount++; 24761d05cddcSAtari911 } else { 24771d05cddcSAtari911 item.style.display = 'none'; 24781d05cddcSAtari911 // Check if this is a past event 24791d05cddcSAtari911 if (item.classList.contains('event-past') || item.classList.contains('event-completed')) { 24801d05cddcSAtari911 hiddenPastCount++; 24811d05cddcSAtari911 } 24821d05cddcSAtari911 } 24831d05cddcSAtari911 }); 24841d05cddcSAtari911 24851d05cddcSAtari911 // Update past events toggle if it exists 24861d05cddcSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 24871d05cddcSAtari911 const pastLabel = eventList.querySelector('.past-events-label'); 24881d05cddcSAtari911 const pastContent = document.getElementById('past-events-' + calId); 24891d05cddcSAtari911 24901d05cddcSAtari911 if (pastToggle && pastLabel && pastContent) { 24911d05cddcSAtari911 const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])'); 24921d05cddcSAtari911 const totalPastVisible = visiblePastEvents.length; 24931d05cddcSAtari911 24941d05cddcSAtari911 if (totalPastVisible > 0) { 24951d05cddcSAtari911 pastLabel.textContent = `Past Events (${totalPastVisible})`; 24961d05cddcSAtari911 pastToggle.style.display = ''; 24971d05cddcSAtari911 } else { 24981d05cddcSAtari911 pastToggle.style.display = 'none'; 24991d05cddcSAtari911 } 25001d05cddcSAtari911 } 25011d05cddcSAtari911 25021d05cddcSAtari911 // Show "no results" message if nothing visible 25031d05cddcSAtari911 let noResultsMsg = eventList.querySelector('.no-search-results'); 25041d05cddcSAtari911 if (visibleCount === 0 && searchTerm) { 25051d05cddcSAtari911 if (!noResultsMsg) { 25061d05cddcSAtari911 noResultsMsg = document.createElement('p'); 25071d05cddcSAtari911 noResultsMsg.className = 'no-search-results no-events-msg'; 25081d05cddcSAtari911 noResultsMsg.textContent = 'No events match your search'; 25091d05cddcSAtari911 eventList.appendChild(noResultsMsg); 25101d05cddcSAtari911 } 25111d05cddcSAtari911 noResultsMsg.style.display = 'block'; 25121d05cddcSAtari911 } else if (noResultsMsg) { 25131d05cddcSAtari911 noResultsMsg.style.display = 'none'; 25141d05cddcSAtari911 } 25151d05cddcSAtari911}; 25161d05cddcSAtari911 25171d05cddcSAtari911// Clear event search 25181d05cddcSAtari911window.clearEventSearch = function(calId) { 25191d05cddcSAtari911 const searchInput = document.getElementById('event-search-' + calId); 25201d05cddcSAtari911 if (searchInput) { 25211d05cddcSAtari911 searchInput.value = ''; 25221d05cddcSAtari911 filterEvents(calId, ''); 25231d05cddcSAtari911 searchInput.focus(); 25241d05cddcSAtari911 } 25251d05cddcSAtari911}; 25261d05cddcSAtari911 25279ccd446eSAtari911// ============================================ 25289ccd446eSAtari911// PINK THEME - GLOWING PARTICLE EFFECTS 25299ccd446eSAtari911// ============================================ 25309ccd446eSAtari911 25319ccd446eSAtari911// Create glowing pink particle effects for pink theme 25329ccd446eSAtari911(function() { 25339ccd446eSAtari911 let pinkThemeActive = false; 25349ccd446eSAtari911 let trailTimer = null; 25359ccd446eSAtari911 let pixelTimer = null; 25369ccd446eSAtari911 25379ccd446eSAtari911 // Check if pink theme is active 25389ccd446eSAtari911 function checkPinkTheme() { 25399ccd446eSAtari911 const pinkCalendars = document.querySelectorAll('.calendar-theme-pink'); 25409ccd446eSAtari911 pinkThemeActive = pinkCalendars.length > 0; 25419ccd446eSAtari911 return pinkThemeActive; 25429ccd446eSAtari911 } 25439ccd446eSAtari911 25449ccd446eSAtari911 // Create trail particle 25459ccd446eSAtari911 function createTrailParticle(clientX, clientY) { 25469ccd446eSAtari911 if (!pinkThemeActive) return; 25479ccd446eSAtari911 25489ccd446eSAtari911 const trail = document.createElement('div'); 25499ccd446eSAtari911 trail.className = 'pink-cursor-trail'; 25509ccd446eSAtari911 trail.style.left = clientX + 'px'; 25519ccd446eSAtari911 trail.style.top = clientY + 'px'; 25529ccd446eSAtari911 trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards'; 25539ccd446eSAtari911 25549ccd446eSAtari911 document.body.appendChild(trail); 25559ccd446eSAtari911 25569ccd446eSAtari911 setTimeout(function() { 25579ccd446eSAtari911 trail.remove(); 25589ccd446eSAtari911 }, 500); 25599ccd446eSAtari911 } 25609ccd446eSAtari911 25619ccd446eSAtari911 // Create pixel sparkles 25629ccd446eSAtari911 function createPixelSparkles(clientX, clientY) { 25639ccd446eSAtari911 if (!pinkThemeActive || pixelTimer) return; 25649ccd446eSAtari911 25659ccd446eSAtari911 const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels 25669ccd446eSAtari911 25679ccd446eSAtari911 for (let i = 0; i < pixelCount; i++) { 25689ccd446eSAtari911 const pixel = document.createElement('div'); 25699ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 25709ccd446eSAtari911 25719ccd446eSAtari911 // Random offset from cursor 25729ccd446eSAtari911 const offsetX = (Math.random() - 0.5) * 30; 25739ccd446eSAtari911 const offsetY = (Math.random() - 0.5) * 30; 25749ccd446eSAtari911 25759ccd446eSAtari911 pixel.style.left = (clientX + offsetX) + 'px'; 25769ccd446eSAtari911 pixel.style.top = (clientY + offsetY) + 'px'; 25779ccd446eSAtari911 25789ccd446eSAtari911 // Random color - bright neon pinks and whites 25799ccd446eSAtari911 const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 25809ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 25819ccd446eSAtari911 pixel.style.background = color; 25829ccd446eSAtari911 pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff'; 25839ccd446eSAtari911 25849ccd446eSAtari911 // Random animation 25859ccd446eSAtari911 if (Math.random() > 0.5) { 25869ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards'; 25879ccd446eSAtari911 } else { 25889ccd446eSAtari911 pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards'; 25899ccd446eSAtari911 } 25909ccd446eSAtari911 25919ccd446eSAtari911 document.body.appendChild(pixel); 25929ccd446eSAtari911 25939ccd446eSAtari911 setTimeout(function() { 25949ccd446eSAtari911 pixel.remove(); 25959ccd446eSAtari911 }, 800); 25969ccd446eSAtari911 } 25979ccd446eSAtari911 25989ccd446eSAtari911 pixelTimer = setTimeout(function() { 25999ccd446eSAtari911 pixelTimer = null; 26009ccd446eSAtari911 }, 40); 26019ccd446eSAtari911 } 26029ccd446eSAtari911 26039ccd446eSAtari911 // Create explosion 26049ccd446eSAtari911 function createExplosion(clientX, clientY) { 26059ccd446eSAtari911 if (!pinkThemeActive) return; 26069ccd446eSAtari911 26079ccd446eSAtari911 const particleCount = 25; 26089ccd446eSAtari911 const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff']; 26099ccd446eSAtari911 26109ccd446eSAtari911 // Add hearts to explosion (8-12 hearts) 26119ccd446eSAtari911 const heartCount = 8 + Math.floor(Math.random() * 5); 26129ccd446eSAtari911 for (let i = 0; i < heartCount; i++) { 26139ccd446eSAtari911 const heart = document.createElement('div'); 26149ccd446eSAtari911 heart.textContent = ''; 26159ccd446eSAtari911 heart.style.position = 'fixed'; 26169ccd446eSAtari911 heart.style.left = clientX + 'px'; 26179ccd446eSAtari911 heart.style.top = clientY + 'px'; 26189ccd446eSAtari911 heart.style.pointerEvents = 'none'; 26199ccd446eSAtari911 heart.style.zIndex = '9999999'; 26209ccd446eSAtari911 heart.style.fontSize = (12 + Math.random() * 16) + 'px'; 26219ccd446eSAtari911 26229ccd446eSAtari911 // Random direction 26239ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 26249ccd446eSAtari911 const velocity = 60 + Math.random() * 80; 26259ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 26269ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 26279ccd446eSAtari911 26289ccd446eSAtari911 heart.style.setProperty('--tx', tx + 'px'); 26299ccd446eSAtari911 heart.style.setProperty('--ty', ty + 'px'); 26309ccd446eSAtari911 26319ccd446eSAtari911 const duration = 0.8 + Math.random() * 0.4; 26329ccd446eSAtari911 heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 26339ccd446eSAtari911 26349ccd446eSAtari911 document.body.appendChild(heart); 26359ccd446eSAtari911 26369ccd446eSAtari911 setTimeout(function() { 26379ccd446eSAtari911 heart.remove(); 26389ccd446eSAtari911 }, duration * 1000); 26399ccd446eSAtari911 } 26409ccd446eSAtari911 26419ccd446eSAtari911 // Main explosion particles 26429ccd446eSAtari911 for (let i = 0; i < particleCount; i++) { 26439ccd446eSAtari911 const particle = document.createElement('div'); 26449ccd446eSAtari911 particle.className = 'pink-particle'; 26459ccd446eSAtari911 26469ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 26479ccd446eSAtari911 particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)'; 26489ccd446eSAtari911 particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color; 26499ccd446eSAtari911 26509ccd446eSAtari911 particle.style.left = clientX + 'px'; 26519ccd446eSAtari911 particle.style.top = clientY + 'px'; 26529ccd446eSAtari911 26539ccd446eSAtari911 const angle = (Math.PI * 2 * i) / particleCount; 26549ccd446eSAtari911 const velocity = 50 + Math.random() * 100; 26559ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 26569ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 26579ccd446eSAtari911 26589ccd446eSAtari911 particle.style.setProperty('--tx', tx + 'px'); 26599ccd446eSAtari911 particle.style.setProperty('--ty', ty + 'px'); 26609ccd446eSAtari911 26619ccd446eSAtari911 const size = 4 + Math.random() * 6; 26629ccd446eSAtari911 particle.style.width = size + 'px'; 26639ccd446eSAtari911 particle.style.height = size + 'px'; 26649ccd446eSAtari911 26659ccd446eSAtari911 const duration = 0.6 + Math.random() * 0.4; 26669ccd446eSAtari911 particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 26679ccd446eSAtari911 26689ccd446eSAtari911 document.body.appendChild(particle); 26699ccd446eSAtari911 26709ccd446eSAtari911 setTimeout(function() { 26719ccd446eSAtari911 particle.remove(); 26729ccd446eSAtari911 }, duration * 1000); 26739ccd446eSAtari911 } 26749ccd446eSAtari911 26759ccd446eSAtari911 // Pixel sparkles 26769ccd446eSAtari911 const pixelSparkleCount = 40; 26779ccd446eSAtari911 26789ccd446eSAtari911 for (let i = 0; i < pixelSparkleCount; i++) { 26799ccd446eSAtari911 const pixel = document.createElement('div'); 26809ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 26819ccd446eSAtari911 26829ccd446eSAtari911 const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 26839ccd446eSAtari911 const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)]; 26849ccd446eSAtari911 pixel.style.background = pixelColor; 26859ccd446eSAtari911 pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff'; 26869ccd446eSAtari911 26879ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 26889ccd446eSAtari911 const distance = 30 + Math.random() * 80; 26899ccd446eSAtari911 const offsetX = Math.cos(angle) * distance; 26909ccd446eSAtari911 const offsetY = Math.sin(angle) * distance; 26919ccd446eSAtari911 26929ccd446eSAtari911 pixel.style.left = clientX + 'px'; 26939ccd446eSAtari911 pixel.style.top = clientY + 'px'; 26949ccd446eSAtari911 pixel.style.setProperty('--tx', offsetX + 'px'); 26959ccd446eSAtari911 pixel.style.setProperty('--ty', offsetY + 'px'); 26969ccd446eSAtari911 26979ccd446eSAtari911 const pixelSize = 1 + Math.random() * 2; 26989ccd446eSAtari911 pixel.style.width = pixelSize + 'px'; 26999ccd446eSAtari911 pixel.style.height = pixelSize + 'px'; 27009ccd446eSAtari911 27019ccd446eSAtari911 const duration = 0.4 + Math.random() * 0.4; 27029ccd446eSAtari911 if (Math.random() > 0.5) { 27039ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards'; 27049ccd446eSAtari911 } else { 27059ccd446eSAtari911 pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 27069ccd446eSAtari911 } 27079ccd446eSAtari911 27089ccd446eSAtari911 document.body.appendChild(pixel); 27099ccd446eSAtari911 27109ccd446eSAtari911 setTimeout(function() { 27119ccd446eSAtari911 pixel.remove(); 27129ccd446eSAtari911 }, duration * 1000); 27139ccd446eSAtari911 } 27149ccd446eSAtari911 27159ccd446eSAtari911 // Flash 27169ccd446eSAtari911 const flash = document.createElement('div'); 27179ccd446eSAtari911 flash.style.position = 'fixed'; 27189ccd446eSAtari911 flash.style.left = clientX + 'px'; 27199ccd446eSAtari911 flash.style.top = clientY + 'px'; 27209ccd446eSAtari911 flash.style.width = '40px'; 27219ccd446eSAtari911 flash.style.height = '40px'; 27229ccd446eSAtari911 flash.style.borderRadius = '50%'; 27239ccd446eSAtari911 flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)'; 27249ccd446eSAtari911 flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4'; 27259ccd446eSAtari911 flash.style.pointerEvents = 'none'; 27269ccd446eSAtari911 flash.style.zIndex = '9999999'; // Above everything including dialogs 27279ccd446eSAtari911 flash.style.transform = 'translate(-50%, -50%)'; 27289ccd446eSAtari911 flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards'; 27299ccd446eSAtari911 27309ccd446eSAtari911 document.body.appendChild(flash); 27319ccd446eSAtari911 27329ccd446eSAtari911 setTimeout(function() { 27339ccd446eSAtari911 flash.remove(); 27349ccd446eSAtari911 }, 300); 27359ccd446eSAtari911 } 27369ccd446eSAtari911 27379ccd446eSAtari911 function initPinkParticles() { 27389ccd446eSAtari911 if (!checkPinkTheme()) return; 27399ccd446eSAtari911 27409ccd446eSAtari911 // Use capture phase to catch events before stopPropagation 27419ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 27429ccd446eSAtari911 if (!pinkThemeActive) return; 27439ccd446eSAtari911 27449ccd446eSAtari911 createTrailParticle(e.clientX, e.clientY); 27459ccd446eSAtari911 createPixelSparkles(e.clientX, e.clientY); 27469ccd446eSAtari911 }, true); // Capture phase! 27479ccd446eSAtari911 27489ccd446eSAtari911 // Throttle main trail 27499ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 27509ccd446eSAtari911 if (!pinkThemeActive || trailTimer) return; 27519ccd446eSAtari911 27529ccd446eSAtari911 trailTimer = setTimeout(function() { 27539ccd446eSAtari911 trailTimer = null; 27549ccd446eSAtari911 }, 30); 27559ccd446eSAtari911 }, true); // Capture phase! 27569ccd446eSAtari911 27579ccd446eSAtari911 // Click explosion - use capture phase 27589ccd446eSAtari911 document.addEventListener('click', function(e) { 27599ccd446eSAtari911 if (!pinkThemeActive) return; 27609ccd446eSAtari911 27619ccd446eSAtari911 createExplosion(e.clientX, e.clientY); 27629ccd446eSAtari911 }, true); // Capture phase! 27639ccd446eSAtari911 } 27649ccd446eSAtari911 27659ccd446eSAtari911 // Initialize on load 27669ccd446eSAtari911 if (document.readyState === 'loading') { 27679ccd446eSAtari911 document.addEventListener('DOMContentLoaded', initPinkParticles); 27689ccd446eSAtari911 } else { 27699ccd446eSAtari911 initPinkParticles(); 27709ccd446eSAtari911 } 27719ccd446eSAtari911 27729ccd446eSAtari911 // Re-check theme if calendar is dynamically added 27739ccd446eSAtari911 if (typeof MutationObserver !== 'undefined') { 27749ccd446eSAtari911 const observer = new MutationObserver(function(mutations) { 27759ccd446eSAtari911 mutations.forEach(function(mutation) { 27769ccd446eSAtari911 if (mutation.addedNodes.length > 0) { 27779ccd446eSAtari911 mutation.addedNodes.forEach(function(node) { 27789ccd446eSAtari911 if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) { 27799ccd446eSAtari911 checkPinkTheme(); 27809ccd446eSAtari911 initPinkParticles(); 27819ccd446eSAtari911 } 27829ccd446eSAtari911 }); 27839ccd446eSAtari911 } 27849ccd446eSAtari911 }); 27859ccd446eSAtari911 }); 27869ccd446eSAtari911 27879ccd446eSAtari911 observer.observe(document.body, { 27889ccd446eSAtari911 childList: true, 27899ccd446eSAtari911 subtree: true 27909ccd446eSAtari911 }); 27919ccd446eSAtari911 } 27929ccd446eSAtari911})(); 27939ccd446eSAtari911 27941d05cddcSAtari911// End of calendar plugin JavaScript 2795