11d05cddcSAtari911/** 21d05cddcSAtari911 * DokuWiki Compact Calendar Plugin JavaScript 31d05cddcSAtari911 * Loaded independently to avoid DokuWiki concatenation issues 4*815440faSAtari911 * @version 7.0.8 51d05cddcSAtari911 */ 61d05cddcSAtari911 7*815440faSAtari911// Debug mode - set to true for console logging 8*815440faSAtari911var CALENDAR_DEBUG = false; 9*815440faSAtari911 10*815440faSAtari911// Debug logging helper 11*815440faSAtari911function calendarLog() { 12*815440faSAtari911 if (CALENDAR_DEBUG && console && console.log) { 13*815440faSAtari911 console.log.apply(console, ['[Calendar]'].concat(Array.prototype.slice.call(arguments))); 14*815440faSAtari911 } 15*815440faSAtari911} 16*815440faSAtari911 17*815440faSAtari911function calendarError() { 18*815440faSAtari911 if (console && console.error) { 19*815440faSAtari911 console.error.apply(console, ['[Calendar]'].concat(Array.prototype.slice.call(arguments))); 20*815440faSAtari911 } 21*815440faSAtari911} 22*815440faSAtari911 23*815440faSAtari911/** 24*815440faSAtari911 * Format a Date object as YYYY-MM-DD in LOCAL time (not UTC) 25*815440faSAtari911 * This avoids timezone issues where toISOString() shifts dates 26*815440faSAtari911 * For example: In Prague (UTC+1), midnight local = 23:00 UTC previous day 27*815440faSAtari911 * @param {Date} date - Date object to format 28*815440faSAtari911 * @returns {string} Date string in YYYY-MM-DD format 29*815440faSAtari911 */ 30*815440faSAtari911function formatLocalDate(date) { 31*815440faSAtari911 var year = date.getFullYear(); 32*815440faSAtari911 var month = String(date.getMonth() + 1).padStart(2, '0'); 33*815440faSAtari911 var day = String(date.getDate()).padStart(2, '0'); 34*815440faSAtari911 return year + '-' + month + '-' + day; 35*815440faSAtari911} 36*815440faSAtari911 371d05cddcSAtari911// Ensure DOKU_BASE is defined - check multiple sources 381d05cddcSAtari911if (typeof DOKU_BASE === 'undefined') { 391d05cddcSAtari911 // Try to get from global jsinfo object (DokuWiki standard) 401d05cddcSAtari911 if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) { 411d05cddcSAtari911 window.DOKU_BASE = window.jsinfo.dokubase; 421d05cddcSAtari911 } else { 431d05cddcSAtari911 // Fallback: extract from script source path 441d05cddcSAtari911 var scripts = document.getElementsByTagName('script'); 451d05cddcSAtari911 var pluginScriptPath = null; 461d05cddcSAtari911 for (var i = 0; i < scripts.length; i++) { 471d05cddcSAtari911 if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) { 481d05cddcSAtari911 pluginScriptPath = scripts[i].src; 491d05cddcSAtari911 break; 501d05cddcSAtari911 } 511d05cddcSAtari911 } 521d05cddcSAtari911 531d05cddcSAtari911 if (pluginScriptPath) { 541d05cddcSAtari911 // Extract base path from: .../lib/plugins/calendar/script.js 551d05cddcSAtari911 var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//); 561d05cddcSAtari911 window.DOKU_BASE = match ? match[1] : '/'; 571d05cddcSAtari911 } else { 581d05cddcSAtari911 // Last resort: use root 591d05cddcSAtari911 window.DOKU_BASE = '/'; 601d05cddcSAtari911 } 611d05cddcSAtari911 } 621d05cddcSAtari911} 631d05cddcSAtari911 641d05cddcSAtari911// Shorthand for convenience 651d05cddcSAtari911var DOKU_BASE = window.DOKU_BASE || '/'; 661d05cddcSAtari911 67b498f308SAtari911/** 68b498f308SAtari911 * Get DokuWiki security token from multiple possible sources 69b498f308SAtari911 * DokuWiki stores this in different places depending on version/config 70b498f308SAtari911 */ 71b498f308SAtari911function getSecurityToken() { 72b498f308SAtari911 // Try JSINFO.sectok (standard location) 73b498f308SAtari911 if (typeof JSINFO !== 'undefined' && JSINFO.sectok) { 74b498f308SAtari911 return JSINFO.sectok; 75b498f308SAtari911 } 76b498f308SAtari911 // Try window.JSINFO 77b498f308SAtari911 if (typeof window.JSINFO !== 'undefined' && window.JSINFO.sectok) { 78b498f308SAtari911 return window.JSINFO.sectok; 79b498f308SAtari911 } 80b498f308SAtari911 // Try finding it in a hidden form field (some templates/plugins add this) 81b498f308SAtari911 var sectokInput = document.querySelector('input[name="sectok"]'); 82b498f308SAtari911 if (sectokInput && sectokInput.value) { 83b498f308SAtari911 return sectokInput.value; 84b498f308SAtari911 } 85b498f308SAtari911 // Try meta tag (some DokuWiki setups) 86b498f308SAtari911 var sectokMeta = document.querySelector('meta[name="sectok"]'); 87b498f308SAtari911 if (sectokMeta && sectokMeta.content) { 88b498f308SAtari911 return sectokMeta.content; 89b498f308SAtari911 } 90b498f308SAtari911 // Return empty string if not found 91b498f308SAtari911 console.warn('Calendar plugin: Security token not found'); 92b498f308SAtari911 return ''; 93b498f308SAtari911} 94b498f308SAtari911 959ccd446eSAtari911// Helper: propagate CSS variables from a calendar container to a target element 969ccd446eSAtari911// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars 979ccd446eSAtari911// from DOM parents per spec, but some DokuWiki templates break this inheritance) 989ccd446eSAtari911function propagateThemeVars(calId, targetEl) { 999ccd446eSAtari911 if (!targetEl) return; 1009ccd446eSAtari911 // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.) 1019ccd446eSAtari911 const container = document.getElementById(calId) 1029ccd446eSAtari911 || document.getElementById('sidebar-widget-' + calId) 1039ccd446eSAtari911 || document.querySelector('[id$="' + calId + '"]'); 1049ccd446eSAtari911 if (!container) return; 1059ccd446eSAtari911 const cs = getComputedStyle(container); 1069ccd446eSAtari911 const vars = [ 1079ccd446eSAtari911 '--background-site', '--background-alt', '--background-header', 1089ccd446eSAtari911 '--text-primary', '--text-bright', '--text-dim', 1099ccd446eSAtari911 '--border-color', '--border-main', 1109ccd446eSAtari911 '--cell-bg', '--cell-today-bg', '--grid-bg', 1119ccd446eSAtari911 '--shadow-color', '--header-border', '--header-shadow', 1129ccd446eSAtari911 '--btn-text' 1139ccd446eSAtari911 ]; 1149ccd446eSAtari911 vars.forEach(v => { 1159ccd446eSAtari911 const val = cs.getPropertyValue(v).trim(); 1169ccd446eSAtari911 if (val) targetEl.style.setProperty(v, val); 1179ccd446eSAtari911 }); 1189ccd446eSAtari911} 1199ccd446eSAtari911 1201d05cddcSAtari911// Filter calendar by namespace 1211d05cddcSAtari911window.filterCalendarByNamespace = function(calId, namespace) { 1221d05cddcSAtari911 // Get current year and month from calendar 1231d05cddcSAtari911 const container = document.getElementById(calId); 1241d05cddcSAtari911 if (!container) { 1251d05cddcSAtari911 console.error('Calendar container not found:', calId); 1261d05cddcSAtari911 return; 1271d05cddcSAtari911 } 1281d05cddcSAtari911 1291d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 1301d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 1311d05cddcSAtari911 1321d05cddcSAtari911 // Reload calendar with the filtered namespace 1331d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1341d05cddcSAtari911}; 1351d05cddcSAtari911 1361d05cddcSAtari911// Navigate to different month 1371d05cddcSAtari911window.navCalendar = function(calId, year, month, namespace) { 1381d05cddcSAtari911 1391d05cddcSAtari911 const params = new URLSearchParams({ 1401d05cddcSAtari911 call: 'plugin_calendar', 1411d05cddcSAtari911 action: 'load_month', 1421d05cddcSAtari911 year: year, 1431d05cddcSAtari911 month: month, 1441d05cddcSAtari911 namespace: namespace, 1451d05cddcSAtari911 _: new Date().getTime() // Cache buster 1461d05cddcSAtari911 }); 1471d05cddcSAtari911 1481d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1491d05cddcSAtari911 method: 'POST', 1501d05cddcSAtari911 headers: { 1511d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 1521d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 1531d05cddcSAtari911 'Pragma': 'no-cache' 1541d05cddcSAtari911 }, 1551d05cddcSAtari911 body: params.toString() 1561d05cddcSAtari911 }) 1571d05cddcSAtari911 .then(r => r.json()) 1581d05cddcSAtari911 .then(data => { 1591d05cddcSAtari911 if (data.success) { 1601d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 1611d05cddcSAtari911 } else { 1621d05cddcSAtari911 console.error('Failed to load month:', data.error); 1631d05cddcSAtari911 } 1641d05cddcSAtari911 }) 1651d05cddcSAtari911 .catch(err => { 1661d05cddcSAtari911 console.error('Error loading month:', err); 1671d05cddcSAtari911 }); 1681d05cddcSAtari911}; 1691d05cddcSAtari911 1701d05cddcSAtari911// Jump to current month 1711d05cddcSAtari911window.jumpToToday = function(calId, namespace) { 1721d05cddcSAtari911 const today = new Date(); 1731d05cddcSAtari911 const year = today.getFullYear(); 1741d05cddcSAtari911 const month = today.getMonth() + 1; // JavaScript months are 0-indexed 1751d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1761d05cddcSAtari911}; 1771d05cddcSAtari911 1781d05cddcSAtari911// Jump to today for event panel 1791d05cddcSAtari911window.jumpTodayPanel = function(calId, namespace) { 1801d05cddcSAtari911 const today = new Date(); 1811d05cddcSAtari911 const year = today.getFullYear(); 1821d05cddcSAtari911 const month = today.getMonth() + 1; 1831d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 1841d05cddcSAtari911}; 1851d05cddcSAtari911 1861d05cddcSAtari911// Open month picker dialog 1871d05cddcSAtari911window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) { 1881d05cddcSAtari911 1891d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 1901d05cddcSAtari911 1911d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 1921d05cddcSAtari911 1931d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 1941d05cddcSAtari911 1951d05cddcSAtari911 if (!overlay) { 1961d05cddcSAtari911 console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId); 1971d05cddcSAtari911 return; 1981d05cddcSAtari911 } 1991d05cddcSAtari911 2001d05cddcSAtari911 if (!monthSelect || !yearSelect) { 2011d05cddcSAtari911 console.error('Select elements not found!'); 2021d05cddcSAtari911 return; 2031d05cddcSAtari911 } 2041d05cddcSAtari911 2051d05cddcSAtari911 // Set current values 2061d05cddcSAtari911 monthSelect.value = currentMonth; 2071d05cddcSAtari911 yearSelect.value = currentYear; 2081d05cddcSAtari911 2091d05cddcSAtari911 // Show overlay 2101d05cddcSAtari911 overlay.style.display = 'flex'; 2111d05cddcSAtari911}; 2121d05cddcSAtari911 2131d05cddcSAtari911// Open month picker dialog for event panel 2141d05cddcSAtari911window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) { 2151d05cddcSAtari911 openMonthPicker(calId, currentYear, currentMonth, namespace); 2161d05cddcSAtari911}; 2171d05cddcSAtari911 2181d05cddcSAtari911// Close month picker dialog 2191d05cddcSAtari911window.closeMonthPicker = function(calId) { 2201d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 2211d05cddcSAtari911 overlay.style.display = 'none'; 2221d05cddcSAtari911}; 2231d05cddcSAtari911 2241d05cddcSAtari911// Jump to selected month 2251d05cddcSAtari911window.jumpToSelectedMonth = function(calId, namespace) { 2261d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 2271d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 2281d05cddcSAtari911 2291d05cddcSAtari911 const month = parseInt(monthSelect.value); 2301d05cddcSAtari911 const year = parseInt(yearSelect.value); 2311d05cddcSAtari911 2321d05cddcSAtari911 closeMonthPicker(calId); 2331d05cddcSAtari911 2341d05cddcSAtari911 // Check if this is a calendar or event panel 2351d05cddcSAtari911 const container = document.getElementById(calId); 2361d05cddcSAtari911 if (container && container.classList.contains('event-panel-standalone')) { 2371d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 2381d05cddcSAtari911 } else { 2391d05cddcSAtari911 navCalendar(calId, year, month, namespace); 2401d05cddcSAtari911 } 2411d05cddcSAtari911}; 2421d05cddcSAtari911 2431d05cddcSAtari911// Rebuild calendar grid after navigation 2441d05cddcSAtari911window.rebuildCalendar = function(calId, year, month, events, namespace) { 2451d05cddcSAtari911 2461d05cddcSAtari911 const container = document.getElementById(calId); 247da206178SAtari911 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 248da206178SAtari911 'July', 'August', 'September', 'October', 'November', 'December']; 2491d05cddcSAtari911 2509ccd446eSAtari911 // Get theme data from container 2519ccd446eSAtari911 const theme = container.dataset.theme || 'matrix'; 2529ccd446eSAtari911 let themeStyles = {}; 2539ccd446eSAtari911 try { 2549ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles || '{}'); 2559ccd446eSAtari911 } catch (e) { 2569ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 2579ccd446eSAtari911 themeStyles = {}; 2589ccd446eSAtari911 } 2599ccd446eSAtari911 2601d05cddcSAtari911 // Preserve original namespace if not yet set 2611d05cddcSAtari911 if (!container.dataset.originalNamespace) { 2621d05cddcSAtari911 container.setAttribute('data-original-namespace', namespace || ''); 2631d05cddcSAtari911 } 2641d05cddcSAtari911 2651d05cddcSAtari911 // Update container data attributes for current month/year 2661d05cddcSAtari911 container.setAttribute('data-year', year); 2671d05cddcSAtari911 container.setAttribute('data-month', month); 2681d05cddcSAtari911 2691d05cddcSAtari911 // Update embedded events data 2701d05cddcSAtari911 let eventsDataEl = document.getElementById('events-data-' + calId); 2711d05cddcSAtari911 if (eventsDataEl) { 2721d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2731d05cddcSAtari911 } else { 2741d05cddcSAtari911 eventsDataEl = document.createElement('script'); 2751d05cddcSAtari911 eventsDataEl.type = 'application/json'; 2761d05cddcSAtari911 eventsDataEl.id = 'events-data-' + calId; 2771d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2781d05cddcSAtari911 container.appendChild(eventsDataEl); 2791d05cddcSAtari911 } 2801d05cddcSAtari911 2811d05cddcSAtari911 // Update header 2821d05cddcSAtari911 const header = container.querySelector('.calendar-compact-header h3'); 2831d05cddcSAtari911 header.textContent = monthNames[month - 1] + ' ' + year; 2841d05cddcSAtari911 2851d05cddcSAtari911 // Update or create namespace filter indicator 2861d05cddcSAtari911 let filterIndicator = container.querySelector('.calendar-namespace-filter'); 2871d05cddcSAtari911 const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' && 2881d05cddcSAtari911 namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1; 2891d05cddcSAtari911 2901d05cddcSAtari911 if (shouldShowFilter) { 2911d05cddcSAtari911 // Show/update filter indicator 2921d05cddcSAtari911 if (!filterIndicator) { 2931d05cddcSAtari911 // Create filter indicator if it doesn't exist 2941d05cddcSAtari911 const headerDiv = container.querySelector('.calendar-compact-header'); 2959ccd446eSAtari911 if (headerDiv) { 2961d05cddcSAtari911 filterIndicator = document.createElement('div'); 2971d05cddcSAtari911 filterIndicator.className = 'calendar-namespace-filter'; 2981d05cddcSAtari911 filterIndicator.id = 'namespace-filter-' + calId; 2991d05cddcSAtari911 headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling); 3001d05cddcSAtari911 } 3011d05cddcSAtari911 } 3021d05cddcSAtari911 3031d05cddcSAtari911 if (filterIndicator) { 3041d05cddcSAtari911 filterIndicator.innerHTML = 3051d05cddcSAtari911 '<span class="namespace-filter-label">Filtering:</span>' + 3061d05cddcSAtari911 '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' + 3071d05cddcSAtari911 '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>'; 3081d05cddcSAtari911 filterIndicator.style.display = 'flex'; 3091d05cddcSAtari911 } 3101d05cddcSAtari911 } else { 3111d05cddcSAtari911 // Hide filter indicator 3121d05cddcSAtari911 if (filterIndicator) { 3131d05cddcSAtari911 filterIndicator.style.display = 'none'; 3141d05cddcSAtari911 } 3151d05cddcSAtari911 } 3161d05cddcSAtari911 3171d05cddcSAtari911 // Update container's namespace attribute 3181d05cddcSAtari911 container.setAttribute('data-namespace', namespace || ''); 3191d05cddcSAtari911 3201d05cddcSAtari911 // Update nav buttons 3211d05cddcSAtari911 let prevMonth = month - 1; 3221d05cddcSAtari911 let prevYear = year; 3231d05cddcSAtari911 if (prevMonth < 1) { 3241d05cddcSAtari911 prevMonth = 12; 3251d05cddcSAtari911 prevYear--; 3261d05cddcSAtari911 } 3271d05cddcSAtari911 3281d05cddcSAtari911 let nextMonth = month + 1; 3291d05cddcSAtari911 let nextYear = year; 3301d05cddcSAtari911 if (nextMonth > 12) { 3311d05cddcSAtari911 nextMonth = 1; 3321d05cddcSAtari911 nextYear++; 3331d05cddcSAtari911 } 3341d05cddcSAtari911 3351d05cddcSAtari911 const navBtns = container.querySelectorAll('.cal-nav-btn'); 3361d05cddcSAtari911 navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 3371d05cddcSAtari911 navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 3381d05cddcSAtari911 3391d05cddcSAtari911 // Rebuild calendar grid 3401d05cddcSAtari911 const tbody = container.querySelector('.calendar-compact-grid tbody'); 3411d05cddcSAtari911 const firstDay = new Date(year, month - 1, 1); 3421d05cddcSAtari911 const daysInMonth = new Date(year, month, 0).getDate(); 3431d05cddcSAtari911 const dayOfWeek = firstDay.getDay(); 3441d05cddcSAtari911 3451d05cddcSAtari911 // Calculate month boundaries 3461d05cddcSAtari911 const monthStart = new Date(year, month - 1, 1); 3471d05cddcSAtari911 const monthEnd = new Date(year, month - 1, daysInMonth); 3481d05cddcSAtari911 3491d05cddcSAtari911 // Build a map of all events with their date ranges 3501d05cddcSAtari911 const eventRanges = {}; 3511d05cddcSAtari911 for (const [dateKey, dayEvents] of Object.entries(events)) { 3521d05cddcSAtari911 // Defensive check: ensure dayEvents is an array 3531d05cddcSAtari911 if (!Array.isArray(dayEvents)) { 3541d05cddcSAtari911 console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents); 3551d05cddcSAtari911 continue; 3561d05cddcSAtari911 } 3571d05cddcSAtari911 3581d05cddcSAtari911 // Only process events that could possibly overlap with this month/year 3591d05cddcSAtari911 const dateYear = parseInt(dateKey.split('-')[0]); 3601d05cddcSAtari911 3611d05cddcSAtari911 // Skip events from completely different years (unless they're very long multi-day events) 3621d05cddcSAtari911 if (Math.abs(dateYear - year) > 1) { 3631d05cddcSAtari911 continue; 3641d05cddcSAtari911 } 3651d05cddcSAtari911 3661d05cddcSAtari911 for (const evt of dayEvents) { 3671d05cddcSAtari911 const startDate = dateKey; 3681d05cddcSAtari911 const endDate = evt.endDate || dateKey; 3691d05cddcSAtari911 3701d05cddcSAtari911 // Check if event overlaps with current month 3711d05cddcSAtari911 const eventStart = new Date(startDate + 'T00:00:00'); 3721d05cddcSAtari911 const eventEnd = new Date(endDate + 'T00:00:00'); 3731d05cddcSAtari911 3741d05cddcSAtari911 // Skip if event doesn't overlap with current month 3751d05cddcSAtari911 if (eventEnd < monthStart || eventStart > monthEnd) { 3761d05cddcSAtari911 continue; 3771d05cddcSAtari911 } 3781d05cddcSAtari911 3791d05cddcSAtari911 // Create entry for each day the event spans 3801d05cddcSAtari911 const start = new Date(startDate + 'T00:00:00'); 3811d05cddcSAtari911 const end = new Date(endDate + 'T00:00:00'); 3821d05cddcSAtari911 const current = new Date(start); 3831d05cddcSAtari911 3841d05cddcSAtari911 while (current <= end) { 385*815440faSAtari911 // Use formatLocalDate to avoid timezone shift issues 386*815440faSAtari911 const currentKey = formatLocalDate(current); 3871d05cddcSAtari911 388*815440faSAtari911 // Check if this date is in current month (use current Date object directly) 389*815440faSAtari911 if (current.getFullYear() === year && current.getMonth() === month - 1) { 3901d05cddcSAtari911 if (!eventRanges[currentKey]) { 3911d05cddcSAtari911 eventRanges[currentKey] = []; 3921d05cddcSAtari911 } 3931d05cddcSAtari911 3941d05cddcSAtari911 // Add event with span information 3951d05cddcSAtari911 const eventCopy = {...evt}; 3961d05cddcSAtari911 eventCopy._span_start = startDate; 3971d05cddcSAtari911 eventCopy._span_end = endDate; 3981d05cddcSAtari911 eventCopy._is_first_day = (currentKey === startDate); 3991d05cddcSAtari911 eventCopy._is_last_day = (currentKey === endDate); 4001d05cddcSAtari911 eventCopy._original_date = dateKey; 4011d05cddcSAtari911 4021d05cddcSAtari911 // Check if event continues from previous month or to next month 4031d05cddcSAtari911 eventCopy._continues_from_prev = (eventStart < monthStart); 4041d05cddcSAtari911 eventCopy._continues_to_next = (eventEnd > monthEnd); 4051d05cddcSAtari911 4061d05cddcSAtari911 eventRanges[currentKey].push(eventCopy); 4071d05cddcSAtari911 } 4081d05cddcSAtari911 4091d05cddcSAtari911 current.setDate(current.getDate() + 1); 4101d05cddcSAtari911 } 4111d05cddcSAtari911 } 4121d05cddcSAtari911 } 4131d05cddcSAtari911 4141d05cddcSAtari911 let html = ''; 4151d05cddcSAtari911 let currentDay = 1; 4161d05cddcSAtari911 const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7); 4171d05cddcSAtari911 4181d05cddcSAtari911 for (let row = 0; row < rowCount; row++) { 4191d05cddcSAtari911 html += '<tr>'; 4201d05cddcSAtari911 for (let col = 0; col < 7; col++) { 4211d05cddcSAtari911 if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) { 4229ccd446eSAtari911 html += `<td class="cal-empty"></td>`; 4231d05cddcSAtari911 } else { 4241d05cddcSAtari911 const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 4251d05cddcSAtari911 4261d05cddcSAtari911 // Get today's date in local timezone 4271d05cddcSAtari911 const todayObj = new Date(); 4281d05cddcSAtari911 const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`; 4291d05cddcSAtari911 4301d05cddcSAtari911 const isToday = dateKey === today; 4311d05cddcSAtari911 const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0; 4321d05cddcSAtari911 4331d05cddcSAtari911 let classes = 'cal-day'; 4341d05cddcSAtari911 if (isToday) classes += ' cal-today'; 4351d05cddcSAtari911 if (hasEvents) classes += ' cal-has-events'; 4361d05cddcSAtari911 4379ccd446eSAtari911 const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num'; 4389ccd446eSAtari911 4391d05cddcSAtari911 html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`; 4409ccd446eSAtari911 html += `<span class="${dayNumClass}">${currentDay}</span>`; 4411d05cddcSAtari911 4421d05cddcSAtari911 if (hasEvents) { 4431d05cddcSAtari911 // Sort events by time (no time first, then by time) 4441d05cddcSAtari911 const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => { 4451d05cddcSAtari911 const timeA = a.time || ''; 4461d05cddcSAtari911 const timeB = b.time || ''; 4471d05cddcSAtari911 if (!timeA && timeB) return -1; 4481d05cddcSAtari911 if (timeA && !timeB) return 1; 4491d05cddcSAtari911 if (!timeA && !timeB) return 0; 4501d05cddcSAtari911 return timeA.localeCompare(timeB); 4511d05cddcSAtari911 }); 4521d05cddcSAtari911 45396df7d3eSAtari911 // Get important namespaces 45496df7d3eSAtari911 let importantNamespaces = ['important']; 45596df7d3eSAtari911 if (container.dataset.importantNamespaces) { 45696df7d3eSAtari911 try { 45796df7d3eSAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 45896df7d3eSAtari911 } catch (e) {} 45996df7d3eSAtari911 } 46096df7d3eSAtari911 4611d05cddcSAtari911 // Show colored stacked bars for each event 4621d05cddcSAtari911 html += '<div class="event-indicators">'; 4631d05cddcSAtari911 for (const evt of sortedEvents) { 4641d05cddcSAtari911 const eventId = evt.id || ''; 4651d05cddcSAtari911 const eventColor = evt.color || '#3498db'; 4661d05cddcSAtari911 const eventTitle = evt.title || 'Event'; 4679ccd446eSAtari911 const eventTime = evt.time || ''; 4681d05cddcSAtari911 const originalDate = evt._original_date || dateKey; 4691d05cddcSAtari911 const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true; 4701d05cddcSAtari911 const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true; 4711d05cddcSAtari911 47296df7d3eSAtari911 // Check if important namespace 47396df7d3eSAtari911 let evtNs = evt.namespace || evt._namespace || ''; 47496df7d3eSAtari911 let isImportant = false; 47596df7d3eSAtari911 for (const impNs of importantNamespaces) { 47696df7d3eSAtari911 if (evtNs === impNs || evtNs.startsWith(impNs + ':')) { 47796df7d3eSAtari911 isImportant = true; 47896df7d3eSAtari911 break; 47996df7d3eSAtari911 } 48096df7d3eSAtari911 } 48196df7d3eSAtari911 4821d05cddcSAtari911 let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed'; 4831d05cddcSAtari911 if (!isFirstDay) barClass += ' event-bar-continues'; 4841d05cddcSAtari911 if (!isLastDay) barClass += ' event-bar-continuing'; 48596df7d3eSAtari911 if (isImportant) { 48696df7d3eSAtari911 barClass += ' event-bar-important'; 48796df7d3eSAtari911 if (isFirstDay) { 48896df7d3eSAtari911 barClass += ' event-bar-has-star'; 48996df7d3eSAtari911 } 49096df7d3eSAtari911 } 4911d05cddcSAtari911 4921d05cddcSAtari911 html += `<span class="event-bar ${barClass}" `; 4931d05cddcSAtari911 html += `style="background: ${eventColor};" `; 49496df7d3eSAtari911 html += `title="${isImportant ? '⭐ ' : ''}${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `; 49596df7d3eSAtari911 html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');">`; 49696df7d3eSAtari911 html += '</span>'; 4971d05cddcSAtari911 } 4981d05cddcSAtari911 html += '</div>'; 4991d05cddcSAtari911 } 5001d05cddcSAtari911 5011d05cddcSAtari911 html += '</td>'; 5021d05cddcSAtari911 currentDay++; 5031d05cddcSAtari911 } 5041d05cddcSAtari911 } 5051d05cddcSAtari911 html += '</tr>'; 5061d05cddcSAtari911 } 5071d05cddcSAtari911 5081d05cddcSAtari911 tbody.innerHTML = html; 5091d05cddcSAtari911 5101d05cddcSAtari911 // Update Today button with current namespace 5111d05cddcSAtari911 const todayBtn = container.querySelector('.cal-today-btn'); 5121d05cddcSAtari911 if (todayBtn) { 5131d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`); 5141d05cddcSAtari911 } 5151d05cddcSAtari911 5161d05cddcSAtari911 // Update month picker with current namespace 5171d05cddcSAtari911 const monthPicker = container.querySelector('.calendar-month-picker'); 5181d05cddcSAtari911 if (monthPicker) { 5191d05cddcSAtari911 monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`); 5201d05cddcSAtari911 } 5211d05cddcSAtari911 5221d05cddcSAtari911 // Rebuild event list - server already filtered to current month 5231d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 5241d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 5251d05cddcSAtari911 5261d05cddcSAtari911 // Auto-scroll to first future event (past events will be above viewport) 5271d05cddcSAtari911 setTimeout(() => { 5281d05cddcSAtari911 const firstFuture = eventList.querySelector('[data-first-future="true"]'); 5291d05cddcSAtari911 if (firstFuture) { 5301d05cddcSAtari911 firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' }); 5311d05cddcSAtari911 } 5321d05cddcSAtari911 }, 100); 5331d05cddcSAtari911 5341d05cddcSAtari911 // Update title 5351d05cddcSAtari911 const title = container.querySelector('#eventlist-title-' + calId); 536da206178SAtari911 title.textContent = 'Events'; 5371d05cddcSAtari911}; 5381d05cddcSAtari911 5391d05cddcSAtari911// Render event list from data 5401d05cddcSAtari911window.renderEventListFromData = function(events, calId, namespace, year, month) { 5411d05cddcSAtari911 if (!events || Object.keys(events).length === 0) { 542da206178SAtari911 return '<p class="no-events-msg">No events this month</p>'; 5431d05cddcSAtari911 } 5441d05cddcSAtari911 5459ccd446eSAtari911 // Get theme data from container 5469ccd446eSAtari911 const container = document.getElementById(calId); 5479ccd446eSAtari911 let themeStyles = {}; 5489ccd446eSAtari911 if (container && container.dataset.themeStyles) { 5499ccd446eSAtari911 try { 5509ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 5519ccd446eSAtari911 } catch (e) { 5529ccd446eSAtari911 console.error('Failed to parse theme styles in renderEventListFromData:', e); 5539ccd446eSAtari911 } 5549ccd446eSAtari911 } 5559ccd446eSAtari911 5561d05cddcSAtari911 // Check for time conflicts 5571d05cddcSAtari911 events = checkTimeConflicts(events, null); 5581d05cddcSAtari911 5591d05cddcSAtari911 let pastHtml = ''; 5601d05cddcSAtari911 let futureHtml = ''; 5611d05cddcSAtari911 let pastCount = 0; 5621d05cddcSAtari911 5631d05cddcSAtari911 const sortedDates = Object.keys(events).sort(); 5641d05cddcSAtari911 const today = new Date(); 5651d05cddcSAtari911 today.setHours(0, 0, 0, 0); 566*815440faSAtari911 const todayStr = formatLocalDate(today); 5671d05cddcSAtari911 5681d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period) 5691d05cddcSAtari911 const isEventPast = function(dateKey, time) { 5701d05cddcSAtari911 // If event is on a past date, it's definitely past 5711d05cddcSAtari911 if (dateKey < todayStr) { 5721d05cddcSAtari911 return true; 5731d05cddcSAtari911 } 5741d05cddcSAtari911 5751d05cddcSAtari911 // If event is on a future date, it's definitely not past 5761d05cddcSAtari911 if (dateKey > todayStr) { 5771d05cddcSAtari911 return false; 5781d05cddcSAtari911 } 5791d05cddcSAtari911 5801d05cddcSAtari911 // Event is today - check time with grace period 5811d05cddcSAtari911 if (time && time.trim() !== '') { 5821d05cddcSAtari911 try { 5831d05cddcSAtari911 const now = new Date(); 5841d05cddcSAtari911 const eventDateTime = new Date(dateKey + 'T' + time); 5851d05cddcSAtari911 5861d05cddcSAtari911 // Add 15-minute grace period 5871d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 5881d05cddcSAtari911 5891d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 5901d05cddcSAtari911 return now > gracePeriodEnd; 5911d05cddcSAtari911 } catch (e) { 5921d05cddcSAtari911 // If time parsing fails, treat as future 5931d05cddcSAtari911 return false; 5941d05cddcSAtari911 } 5951d05cddcSAtari911 } 5961d05cddcSAtari911 5971d05cddcSAtari911 // No time specified for today's event, treat as future 5981d05cddcSAtari911 return false; 5991d05cddcSAtari911 }; 6001d05cddcSAtari911 6011d05cddcSAtari911 // Filter events to only current month if year/month provided 6021d05cddcSAtari911 const monthStart = year && month ? new Date(year, month - 1, 1) : null; 6031d05cddcSAtari911 const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null; 6041d05cddcSAtari911 6051d05cddcSAtari911 for (const dateKey of sortedDates) { 6061d05cddcSAtari911 // Skip events not in current month if filtering 6071d05cddcSAtari911 if (monthStart && monthEnd) { 6081d05cddcSAtari911 const eventDate = new Date(dateKey + 'T00:00:00'); 6091d05cddcSAtari911 6101d05cddcSAtari911 if (eventDate < monthStart || eventDate > monthEnd) { 6111d05cddcSAtari911 continue; 6121d05cddcSAtari911 } 6131d05cddcSAtari911 } 6141d05cddcSAtari911 6151d05cddcSAtari911 // Sort events within this day by time (all-day events at top) 6161d05cddcSAtari911 const dayEvents = events[dateKey]; 6171d05cddcSAtari911 dayEvents.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 for (const event of dayEvents) { 6311d05cddcSAtari911 const isTask = event.isTask || false; 6321d05cddcSAtari911 const completed = event.completed || false; 6331d05cddcSAtari911 6341d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 6351d05cddcSAtari911 const isPast = isEventPast(dateKey, event.time); 6361d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 6371d05cddcSAtari911 6381d05cddcSAtari911 // Determine if this goes in past section 6391d05cddcSAtari911 const isPastOrCompleted = (isPast && (!isTask || completed)) || completed; 6401d05cddcSAtari911 6411d05cddcSAtari911 const eventHtml = renderEventItem(event, dateKey, calId, namespace); 6421d05cddcSAtari911 6431d05cddcSAtari911 if (isPastOrCompleted) { 6441d05cddcSAtari911 pastCount++; 6451d05cddcSAtari911 pastHtml += eventHtml; 6461d05cddcSAtari911 } else { 6471d05cddcSAtari911 futureHtml += eventHtml; 6481d05cddcSAtari911 } 6491d05cddcSAtari911 } 6501d05cddcSAtari911 } 6511d05cddcSAtari911 6521d05cddcSAtari911 let html = ''; 6531d05cddcSAtari911 6541d05cddcSAtari911 // Add collapsible past events section if any exist 6551d05cddcSAtari911 if (pastCount > 0) { 6561d05cddcSAtari911 html += '<div class="past-events-section">'; 6571d05cddcSAtari911 html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">'; 6581d05cddcSAtari911 html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> '; 659da206178SAtari911 html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>'; 6601d05cddcSAtari911 html += '</div>'; 6611d05cddcSAtari911 html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">'; 6621d05cddcSAtari911 html += pastHtml; 6631d05cddcSAtari911 html += '</div>'; 6641d05cddcSAtari911 html += '</div>'; 6651d05cddcSAtari911 } else { 6661d05cddcSAtari911 } 6671d05cddcSAtari911 6681d05cddcSAtari911 // Add future events 6691d05cddcSAtari911 html += futureHtml; 6701d05cddcSAtari911 6711d05cddcSAtari911 6721d05cddcSAtari911 if (!html) { 673da206178SAtari911 return '<p class="no-events-msg">No events this month</p>'; 6741d05cddcSAtari911 } 6751d05cddcSAtari911 6761d05cddcSAtari911 return html; 6771d05cddcSAtari911}; 6781d05cddcSAtari911 6791d05cddcSAtari911// Show day popup with events when clicking a date 6801d05cddcSAtari911window.showDayPopup = function(calId, date, namespace) { 6811d05cddcSAtari911 // Get events for this calendar 6821d05cddcSAtari911 const eventsDataEl = document.getElementById('events-data-' + calId); 6831d05cddcSAtari911 let events = {}; 6841d05cddcSAtari911 6851d05cddcSAtari911 if (eventsDataEl) { 6861d05cddcSAtari911 try { 6871d05cddcSAtari911 events = JSON.parse(eventsDataEl.textContent); 6881d05cddcSAtari911 } catch (e) { 6891d05cddcSAtari911 console.error('Failed to parse events data:', e); 6901d05cddcSAtari911 } 6911d05cddcSAtari911 } 6921d05cddcSAtari911 6931d05cddcSAtari911 const dayEvents = events[date] || []; 6941d05cddcSAtari911 6951d05cddcSAtari911 // Check for conflicts on this day 6961d05cddcSAtari911 const dayEventsObj = {[date]: dayEvents}; 6971d05cddcSAtari911 const checkedEvents = checkTimeConflicts(dayEventsObj, null); 6981d05cddcSAtari911 const dayEventsWithConflicts = checkedEvents[date] || dayEvents; 6991d05cddcSAtari911 7001d05cddcSAtari911 // Sort events: all-day at top, then chronological by time 7011d05cddcSAtari911 dayEventsWithConflicts.sort((a, b) => { 7021d05cddcSAtari911 const timeA = a.time && a.time.trim() !== '' ? a.time : null; 7031d05cddcSAtari911 const timeB = b.time && b.time.trim() !== '' ? b.time : null; 7041d05cddcSAtari911 7051d05cddcSAtari911 // All-day events (no time) go to the TOP 7061d05cddcSAtari911 if (timeA === null && timeB !== null) return -1; // A before B 7071d05cddcSAtari911 if (timeA !== null && timeB === null) return 1; // A after B 7081d05cddcSAtari911 if (timeA === null && timeB === null) return 0; // Both all-day, equal 7091d05cddcSAtari911 7101d05cddcSAtari911 // Both have times, sort chronologically 7111d05cddcSAtari911 return timeA.localeCompare(timeB); 7121d05cddcSAtari911 }); 7131d05cddcSAtari911 7141d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 7151d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 7161d05cddcSAtari911 weekday: 'long', 7171d05cddcSAtari911 month: 'long', 7181d05cddcSAtari911 day: 'numeric', 7191d05cddcSAtari911 year: 'numeric' 7201d05cddcSAtari911 }); 7211d05cddcSAtari911 7221d05cddcSAtari911 // Create popup 7231d05cddcSAtari911 let popup = document.getElementById('day-popup-' + calId); 7241d05cddcSAtari911 if (!popup) { 7251d05cddcSAtari911 popup = document.createElement('div'); 7261d05cddcSAtari911 popup.id = 'day-popup-' + calId; 7271d05cddcSAtari911 popup.className = 'day-popup'; 7281d05cddcSAtari911 document.body.appendChild(popup); 7291d05cddcSAtari911 } 7301d05cddcSAtari911 731da206178SAtari911 // Get theme styles and important namespaces 7329ccd446eSAtari911 const container = document.getElementById(calId); 7339ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 7349ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 7359ccd446eSAtari911 736da206178SAtari911 // Get important namespaces 737da206178SAtari911 let importantNamespaces = ['important']; 738da206178SAtari911 if (container && container.dataset.importantNamespaces) { 739da206178SAtari911 try { 740da206178SAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 741da206178SAtari911 } catch (e) { 742da206178SAtari911 importantNamespaces = ['important']; 743da206178SAtari911 } 744da206178SAtari911 } 745da206178SAtari911 7461d05cddcSAtari911 let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>'; 7471d05cddcSAtari911 html += '<div class="day-popup-content">'; 7481d05cddcSAtari911 html += '<div class="day-popup-header">'; 7491d05cddcSAtari911 html += '<h4>' + displayDate + '</h4>'; 7501d05cddcSAtari911 html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>'; 7511d05cddcSAtari911 html += '</div>'; 7521d05cddcSAtari911 7531d05cddcSAtari911 html += '<div class="day-popup-body">'; 7541d05cddcSAtari911 7551d05cddcSAtari911 if (dayEventsWithConflicts.length === 0) { 7561d05cddcSAtari911 html += '<p class="no-events-msg">No events on this day</p>'; 7571d05cddcSAtari911 } else { 7581d05cddcSAtari911 html += '<div class="popup-events-list">'; 7591d05cddcSAtari911 dayEventsWithConflicts.forEach(event => { 7601d05cddcSAtari911 const color = event.color || '#3498db'; 7611d05cddcSAtari911 7621d05cddcSAtari911 // Use individual event namespace if available (for multi-namespace support) 7631d05cddcSAtari911 const eventNamespace = event._namespace !== undefined ? event._namespace : namespace; 7641d05cddcSAtari911 765da206178SAtari911 // Check if this is an important namespace event 766da206178SAtari911 let isImportant = false; 767da206178SAtari911 if (eventNamespace) { 768da206178SAtari911 for (const impNs of importantNamespaces) { 769da206178SAtari911 if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) { 770da206178SAtari911 isImportant = true; 771da206178SAtari911 break; 772da206178SAtari911 } 773da206178SAtari911 } 774da206178SAtari911 } 775da206178SAtari911 7761d05cddcSAtari911 // Check if this is a continuation (event started before this date) 7771d05cddcSAtari911 const originalStartDate = event.originalStartDate || event._dateKey || date; 7781d05cddcSAtari911 const isContinuation = originalStartDate < date; 7791d05cddcSAtari911 7801d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 7811d05cddcSAtari911 let displayTime = ''; 7821d05cddcSAtari911 if (event.time) { 7831d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 7841d05cddcSAtari911 } 7851d05cddcSAtari911 7861d05cddcSAtari911 // Multi-day indicator 7871d05cddcSAtari911 let multiDay = ''; 7881d05cddcSAtari911 if (event.endDate && event.endDate !== date) { 7891d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 7901d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 7911d05cddcSAtari911 month: 'short', 7921d05cddcSAtari911 day: 'numeric' 7931d05cddcSAtari911 }); 7941d05cddcSAtari911 } 7951d05cddcSAtari911 7961d05cddcSAtari911 // Continuation message 7971d05cddcSAtari911 if (isContinuation) { 7981d05cddcSAtari911 const startObj = new Date(originalStartDate + 'T00:00:00'); 7991d05cddcSAtari911 const startDisplay = startObj.toLocaleDateString('en-US', { 8001d05cddcSAtari911 weekday: 'short', 8011d05cddcSAtari911 month: 'short', 8021d05cddcSAtari911 day: 'numeric' 8031d05cddcSAtari911 }); 8041d05cddcSAtari911 html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>'; 8051d05cddcSAtari911 } 8061d05cddcSAtari911 807da206178SAtari911 const importantClass = isImportant ? ' popup-event-important' : ''; 808*815440faSAtari911 html += '<div class="popup-event-item' + importantClass + '" tabindex="0" role="listitem" aria-label="' + escapeHtml(event.title) + (displayTime ? ', ' + displayTime : '') + '">'; 8091d05cddcSAtari911 html += '<div class="event-color-bar" style="background: ' + color + ';"></div>'; 8101d05cddcSAtari911 html += '<div class="popup-event-content">'; 8111d05cddcSAtari911 8121d05cddcSAtari911 // Single line with title, time, date range, namespace, and actions 8131d05cddcSAtari911 html += '<div class="popup-event-main-row">'; 8141d05cddcSAtari911 html += '<div class="popup-event-info-inline">'; 815da206178SAtari911 816da206178SAtari911 // Add star for important events 817da206178SAtari911 if (isImportant) { 818da206178SAtari911 html += '<span class="popup-event-star">⭐</span>'; 819da206178SAtari911 } 820da206178SAtari911 8211d05cddcSAtari911 html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>'; 8221d05cddcSAtari911 if (displayTime) { 8231d05cddcSAtari911 html += '<span class="popup-event-time"> ' + displayTime + '</span>'; 8241d05cddcSAtari911 } 8251d05cddcSAtari911 if (multiDay) { 8261d05cddcSAtari911 html += '<span class="popup-event-multiday">' + multiDay + '</span>'; 8271d05cddcSAtari911 } 8281d05cddcSAtari911 if (eventNamespace) { 8291d05cddcSAtari911 html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>'; 8301d05cddcSAtari911 } 8311d05cddcSAtari911 8321d05cddcSAtari911 // Add conflict warning badge if event has conflicts 8331d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 8341d05cddcSAtari911 // Build conflict list for tooltip 8351d05cddcSAtari911 let conflictList = []; 8361d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 8371d05cddcSAtari911 let conflictText = conflict.title; 8381d05cddcSAtari911 if (conflict.time) { 8391d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 8401d05cddcSAtari911 } 8411d05cddcSAtari911 conflictList.push(conflictText); 8421d05cddcSAtari911 }); 8431d05cddcSAtari911 8449ccd446eSAtari911 html += '<span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 8451d05cddcSAtari911 } 8461d05cddcSAtari911 8471d05cddcSAtari911 html += '</div>'; 8481d05cddcSAtari911 html += '<div class="popup-event-actions">'; 849da206178SAtari911 html += '<button type="button" class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>'; 850da206178SAtari911 html += '<button type="button" class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">️</button>'; 8511d05cddcSAtari911 html += '</div>'; 8521d05cddcSAtari911 html += '</div>'; 8531d05cddcSAtari911 8541d05cddcSAtari911 // Description on separate line if present 8551d05cddcSAtari911 if (event.description) { 8561d05cddcSAtari911 html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>'; 8571d05cddcSAtari911 } 8581d05cddcSAtari911 8591d05cddcSAtari911 html += '</div></div>'; 8601d05cddcSAtari911 }); 8611d05cddcSAtari911 html += '</div>'; 8621d05cddcSAtari911 } 8631d05cddcSAtari911 8641d05cddcSAtari911 html += '</div>'; 8651d05cddcSAtari911 8661d05cddcSAtari911 html += '<div class="day-popup-footer">'; 867da206178SAtari911 html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>'; 8681d05cddcSAtari911 html += '</div>'; 8691d05cddcSAtari911 8701d05cddcSAtari911 html += '</div>'; 8711d05cddcSAtari911 8721d05cddcSAtari911 popup.innerHTML = html; 8731d05cddcSAtari911 popup.style.display = 'flex'; 8749ccd446eSAtari911 8759ccd446eSAtari911 // Propagate CSS vars from calendar container to popup (popup is outside container in DOM) 8769ccd446eSAtari911 if (container) { 8779ccd446eSAtari911 propagateThemeVars(calId, popup.querySelector('.day-popup-content')); 8789ccd446eSAtari911 } 87996df7d3eSAtari911 88096df7d3eSAtari911 // Make popup draggable by header 88196df7d3eSAtari911 const popupContent = popup.querySelector('.day-popup-content'); 88296df7d3eSAtari911 const popupHeader = popup.querySelector('.day-popup-header'); 88396df7d3eSAtari911 88496df7d3eSAtari911 if (popupContent && popupHeader) { 88596df7d3eSAtari911 // Reset position to center 88696df7d3eSAtari911 popupContent.style.position = 'relative'; 88796df7d3eSAtari911 popupContent.style.left = '0'; 88896df7d3eSAtari911 popupContent.style.top = '0'; 88996df7d3eSAtari911 89096df7d3eSAtari911 // Store drag state on the element itself 89196df7d3eSAtari911 popupHeader._isDragging = false; 89296df7d3eSAtari911 89396df7d3eSAtari911 popupHeader.onmousedown = function(e) { 89496df7d3eSAtari911 // Ignore if clicking the close button 89596df7d3eSAtari911 if (e.target.classList.contains('popup-close')) return; 89696df7d3eSAtari911 89796df7d3eSAtari911 popupHeader._isDragging = true; 89896df7d3eSAtari911 popupHeader._dragStartX = e.clientX; 89996df7d3eSAtari911 popupHeader._dragStartY = e.clientY; 90096df7d3eSAtari911 90196df7d3eSAtari911 const rect = popupContent.getBoundingClientRect(); 90296df7d3eSAtari911 const parentRect = popup.getBoundingClientRect(); 90396df7d3eSAtari911 popupHeader._initialLeft = rect.left - parentRect.left - (parentRect.width / 2 - rect.width / 2); 90496df7d3eSAtari911 popupHeader._initialTop = rect.top - parentRect.top - (parentRect.height / 2 - rect.height / 2); 90596df7d3eSAtari911 90696df7d3eSAtari911 popupContent.style.transition = 'none'; 90796df7d3eSAtari911 e.preventDefault(); 90896df7d3eSAtari911 }; 90996df7d3eSAtari911 91096df7d3eSAtari911 popup.onmousemove = function(e) { 91196df7d3eSAtari911 if (!popupHeader._isDragging) return; 91296df7d3eSAtari911 91396df7d3eSAtari911 const deltaX = e.clientX - popupHeader._dragStartX; 91496df7d3eSAtari911 const deltaY = e.clientY - popupHeader._dragStartY; 91596df7d3eSAtari911 91696df7d3eSAtari911 popupContent.style.left = (popupHeader._initialLeft + deltaX) + 'px'; 91796df7d3eSAtari911 popupContent.style.top = (popupHeader._initialTop + deltaY) + 'px'; 91896df7d3eSAtari911 }; 91996df7d3eSAtari911 92096df7d3eSAtari911 popup.onmouseup = function() { 92196df7d3eSAtari911 if (popupHeader._isDragging) { 92296df7d3eSAtari911 popupHeader._isDragging = false; 92396df7d3eSAtari911 popupContent.style.transition = ''; 92496df7d3eSAtari911 } 92596df7d3eSAtari911 }; 92696df7d3eSAtari911 92796df7d3eSAtari911 popup.onmouseleave = function() { 92896df7d3eSAtari911 if (popupHeader._isDragging) { 92996df7d3eSAtari911 popupHeader._isDragging = false; 93096df7d3eSAtari911 popupContent.style.transition = ''; 93196df7d3eSAtari911 } 93296df7d3eSAtari911 }; 93396df7d3eSAtari911 } 9341d05cddcSAtari911}; 9351d05cddcSAtari911 9361d05cddcSAtari911// Close day popup 9371d05cddcSAtari911window.closeDayPopup = function(calId) { 9381d05cddcSAtari911 const popup = document.getElementById('day-popup-' + calId); 9391d05cddcSAtari911 if (popup) { 9401d05cddcSAtari911 popup.style.display = 'none'; 9411d05cddcSAtari911 } 9421d05cddcSAtari911}; 9431d05cddcSAtari911 9441d05cddcSAtari911// Show events for a specific day (for event list panel) 9451d05cddcSAtari911window.showDayEvents = function(calId, date, namespace) { 9461d05cddcSAtari911 const params = new URLSearchParams({ 9471d05cddcSAtari911 call: 'plugin_calendar', 9481d05cddcSAtari911 action: 'load_month', 9491d05cddcSAtari911 year: date.split('-')[0], 9501d05cddcSAtari911 month: parseInt(date.split('-')[1]), 9511d05cddcSAtari911 namespace: namespace, 9521d05cddcSAtari911 _: new Date().getTime() // Cache buster 9531d05cddcSAtari911 }); 9541d05cddcSAtari911 9551d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 9561d05cddcSAtari911 method: 'POST', 9571d05cddcSAtari911 headers: { 9581d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 9591d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 9601d05cddcSAtari911 'Pragma': 'no-cache' 9611d05cddcSAtari911 }, 9621d05cddcSAtari911 body: params.toString() 9631d05cddcSAtari911 }) 9641d05cddcSAtari911 .then(r => r.json()) 9651d05cddcSAtari911 .then(data => { 9661d05cddcSAtari911 if (data.success) { 9671d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 9681d05cddcSAtari911 const events = data.events; 9691d05cddcSAtari911 const title = document.getElementById('eventlist-title-' + calId); 9701d05cddcSAtari911 9711d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 972da206178SAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 9731d05cddcSAtari911 weekday: 'short', 9741d05cddcSAtari911 month: 'short', 9751d05cddcSAtari911 day: 'numeric' 9761d05cddcSAtari911 }); 9771d05cddcSAtari911 978da206178SAtari911 title.textContent = 'Events - ' + displayDate; 9791d05cddcSAtari911 9801d05cddcSAtari911 // Filter events for this day 9811d05cddcSAtari911 const dayEvents = events[date] || []; 9821d05cddcSAtari911 9831d05cddcSAtari911 if (dayEvents.length === 0) { 984da206178SAtari911 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>'; 9851d05cddcSAtari911 } else { 9861d05cddcSAtari911 let html = ''; 9871d05cddcSAtari911 dayEvents.forEach(event => { 9881d05cddcSAtari911 html += renderEventItem(event, date, calId, namespace); 9891d05cddcSAtari911 }); 9901d05cddcSAtari911 eventList.innerHTML = html; 9911d05cddcSAtari911 } 9921d05cddcSAtari911 } 9931d05cddcSAtari911 }) 9941d05cddcSAtari911 .catch(err => console.error('Error:', err)); 9951d05cddcSAtari911}; 9961d05cddcSAtari911 9971d05cddcSAtari911// Render a single event item 9981d05cddcSAtari911window.renderEventItem = function(event, date, calId, namespace) { 9999ccd446eSAtari911 // Get theme data from container 10009ccd446eSAtari911 const container = document.getElementById(calId); 10019ccd446eSAtari911 let themeStyles = {}; 100296df7d3eSAtari911 let importantNamespaces = ['important']; // default 10039ccd446eSAtari911 if (container && container.dataset.themeStyles) { 10049ccd446eSAtari911 try { 10059ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 10069ccd446eSAtari911 } catch (e) { 10079ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 10089ccd446eSAtari911 } 10099ccd446eSAtari911 } 101096df7d3eSAtari911 // Get important namespaces from container data attribute 101196df7d3eSAtari911 if (container && container.dataset.importantNamespaces) { 101296df7d3eSAtari911 try { 101396df7d3eSAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 101496df7d3eSAtari911 } catch (e) { 101596df7d3eSAtari911 importantNamespaces = ['important']; 101696df7d3eSAtari911 } 101796df7d3eSAtari911 } 10189ccd446eSAtari911 10191d05cddcSAtari911 // Check if this event is in the past or today (with 15-minute grace period) 10201d05cddcSAtari911 const today = new Date(); 10211d05cddcSAtari911 today.setHours(0, 0, 0, 0); 1022*815440faSAtari911 const todayStr = formatLocalDate(today); 10231d05cddcSAtari911 const eventDate = new Date(date + 'T00:00:00'); 10241d05cddcSAtari911 10251d05cddcSAtari911 // Helper to determine if event is past with grace period 10261d05cddcSAtari911 let isPast; 10271d05cddcSAtari911 if (date < todayStr) { 10281d05cddcSAtari911 isPast = true; // Past date 10291d05cddcSAtari911 } else if (date > todayStr) { 10301d05cddcSAtari911 isPast = false; // Future date 10311d05cddcSAtari911 } else { 10321d05cddcSAtari911 // Today - check time with grace period 10331d05cddcSAtari911 if (event.time && event.time.trim() !== '') { 10341d05cddcSAtari911 try { 10351d05cddcSAtari911 const now = new Date(); 10361d05cddcSAtari911 const eventDateTime = new Date(date + 'T' + event.time); 10371d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 10381d05cddcSAtari911 isPast = now > gracePeriodEnd; 10391d05cddcSAtari911 } catch (e) { 10401d05cddcSAtari911 isPast = false; 10411d05cddcSAtari911 } 10421d05cddcSAtari911 } else { 10431d05cddcSAtari911 isPast = false; // No time, treat as future 10441d05cddcSAtari911 } 10451d05cddcSAtari911 } 10461d05cddcSAtari911 10471d05cddcSAtari911 const isToday = eventDate.getTime() === today.getTime(); 10481d05cddcSAtari911 104996df7d3eSAtari911 // Check if this is an important namespace event 105096df7d3eSAtari911 let eventNamespace = event.namespace || ''; 105196df7d3eSAtari911 if (!eventNamespace && event._namespace !== undefined) { 105296df7d3eSAtari911 eventNamespace = event._namespace; 105396df7d3eSAtari911 } 105496df7d3eSAtari911 let isImportantNs = false; 105596df7d3eSAtari911 if (eventNamespace) { 105696df7d3eSAtari911 for (const impNs of importantNamespaces) { 105796df7d3eSAtari911 if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) { 105896df7d3eSAtari911 isImportantNs = true; 105996df7d3eSAtari911 break; 106096df7d3eSAtari911 } 106196df7d3eSAtari911 } 106296df7d3eSAtari911 } 106396df7d3eSAtari911 10641d05cddcSAtari911 // Format date display with day of week 10651d05cddcSAtari911 const displayDateKey = event.originalStartDate || date; 10661d05cddcSAtari911 const dateObj = new Date(displayDateKey + 'T00:00:00'); 10671d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 10681d05cddcSAtari911 weekday: 'short', 10691d05cddcSAtari911 month: 'short', 10701d05cddcSAtari911 day: 'numeric' 10711d05cddcSAtari911 }); 10721d05cddcSAtari911 10731d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 10741d05cddcSAtari911 let displayTime = ''; 10751d05cddcSAtari911 if (event.time) { 10761d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 10771d05cddcSAtari911 } 10781d05cddcSAtari911 10791d05cddcSAtari911 // Multi-day indicator 10801d05cddcSAtari911 let multiDay = ''; 10811d05cddcSAtari911 if (event.endDate && event.endDate !== displayDateKey) { 10821d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 10831d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 10841d05cddcSAtari911 weekday: 'short', 10851d05cddcSAtari911 month: 'short', 10861d05cddcSAtari911 day: 'numeric' 10871d05cddcSAtari911 }); 10881d05cddcSAtari911 } 10891d05cddcSAtari911 10901d05cddcSAtari911 const completedClass = event.completed ? ' event-completed' : ''; 10911d05cddcSAtari911 const isTask = event.isTask || false; 10921d05cddcSAtari911 const completed = event.completed || false; 10931d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 10941d05cddcSAtari911 const pastClass = (isPast && !isPastDue) ? ' event-past' : ''; 10951d05cddcSAtari911 const pastDueClass = isPastDue ? ' event-pastdue' : ''; 109696df7d3eSAtari911 const importantClass = isImportantNs ? ' event-important' : ''; 10971d05cddcSAtari911 const color = event.color || '#3498db'; 10981d05cddcSAtari911 10999ccd446eSAtari911 // Only inline style needed: border-left-color for event color indicator 110096df7d3eSAtari911 let html = '<div class="event-compact-item' + completedClass + pastClass + pastDueClass + importantClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ' !important;" onclick="' + (isPast && !isPastDue ? 'togglePastEventExpand(this)' : '') + '">'; 11011d05cddcSAtari911 11021d05cddcSAtari911 html += '<div class="event-info">'; 11031d05cddcSAtari911 html += '<div class="event-title-row">'; 110496df7d3eSAtari911 // Add star for important namespace events 110596df7d3eSAtari911 if (isImportantNs) { 1106da206178SAtari911 html += '<span class="event-important-star" title="Important">⭐</span> '; 110796df7d3eSAtari911 } 11081d05cddcSAtari911 html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>'; 11091d05cddcSAtari911 html += '</div>'; 11101d05cddcSAtari911 11111d05cddcSAtari911 // Show meta and description for non-past events AND past due tasks 11121d05cddcSAtari911 if (!isPast || isPastDue) { 11131d05cddcSAtari911 html += '<div class="event-meta-compact">'; 11141d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 11151d05cddcSAtari911 if (displayTime) { 11161d05cddcSAtari911 html += ' • ' + displayTime; 11171d05cddcSAtari911 } 11181d05cddcSAtari911 // Add PAST DUE or TODAY badge 11191d05cddcSAtari911 if (isPastDue) { 1120da206178SAtari911 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>'; 11211d05cddcSAtari911 } else if (isToday) { 1122da206178SAtari911 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>'; 11231d05cddcSAtari911 } 11249ccd446eSAtari911 // Add namespace badge 11251d05cddcSAtari911 if (eventNamespace) { 11267e8ea635SAtari911 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>'; 11271d05cddcSAtari911 } 11281d05cddcSAtari911 // Add conflict warning if event has time conflicts 11291d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 11301d05cddcSAtari911 let conflictList = []; 11311d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 11321d05cddcSAtari911 let conflictText = conflict.title; 11331d05cddcSAtari911 if (conflict.time) { 11341d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 11351d05cddcSAtari911 } 11361d05cddcSAtari911 conflictList.push(conflictText); 11371d05cddcSAtari911 }); 11381d05cddcSAtari911 11399ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 11401d05cddcSAtari911 } 11411d05cddcSAtari911 html += '</span>'; 11421d05cddcSAtari911 html += '</div>'; 11431d05cddcSAtari911 11441d05cddcSAtari911 if (event.description) { 11451d05cddcSAtari911 html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>'; 11461d05cddcSAtari911 } 11471d05cddcSAtari911 } else { 11481d05cddcSAtari911 // For past events (not past due), store data in hidden divs for expand/collapse 11491d05cddcSAtari911 html += '<div class="event-meta-compact" style="display: none;">'; 11501d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 11511d05cddcSAtari911 if (displayTime) { 11521d05cddcSAtari911 html += ' • ' + displayTime; 11531d05cddcSAtari911 } 11541d05cddcSAtari911 // Add namespace badge for past events too 11551d05cddcSAtari911 let eventNamespace = event.namespace || ''; 11561d05cddcSAtari911 if (!eventNamespace && event._namespace !== undefined) { 11571d05cddcSAtari911 eventNamespace = event._namespace; 11581d05cddcSAtari911 } 11591d05cddcSAtari911 if (eventNamespace) { 11607e8ea635SAtari911 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>'; 11619ccd446eSAtari911 } 11629ccd446eSAtari911 // Add conflict warning for past events too 11639ccd446eSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 11649ccd446eSAtari911 let conflictList = []; 11659ccd446eSAtari911 event.conflictsWith.forEach(conflict => { 11669ccd446eSAtari911 let conflictText = conflict.title; 11679ccd446eSAtari911 if (conflict.time) { 11689ccd446eSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 11699ccd446eSAtari911 } 11709ccd446eSAtari911 conflictList.push(conflictText); 11719ccd446eSAtari911 }); 11729ccd446eSAtari911 11739ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 11741d05cddcSAtari911 } 11751d05cddcSAtari911 html += '</span>'; 11761d05cddcSAtari911 html += '</div>'; 11771d05cddcSAtari911 11781d05cddcSAtari911 if (event.description) { 11791d05cddcSAtari911 html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>'; 11801d05cddcSAtari911 } 11811d05cddcSAtari911 } 11821d05cddcSAtari911 11831d05cddcSAtari911 html += '</div>'; // event-info 11841d05cddcSAtari911 11851d05cddcSAtari911 // Use stored namespace from event, fallback to _namespace, then passed namespace 11861d05cddcSAtari911 let buttonNamespace = event.namespace || ''; 11871d05cddcSAtari911 if (!buttonNamespace && event._namespace !== undefined) { 11881d05cddcSAtari911 buttonNamespace = event._namespace; 11891d05cddcSAtari911 } 11901d05cddcSAtari911 if (!buttonNamespace) { 11911d05cddcSAtari911 buttonNamespace = namespace; 11921d05cddcSAtari911 } 11931d05cddcSAtari911 11941d05cddcSAtari911 html += '<div class="event-actions-compact">'; 11951d05cddcSAtari911 html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">️</button>'; 11961d05cddcSAtari911 html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>'; 11971d05cddcSAtari911 html += '</div>'; 11981d05cddcSAtari911 11991d05cddcSAtari911 // Checkbox for tasks - ON THE FAR RIGHT 12001d05cddcSAtari911 if (isTask) { 12011d05cddcSAtari911 const checked = completed ? 'checked' : ''; 12021d05cddcSAtari911 html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">'; 12031d05cddcSAtari911 } 12041d05cddcSAtari911 12051d05cddcSAtari911 html += '</div>'; 12061d05cddcSAtari911 12071d05cddcSAtari911 return html; 12081d05cddcSAtari911}; 12091d05cddcSAtari911 12101d05cddcSAtari911// Render description with rich content support 12111d05cddcSAtari911window.renderDescription = function(description) { 12121d05cddcSAtari911 if (!description) return ''; 12131d05cddcSAtari911 12141d05cddcSAtari911 // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping) 12151d05cddcSAtari911 // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00 12161d05cddcSAtari911 12171d05cddcSAtari911 let rendered = description; 12181d05cddcSAtari911 const tokens = []; 12191d05cddcSAtari911 let tokenIndex = 0; 12201d05cddcSAtari911 12211d05cddcSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 12221d05cddcSAtari911 rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) { 12231d05cddcSAtari911 imagePath = imagePath.trim(); 12241d05cddcSAtari911 alt = alt ? alt.trim() : ''; 12251d05cddcSAtari911 12261d05cddcSAtari911 let imageHtml; 12271d05cddcSAtari911 // Handle external URLs 12281d05cddcSAtari911 if (imagePath.match(/^https?:\/\//)) { 12291d05cddcSAtari911 imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 12301d05cddcSAtari911 } else { 12311d05cddcSAtari911 // Handle internal DokuWiki images 12321d05cddcSAtari911 const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath); 12331d05cddcSAtari911 imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 12341d05cddcSAtari911 } 12351d05cddcSAtari911 12361d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12371d05cddcSAtari911 tokens[tokenIndex] = imageHtml; 12381d05cddcSAtari911 tokenIndex++; 12391d05cddcSAtari911 return token; 12401d05cddcSAtari911 }); 12411d05cddcSAtari911 12421d05cddcSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 12431d05cddcSAtari911 rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) { 12441d05cddcSAtari911 link = link.trim(); 12451d05cddcSAtari911 text = text ? text.trim() : link; 12461d05cddcSAtari911 12471d05cddcSAtari911 let linkHtml; 12481d05cddcSAtari911 // Handle external URLs 12491d05cddcSAtari911 if (link.match(/^https?:\/\//)) { 12501d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 12511d05cddcSAtari911 } else { 12521d05cddcSAtari911 // Handle internal DokuWiki links with section anchors 12531d05cddcSAtari911 const hashIndex = link.indexOf('#'); 12541d05cddcSAtari911 let pagePart = link; 12551d05cddcSAtari911 let sectionPart = ''; 12561d05cddcSAtari911 12571d05cddcSAtari911 if (hashIndex !== -1) { 12581d05cddcSAtari911 pagePart = link.substring(0, hashIndex); 12591d05cddcSAtari911 sectionPart = link.substring(hashIndex); // Includes the # 12601d05cddcSAtari911 } 12611d05cddcSAtari911 12621d05cddcSAtari911 const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart; 12631d05cddcSAtari911 linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>'; 12641d05cddcSAtari911 } 12651d05cddcSAtari911 12661d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12671d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12681d05cddcSAtari911 tokenIndex++; 12691d05cddcSAtari911 return token; 12701d05cddcSAtari911 }); 12711d05cddcSAtari911 12721d05cddcSAtari911 // Convert markdown-style links [text](url) to tokens 12731d05cddcSAtari911 rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) { 12741d05cddcSAtari911 text = text.trim(); 12751d05cddcSAtari911 url = url.trim(); 12761d05cddcSAtari911 12771d05cddcSAtari911 let linkHtml; 12781d05cddcSAtari911 if (url.match(/^https?:\/\//)) { 12791d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 12801d05cddcSAtari911 } else { 12811d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>'; 12821d05cddcSAtari911 } 12831d05cddcSAtari911 12841d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12851d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12861d05cddcSAtari911 tokenIndex++; 12871d05cddcSAtari911 return token; 12881d05cddcSAtari911 }); 12891d05cddcSAtari911 12901d05cddcSAtari911 // Convert plain URLs to tokens 12911d05cddcSAtari911 rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) { 12921d05cddcSAtari911 const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>'; 12931d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12941d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12951d05cddcSAtari911 tokenIndex++; 12961d05cddcSAtari911 return token; 12971d05cddcSAtari911 }); 12981d05cddcSAtari911 12991d05cddcSAtari911 // NOW escape the remaining text (tokens are protected with null bytes) 13001d05cddcSAtari911 rendered = escapeHtml(rendered); 13011d05cddcSAtari911 13021d05cddcSAtari911 // Convert newlines to <br> 13031d05cddcSAtari911 rendered = rendered.replace(/\n/g, '<br>'); 13041d05cddcSAtari911 13051d05cddcSAtari911 // DokuWiki text formatting (on escaped text) 13061d05cddcSAtari911 // Bold: **text** or __text__ 13071d05cddcSAtari911 rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); 13081d05cddcSAtari911 rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>'); 13091d05cddcSAtari911 13101d05cddcSAtari911 // Italic: //text// 13111d05cddcSAtari911 rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>'); 13121d05cddcSAtari911 13131d05cddcSAtari911 // Strikethrough: <del>text</del> 13141d05cddcSAtari911 rendered = rendered.replace(/<del>(.+?)<\/del>/g, '<del>$1</del>'); 13151d05cddcSAtari911 13161d05cddcSAtari911 // Monospace: ''text'' 13171d05cddcSAtari911 rendered = rendered.replace(/''(.+?)''/g, '<code>$1</code>'); 13181d05cddcSAtari911 13191d05cddcSAtari911 // Subscript: <sub>text</sub> 13201d05cddcSAtari911 rendered = rendered.replace(/<sub>(.+?)<\/sub>/g, '<sub>$1</sub>'); 13211d05cddcSAtari911 13221d05cddcSAtari911 // Superscript: <sup>text</sup> 13231d05cddcSAtari911 rendered = rendered.replace(/<sup>(.+?)<\/sup>/g, '<sup>$1</sup>'); 13241d05cddcSAtari911 13251d05cddcSAtari911 // Restore tokens (replace with actual HTML) 13261d05cddcSAtari911 for (let i = 0; i < tokens.length; i++) { 13271d05cddcSAtari911 const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g'); 13281d05cddcSAtari911 rendered = rendered.replace(tokenPattern, tokens[i]); 13291d05cddcSAtari911 } 13301d05cddcSAtari911 13311d05cddcSAtari911 return rendered; 13321d05cddcSAtari911} 13331d05cddcSAtari911 13341d05cddcSAtari911// Open add event dialog 13351d05cddcSAtari911window.openAddEvent = function(calId, namespace, date) { 13361d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 13371d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 13381d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 13391d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 13401d05cddcSAtari911 13411d05cddcSAtari911 if (!dateField) { 13421d05cddcSAtari911 console.error('Date field not found! ID: event-date-' + calId); 13431d05cddcSAtari911 return; 13441d05cddcSAtari911 } 13451d05cddcSAtari911 1346231d0edbSAtari911 // Check if there's a filtered namespace active (only for regular calendars) 13471d05cddcSAtari911 const calendar = document.getElementById(calId); 1348231d0edbSAtari911 const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null; 13491d05cddcSAtari911 13501d05cddcSAtari911 // Use filtered namespace if available, otherwise use the passed namespace 13511d05cddcSAtari911 const effectiveNamespace = filteredNamespace || namespace; 13521d05cddcSAtari911 13531d05cddcSAtari911 13541d05cddcSAtari911 // Reset form 13551d05cddcSAtari911 form.reset(); 13561d05cddcSAtari911 document.getElementById('event-id-' + calId).value = ''; 13571d05cddcSAtari911 13581d05cddcSAtari911 // Store the effective namespace in a hidden field or data attribute 13591d05cddcSAtari911 form.dataset.effectiveNamespace = effectiveNamespace; 13601d05cddcSAtari911 13611d05cddcSAtari911 // Set namespace dropdown to effective namespace 13621d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 13631d05cddcSAtari911 if (namespaceSelect) { 13641d05cddcSAtari911 if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) { 13651d05cddcSAtari911 // Set to specific namespace if not wildcard or multi-namespace 13661d05cddcSAtari911 namespaceSelect.value = effectiveNamespace; 13671d05cddcSAtari911 } else { 13681d05cddcSAtari911 // Default to empty (default namespace) for wildcard/multi views 13691d05cddcSAtari911 namespaceSelect.value = ''; 13701d05cddcSAtari911 } 13711d05cddcSAtari911 } 13721d05cddcSAtari911 13731d05cddcSAtari911 // Clear event namespace from previous edits 13741d05cddcSAtari911 delete form.dataset.eventNamespace; 13751d05cddcSAtari911 13761d05cddcSAtari911 // Set date - use local date, not UTC 13771d05cddcSAtari911 let defaultDate = date; 13781d05cddcSAtari911 if (!defaultDate) { 13791d05cddcSAtari911 // Get the currently displayed month from the calendar container 13801d05cddcSAtari911 const container = document.getElementById(calId); 13811d05cddcSAtari911 const displayedYear = parseInt(container.getAttribute('data-year')); 13821d05cddcSAtari911 const displayedMonth = parseInt(container.getAttribute('data-month')); 13831d05cddcSAtari911 13841d05cddcSAtari911 13851d05cddcSAtari911 if (displayedYear && displayedMonth) { 13861d05cddcSAtari911 // Use first day of the displayed month 13871d05cddcSAtari911 const year = displayedYear; 13881d05cddcSAtari911 const month = String(displayedMonth).padStart(2, '0'); 13891d05cddcSAtari911 defaultDate = `${year}-${month}-01`; 13901d05cddcSAtari911 } else { 13911d05cddcSAtari911 // Fallback to today if attributes not found 13921d05cddcSAtari911 const today = new Date(); 13931d05cddcSAtari911 const year = today.getFullYear(); 13941d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 13951d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 13961d05cddcSAtari911 defaultDate = `${year}-${month}-${day}`; 13971d05cddcSAtari911 } 13981d05cddcSAtari911 } 13991d05cddcSAtari911 dateField.value = defaultDate; 14001d05cddcSAtari911 dateField.removeAttribute('data-original-date'); 14011d05cddcSAtari911 14021d05cddcSAtari911 // Also set the end date field to the same default (user can change it) 14031d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 14041d05cddcSAtari911 if (endDateField) { 14051d05cddcSAtari911 endDateField.value = ''; // Empty by default (single-day event) 14061d05cddcSAtari911 } 14071d05cddcSAtari911 14081d05cddcSAtari911 // Set default color 14091d05cddcSAtari911 document.getElementById('event-color-' + calId).value = '#3498db'; 14101d05cddcSAtari911 1411*815440faSAtari911 // Reset time pickers to default state 1412*815440faSAtari911 setTimePicker(calId, false, ''); // Start time = All day 1413*815440faSAtari911 setTimePicker(calId, true, ''); // End time = Same as start 1414*815440faSAtari911 1415*815440faSAtari911 // Set date pickers 1416*815440faSAtari911 setDatePicker(calId, false, defaultDate); // Start date 1417*815440faSAtari911 setDatePicker(calId, true, ''); // End date = Optional 14181d05cddcSAtari911 14191d05cddcSAtari911 // Initialize namespace search 14201d05cddcSAtari911 initNamespaceSearch(calId); 14211d05cddcSAtari911 14221d05cddcSAtari911 // Set title 1423da206178SAtari911 title.textContent = 'Add Event'; 14241d05cddcSAtari911 14251d05cddcSAtari911 // Show dialog 14261d05cddcSAtari911 dialog.style.display = 'flex'; 14271d05cddcSAtari911 14289ccd446eSAtari911 // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates) 14299ccd446eSAtari911 propagateThemeVars(calId, dialog); 14309ccd446eSAtari911 1431*815440faSAtari911 // Initialize custom pickers 1432*815440faSAtari911 initCustomTimePickers(calId); 1433*815440faSAtari911 initCustomDatePickers(calId); 1434*815440faSAtari911 1435da206178SAtari911 // Make dialog draggable 1436da206178SAtari911 setTimeout(() => makeDialogDraggable(calId), 50); 1437da206178SAtari911 14381d05cddcSAtari911 // Focus title field 14391d05cddcSAtari911 setTimeout(() => { 14401d05cddcSAtari911 const titleField = document.getElementById('event-title-' + calId); 14411d05cddcSAtari911 if (titleField) titleField.focus(); 14421d05cddcSAtari911 }, 100); 14431d05cddcSAtari911}; 14441d05cddcSAtari911 14451d05cddcSAtari911// Edit event 14461d05cddcSAtari911window.editEvent = function(calId, eventId, date, namespace) { 14471d05cddcSAtari911 const params = new URLSearchParams({ 14481d05cddcSAtari911 call: 'plugin_calendar', 14491d05cddcSAtari911 action: 'get_event', 14501d05cddcSAtari911 namespace: namespace, 14511d05cddcSAtari911 date: date, 14521d05cddcSAtari911 eventId: eventId 14531d05cddcSAtari911 }); 14541d05cddcSAtari911 14551d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 14561d05cddcSAtari911 method: 'POST', 14571d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 14581d05cddcSAtari911 body: params.toString() 14591d05cddcSAtari911 }) 14601d05cddcSAtari911 .then(r => r.json()) 14611d05cddcSAtari911 .then(data => { 14621d05cddcSAtari911 if (data.success && data.event) { 14631d05cddcSAtari911 const event = data.event; 14641d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 14651d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 14661d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 14671d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 14681d05cddcSAtari911 14691d05cddcSAtari911 if (!dateField) { 14701d05cddcSAtari911 console.error('Date field not found when editing!'); 14711d05cddcSAtari911 return; 14721d05cddcSAtari911 } 14731d05cddcSAtari911 14741d05cddcSAtari911 // Store the event's actual namespace for saving (important for namespace=* views) 14751d05cddcSAtari911 if (event.namespace !== undefined) { 14761d05cddcSAtari911 form.dataset.eventNamespace = event.namespace; 14771d05cddcSAtari911 } 14781d05cddcSAtari911 14791d05cddcSAtari911 // Populate form 14801d05cddcSAtari911 document.getElementById('event-id-' + calId).value = event.id; 14811d05cddcSAtari911 dateField.value = date; 14821d05cddcSAtari911 dateField.setAttribute('data-original-date', date); 14831d05cddcSAtari911 14841d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 14851d05cddcSAtari911 endDateField.value = event.endDate || ''; 14861d05cddcSAtari911 14871d05cddcSAtari911 document.getElementById('event-title-' + calId).value = event.title; 14881d05cddcSAtari911 document.getElementById('event-color-' + calId).value = event.color || '#3498db'; 14891d05cddcSAtari911 document.getElementById('event-desc-' + calId).value = event.description || ''; 14901d05cddcSAtari911 document.getElementById('event-is-task-' + calId).checked = event.isTask || false; 14911d05cddcSAtari911 1492*815440faSAtari911 // Set time picker values using custom picker API 1493*815440faSAtari911 setTimePicker(calId, false, event.time || ''); 1494*815440faSAtari911 setTimePicker(calId, true, event.endTime || ''); 1495*815440faSAtari911 1496*815440faSAtari911 // Set date picker values 1497*815440faSAtari911 setDatePicker(calId, false, date); 1498*815440faSAtari911 setDatePicker(calId, true, event.endDate || ''); 14991d05cddcSAtari911 15001d05cddcSAtari911 // Initialize namespace search 15011d05cddcSAtari911 initNamespaceSearch(calId); 15021d05cddcSAtari911 15031d05cddcSAtari911 // Set namespace fields if available 15041d05cddcSAtari911 const namespaceHidden = document.getElementById('event-namespace-' + calId); 15051d05cddcSAtari911 const namespaceSearch = document.getElementById('event-namespace-search-' + calId); 15061d05cddcSAtari911 if (namespaceHidden && event.namespace !== undefined) { 15079ccd446eSAtari911 // Set the hidden input (this is what gets submitted) 15089ccd446eSAtari911 namespaceHidden.value = event.namespace || ''; 15099ccd446eSAtari911 // Set the search input to display the namespace 15101d05cddcSAtari911 if (namespaceSearch) { 15111d05cddcSAtari911 namespaceSearch.value = event.namespace || '(default)'; 15121d05cddcSAtari911 } 15139ccd446eSAtari911 } else { 15149ccd446eSAtari911 // No namespace on event, set to default 15159ccd446eSAtari911 if (namespaceHidden) { 15169ccd446eSAtari911 namespaceHidden.value = ''; 15179ccd446eSAtari911 } 15189ccd446eSAtari911 if (namespaceSearch) { 15199ccd446eSAtari911 namespaceSearch.value = '(default)'; 15209ccd446eSAtari911 } 15211d05cddcSAtari911 } 15221d05cddcSAtari911 1523da206178SAtari911 title.textContent = 'Edit Event'; 15241d05cddcSAtari911 dialog.style.display = 'flex'; 15259ccd446eSAtari911 15269ccd446eSAtari911 // Propagate CSS vars to dialog 15279ccd446eSAtari911 propagateThemeVars(calId, dialog); 1528da206178SAtari911 1529*815440faSAtari911 // Initialize custom pickers 1530*815440faSAtari911 initCustomTimePickers(calId); 1531*815440faSAtari911 initCustomDatePickers(calId); 1532*815440faSAtari911 1533da206178SAtari911 // Make dialog draggable 1534da206178SAtari911 setTimeout(() => makeDialogDraggable(calId), 50); 15351d05cddcSAtari911 } 15361d05cddcSAtari911 }) 15371d05cddcSAtari911 .catch(err => console.error('Error editing event:', err)); 15381d05cddcSAtari911}; 15391d05cddcSAtari911 15401d05cddcSAtari911// Delete event 15411d05cddcSAtari911window.deleteEvent = function(calId, eventId, date, namespace) { 1542da206178SAtari911 if (!confirm('Delete this event?')) return; 15431d05cddcSAtari911 15441d05cddcSAtari911 const params = new URLSearchParams({ 15451d05cddcSAtari911 call: 'plugin_calendar', 15461d05cddcSAtari911 action: 'delete_event', 15471d05cddcSAtari911 namespace: namespace, 15481d05cddcSAtari911 date: date, 15497e8ea635SAtari911 eventId: eventId, 1550b498f308SAtari911 sectok: getSecurityToken() 15511d05cddcSAtari911 }); 15521d05cddcSAtari911 15531d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 15541d05cddcSAtari911 method: 'POST', 15551d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 15561d05cddcSAtari911 body: params.toString() 15571d05cddcSAtari911 }) 15581d05cddcSAtari911 .then(r => r.json()) 15591d05cddcSAtari911 .then(data => { 15601d05cddcSAtari911 if (data.success) { 1561*815440faSAtari911 // Announce to screen readers 1562*815440faSAtari911 announceToScreenReader('Event deleted'); 1563*815440faSAtari911 15641d05cddcSAtari911 // Extract year and month from date 15651d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 15661d05cddcSAtari911 156796df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting (not the deleted event's namespace) 156896df7d3eSAtari911 // This preserves wildcard/multi-namespace views 156996df7d3eSAtari911 const container = document.getElementById(calId); 157096df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 157196df7d3eSAtari911 157296df7d3eSAtari911 // Reload calendar data via AJAX with the calendar's original namespace 157396df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 15741d05cddcSAtari911 } 15751d05cddcSAtari911 }) 15761d05cddcSAtari911 .catch(err => console.error('Error:', err)); 15771d05cddcSAtari911}; 15781d05cddcSAtari911 15791d05cddcSAtari911// Save event (add or edit) 15801d05cddcSAtari911window.saveEventCompact = function(calId, namespace) { 15811d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 15821d05cddcSAtari911 15831d05cddcSAtari911 // Get namespace from dropdown - this is what the user selected 15841d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 15851d05cddcSAtari911 const selectedNamespace = namespaceSelect ? namespaceSelect.value : ''; 15861d05cddcSAtari911 15871d05cddcSAtari911 // ALWAYS use what the user selected in the dropdown 15881d05cddcSAtari911 // This allows changing namespace when editing 15891d05cddcSAtari911 const finalNamespace = selectedNamespace; 15901d05cddcSAtari911 15911d05cddcSAtari911 const eventId = document.getElementById('event-id-' + calId).value; 15921d05cddcSAtari911 15931d05cddcSAtari911 // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event) 15941d05cddcSAtari911 const originalNamespace = form.dataset.eventNamespace; 15951d05cddcSAtari911 15961d05cddcSAtari911 15971d05cddcSAtari911 const dateInput = document.getElementById('event-date-' + calId); 15981d05cddcSAtari911 const date = dateInput.value; 15991d05cddcSAtari911 const oldDate = dateInput.getAttribute('data-original-date') || date; 16001d05cddcSAtari911 const endDate = document.getElementById('event-end-date-' + calId).value; 16011d05cddcSAtari911 const title = document.getElementById('event-title-' + calId).value; 16021d05cddcSAtari911 const time = document.getElementById('event-time-' + calId).value; 16031d05cddcSAtari911 const endTime = document.getElementById('event-end-time-' + calId).value; 16041d05cddcSAtari911 const colorSelect = document.getElementById('event-color-' + calId); 16051d05cddcSAtari911 let color = colorSelect.value; 16061d05cddcSAtari911 16071d05cddcSAtari911 // Handle custom color 16081d05cddcSAtari911 if (color === 'custom') { 16091d05cddcSAtari911 color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value; 16101d05cddcSAtari911 } 16111d05cddcSAtari911 16121d05cddcSAtari911 const description = document.getElementById('event-desc-' + calId).value; 16131d05cddcSAtari911 const isTask = document.getElementById('event-is-task-' + calId).checked; 16141d05cddcSAtari911 const completed = false; // New tasks are not completed 16151d05cddcSAtari911 const isRecurring = document.getElementById('event-recurring-' + calId).checked; 16161d05cddcSAtari911 const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value; 16171d05cddcSAtari911 const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value; 16181d05cddcSAtari911 161996df7d3eSAtari911 // New recurrence options 162096df7d3eSAtari911 const recurrenceIntervalInput = document.getElementById('event-recurrence-interval-' + calId); 162196df7d3eSAtari911 const recurrenceInterval = recurrenceIntervalInput ? parseInt(recurrenceIntervalInput.value) || 1 : 1; 162296df7d3eSAtari911 162396df7d3eSAtari911 // Weekly: collect selected days 162496df7d3eSAtari911 let weekDays = []; 162596df7d3eSAtari911 const weeklyOptions = document.getElementById('weekly-options-' + calId); 162696df7d3eSAtari911 if (weeklyOptions && recurrenceType === 'weekly') { 162796df7d3eSAtari911 const checkboxes = weeklyOptions.querySelectorAll('input[name="weekDays[]"]:checked'); 162896df7d3eSAtari911 weekDays = Array.from(checkboxes).map(cb => cb.value); 162996df7d3eSAtari911 } 163096df7d3eSAtari911 163196df7d3eSAtari911 // Monthly: collect day-of-month or ordinal weekday 163296df7d3eSAtari911 let monthDay = ''; 163396df7d3eSAtari911 let monthlyType = 'dayOfMonth'; 163496df7d3eSAtari911 let ordinalWeek = ''; 163596df7d3eSAtari911 let ordinalDay = ''; 163696df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 163796df7d3eSAtari911 if (monthlyOptions && recurrenceType === 'monthly') { 163896df7d3eSAtari911 const monthlyTypeRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked'); 163996df7d3eSAtari911 monthlyType = monthlyTypeRadio ? monthlyTypeRadio.value : 'dayOfMonth'; 164096df7d3eSAtari911 164196df7d3eSAtari911 if (monthlyType === 'dayOfMonth') { 164296df7d3eSAtari911 const monthDayInput = document.getElementById('event-month-day-' + calId); 164396df7d3eSAtari911 monthDay = monthDayInput ? monthDayInput.value : ''; 164496df7d3eSAtari911 } else { 164596df7d3eSAtari911 const ordinalSelect = document.getElementById('event-ordinal-' + calId); 164696df7d3eSAtari911 const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId); 164796df7d3eSAtari911 ordinalWeek = ordinalSelect ? ordinalSelect.value : '1'; 164896df7d3eSAtari911 ordinalDay = ordinalDaySelect ? ordinalDaySelect.value : '0'; 164996df7d3eSAtari911 } 165096df7d3eSAtari911 } 165196df7d3eSAtari911 16521d05cddcSAtari911 if (!title) { 16531d05cddcSAtari911 alert('Please enter a title'); 16541d05cddcSAtari911 return; 16551d05cddcSAtari911 } 16561d05cddcSAtari911 16571d05cddcSAtari911 if (!date) { 16581d05cddcSAtari911 alert('Please select a date'); 16591d05cddcSAtari911 return; 16601d05cddcSAtari911 } 16611d05cddcSAtari911 16621d05cddcSAtari911 const params = new URLSearchParams({ 16631d05cddcSAtari911 call: 'plugin_calendar', 16641d05cddcSAtari911 action: 'save_event', 16651d05cddcSAtari911 namespace: finalNamespace, 16661d05cddcSAtari911 eventId: eventId, 16671d05cddcSAtari911 date: date, 16681d05cddcSAtari911 oldDate: oldDate, 16691d05cddcSAtari911 endDate: endDate, 16701d05cddcSAtari911 title: title, 16711d05cddcSAtari911 time: time, 16721d05cddcSAtari911 endTime: endTime, 16731d05cddcSAtari911 color: color, 16741d05cddcSAtari911 description: description, 16751d05cddcSAtari911 isTask: isTask ? '1' : '0', 16761d05cddcSAtari911 completed: completed ? '1' : '0', 16771d05cddcSAtari911 isRecurring: isRecurring ? '1' : '0', 16781d05cddcSAtari911 recurrenceType: recurrenceType, 167996df7d3eSAtari911 recurrenceInterval: recurrenceInterval, 16807e8ea635SAtari911 recurrenceEnd: recurrenceEnd, 168196df7d3eSAtari911 weekDays: weekDays.join(','), 168296df7d3eSAtari911 monthlyType: monthlyType, 168396df7d3eSAtari911 monthDay: monthDay, 168496df7d3eSAtari911 ordinalWeek: ordinalWeek, 168596df7d3eSAtari911 ordinalDay: ordinalDay, 1686b498f308SAtari911 sectok: getSecurityToken() 16871d05cddcSAtari911 }); 16881d05cddcSAtari911 16891d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 16901d05cddcSAtari911 method: 'POST', 16911d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 16921d05cddcSAtari911 body: params.toString() 16931d05cddcSAtari911 }) 16941d05cddcSAtari911 .then(r => r.json()) 16951d05cddcSAtari911 .then(data => { 16961d05cddcSAtari911 if (data.success) { 1697*815440faSAtari911 // Announce to screen readers 1698*815440faSAtari911 announceToScreenReader(eventId ? 'Event updated' : 'Event created'); 1699*815440faSAtari911 17001d05cddcSAtari911 closeEventDialog(calId); 17011d05cddcSAtari911 17021d05cddcSAtari911 // For recurring events, do a full page reload to show all occurrences 17031d05cddcSAtari911 if (isRecurring) { 17041d05cddcSAtari911 location.reload(); 17051d05cddcSAtari911 return; 17061d05cddcSAtari911 } 17071d05cddcSAtari911 17081d05cddcSAtari911 // Extract year and month from the NEW date (in case date was changed) 17091d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 17101d05cddcSAtari911 171196df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting from the container 171296df7d3eSAtari911 // This preserves wildcard/multi-namespace views after editing 171396df7d3eSAtari911 const container = document.getElementById(calId); 171496df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 171596df7d3eSAtari911 17161d05cddcSAtari911 // Reload calendar data via AJAX to the month of the event 171796df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 17181d05cddcSAtari911 } else { 17191d05cddcSAtari911 alert('Error: ' + (data.error || 'Unknown error')); 17201d05cddcSAtari911 } 17211d05cddcSAtari911 }) 17221d05cddcSAtari911 .catch(err => { 17231d05cddcSAtari911 console.error('Error:', err); 17241d05cddcSAtari911 alert('Error saving event'); 17251d05cddcSAtari911 }); 17261d05cddcSAtari911}; 17271d05cddcSAtari911 17281d05cddcSAtari911// Reload calendar data without page refresh 17291d05cddcSAtari911window.reloadCalendarData = function(calId, year, month, namespace) { 17301d05cddcSAtari911 const params = new URLSearchParams({ 17311d05cddcSAtari911 call: 'plugin_calendar', 17321d05cddcSAtari911 action: 'load_month', 17331d05cddcSAtari911 year: year, 17341d05cddcSAtari911 month: month, 17351d05cddcSAtari911 namespace: namespace, 17361d05cddcSAtari911 _: new Date().getTime() // Cache buster 17371d05cddcSAtari911 }); 17381d05cddcSAtari911 17391d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 17401d05cddcSAtari911 method: 'POST', 17411d05cddcSAtari911 headers: { 17421d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 17431d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 17441d05cddcSAtari911 'Pragma': 'no-cache' 17451d05cddcSAtari911 }, 17461d05cddcSAtari911 body: params.toString() 17471d05cddcSAtari911 }) 17481d05cddcSAtari911 .then(r => r.json()) 17491d05cddcSAtari911 .then(data => { 17501d05cddcSAtari911 if (data.success) { 17511d05cddcSAtari911 const container = document.getElementById(calId); 17521d05cddcSAtari911 17531d05cddcSAtari911 // Check if this is a full calendar or just event panel 17541d05cddcSAtari911 if (container.classList.contains('calendar-compact-container')) { 17551d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 17561d05cddcSAtari911 } else if (container.classList.contains('event-panel-standalone')) { 17571d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 17581d05cddcSAtari911 } 17591d05cddcSAtari911 } 17601d05cddcSAtari911 }) 17611d05cddcSAtari911 .catch(err => console.error('Error:', err)); 17621d05cddcSAtari911}; 17631d05cddcSAtari911 17641d05cddcSAtari911// Close event dialog 17651d05cddcSAtari911window.closeEventDialog = function(calId) { 17661d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 17671d05cddcSAtari911 dialog.style.display = 'none'; 17681d05cddcSAtari911}; 17691d05cddcSAtari911 17701d05cddcSAtari911// Escape HTML 17711d05cddcSAtari911window.escapeHtml = function(text) { 17721d05cddcSAtari911 const div = document.createElement('div'); 17731d05cddcSAtari911 div.textContent = text; 17741d05cddcSAtari911 return div.innerHTML; 17751d05cddcSAtari911}; 17761d05cddcSAtari911 17771d05cddcSAtari911// Highlight event when clicking on bar in calendar 17781d05cddcSAtari911window.highlightEvent = function(calId, eventId, date) { 17799ccd446eSAtari911 17801d05cddcSAtari911 // Find the event item in the event list 17811d05cddcSAtari911 const eventList = document.querySelector('#' + calId + ' .event-list-compact'); 17829ccd446eSAtari911 if (!eventList) { 17839ccd446eSAtari911 return; 17849ccd446eSAtari911 } 17851d05cddcSAtari911 17861d05cddcSAtari911 const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]'); 17879ccd446eSAtari911 if (!eventItem) { 17889ccd446eSAtari911 return; 17899ccd446eSAtari911 } 17901d05cddcSAtari911 17919ccd446eSAtari911 17929ccd446eSAtari911 // Get theme 17939ccd446eSAtari911 const container = document.getElementById(calId); 17949ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 17959ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 17969ccd446eSAtari911 17979ccd446eSAtari911 17989ccd446eSAtari911 // Theme-specific highlight colors 17999ccd446eSAtari911 let highlightBg, highlightShadow; 18009ccd446eSAtari911 if (theme === 'matrix') { 18019ccd446eSAtari911 highlightBg = '#1a3d1a'; // Darker green 18029ccd446eSAtari911 highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)'; 18039ccd446eSAtari911 } else if (theme === 'purple') { 18049ccd446eSAtari911 highlightBg = '#3d2b4d'; // Darker purple 18059ccd446eSAtari911 highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)'; 18069ccd446eSAtari911 } else if (theme === 'professional') { 18079ccd446eSAtari911 highlightBg = '#e3f2fd'; // Light blue 18089ccd446eSAtari911 highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)'; 18099ccd446eSAtari911 } else if (theme === 'pink') { 18109ccd446eSAtari911 highlightBg = '#3d2030'; // Darker pink 18119ccd446eSAtari911 highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)'; 18129ccd446eSAtari911 } else if (theme === 'wiki') { 18137e8ea635SAtari911 highlightBg = themeStyles.header_bg || '#e8e8e8'; // __background_alt__ 18147e8ea635SAtari911 highlightShadow = '0 0 10px rgba(0, 0, 0, 0.15)'; 18159ccd446eSAtari911 } 18169ccd446eSAtari911 18179ccd446eSAtari911 18189ccd446eSAtari911 // Store original styles 18199ccd446eSAtari911 const originalBg = eventItem.style.background; 18209ccd446eSAtari911 const originalShadow = eventItem.style.boxShadow; 18219ccd446eSAtari911 18229ccd446eSAtari911 // Remove previous highlights (restore their original styles) 18231d05cddcSAtari911 const previousHighlights = eventList.querySelectorAll('.event-highlighted'); 18249ccd446eSAtari911 previousHighlights.forEach(el => { 18259ccd446eSAtari911 el.classList.remove('event-highlighted'); 18269ccd446eSAtari911 }); 18271d05cddcSAtari911 18289ccd446eSAtari911 // Add highlight class and apply theme-aware glow 18291d05cddcSAtari911 eventItem.classList.add('event-highlighted'); 18301d05cddcSAtari911 18319ccd446eSAtari911 // Set CSS properties directly 18329ccd446eSAtari911 eventItem.style.setProperty('background', highlightBg, 'important'); 18339ccd446eSAtari911 eventItem.style.setProperty('box-shadow', highlightShadow, 'important'); 18349ccd446eSAtari911 eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important'); 18359ccd446eSAtari911 18369ccd446eSAtari911 18371d05cddcSAtari911 // Scroll to event 18381d05cddcSAtari911 eventItem.scrollIntoView({ 18391d05cddcSAtari911 behavior: 'smooth', 18401d05cddcSAtari911 block: 'nearest', 18411d05cddcSAtari911 inline: 'nearest' 18421d05cddcSAtari911 }); 18431d05cddcSAtari911 18449ccd446eSAtari911 // Remove highlight after 3 seconds and restore original styles 18451d05cddcSAtari911 setTimeout(() => { 18461d05cddcSAtari911 eventItem.classList.remove('event-highlighted'); 18479ccd446eSAtari911 eventItem.style.setProperty('background', originalBg); 18489ccd446eSAtari911 eventItem.style.setProperty('box-shadow', originalShadow); 18499ccd446eSAtari911 eventItem.style.setProperty('transition', ''); 18501d05cddcSAtari911 }, 3000); 18511d05cddcSAtari911}; 18521d05cddcSAtari911 18531d05cddcSAtari911// Toggle recurring event options 18541d05cddcSAtari911window.toggleRecurringOptions = function(calId) { 18551d05cddcSAtari911 const checkbox = document.getElementById('event-recurring-' + calId); 18561d05cddcSAtari911 const options = document.getElementById('recurring-options-' + calId); 18571d05cddcSAtari911 18581d05cddcSAtari911 if (checkbox && options) { 18591d05cddcSAtari911 options.style.display = checkbox.checked ? 'block' : 'none'; 186096df7d3eSAtari911 if (checkbox.checked) { 186196df7d3eSAtari911 // Initialize the sub-options based on current selection 186296df7d3eSAtari911 updateRecurrenceOptions(calId); 186396df7d3eSAtari911 } 186496df7d3eSAtari911 } 186596df7d3eSAtari911}; 186696df7d3eSAtari911 186796df7d3eSAtari911// Update visible recurrence options based on type (daily/weekly/monthly/yearly) 186896df7d3eSAtari911window.updateRecurrenceOptions = function(calId) { 186996df7d3eSAtari911 const typeSelect = document.getElementById('event-recurrence-type-' + calId); 187096df7d3eSAtari911 const weeklyOptions = document.getElementById('weekly-options-' + calId); 187196df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 187296df7d3eSAtari911 187396df7d3eSAtari911 if (!typeSelect) return; 187496df7d3eSAtari911 187596df7d3eSAtari911 const recurrenceType = typeSelect.value; 187696df7d3eSAtari911 187796df7d3eSAtari911 // Hide all conditional options first 187896df7d3eSAtari911 if (weeklyOptions) weeklyOptions.style.display = 'none'; 187996df7d3eSAtari911 if (monthlyOptions) monthlyOptions.style.display = 'none'; 188096df7d3eSAtari911 188196df7d3eSAtari911 // Show relevant options 188296df7d3eSAtari911 if (recurrenceType === 'weekly' && weeklyOptions) { 188396df7d3eSAtari911 weeklyOptions.style.display = 'block'; 188496df7d3eSAtari911 // Auto-select today's day of week if nothing selected 188596df7d3eSAtari911 const checkboxes = weeklyOptions.querySelectorAll('input[type="checkbox"]'); 188696df7d3eSAtari911 const anyChecked = Array.from(checkboxes).some(cb => cb.checked); 188796df7d3eSAtari911 if (!anyChecked) { 188896df7d3eSAtari911 const today = new Date().getDay(); 188996df7d3eSAtari911 const todayCheckbox = weeklyOptions.querySelector('input[value="' + today + '"]'); 189096df7d3eSAtari911 if (todayCheckbox) todayCheckbox.checked = true; 189196df7d3eSAtari911 } 189296df7d3eSAtari911 } else if (recurrenceType === 'monthly' && monthlyOptions) { 189396df7d3eSAtari911 monthlyOptions.style.display = 'block'; 189496df7d3eSAtari911 // Set default day to current day of month 189596df7d3eSAtari911 const monthDayInput = document.getElementById('event-month-day-' + calId); 189696df7d3eSAtari911 if (monthDayInput && !monthDayInput.dataset.userSet) { 189796df7d3eSAtari911 monthDayInput.value = new Date().getDate(); 189896df7d3eSAtari911 } 189996df7d3eSAtari911 } 190096df7d3eSAtari911}; 190196df7d3eSAtari911 190296df7d3eSAtari911// Toggle between day-of-month and ordinal weekday for monthly recurrence 190396df7d3eSAtari911window.updateMonthlyType = function(calId) { 190496df7d3eSAtari911 const dayOfMonthDiv = document.getElementById('monthly-day-' + calId); 190596df7d3eSAtari911 const ordinalDiv = document.getElementById('monthly-ordinal-' + calId); 190696df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 190796df7d3eSAtari911 190896df7d3eSAtari911 if (!monthlyOptions) return; 190996df7d3eSAtari911 191096df7d3eSAtari911 const selectedRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked'); 191196df7d3eSAtari911 if (!selectedRadio) return; 191296df7d3eSAtari911 191396df7d3eSAtari911 if (selectedRadio.value === 'dayOfMonth') { 191496df7d3eSAtari911 if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'flex'; 191596df7d3eSAtari911 if (ordinalDiv) ordinalDiv.style.display = 'none'; 191696df7d3eSAtari911 } else { 191796df7d3eSAtari911 if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'none'; 191896df7d3eSAtari911 if (ordinalDiv) ordinalDiv.style.display = 'block'; 191996df7d3eSAtari911 192096df7d3eSAtari911 // Set defaults based on current date 192196df7d3eSAtari911 const now = new Date(); 192296df7d3eSAtari911 const dayOfWeek = now.getDay(); 192396df7d3eSAtari911 const weekOfMonth = Math.ceil(now.getDate() / 7); 192496df7d3eSAtari911 192596df7d3eSAtari911 const ordinalSelect = document.getElementById('event-ordinal-' + calId); 192696df7d3eSAtari911 const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId); 192796df7d3eSAtari911 192896df7d3eSAtari911 if (ordinalSelect && !ordinalSelect.dataset.userSet) { 192996df7d3eSAtari911 ordinalSelect.value = weekOfMonth; 193096df7d3eSAtari911 } 193196df7d3eSAtari911 if (ordinalDaySelect && !ordinalDaySelect.dataset.userSet) { 193296df7d3eSAtari911 ordinalDaySelect.value = dayOfWeek; 193396df7d3eSAtari911 } 19341d05cddcSAtari911 } 19351d05cddcSAtari911}; 19361d05cddcSAtari911 19379ccd446eSAtari911// ============================================================ 19389ccd446eSAtari911// Document-level event delegation (guarded - only attach once) 19399ccd446eSAtari911// These use event delegation so they work for AJAX-rebuilt content. 19409ccd446eSAtari911// ============================================================ 19419ccd446eSAtari911if (!window._calendarDelegationInit) { 19429ccd446eSAtari911 window._calendarDelegationInit = true; 19439ccd446eSAtari911 1944*815440faSAtari911 // Keyboard navigation for accessibility 19451d05cddcSAtari911 document.addEventListener('keydown', function(e) { 1946*815440faSAtari911 // ESC closes dialogs, popups, tooltips, dropdowns 19471d05cddcSAtari911 if (e.key === 'Escape') { 1948*815440faSAtari911 // Close dialogs 19499ccd446eSAtari911 document.querySelectorAll('.event-dialog-compact').forEach(function(d) { 19509ccd446eSAtari911 if (d.style.display === 'flex') d.style.display = 'none'; 19519ccd446eSAtari911 }); 1952*815440faSAtari911 // Close day popups 19539ccd446eSAtari911 document.querySelectorAll('.day-popup').forEach(function(p) { 19549ccd446eSAtari911 p.style.display = 'none'; 19559ccd446eSAtari911 }); 1956*815440faSAtari911 // Close custom pickers 1957*815440faSAtari911 document.querySelectorAll('.time-dropdown.open, .date-dropdown.open').forEach(function(d) { 1958*815440faSAtari911 d.classList.remove('open'); 1959*815440faSAtari911 d.innerHTML = ''; 1960*815440faSAtari911 }); 1961*815440faSAtari911 document.querySelectorAll('.custom-time-picker.open, .custom-date-picker.open').forEach(function(b) { 1962*815440faSAtari911 b.classList.remove('open'); 1963*815440faSAtari911 }); 19649ccd446eSAtari911 hideConflictTooltip(); 1965*815440faSAtari911 return; 1966*815440faSAtari911 } 1967*815440faSAtari911 1968*815440faSAtari911 // Calendar grid navigation with arrow keys 1969*815440faSAtari911 var focusedDay = document.activeElement; 1970*815440faSAtari911 if (focusedDay && focusedDay.classList.contains('calendar-day')) { 1971*815440faSAtari911 var calGrid = focusedDay.closest('.calendar-grid'); 1972*815440faSAtari911 if (!calGrid) return; 1973*815440faSAtari911 1974*815440faSAtari911 var days = Array.from(calGrid.querySelectorAll('.calendar-day:not(.empty)')); 1975*815440faSAtari911 var currentIndex = days.indexOf(focusedDay); 1976*815440faSAtari911 if (currentIndex === -1) return; 1977*815440faSAtari911 1978*815440faSAtari911 var newIndex = currentIndex; 1979*815440faSAtari911 1980*815440faSAtari911 if (e.key === 'ArrowRight') { 1981*815440faSAtari911 newIndex = Math.min(currentIndex + 1, days.length - 1); 1982*815440faSAtari911 e.preventDefault(); 1983*815440faSAtari911 } else if (e.key === 'ArrowLeft') { 1984*815440faSAtari911 newIndex = Math.max(currentIndex - 1, 0); 1985*815440faSAtari911 e.preventDefault(); 1986*815440faSAtari911 } else if (e.key === 'ArrowDown') { 1987*815440faSAtari911 newIndex = Math.min(currentIndex + 7, days.length - 1); 1988*815440faSAtari911 e.preventDefault(); 1989*815440faSAtari911 } else if (e.key === 'ArrowUp') { 1990*815440faSAtari911 newIndex = Math.max(currentIndex - 7, 0); 1991*815440faSAtari911 e.preventDefault(); 1992*815440faSAtari911 } else if (e.key === 'Enter' || e.key === ' ') { 1993*815440faSAtari911 // Activate the day (click it) 1994*815440faSAtari911 focusedDay.click(); 1995*815440faSAtari911 e.preventDefault(); 1996*815440faSAtari911 return; 1997*815440faSAtari911 } 1998*815440faSAtari911 1999*815440faSAtari911 if (newIndex !== currentIndex && days[newIndex]) { 2000*815440faSAtari911 days[newIndex].focus(); 2001*815440faSAtari911 } 2002*815440faSAtari911 } 2003*815440faSAtari911 2004*815440faSAtari911 // Event item navigation with arrow keys 2005*815440faSAtari911 var focusedEvent = document.activeElement; 2006*815440faSAtari911 if (focusedEvent && focusedEvent.classList.contains('event-item')) { 2007*815440faSAtari911 var eventList = focusedEvent.closest('.event-list-items, .day-popup-events'); 2008*815440faSAtari911 if (!eventList) return; 2009*815440faSAtari911 2010*815440faSAtari911 var events = Array.from(eventList.querySelectorAll('.event-item')); 2011*815440faSAtari911 var currentIdx = events.indexOf(focusedEvent); 2012*815440faSAtari911 if (currentIdx === -1) return; 2013*815440faSAtari911 2014*815440faSAtari911 if (e.key === 'ArrowDown') { 2015*815440faSAtari911 var nextIdx = Math.min(currentIdx + 1, events.length - 1); 2016*815440faSAtari911 events[nextIdx].focus(); 2017*815440faSAtari911 e.preventDefault(); 2018*815440faSAtari911 } else if (e.key === 'ArrowUp') { 2019*815440faSAtari911 var prevIdx = Math.max(currentIdx - 1, 0); 2020*815440faSAtari911 events[prevIdx].focus(); 2021*815440faSAtari911 e.preventDefault(); 2022*815440faSAtari911 } else if (e.key === 'Enter') { 2023*815440faSAtari911 // Find and click the edit button 2024*815440faSAtari911 var editBtn = focusedEvent.querySelector('.event-action-edit'); 2025*815440faSAtari911 if (editBtn) editBtn.click(); 2026*815440faSAtari911 e.preventDefault(); 2027*815440faSAtari911 } else if (e.key === 'Delete' || e.key === 'Backspace') { 2028*815440faSAtari911 // Find and click the delete button 2029*815440faSAtari911 var deleteBtn = focusedEvent.querySelector('.event-action-delete'); 2030*815440faSAtari911 if (deleteBtn) deleteBtn.click(); 2031*815440faSAtari911 e.preventDefault(); 2032*815440faSAtari911 } 20331d05cddcSAtari911 } 20341d05cddcSAtari911 }); 20359ccd446eSAtari911 20369ccd446eSAtari911 // Conflict tooltip delegation (capture phase for mouseenter/leave) 20379ccd446eSAtari911 document.addEventListener('mouseenter', function(e) { 20389ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 20399ccd446eSAtari911 showConflictTooltip(e.target); 20401d05cddcSAtari911 } 20419ccd446eSAtari911 }, true); 20429ccd446eSAtari911 20439ccd446eSAtari911 document.addEventListener('mouseleave', function(e) { 20449ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 20459ccd446eSAtari911 hideConflictTooltip(); 20469ccd446eSAtari911 } 20479ccd446eSAtari911 }, true); 20489ccd446eSAtari911} // end delegation guard 20491d05cddcSAtari911 20501d05cddcSAtari911// Event panel navigation 20511d05cddcSAtari911window.navEventPanel = function(calId, year, month, namespace) { 20521d05cddcSAtari911 const params = new URLSearchParams({ 20531d05cddcSAtari911 call: 'plugin_calendar', 20541d05cddcSAtari911 action: 'load_month', 20551d05cddcSAtari911 year: year, 20561d05cddcSAtari911 month: month, 20571d05cddcSAtari911 namespace: namespace, 20581d05cddcSAtari911 _: new Date().getTime() // Cache buster 20591d05cddcSAtari911 }); 20601d05cddcSAtari911 20611d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 20621d05cddcSAtari911 method: 'POST', 20631d05cddcSAtari911 headers: { 20641d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 20651d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 20661d05cddcSAtari911 'Pragma': 'no-cache' 20671d05cddcSAtari911 }, 20681d05cddcSAtari911 body: params.toString() 20691d05cddcSAtari911 }) 20701d05cddcSAtari911 .then(r => r.json()) 20711d05cddcSAtari911 .then(data => { 20721d05cddcSAtari911 if (data.success) { 20731d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 20741d05cddcSAtari911 } 20751d05cddcSAtari911 }) 20761d05cddcSAtari911 .catch(err => console.error('Error:', err)); 20771d05cddcSAtari911}; 20781d05cddcSAtari911 20791d05cddcSAtari911// Rebuild event panel only 20801d05cddcSAtari911window.rebuildEventPanel = function(calId, year, month, events, namespace) { 20811d05cddcSAtari911 const container = document.getElementById(calId); 2082da206178SAtari911 const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 2083da206178SAtari911 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 20841d05cddcSAtari911 20851d05cddcSAtari911 // Update month title in new compact header 20861d05cddcSAtari911 const monthTitle = container.querySelector('.panel-month-title'); 20871d05cddcSAtari911 if (monthTitle) { 20881d05cddcSAtari911 monthTitle.textContent = monthNames[month - 1] + ' ' + year; 20891d05cddcSAtari911 monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 2090da206178SAtari911 monthTitle.setAttribute('title', 'Click to jump to month'); 20911d05cddcSAtari911 } 20921d05cddcSAtari911 20931d05cddcSAtari911 // Fallback: Update old header format if exists 20941d05cddcSAtari911 const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker'); 20951d05cddcSAtari911 if (oldHeader && !monthTitle) { 2096da206178SAtari911 oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events'; 20971d05cddcSAtari911 oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 20981d05cddcSAtari911 } 20991d05cddcSAtari911 21001d05cddcSAtari911 // Update nav buttons 21011d05cddcSAtari911 let prevMonth = month - 1; 21021d05cddcSAtari911 let prevYear = year; 21031d05cddcSAtari911 if (prevMonth < 1) { 21041d05cddcSAtari911 prevMonth = 12; 21051d05cddcSAtari911 prevYear--; 21061d05cddcSAtari911 } 21071d05cddcSAtari911 21081d05cddcSAtari911 let nextMonth = month + 1; 21091d05cddcSAtari911 let nextYear = year; 21101d05cddcSAtari911 if (nextMonth > 12) { 21111d05cddcSAtari911 nextMonth = 1; 21121d05cddcSAtari911 nextYear++; 21131d05cddcSAtari911 } 21141d05cddcSAtari911 21151d05cddcSAtari911 // Update new compact nav buttons 21161d05cddcSAtari911 const navBtns = container.querySelectorAll('.panel-nav-btn'); 21171d05cddcSAtari911 if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 21181d05cddcSAtari911 if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 21191d05cddcSAtari911 21201d05cddcSAtari911 // Fallback for old nav buttons 21211d05cddcSAtari911 const oldNavBtns = container.querySelectorAll('.cal-nav-btn'); 21221d05cddcSAtari911 if (oldNavBtns.length > 0 && navBtns.length === 0) { 21231d05cddcSAtari911 if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 21241d05cddcSAtari911 if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 21251d05cddcSAtari911 } 21261d05cddcSAtari911 21271d05cddcSAtari911 // Update Today button (works for both old and new) 21281d05cddcSAtari911 const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact'); 21291d05cddcSAtari911 if (todayBtn) { 21301d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`); 21311d05cddcSAtari911 } 21321d05cddcSAtari911 21331d05cddcSAtari911 // Rebuild event list 21341d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 21351d05cddcSAtari911 if (eventList) { 21361d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 21371d05cddcSAtari911 } 21381d05cddcSAtari911}; 21391d05cddcSAtari911 21401d05cddcSAtari911// Open add event for panel 21411d05cddcSAtari911window.openAddEventPanel = function(calId, namespace) { 21421d05cddcSAtari911 const today = new Date(); 21431d05cddcSAtari911 const year = today.getFullYear(); 21441d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 21451d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 21461d05cddcSAtari911 const localDate = `${year}-${month}-${day}`; 21471d05cddcSAtari911 openAddEvent(calId, namespace, localDate); 21481d05cddcSAtari911}; 21491d05cddcSAtari911 21501d05cddcSAtari911// Toggle task completion 21511d05cddcSAtari911window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) { 21521d05cddcSAtari911 const params = new URLSearchParams({ 21531d05cddcSAtari911 call: 'plugin_calendar', 21541d05cddcSAtari911 action: 'toggle_task', 21551d05cddcSAtari911 namespace: namespace, 21561d05cddcSAtari911 date: date, 21571d05cddcSAtari911 eventId: eventId, 21587e8ea635SAtari911 completed: completed ? '1' : '0', 2159b498f308SAtari911 sectok: getSecurityToken() 21601d05cddcSAtari911 }); 21611d05cddcSAtari911 21621d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 21631d05cddcSAtari911 method: 'POST', 21641d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 21651d05cddcSAtari911 body: params.toString() 21661d05cddcSAtari911 }) 21671d05cddcSAtari911 .then(r => r.json()) 21681d05cddcSAtari911 .then(data => { 21691d05cddcSAtari911 if (data.success) { 2170*815440faSAtari911 // Announce to screen readers 2171*815440faSAtari911 announceToScreenReader(completed ? 'Task marked complete' : 'Task marked incomplete'); 2172*815440faSAtari911 21731d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 217496df7d3eSAtari911 217596df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting from the container 217696df7d3eSAtari911 const container = document.getElementById(calId); 217796df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 217896df7d3eSAtari911 217996df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 21801d05cddcSAtari911 } 21811d05cddcSAtari911 }) 21821d05cddcSAtari911 .catch(err => console.error('Error toggling task:', err)); 21831d05cddcSAtari911}; 21841d05cddcSAtari911 21851d05cddcSAtari911// Make dialog draggable 21861d05cddcSAtari911window.makeDialogDraggable = function(calId) { 21871d05cddcSAtari911 const dialog = document.getElementById('dialog-content-' + calId); 21881d05cddcSAtari911 const handle = document.getElementById('drag-handle-' + calId); 21891d05cddcSAtari911 21901d05cddcSAtari911 if (!dialog || !handle) return; 21911d05cddcSAtari911 2192da206178SAtari911 // Remove any existing drag setup to prevent duplicate listeners 2193da206178SAtari911 if (handle._dragCleanup) { 2194da206178SAtari911 handle._dragCleanup(); 2195da206178SAtari911 } 2196da206178SAtari911 2197da206178SAtari911 // Reset position when dialog opens 2198da206178SAtari911 dialog.style.transform = ''; 2199da206178SAtari911 22001d05cddcSAtari911 let isDragging = false; 2201da206178SAtari911 let currentX = 0; 2202da206178SAtari911 let currentY = 0; 22031d05cddcSAtari911 let initialX; 22041d05cddcSAtari911 let initialY; 22051d05cddcSAtari911 let xOffset = 0; 22061d05cddcSAtari911 let yOffset = 0; 22071d05cddcSAtari911 22081d05cddcSAtari911 function dragStart(e) { 2209da206178SAtari911 // Only start drag if clicking on the handle itself, not buttons inside it 2210da206178SAtari911 if (e.target.tagName === 'BUTTON') return; 2211da206178SAtari911 22121d05cddcSAtari911 initialX = e.clientX - xOffset; 22131d05cddcSAtari911 initialY = e.clientY - yOffset; 22141d05cddcSAtari911 isDragging = true; 2215da206178SAtari911 handle.style.cursor = 'grabbing'; 22161d05cddcSAtari911 } 22171d05cddcSAtari911 22181d05cddcSAtari911 function drag(e) { 22191d05cddcSAtari911 if (isDragging) { 22201d05cddcSAtari911 e.preventDefault(); 22211d05cddcSAtari911 currentX = e.clientX - initialX; 22221d05cddcSAtari911 currentY = e.clientY - initialY; 22231d05cddcSAtari911 xOffset = currentX; 22241d05cddcSAtari911 yOffset = currentY; 2225da206178SAtari911 dialog.style.transform = `translate(${currentX}px, ${currentY}px)`; 22261d05cddcSAtari911 } 22271d05cddcSAtari911 } 22281d05cddcSAtari911 22291d05cddcSAtari911 function dragEnd(e) { 2230da206178SAtari911 if (isDragging) { 22311d05cddcSAtari911 initialX = currentX; 22321d05cddcSAtari911 initialY = currentY; 22331d05cddcSAtari911 isDragging = false; 2234da206178SAtari911 handle.style.cursor = 'move'; 2235da206178SAtari911 } 22361d05cddcSAtari911 } 22371d05cddcSAtari911 2238da206178SAtari911 // Add listeners 2239da206178SAtari911 handle.addEventListener('mousedown', dragStart); 2240da206178SAtari911 document.addEventListener('mousemove', drag); 2241da206178SAtari911 document.addEventListener('mouseup', dragEnd); 22421d05cddcSAtari911 2243da206178SAtari911 // Store cleanup function to remove listeners later 2244da206178SAtari911 handle._dragCleanup = function() { 2245da206178SAtari911 handle.removeEventListener('mousedown', dragStart); 2246da206178SAtari911 document.removeEventListener('mousemove', drag); 2247da206178SAtari911 document.removeEventListener('mouseup', dragEnd); 22481d05cddcSAtari911 }; 22491d05cddcSAtari911}; 22501d05cddcSAtari911 22511d05cddcSAtari911// Toggle expand/collapse for past events 22521d05cddcSAtari911window.togglePastEventExpand = function(element) { 22531d05cddcSAtari911 // Stop propagation to prevent any parent click handlers 22541d05cddcSAtari911 event.stopPropagation(); 22551d05cddcSAtari911 22561d05cddcSAtari911 const meta = element.querySelector(".event-meta-compact"); 22571d05cddcSAtari911 const desc = element.querySelector(".event-desc-compact"); 22581d05cddcSAtari911 22591d05cddcSAtari911 // Toggle visibility 22601d05cddcSAtari911 if (meta.style.display === "none") { 22611d05cddcSAtari911 // Expand 22621d05cddcSAtari911 meta.style.display = "block"; 22631d05cddcSAtari911 if (desc) desc.style.display = "block"; 22641d05cddcSAtari911 element.classList.add("event-past-expanded"); 22651d05cddcSAtari911 } else { 22661d05cddcSAtari911 // Collapse 22671d05cddcSAtari911 meta.style.display = "none"; 22681d05cddcSAtari911 if (desc) desc.style.display = "none"; 22691d05cddcSAtari911 element.classList.remove("event-past-expanded"); 22701d05cddcSAtari911 } 22711d05cddcSAtari911}; 22721d05cddcSAtari911 22739ccd446eSAtari911// Filter calendar by namespace when clicking namespace badge (guarded) 22749ccd446eSAtari911if (!window._calendarClickDelegationInit) { 22759ccd446eSAtari911 window._calendarClickDelegationInit = true; 22761d05cddcSAtari911 document.addEventListener('click', function(e) { 22771d05cddcSAtari911 if (e.target.classList.contains('event-namespace-badge')) { 22781d05cddcSAtari911 const namespace = e.target.textContent; 22791d05cddcSAtari911 const calendar = e.target.closest('.calendar-compact-container'); 22801d05cddcSAtari911 22817e8ea635SAtari911 if (!calendar) return; 22821d05cddcSAtari911 22831d05cddcSAtari911 const calId = calendar.id; 22841d05cddcSAtari911 22857e8ea635SAtari911 // Use AJAX reload to filter both calendar grid and event list 22867e8ea635SAtari911 filterCalendarByNamespace(calId, namespace); 22871d05cddcSAtari911 } 22881d05cddcSAtari911 }); 22899ccd446eSAtari911} // end click delegation guard 22901d05cddcSAtari911 22911d05cddcSAtari911// Update the displayed filtered namespace in event list header 22927e8ea635SAtari911// Legacy badge removed - namespace filtering still works but badge no longer shown 22931d05cddcSAtari911window.updateFilteredNamespaceDisplay = function(calId, namespace) { 22941d05cddcSAtari911 const calendar = document.getElementById(calId); 22951d05cddcSAtari911 if (!calendar) return; 22961d05cddcSAtari911 22971d05cddcSAtari911 const headerContent = calendar.querySelector('.event-list-header-content'); 22981d05cddcSAtari911 if (!headerContent) return; 22991d05cddcSAtari911 23007e8ea635SAtari911 // Remove any existing filter badge (cleanup) 23011d05cddcSAtari911 let filterBadge = headerContent.querySelector('.namespace-filter-badge'); 23021d05cddcSAtari911 if (filterBadge) { 23031d05cddcSAtari911 filterBadge.remove(); 23041d05cddcSAtari911 } 23051d05cddcSAtari911}; 23061d05cddcSAtari911 23071d05cddcSAtari911// Clear namespace filter 23081d05cddcSAtari911window.clearNamespaceFilter = function(calId) { 23091d05cddcSAtari911 23101d05cddcSAtari911 const container = document.getElementById(calId); 23111d05cddcSAtari911 if (!container) { 23121d05cddcSAtari911 console.error('Calendar container not found:', calId); 23131d05cddcSAtari911 return; 23141d05cddcSAtari911 } 23151d05cddcSAtari911 23169ccd446eSAtari911 // Immediately hide/remove the filter badge 23179ccd446eSAtari911 const filterBadge = container.querySelector('.calendar-namespace-filter'); 23189ccd446eSAtari911 if (filterBadge) { 23199ccd446eSAtari911 filterBadge.style.display = 'none'; 23209ccd446eSAtari911 filterBadge.remove(); 23219ccd446eSAtari911 } 23229ccd446eSAtari911 23231d05cddcSAtari911 // Get current year and month 23241d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 23251d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 23261d05cddcSAtari911 23271d05cddcSAtari911 // Get original namespace (what the calendar was initialized with) 23281d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 23291d05cddcSAtari911 23309ccd446eSAtari911 // Also check for sidebar widget 23319ccd446eSAtari911 const sidebarContainer = document.getElementById('sidebar-widget-' + calId); 23329ccd446eSAtari911 if (sidebarContainer) { 23339ccd446eSAtari911 // For sidebar widget, just reload the page without namespace filter 23349ccd446eSAtari911 // Remove the namespace from the URL and reload 23359ccd446eSAtari911 const url = new URL(window.location.href); 23369ccd446eSAtari911 url.searchParams.delete('namespace'); 23379ccd446eSAtari911 window.location.href = url.toString(); 23389ccd446eSAtari911 return; 23399ccd446eSAtari911 } 23401d05cddcSAtari911 23419ccd446eSAtari911 // For regular calendar, reload calendar with original namespace 23421d05cddcSAtari911 navCalendar(calId, year, month, originalNamespace); 23431d05cddcSAtari911}; 23441d05cddcSAtari911 23451d05cddcSAtari911window.clearNamespaceFilterPanel = function(calId) { 23461d05cddcSAtari911 23471d05cddcSAtari911 const container = document.getElementById(calId); 23481d05cddcSAtari911 if (!container) { 23491d05cddcSAtari911 console.error('Event panel container not found:', calId); 23501d05cddcSAtari911 return; 23511d05cddcSAtari911 } 23521d05cddcSAtari911 23531d05cddcSAtari911 // Get current year and month from URL params or container 23541d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 23551d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 23561d05cddcSAtari911 23571d05cddcSAtari911 // Get original namespace (what the panel was initialized with) 23581d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 23591d05cddcSAtari911 23601d05cddcSAtari911 23611d05cddcSAtari911 // Reload event panel with original namespace 23621d05cddcSAtari911 navEventPanel(calId, year, month, originalNamespace); 23631d05cddcSAtari911}; 23641d05cddcSAtari911 23651d05cddcSAtari911// Color picker functions 23661d05cddcSAtari911window.updateCustomColorPicker = function(calId) { 23671d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 23681d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 23691d05cddcSAtari911 23701d05cddcSAtari911 if (select.value === 'custom') { 23711d05cddcSAtari911 // Show color picker 23721d05cddcSAtari911 picker.style.display = 'inline-block'; 23731d05cddcSAtari911 picker.click(); // Open color picker 23741d05cddcSAtari911 } else { 23751d05cddcSAtari911 // Hide color picker and sync value 23761d05cddcSAtari911 picker.style.display = 'none'; 23771d05cddcSAtari911 picker.value = select.value; 23781d05cddcSAtari911 } 23791d05cddcSAtari911}; 23801d05cddcSAtari911 23811d05cddcSAtari911function updateColorFromPicker(calId) { 23821d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 23831d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 23841d05cddcSAtari911 23851d05cddcSAtari911 // Set select to custom and update its underlying value 23861d05cddcSAtari911 select.value = 'custom'; 23871d05cddcSAtari911 // Store the actual color value in a data attribute 23881d05cddcSAtari911 select.dataset.customColor = picker.value; 23891d05cddcSAtari911} 23901d05cddcSAtari911 23911d05cddcSAtari911// Toggle past events visibility 23921d05cddcSAtari911window.togglePastEvents = function(calId) { 23931d05cddcSAtari911 const content = document.getElementById('past-events-' + calId); 23941d05cddcSAtari911 const arrow = document.getElementById('past-arrow-' + calId); 23951d05cddcSAtari911 23961d05cddcSAtari911 if (!content || !arrow) { 23971d05cddcSAtari911 console.error('Past events elements not found for:', calId); 23981d05cddcSAtari911 return; 23991d05cddcSAtari911 } 24001d05cddcSAtari911 24011d05cddcSAtari911 // Check computed style instead of inline style 24021d05cddcSAtari911 const isHidden = window.getComputedStyle(content).display === 'none'; 24031d05cddcSAtari911 24041d05cddcSAtari911 if (isHidden) { 24051d05cddcSAtari911 content.style.display = 'block'; 24061d05cddcSAtari911 arrow.textContent = '▼'; 24071d05cddcSAtari911 } else { 24081d05cddcSAtari911 content.style.display = 'none'; 24091d05cddcSAtari911 arrow.textContent = '▶'; 24101d05cddcSAtari911 } 24111d05cddcSAtari911}; 24121d05cddcSAtari911 24131d05cddcSAtari911// Fuzzy match scoring function 24141d05cddcSAtari911window.fuzzyMatch = function(pattern, str) { 24151d05cddcSAtari911 pattern = pattern.toLowerCase(); 24161d05cddcSAtari911 str = str.toLowerCase(); 24171d05cddcSAtari911 24181d05cddcSAtari911 let patternIdx = 0; 24191d05cddcSAtari911 let score = 0; 24201d05cddcSAtari911 let consecutiveMatches = 0; 24211d05cddcSAtari911 24221d05cddcSAtari911 for (let i = 0; i < str.length; i++) { 24231d05cddcSAtari911 if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) { 24241d05cddcSAtari911 score += 1 + consecutiveMatches; 24251d05cddcSAtari911 consecutiveMatches++; 24261d05cddcSAtari911 patternIdx++; 24271d05cddcSAtari911 } else { 24281d05cddcSAtari911 consecutiveMatches = 0; 24291d05cddcSAtari911 } 24301d05cddcSAtari911 } 24311d05cddcSAtari911 24321d05cddcSAtari911 // Return null if not all characters matched 24331d05cddcSAtari911 if (patternIdx !== pattern.length) { 24341d05cddcSAtari911 return null; 24351d05cddcSAtari911 } 24361d05cddcSAtari911 24371d05cddcSAtari911 // Bonus for exact match 24381d05cddcSAtari911 if (str === pattern) { 24391d05cddcSAtari911 score += 100; 24401d05cddcSAtari911 } 24411d05cddcSAtari911 24421d05cddcSAtari911 // Bonus for starts with 24431d05cddcSAtari911 if (str.startsWith(pattern)) { 24441d05cddcSAtari911 score += 50; 24451d05cddcSAtari911 } 24461d05cddcSAtari911 24471d05cddcSAtari911 return score; 24481d05cddcSAtari911}; 24491d05cddcSAtari911 24501d05cddcSAtari911// Initialize namespace search for a calendar 24511d05cddcSAtari911window.initNamespaceSearch = function(calId) { 24521d05cddcSAtari911 const searchInput = document.getElementById('event-namespace-search-' + calId); 24531d05cddcSAtari911 const hiddenInput = document.getElementById('event-namespace-' + calId); 24541d05cddcSAtari911 const dropdown = document.getElementById('event-namespace-dropdown-' + calId); 24551d05cddcSAtari911 const dataElement = document.getElementById('namespaces-data-' + calId); 24561d05cddcSAtari911 24571d05cddcSAtari911 if (!searchInput || !hiddenInput || !dropdown || !dataElement) { 24581d05cddcSAtari911 return; // Elements not found 24591d05cddcSAtari911 } 24601d05cddcSAtari911 2461*815440faSAtari911 // PERFORMANCE FIX: Prevent re-binding event listeners on each dialog open 2462*815440faSAtari911 if (searchInput.dataset.initialized === 'true') { 2463*815440faSAtari911 return; 2464*815440faSAtari911 } 2465*815440faSAtari911 searchInput.dataset.initialized = 'true'; 2466*815440faSAtari911 24671d05cddcSAtari911 let namespaces = []; 24681d05cddcSAtari911 try { 24691d05cddcSAtari911 namespaces = JSON.parse(dataElement.textContent); 24701d05cddcSAtari911 } catch (e) { 24711d05cddcSAtari911 console.error('Failed to parse namespaces data:', e); 24721d05cddcSAtari911 return; 24731d05cddcSAtari911 } 24741d05cddcSAtari911 24751d05cddcSAtari911 let selectedIndex = -1; 24761d05cddcSAtari911 24771d05cddcSAtari911 // Filter and show dropdown 24781d05cddcSAtari911 function filterNamespaces(query) { 24791d05cddcSAtari911 if (!query || query.trim() === '') { 24801d05cddcSAtari911 // Show all namespaces when empty 24811d05cddcSAtari911 hiddenInput.value = ''; 24821d05cddcSAtari911 const results = namespaces.slice(0, 20); // Limit to 20 24831d05cddcSAtari911 showDropdown(results); 24841d05cddcSAtari911 return; 24851d05cddcSAtari911 } 24861d05cddcSAtari911 24871d05cddcSAtari911 // Fuzzy match and score 24881d05cddcSAtari911 const matches = []; 24891d05cddcSAtari911 for (let i = 0; i < namespaces.length; i++) { 24901d05cddcSAtari911 const score = fuzzyMatch(query, namespaces[i]); 24911d05cddcSAtari911 if (score !== null) { 24921d05cddcSAtari911 matches.push({ namespace: namespaces[i], score: score }); 24931d05cddcSAtari911 } 24941d05cddcSAtari911 } 24951d05cddcSAtari911 24961d05cddcSAtari911 // Sort by score (descending) 24971d05cddcSAtari911 matches.sort((a, b) => b.score - a.score); 24981d05cddcSAtari911 24991d05cddcSAtari911 // Take top 20 results 25001d05cddcSAtari911 const results = matches.slice(0, 20).map(m => m.namespace); 25011d05cddcSAtari911 showDropdown(results); 25021d05cddcSAtari911 } 25031d05cddcSAtari911 25041d05cddcSAtari911 function showDropdown(results) { 25051d05cddcSAtari911 dropdown.innerHTML = ''; 25061d05cddcSAtari911 selectedIndex = -1; 25071d05cddcSAtari911 25081d05cddcSAtari911 if (results.length === 0) { 25091d05cddcSAtari911 dropdown.style.display = 'none'; 25101d05cddcSAtari911 return; 25111d05cddcSAtari911 } 25121d05cddcSAtari911 25131d05cddcSAtari911 // Add (default) option 25141d05cddcSAtari911 const defaultOption = document.createElement('div'); 25151d05cddcSAtari911 defaultOption.className = 'namespace-option'; 25161d05cddcSAtari911 defaultOption.textContent = '(default)'; 25171d05cddcSAtari911 defaultOption.dataset.value = ''; 25181d05cddcSAtari911 dropdown.appendChild(defaultOption); 25191d05cddcSAtari911 25201d05cddcSAtari911 results.forEach(ns => { 25211d05cddcSAtari911 const option = document.createElement('div'); 25221d05cddcSAtari911 option.className = 'namespace-option'; 25231d05cddcSAtari911 option.textContent = ns; 25241d05cddcSAtari911 option.dataset.value = ns; 25251d05cddcSAtari911 dropdown.appendChild(option); 25261d05cddcSAtari911 }); 25271d05cddcSAtari911 25281d05cddcSAtari911 dropdown.style.display = 'block'; 25291d05cddcSAtari911 } 25301d05cddcSAtari911 25311d05cddcSAtari911 function hideDropdown() { 25321d05cddcSAtari911 dropdown.style.display = 'none'; 25331d05cddcSAtari911 selectedIndex = -1; 25341d05cddcSAtari911 } 25351d05cddcSAtari911 25361d05cddcSAtari911 function selectOption(namespace) { 25371d05cddcSAtari911 hiddenInput.value = namespace; 25381d05cddcSAtari911 searchInput.value = namespace || '(default)'; 25391d05cddcSAtari911 hideDropdown(); 25401d05cddcSAtari911 } 25411d05cddcSAtari911 2542*815440faSAtari911 // Event listeners - only bound once now 25431d05cddcSAtari911 searchInput.addEventListener('input', function(e) { 25441d05cddcSAtari911 filterNamespaces(e.target.value); 25451d05cddcSAtari911 }); 25461d05cddcSAtari911 25471d05cddcSAtari911 searchInput.addEventListener('focus', function(e) { 25481d05cddcSAtari911 filterNamespaces(e.target.value); 25491d05cddcSAtari911 }); 25501d05cddcSAtari911 25511d05cddcSAtari911 searchInput.addEventListener('blur', function(e) { 25521d05cddcSAtari911 // Delay to allow click on dropdown 25531d05cddcSAtari911 setTimeout(hideDropdown, 200); 25541d05cddcSAtari911 }); 25551d05cddcSAtari911 25561d05cddcSAtari911 searchInput.addEventListener('keydown', function(e) { 25571d05cddcSAtari911 const options = dropdown.querySelectorAll('.namespace-option'); 25581d05cddcSAtari911 25591d05cddcSAtari911 if (e.key === 'ArrowDown') { 25601d05cddcSAtari911 e.preventDefault(); 25611d05cddcSAtari911 selectedIndex = Math.min(selectedIndex + 1, options.length - 1); 25621d05cddcSAtari911 updateSelection(options); 25631d05cddcSAtari911 } else if (e.key === 'ArrowUp') { 25641d05cddcSAtari911 e.preventDefault(); 25651d05cddcSAtari911 selectedIndex = Math.max(selectedIndex - 1, -1); 25661d05cddcSAtari911 updateSelection(options); 25671d05cddcSAtari911 } else if (e.key === 'Enter') { 25681d05cddcSAtari911 e.preventDefault(); 25691d05cddcSAtari911 if (selectedIndex >= 0 && options[selectedIndex]) { 25701d05cddcSAtari911 selectOption(options[selectedIndex].dataset.value); 25711d05cddcSAtari911 } 25721d05cddcSAtari911 } else if (e.key === 'Escape') { 25731d05cddcSAtari911 hideDropdown(); 25741d05cddcSAtari911 } 25751d05cddcSAtari911 }); 25761d05cddcSAtari911 25771d05cddcSAtari911 function updateSelection(options) { 25781d05cddcSAtari911 options.forEach((opt, idx) => { 25791d05cddcSAtari911 if (idx === selectedIndex) { 25801d05cddcSAtari911 opt.classList.add('selected'); 25811d05cddcSAtari911 opt.scrollIntoView({ block: 'nearest' }); 25821d05cddcSAtari911 } else { 25831d05cddcSAtari911 opt.classList.remove('selected'); 25841d05cddcSAtari911 } 25851d05cddcSAtari911 }); 25861d05cddcSAtari911 } 25871d05cddcSAtari911 25881d05cddcSAtari911 // Click on dropdown option 25891d05cddcSAtari911 dropdown.addEventListener('mousedown', function(e) { 25901d05cddcSAtari911 if (e.target.classList.contains('namespace-option')) { 25911d05cddcSAtari911 selectOption(e.target.dataset.value); 25921d05cddcSAtari911 } 25931d05cddcSAtari911 }); 25941d05cddcSAtari911}; 25951d05cddcSAtari911 2596*815440faSAtari911// Legacy function - kept for compatibility, now handled by custom pickers 25971d05cddcSAtari911window.updateEndTimeOptions = function(calId) { 2598*815440faSAtari911 updateEndTimeButtonState(calId); 2599*815440faSAtari911}; 26001d05cddcSAtari911 2601*815440faSAtari911// ============================================================================ 2602*815440faSAtari911// CUSTOM TIME PICKER - Fast, lightweight time selection 2603*815440faSAtari911// ============================================================================ 26041d05cddcSAtari911 2605*815440faSAtari911// Time data - generated once, reused for all pickers 2606*815440faSAtari911window._calendarTimeData = null; 2607*815440faSAtari911window.getTimeData = function() { 2608*815440faSAtari911 if (window._calendarTimeData) return window._calendarTimeData; 26091d05cddcSAtari911 2610*815440faSAtari911 const periods = [ 2611*815440faSAtari911 { name: 'Morning', hours: [6, 7, 8, 9, 10, 11] }, 2612*815440faSAtari911 { name: 'Afternoon', hours: [12, 13, 14, 15, 16, 17] }, 2613*815440faSAtari911 { name: 'Evening', hours: [18, 19, 20, 21, 22, 23] }, 2614*815440faSAtari911 { name: 'Night', hours: [0, 1, 2, 3, 4, 5] } 2615*815440faSAtari911 ]; 2616*815440faSAtari911 2617*815440faSAtari911 const data = []; 2618*815440faSAtari911 periods.forEach(period => { 2619*815440faSAtari911 const times = []; 2620*815440faSAtari911 period.hours.forEach(hour => { 2621*815440faSAtari911 for (let minute = 0; minute < 60; minute += 15) { 2622*815440faSAtari911 const value = String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0'); 2623*815440faSAtari911 const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour); 2624*815440faSAtari911 const ampm = hour < 12 ? 'AM' : 'PM'; 2625*815440faSAtari911 const display = displayHour + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 2626*815440faSAtari911 const minutes = hour * 60 + minute; 2627*815440faSAtari911 times.push({ value, display, minutes }); 2628*815440faSAtari911 } 2629*815440faSAtari911 }); 2630*815440faSAtari911 data.push({ name: period.name, times }); 2631*815440faSAtari911 }); 2632*815440faSAtari911 2633*815440faSAtari911 window._calendarTimeData = data; 2634*815440faSAtari911 return data; 2635*815440faSAtari911}; 2636*815440faSAtari911 2637*815440faSAtari911// Format time value to display string 2638*815440faSAtari911window.formatTimeDisplay = function(value) { 2639*815440faSAtari911 if (!value) return ''; 2640*815440faSAtari911 const [hour, minute] = value.split(':').map(Number); 2641*815440faSAtari911 const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour); 2642*815440faSAtari911 const ampm = hour < 12 ? 'AM' : 'PM'; 2643*815440faSAtari911 return displayHour + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 2644*815440faSAtari911}; 2645*815440faSAtari911 2646*815440faSAtari911// Build dropdown HTML - called only when opening 2647*815440faSAtari911window.buildTimeDropdown = function(calId, isEndTime, startTimeValue, isMultiDay) { 2648*815440faSAtari911 const data = getTimeData(); 2649*815440faSAtari911 let html = ''; 2650*815440faSAtari911 2651*815440faSAtari911 // Calculate start time minutes for filtering end time options 2652*815440faSAtari911 let startMinutes = -1; 2653*815440faSAtari911 if (isEndTime && startTimeValue && !isMultiDay) { 2654*815440faSAtari911 const [h, m] = startTimeValue.split(':').map(Number); 2655*815440faSAtari911 startMinutes = h * 60 + m; 2656*815440faSAtari911 } 2657*815440faSAtari911 2658*815440faSAtari911 // Add "All day" / "Same as start" option 2659*815440faSAtari911 const defaultText = isEndTime ? 'Same as start' : 'All day'; 2660*815440faSAtari911 html += '<div class="time-option" data-value="">' + defaultText + '</div>'; 2661*815440faSAtari911 2662*815440faSAtari911 data.forEach(period => { 2663*815440faSAtari911 html += '<div class="time-dropdown-section">'; 2664*815440faSAtari911 html += '<div class="time-dropdown-header">' + period.name + '</div>'; 2665*815440faSAtari911 period.times.forEach(time => { 2666*815440faSAtari911 const disabled = (isEndTime && !isMultiDay && startMinutes >= 0 && time.minutes <= startMinutes); 2667*815440faSAtari911 const disabledClass = disabled ? ' disabled' : ''; 2668*815440faSAtari911 html += '<div class="time-option' + disabledClass + '" data-value="' + time.value + '" data-minutes="' + time.minutes + '">' + time.display + '</div>'; 2669*815440faSAtari911 }); 2670*815440faSAtari911 html += '</div>'; 2671*815440faSAtari911 }); 2672*815440faSAtari911 2673*815440faSAtari911 return html; 2674*815440faSAtari911}; 2675*815440faSAtari911 2676*815440faSAtari911// Open time dropdown 2677*815440faSAtari911window.openTimeDropdown = function(calId, isEndTime) { 2678*815440faSAtari911 const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId; 2679*815440faSAtari911 const dropdownId = isEndTime ? 'end-time-dropdown-' + calId : 'time-dropdown-' + calId; 2680*815440faSAtari911 const btn = document.getElementById(btnId); 2681*815440faSAtari911 const dropdown = document.getElementById(dropdownId); 2682*815440faSAtari911 2683*815440faSAtari911 if (!btn || !dropdown) return; 2684*815440faSAtari911 2685*815440faSAtari911 // Close any other open dropdowns first 2686*815440faSAtari911 document.querySelectorAll('.time-dropdown.open').forEach(d => { 2687*815440faSAtari911 if (d.id !== dropdownId) { 2688*815440faSAtari911 d.classList.remove('open'); 2689*815440faSAtari911 d.innerHTML = ''; 2690*815440faSAtari911 } 2691*815440faSAtari911 }); 2692*815440faSAtari911 document.querySelectorAll('.custom-time-picker.open').forEach(b => { 2693*815440faSAtari911 if (b.id !== btnId) b.classList.remove('open'); 2694*815440faSAtari911 }); 2695*815440faSAtari911 2696*815440faSAtari911 // Toggle this dropdown 2697*815440faSAtari911 if (dropdown.classList.contains('open')) { 2698*815440faSAtari911 dropdown.classList.remove('open'); 2699*815440faSAtari911 btn.classList.remove('open'); 2700*815440faSAtari911 dropdown.innerHTML = ''; 2701*815440faSAtari911 return; 2702*815440faSAtari911 } 2703*815440faSAtari911 2704*815440faSAtari911 // Get current state 2705*815440faSAtari911 const startTimeInput = document.getElementById('event-time-' + calId); 2706*815440faSAtari911 const startDateInput = document.getElementById('event-date-' + calId); 2707*815440faSAtari911 const endDateInput = document.getElementById('event-end-date-' + calId); 2708*815440faSAtari911 2709*815440faSAtari911 const startTime = startTimeInput ? startTimeInput.value : ''; 2710*815440faSAtari911 const startDate = startDateInput ? startDateInput.value : ''; 2711*815440faSAtari911 const endDate = endDateInput ? endDateInput.value : ''; 2712da206178SAtari911 const isMultiDay = endDate && endDate !== startDate; 2713da206178SAtari911 2714*815440faSAtari911 // Build and show dropdown 2715*815440faSAtari911 dropdown.innerHTML = buildTimeDropdown(calId, isEndTime, startTime, isMultiDay); 2716*815440faSAtari911 dropdown.classList.add('open'); 2717*815440faSAtari911 btn.classList.add('open'); 2718*815440faSAtari911 2719*815440faSAtari911 // Scroll to appropriate option 2720*815440faSAtari911 const currentValue = isEndTime ? 2721*815440faSAtari911 document.getElementById('event-end-time-' + calId).value : 2722*815440faSAtari911 document.getElementById('event-time-' + calId).value; 2723*815440faSAtari911 2724*815440faSAtari911 if (currentValue) { 2725*815440faSAtari911 // Scroll to selected option 2726*815440faSAtari911 const selected = dropdown.querySelector('[data-value="' + currentValue + '"]'); 2727*815440faSAtari911 if (selected) { 2728*815440faSAtari911 selected.classList.add('selected'); 2729*815440faSAtari911 selected.scrollIntoView({ block: 'center', behavior: 'instant' }); 2730*815440faSAtari911 } 2731*815440faSAtari911 } else if (isEndTime && startTime) { 2732*815440faSAtari911 // For end time with no selection, scroll to first available option after start time 2733*815440faSAtari911 const firstAvailable = dropdown.querySelector('.time-option:not(.disabled):not([data-value=""])'); 2734*815440faSAtari911 if (firstAvailable) { 2735*815440faSAtari911 firstAvailable.scrollIntoView({ block: 'center', behavior: 'instant' }); 2736*815440faSAtari911 } 2737*815440faSAtari911 } 2738*815440faSAtari911}; 2739*815440faSAtari911 2740*815440faSAtari911// Select time option 2741*815440faSAtari911window.selectTimeOption = function(calId, isEndTime, value) { 2742*815440faSAtari911 const inputId = isEndTime ? 'event-end-time-' + calId : 'event-time-' + calId; 2743*815440faSAtari911 const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId; 2744*815440faSAtari911 const dropdownId = isEndTime ? 'end-time-dropdown-' + calId : 'time-dropdown-' + calId; 2745*815440faSAtari911 2746*815440faSAtari911 const input = document.getElementById(inputId); 2747*815440faSAtari911 const btn = document.getElementById(btnId); 2748*815440faSAtari911 const dropdown = document.getElementById(dropdownId); 2749*815440faSAtari911 2750*815440faSAtari911 if (input) { 2751*815440faSAtari911 input.value = value; 2752*815440faSAtari911 } 2753*815440faSAtari911 2754*815440faSAtari911 if (btn) { 2755*815440faSAtari911 const display = btn.querySelector('.time-display'); 2756*815440faSAtari911 if (display) { 2757*815440faSAtari911 if (value) { 2758*815440faSAtari911 display.textContent = formatTimeDisplay(value); 2759*815440faSAtari911 } else { 2760*815440faSAtari911 display.textContent = isEndTime ? 'Same as start' : 'All day'; 2761*815440faSAtari911 } 2762*815440faSAtari911 } 2763*815440faSAtari911 btn.classList.remove('open'); 2764*815440faSAtari911 } 2765*815440faSAtari911 2766*815440faSAtari911 if (dropdown) { 2767*815440faSAtari911 dropdown.classList.remove('open'); 2768*815440faSAtari911 dropdown.innerHTML = ''; 2769*815440faSAtari911 } 2770*815440faSAtari911 2771*815440faSAtari911 // If start time changed, update end time button state 2772*815440faSAtari911 if (!isEndTime) { 2773*815440faSAtari911 updateEndTimeButtonState(calId); 2774*815440faSAtari911 } 2775*815440faSAtari911}; 2776*815440faSAtari911 2777*815440faSAtari911// Update end time button enabled/disabled state 2778*815440faSAtari911window.updateEndTimeButtonState = function(calId) { 2779*815440faSAtari911 const startTimeInput = document.getElementById('event-time-' + calId); 2780*815440faSAtari911 const endTimeBtn = document.getElementById('end-time-picker-btn-' + calId); 2781*815440faSAtari911 const endTimeInput = document.getElementById('event-end-time-' + calId); 2782*815440faSAtari911 2783*815440faSAtari911 if (!startTimeInput || !endTimeBtn) return; 2784*815440faSAtari911 2785*815440faSAtari911 const startTime = startTimeInput.value; 2786*815440faSAtari911 27871d05cddcSAtari911 if (!startTime) { 2788*815440faSAtari911 // All day - disable end time 2789*815440faSAtari911 endTimeBtn.disabled = true; 2790*815440faSAtari911 if (endTimeInput) endTimeInput.value = ''; 2791*815440faSAtari911 const display = endTimeBtn.querySelector('.time-display'); 2792*815440faSAtari911 if (display) display.textContent = 'Same as start'; 27931d05cddcSAtari911 } else { 2794*815440faSAtari911 endTimeBtn.disabled = false; 27951d05cddcSAtari911 } 2796*815440faSAtari911}; 2797*815440faSAtari911 2798*815440faSAtari911// Initialize custom time pickers for a dialog 2799*815440faSAtari911window.initCustomTimePickers = function(calId) { 2800*815440faSAtari911 const startBtn = document.getElementById('time-picker-btn-' + calId); 2801*815440faSAtari911 const endBtn = document.getElementById('end-time-picker-btn-' + calId); 2802*815440faSAtari911 const startDropdown = document.getElementById('time-dropdown-' + calId); 2803*815440faSAtari911 const endDropdown = document.getElementById('end-time-dropdown-' + calId); 2804*815440faSAtari911 2805*815440faSAtari911 // Prevent re-initialization 2806*815440faSAtari911 if (startBtn && startBtn.dataset.initialized) return; 2807*815440faSAtari911 2808*815440faSAtari911 if (startBtn) { 2809*815440faSAtari911 startBtn.dataset.initialized = 'true'; 2810*815440faSAtari911 startBtn.addEventListener('click', function(e) { 2811*815440faSAtari911 e.preventDefault(); 2812*815440faSAtari911 e.stopPropagation(); 2813*815440faSAtari911 openTimeDropdown(calId, false); 2814*815440faSAtari911 }); 2815*815440faSAtari911 } 2816*815440faSAtari911 2817*815440faSAtari911 if (endBtn) { 2818*815440faSAtari911 endBtn.addEventListener('click', function(e) { 2819*815440faSAtari911 e.preventDefault(); 2820*815440faSAtari911 e.stopPropagation(); 2821*815440faSAtari911 if (!endBtn.disabled) { 2822*815440faSAtari911 openTimeDropdown(calId, true); 28231d05cddcSAtari911 } 2824da206178SAtari911 }); 28251d05cddcSAtari911 } 28261d05cddcSAtari911 2827*815440faSAtari911 // Handle clicks on time options 2828*815440faSAtari911 if (startDropdown) { 2829*815440faSAtari911 startDropdown.addEventListener('click', function(e) { 2830*815440faSAtari911 const option = e.target.closest('.time-option'); 2831*815440faSAtari911 if (option && !option.classList.contains('disabled')) { 2832*815440faSAtari911 e.stopPropagation(); 2833*815440faSAtari911 selectTimeOption(calId, false, option.dataset.value); 2834*815440faSAtari911 } 2835*815440faSAtari911 }); 2836*815440faSAtari911 } 28371d05cddcSAtari911 2838*815440faSAtari911 if (endDropdown) { 2839*815440faSAtari911 endDropdown.addEventListener('click', function(e) { 2840*815440faSAtari911 const option = e.target.closest('.time-option'); 2841*815440faSAtari911 if (option && !option.classList.contains('disabled')) { 2842*815440faSAtari911 e.stopPropagation(); 2843*815440faSAtari911 selectTimeOption(calId, true, option.dataset.value); 2844*815440faSAtari911 } 2845*815440faSAtari911 }); 2846*815440faSAtari911 } 28471d05cddcSAtari911 2848*815440faSAtari911 // Handle date changes - update end time options when dates change 2849*815440faSAtari911 const startDateInput = document.getElementById('event-date-' + calId); 2850*815440faSAtari911 const endDateInput = document.getElementById('event-end-date-' + calId); 2851*815440faSAtari911 2852*815440faSAtari911 if (startDateInput && !startDateInput.dataset.initialized) { 2853*815440faSAtari911 startDateInput.dataset.initialized = 'true'; 2854*815440faSAtari911 startDateInput.addEventListener('change', function() { 2855*815440faSAtari911 // Just close any open dropdowns - they'll rebuild with correct state when reopened 2856*815440faSAtari911 const dropdown = document.getElementById('end-time-dropdown-' + calId); 2857*815440faSAtari911 if (dropdown && dropdown.classList.contains('open')) { 2858*815440faSAtari911 dropdown.classList.remove('open'); 2859*815440faSAtari911 dropdown.innerHTML = ''; 2860*815440faSAtari911 } 2861*815440faSAtari911 }); 2862*815440faSAtari911 } 2863*815440faSAtari911 2864*815440faSAtari911 if (endDateInput && !endDateInput.dataset.initialized) { 2865*815440faSAtari911 endDateInput.dataset.initialized = 'true'; 2866*815440faSAtari911 endDateInput.addEventListener('change', function() { 2867*815440faSAtari911 const dropdown = document.getElementById('end-time-dropdown-' + calId); 2868*815440faSAtari911 if (dropdown && dropdown.classList.contains('open')) { 2869*815440faSAtari911 dropdown.classList.remove('open'); 2870*815440faSAtari911 dropdown.innerHTML = ''; 2871*815440faSAtari911 } 2872*815440faSAtari911 }); 2873*815440faSAtari911 } 2874*815440faSAtari911}; 2875*815440faSAtari911 2876*815440faSAtari911// Close dropdowns when clicking outside 2877*815440faSAtari911if (!window._calendarDropdownCloseInit) { 2878*815440faSAtari911 window._calendarDropdownCloseInit = true; 2879*815440faSAtari911 document.addEventListener('click', function(e) { 2880*815440faSAtari911 // Don't close if clicking inside a picker button or dropdown 2881*815440faSAtari911 if (e.target.closest('.custom-time-picker') || e.target.closest('.time-dropdown') || 2882*815440faSAtari911 e.target.closest('.custom-date-picker') || e.target.closest('.date-dropdown')) { 2883*815440faSAtari911 return; 2884*815440faSAtari911 } 2885*815440faSAtari911 2886*815440faSAtari911 // Close all open time dropdowns 2887*815440faSAtari911 document.querySelectorAll('.time-dropdown.open').forEach(d => { 2888*815440faSAtari911 d.classList.remove('open'); 2889*815440faSAtari911 d.innerHTML = ''; 2890*815440faSAtari911 }); 2891*815440faSAtari911 document.querySelectorAll('.custom-time-picker.open').forEach(b => { 2892*815440faSAtari911 b.classList.remove('open'); 2893*815440faSAtari911 }); 2894*815440faSAtari911 2895*815440faSAtari911 // Close all open date dropdowns 2896*815440faSAtari911 document.querySelectorAll('.date-dropdown.open').forEach(d => { 2897*815440faSAtari911 d.classList.remove('open'); 2898*815440faSAtari911 d.innerHTML = ''; 2899*815440faSAtari911 }); 2900*815440faSAtari911 document.querySelectorAll('.custom-date-picker.open').forEach(b => { 2901*815440faSAtari911 b.classList.remove('open'); 2902*815440faSAtari911 }); 2903*815440faSAtari911 }); 2904*815440faSAtari911} 2905*815440faSAtari911 2906*815440faSAtari911// Set time picker value programmatically (for edit mode) 2907*815440faSAtari911window.setTimePicker = function(calId, isEndTime, value) { 2908*815440faSAtari911 const inputId = isEndTime ? 'event-end-time-' + calId : 'event-time-' + calId; 2909*815440faSAtari911 const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId; 2910*815440faSAtari911 2911*815440faSAtari911 const input = document.getElementById(inputId); 2912*815440faSAtari911 const btn = document.getElementById(btnId); 2913*815440faSAtari911 2914*815440faSAtari911 if (input) { 2915*815440faSAtari911 input.value = value || ''; 2916*815440faSAtari911 } 2917*815440faSAtari911 2918*815440faSAtari911 if (btn) { 2919*815440faSAtari911 const display = btn.querySelector('.time-display'); 2920*815440faSAtari911 if (display) { 2921*815440faSAtari911 if (value) { 2922*815440faSAtari911 display.textContent = formatTimeDisplay(value); 29231d05cddcSAtari911 } else { 2924*815440faSAtari911 display.textContent = isEndTime ? 'Same as start' : 'All day'; 2925*815440faSAtari911 } 2926*815440faSAtari911 } 2927*815440faSAtari911 2928*815440faSAtari911 // Update disabled state for end time 2929*815440faSAtari911 if (isEndTime) { 2930*815440faSAtari911 const startTimeInput = document.getElementById('event-time-' + calId); 2931*815440faSAtari911 btn.disabled = !startTimeInput || !startTimeInput.value; 2932*815440faSAtari911 } 2933*815440faSAtari911 } 2934*815440faSAtari911}; 2935*815440faSAtari911 2936*815440faSAtari911// ============================================================================ 2937*815440faSAtari911// CUSTOM DATE PICKER - Fast, lightweight date selection 2938*815440faSAtari911// ============================================================================ 2939*815440faSAtari911 2940*815440faSAtari911// Format date for display 2941*815440faSAtari911window.formatDateDisplay = function(dateStr) { 2942*815440faSAtari911 if (!dateStr) return ''; 2943*815440faSAtari911 const date = new Date(dateStr + 'T00:00:00'); 2944*815440faSAtari911 return date.toLocaleDateString('en-US', { 2945*815440faSAtari911 weekday: 'short', 2946*815440faSAtari911 month: 'short', 2947*815440faSAtari911 day: 'numeric', 2948*815440faSAtari911 year: 'numeric' 2949*815440faSAtari911 }); 2950*815440faSAtari911}; 2951*815440faSAtari911 2952*815440faSAtari911// Build date picker calendar HTML 2953*815440faSAtari911window.buildDateCalendar = function(calId, isEndDate, year, month, selectedDate, minDate) { 2954*815440faSAtari911 const today = new Date(); 2955*815440faSAtari911 today.setHours(0, 0, 0, 0); 2956*815440faSAtari911 2957*815440faSAtari911 const firstDay = new Date(year, month, 1); 2958*815440faSAtari911 const lastDay = new Date(year, month + 1, 0); 2959*815440faSAtari911 const startDayOfWeek = firstDay.getDay(); 2960*815440faSAtari911 const daysInMonth = lastDay.getDate(); 2961*815440faSAtari911 2962*815440faSAtari911 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 2963*815440faSAtari911 'July', 'August', 'September', 'October', 'November', 'December']; 2964*815440faSAtari911 2965*815440faSAtari911 let html = '<div class="date-picker-calendar">'; 2966*815440faSAtari911 2967*815440faSAtari911 // Header with navigation 2968*815440faSAtari911 html += '<div class="date-picker-header">'; 2969*815440faSAtari911 html += '<button type="button" class="date-picker-nav" data-action="prev">◀</button>'; 2970*815440faSAtari911 html += '<span class="date-picker-title">' + monthNames[month] + ' ' + year + '</span>'; 2971*815440faSAtari911 html += '<button type="button" class="date-picker-nav" data-action="next">▶</button>'; 2972*815440faSAtari911 html += '</div>'; 2973*815440faSAtari911 2974*815440faSAtari911 // Weekday headers 2975*815440faSAtari911 html += '<div class="date-picker-weekdays">'; 2976*815440faSAtari911 ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].forEach(d => { 2977*815440faSAtari911 html += '<div class="date-picker-weekday">' + d + '</div>'; 2978*815440faSAtari911 }); 2979*815440faSAtari911 html += '</div>'; 2980*815440faSAtari911 2981*815440faSAtari911 // Days grid 2982*815440faSAtari911 html += '<div class="date-picker-days">'; 2983*815440faSAtari911 2984*815440faSAtari911 // Previous month days 2985*815440faSAtari911 const prevMonth = new Date(year, month, 0); 2986*815440faSAtari911 const prevMonthDays = prevMonth.getDate(); 2987*815440faSAtari911 for (let i = startDayOfWeek - 1; i >= 0; i--) { 2988*815440faSAtari911 const day = prevMonthDays - i; 2989*815440faSAtari911 const dateStr = formatDateValue(year, month - 1, day); 2990*815440faSAtari911 html += '<button type="button" class="date-picker-day other-month" data-date="' + dateStr + '">' + day + '</button>'; 2991*815440faSAtari911 } 2992*815440faSAtari911 2993*815440faSAtari911 // Current month days 2994*815440faSAtari911 for (let day = 1; day <= daysInMonth; day++) { 2995*815440faSAtari911 const dateStr = formatDateValue(year, month, day); 2996*815440faSAtari911 const dateObj = new Date(year, month, day); 2997*815440faSAtari911 dateObj.setHours(0, 0, 0, 0); 2998*815440faSAtari911 2999*815440faSAtari911 let classes = 'date-picker-day'; 3000*815440faSAtari911 if (dateObj.getTime() === today.getTime()) classes += ' today'; 3001*815440faSAtari911 if (dateStr === selectedDate) classes += ' selected'; 3002*815440faSAtari911 3003*815440faSAtari911 // For end date, disable dates before start date 3004*815440faSAtari911 if (isEndDate && minDate) { 3005*815440faSAtari911 const minDateObj = new Date(minDate + 'T00:00:00'); 3006*815440faSAtari911 if (dateObj < minDateObj) classes += ' disabled'; 3007*815440faSAtari911 } 3008*815440faSAtari911 3009*815440faSAtari911 html += '<button type="button" class="' + classes + '" data-date="' + dateStr + '">' + day + '</button>'; 3010*815440faSAtari911 } 3011*815440faSAtari911 3012*815440faSAtari911 // Next month days to fill grid 3013*815440faSAtari911 const totalCells = startDayOfWeek + daysInMonth; 3014*815440faSAtari911 const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7); 3015*815440faSAtari911 for (let i = 1; i <= remainingCells; i++) { 3016*815440faSAtari911 const dateStr = formatDateValue(year, month + 1, i); 3017*815440faSAtari911 html += '<button type="button" class="date-picker-day other-month" data-date="' + dateStr + '">' + i + '</button>'; 3018*815440faSAtari911 } 3019*815440faSAtari911 3020*815440faSAtari911 html += '</div>'; 3021*815440faSAtari911 3022*815440faSAtari911 // Clear button for end date 3023*815440faSAtari911 if (isEndDate) { 3024*815440faSAtari911 html += '<button type="button" class="date-picker-clear" data-action="clear">Clear End Date</button>'; 3025*815440faSAtari911 } 3026*815440faSAtari911 3027*815440faSAtari911 html += '</div>'; 3028*815440faSAtari911 return html; 3029*815440faSAtari911}; 3030*815440faSAtari911 3031*815440faSAtari911// Format date value as YYYY-MM-DD 3032*815440faSAtari911window.formatDateValue = function(year, month, day) { 3033*815440faSAtari911 // Handle month overflow 3034*815440faSAtari911 const date = new Date(year, month, day); 3035*815440faSAtari911 const y = date.getFullYear(); 3036*815440faSAtari911 const m = String(date.getMonth() + 1).padStart(2, '0'); 3037*815440faSAtari911 const d = String(date.getDate()).padStart(2, '0'); 3038*815440faSAtari911 return y + '-' + m + '-' + d; 3039*815440faSAtari911}; 3040*815440faSAtari911 3041*815440faSAtari911// Open date dropdown 3042*815440faSAtari911window.openDateDropdown = function(calId, isEndDate) { 3043*815440faSAtari911 const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId; 3044*815440faSAtari911 const dropdownId = isEndDate ? 'end-date-dropdown-' + calId : 'date-dropdown-' + calId; 3045*815440faSAtari911 const btn = document.getElementById(btnId); 3046*815440faSAtari911 const dropdown = document.getElementById(dropdownId); 3047*815440faSAtari911 3048*815440faSAtari911 if (!btn || !dropdown) return; 3049*815440faSAtari911 3050*815440faSAtari911 // Close any other open dropdowns first 3051*815440faSAtari911 document.querySelectorAll('.date-dropdown.open, .time-dropdown.open').forEach(d => { 3052*815440faSAtari911 if (d.id !== dropdownId) { 3053*815440faSAtari911 d.classList.remove('open'); 3054*815440faSAtari911 d.innerHTML = ''; 3055*815440faSAtari911 } 3056*815440faSAtari911 }); 3057*815440faSAtari911 document.querySelectorAll('.custom-date-picker.open, .custom-time-picker.open').forEach(b => { 3058*815440faSAtari911 if (b.id !== btnId) b.classList.remove('open'); 3059*815440faSAtari911 }); 3060*815440faSAtari911 3061*815440faSAtari911 // Toggle this dropdown 3062*815440faSAtari911 if (dropdown.classList.contains('open')) { 3063*815440faSAtari911 dropdown.classList.remove('open'); 3064*815440faSAtari911 btn.classList.remove('open'); 3065*815440faSAtari911 dropdown.innerHTML = ''; 3066*815440faSAtari911 return; 3067*815440faSAtari911 } 3068*815440faSAtari911 3069*815440faSAtari911 // Get current value and min date 3070*815440faSAtari911 const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId; 3071*815440faSAtari911 const input = document.getElementById(inputId); 3072*815440faSAtari911 const selectedDate = input ? input.value : ''; 3073*815440faSAtari911 3074*815440faSAtari911 let minDate = null; 3075*815440faSAtari911 if (isEndDate) { 3076*815440faSAtari911 const startInput = document.getElementById('event-date-' + calId); 3077*815440faSAtari911 minDate = startInput ? startInput.value : null; 3078*815440faSAtari911 } 3079*815440faSAtari911 3080*815440faSAtari911 // Determine which month to show 3081*815440faSAtari911 let year, month; 3082*815440faSAtari911 if (selectedDate) { 3083*815440faSAtari911 // If there's a selected date, show that month 3084*815440faSAtari911 const d = new Date(selectedDate + 'T00:00:00'); 3085*815440faSAtari911 year = d.getFullYear(); 3086*815440faSAtari911 month = d.getMonth(); 3087*815440faSAtari911 } else if (isEndDate && minDate) { 3088*815440faSAtari911 // For end date with no value, start on the start date's month 3089*815440faSAtari911 const d = new Date(minDate + 'T00:00:00'); 3090*815440faSAtari911 year = d.getFullYear(); 3091*815440faSAtari911 month = d.getMonth(); 3092*815440faSAtari911 } else { 3093*815440faSAtari911 // Fallback to current month 3094*815440faSAtari911 const now = new Date(); 3095*815440faSAtari911 year = now.getFullYear(); 3096*815440faSAtari911 month = now.getMonth(); 3097*815440faSAtari911 } 3098*815440faSAtari911 3099*815440faSAtari911 // Store current view state 3100*815440faSAtari911 dropdown.dataset.year = year; 3101*815440faSAtari911 dropdown.dataset.month = month; 3102*815440faSAtari911 dropdown.dataset.isEnd = isEndDate ? '1' : '0'; 3103*815440faSAtari911 dropdown.dataset.calId = calId; 3104*815440faSAtari911 3105*815440faSAtari911 // Build and show 3106*815440faSAtari911 dropdown.innerHTML = buildDateCalendar(calId, isEndDate, year, month, selectedDate, minDate); 3107*815440faSAtari911 dropdown.classList.add('open'); 3108*815440faSAtari911 btn.classList.add('open'); 3109*815440faSAtari911}; 3110*815440faSAtari911 3111*815440faSAtari911// Select date 3112*815440faSAtari911window.selectDate = function(calId, isEndDate, dateStr) { 3113*815440faSAtari911 const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId; 3114*815440faSAtari911 const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId; 3115*815440faSAtari911 const dropdownId = isEndDate ? 'end-date-dropdown-' + calId : 'date-dropdown-' + calId; 3116*815440faSAtari911 3117*815440faSAtari911 const input = document.getElementById(inputId); 3118*815440faSAtari911 const btn = document.getElementById(btnId); 3119*815440faSAtari911 const dropdown = document.getElementById(dropdownId); 3120*815440faSAtari911 3121*815440faSAtari911 if (input) { 3122*815440faSAtari911 input.value = dateStr || ''; 3123*815440faSAtari911 } 3124*815440faSAtari911 3125*815440faSAtari911 if (btn) { 3126*815440faSAtari911 const display = btn.querySelector('.date-display'); 3127*815440faSAtari911 if (display) { 3128*815440faSAtari911 display.textContent = dateStr ? formatDateDisplay(dateStr) : (isEndDate ? 'Optional' : 'Select date'); 3129*815440faSAtari911 } 3130*815440faSAtari911 btn.classList.remove('open'); 3131*815440faSAtari911 } 3132*815440faSAtari911 3133*815440faSAtari911 if (dropdown) { 3134*815440faSAtari911 dropdown.classList.remove('open'); 3135*815440faSAtari911 dropdown.innerHTML = ''; 3136*815440faSAtari911 } 3137*815440faSAtari911}; 3138*815440faSAtari911 3139*815440faSAtari911// Navigate date picker month 3140*815440faSAtari911window.navigateDatePicker = function(dropdown, direction) { 3141*815440faSAtari911 let year = parseInt(dropdown.dataset.year); 3142*815440faSAtari911 let month = parseInt(dropdown.dataset.month); 3143*815440faSAtari911 const isEndDate = dropdown.dataset.isEnd === '1'; 3144*815440faSAtari911 const calId = dropdown.dataset.calId; 3145*815440faSAtari911 3146*815440faSAtari911 month += direction; 3147*815440faSAtari911 if (month < 0) { month = 11; year--; } 3148*815440faSAtari911 if (month > 11) { month = 0; year++; } 3149*815440faSAtari911 3150*815440faSAtari911 dropdown.dataset.year = year; 3151*815440faSAtari911 dropdown.dataset.month = month; 3152*815440faSAtari911 3153*815440faSAtari911 const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId; 3154*815440faSAtari911 const input = document.getElementById(inputId); 3155*815440faSAtari911 const selectedDate = input ? input.value : ''; 3156*815440faSAtari911 3157*815440faSAtari911 let minDate = null; 3158*815440faSAtari911 if (isEndDate) { 3159*815440faSAtari911 const startInput = document.getElementById('event-date-' + calId); 3160*815440faSAtari911 minDate = startInput ? startInput.value : null; 3161*815440faSAtari911 } 3162*815440faSAtari911 3163*815440faSAtari911 dropdown.innerHTML = buildDateCalendar(calId, isEndDate, year, month, selectedDate, minDate); 3164*815440faSAtari911}; 3165*815440faSAtari911 3166*815440faSAtari911// Initialize custom date pickers for a dialog 3167*815440faSAtari911window.initCustomDatePickers = function(calId) { 3168*815440faSAtari911 const startBtn = document.getElementById('date-picker-btn-' + calId); 3169*815440faSAtari911 const endBtn = document.getElementById('end-date-picker-btn-' + calId); 3170*815440faSAtari911 const startDropdown = document.getElementById('date-dropdown-' + calId); 3171*815440faSAtari911 const endDropdown = document.getElementById('end-date-dropdown-' + calId); 3172*815440faSAtari911 3173*815440faSAtari911 // Prevent re-initialization 3174*815440faSAtari911 if (startBtn && startBtn.dataset.initialized) return; 3175*815440faSAtari911 3176*815440faSAtari911 if (startBtn) { 3177*815440faSAtari911 startBtn.dataset.initialized = 'true'; 3178*815440faSAtari911 startBtn.addEventListener('click', function(e) { 3179*815440faSAtari911 e.preventDefault(); 3180*815440faSAtari911 e.stopPropagation(); 3181*815440faSAtari911 openDateDropdown(calId, false); 3182*815440faSAtari911 }); 3183*815440faSAtari911 } 3184*815440faSAtari911 3185*815440faSAtari911 if (endBtn) { 3186*815440faSAtari911 endBtn.addEventListener('click', function(e) { 3187*815440faSAtari911 e.preventDefault(); 3188*815440faSAtari911 e.stopPropagation(); 3189*815440faSAtari911 openDateDropdown(calId, true); 3190*815440faSAtari911 }); 3191*815440faSAtari911 } 3192*815440faSAtari911 3193*815440faSAtari911 // Handle clicks inside date dropdowns 3194*815440faSAtari911 [startDropdown, endDropdown].forEach((dropdown, idx) => { 3195*815440faSAtari911 if (!dropdown) return; 3196*815440faSAtari911 const isEnd = idx === 1; 3197*815440faSAtari911 3198*815440faSAtari911 dropdown.addEventListener('click', function(e) { 3199*815440faSAtari911 e.stopPropagation(); 3200*815440faSAtari911 3201*815440faSAtari911 const nav = e.target.closest('.date-picker-nav'); 3202*815440faSAtari911 if (nav) { 3203*815440faSAtari911 const direction = nav.dataset.action === 'prev' ? -1 : 1; 3204*815440faSAtari911 navigateDatePicker(dropdown, direction); 3205*815440faSAtari911 return; 3206*815440faSAtari911 } 3207*815440faSAtari911 3208*815440faSAtari911 const clear = e.target.closest('.date-picker-clear'); 3209*815440faSAtari911 if (clear) { 3210*815440faSAtari911 selectDate(calId, true, ''); 3211*815440faSAtari911 return; 3212*815440faSAtari911 } 3213*815440faSAtari911 3214*815440faSAtari911 const day = e.target.closest('.date-picker-day'); 3215*815440faSAtari911 if (day && !day.classList.contains('disabled')) { 3216*815440faSAtari911 selectDate(calId, isEnd, day.dataset.date); 3217*815440faSAtari911 } 3218*815440faSAtari911 }); 3219*815440faSAtari911 }); 3220*815440faSAtari911}; 3221*815440faSAtari911 3222*815440faSAtari911// Set date picker value programmatically 3223*815440faSAtari911window.setDatePicker = function(calId, isEndDate, value) { 3224*815440faSAtari911 const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId; 3225*815440faSAtari911 const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId; 3226*815440faSAtari911 3227*815440faSAtari911 const input = document.getElementById(inputId); 3228*815440faSAtari911 const btn = document.getElementById(btnId); 3229*815440faSAtari911 3230*815440faSAtari911 if (input) { 3231*815440faSAtari911 input.value = value || ''; 3232*815440faSAtari911 } 3233*815440faSAtari911 3234*815440faSAtari911 if (btn) { 3235*815440faSAtari911 const display = btn.querySelector('.date-display'); 3236*815440faSAtari911 if (display) { 3237*815440faSAtari911 display.textContent = value ? formatDateDisplay(value) : (isEndDate ? 'Optional' : 'Select date'); 32381d05cddcSAtari911 } 32391d05cddcSAtari911 } 32401d05cddcSAtari911}; 32411d05cddcSAtari911 32421d05cddcSAtari911// Check for time conflicts between events on the same date 32431d05cddcSAtari911window.checkTimeConflicts = function(events, currentEventId) { 32441d05cddcSAtari911 const conflicts = []; 32451d05cddcSAtari911 32461d05cddcSAtari911 // Group events by date 32471d05cddcSAtari911 const eventsByDate = {}; 32481d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(events)) { 32491d05cddcSAtari911 if (!Array.isArray(dateEvents)) continue; 32501d05cddcSAtari911 32511d05cddcSAtari911 dateEvents.forEach(evt => { 32521d05cddcSAtari911 if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event 32531d05cddcSAtari911 32541d05cddcSAtari911 if (!eventsByDate[date]) eventsByDate[date] = []; 32551d05cddcSAtari911 eventsByDate[date].push(evt); 32561d05cddcSAtari911 }); 32571d05cddcSAtari911 } 32581d05cddcSAtari911 32591d05cddcSAtari911 // Check for overlaps on each date 32601d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(eventsByDate)) { 32611d05cddcSAtari911 for (let i = 0; i < dateEvents.length; i++) { 32621d05cddcSAtari911 for (let j = i + 1; j < dateEvents.length; j++) { 32631d05cddcSAtari911 const evt1 = dateEvents[i]; 32641d05cddcSAtari911 const evt2 = dateEvents[j]; 32651d05cddcSAtari911 32661d05cddcSAtari911 if (eventsOverlap(evt1, evt2)) { 32671d05cddcSAtari911 // Mark both events as conflicting 32681d05cddcSAtari911 if (!evt1.hasConflict) evt1.hasConflict = true; 32691d05cddcSAtari911 if (!evt2.hasConflict) evt2.hasConflict = true; 32701d05cddcSAtari911 32711d05cddcSAtari911 // Store conflict info 32721d05cddcSAtari911 if (!evt1.conflictsWith) evt1.conflictsWith = []; 32731d05cddcSAtari911 if (!evt2.conflictsWith) evt2.conflictsWith = []; 32741d05cddcSAtari911 32751d05cddcSAtari911 evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime}); 32761d05cddcSAtari911 evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime}); 32771d05cddcSAtari911 } 32781d05cddcSAtari911 } 32791d05cddcSAtari911 } 32801d05cddcSAtari911 } 32811d05cddcSAtari911 32821d05cddcSAtari911 return events; 32831d05cddcSAtari911}; 32841d05cddcSAtari911 32851d05cddcSAtari911// Check if two events overlap in time 32861d05cddcSAtari911function eventsOverlap(evt1, evt2) { 32871d05cddcSAtari911 if (!evt1.time || !evt2.time) return false; // All-day events don't conflict 32881d05cddcSAtari911 32891d05cddcSAtari911 const start1 = evt1.time; 32901d05cddcSAtari911 const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start 32911d05cddcSAtari911 32921d05cddcSAtari911 const start2 = evt2.time; 32931d05cddcSAtari911 const end2 = evt2.endTime || evt2.time; 32941d05cddcSAtari911 32951d05cddcSAtari911 // Convert to minutes for easier comparison 32961d05cddcSAtari911 const start1Mins = timeToMinutes(start1); 32971d05cddcSAtari911 const end1Mins = timeToMinutes(end1); 32981d05cddcSAtari911 const start2Mins = timeToMinutes(start2); 32991d05cddcSAtari911 const end2Mins = timeToMinutes(end2); 33001d05cddcSAtari911 33011d05cddcSAtari911 // Check for overlap 33021d05cddcSAtari911 // Events overlap if: start1 < end2 AND start2 < end1 33031d05cddcSAtari911 return start1Mins < end2Mins && start2Mins < end1Mins; 33041d05cddcSAtari911} 33051d05cddcSAtari911 33061d05cddcSAtari911// Convert HH:MM time to minutes since midnight 33071d05cddcSAtari911function timeToMinutes(timeStr) { 33081d05cddcSAtari911 const [hours, minutes] = timeStr.split(':').map(Number); 33091d05cddcSAtari911 return hours * 60 + minutes; 33101d05cddcSAtari911} 33111d05cddcSAtari911 33121d05cddcSAtari911// Format time range for display 33131d05cddcSAtari911window.formatTimeRange = function(startTime, endTime) { 33141d05cddcSAtari911 if (!startTime) return ''; 33151d05cddcSAtari911 33161d05cddcSAtari911 const formatTime = (timeStr) => { 33171d05cddcSAtari911 const [hour24, minute] = timeStr.split(':').map(Number); 33181d05cddcSAtari911 const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24); 33191d05cddcSAtari911 const ampm = hour24 < 12 ? 'AM' : 'PM'; 33201d05cddcSAtari911 return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 33211d05cddcSAtari911 }; 33221d05cddcSAtari911 33231d05cddcSAtari911 if (!endTime || endTime === startTime) { 33241d05cddcSAtari911 return formatTime(startTime); 33251d05cddcSAtari911 } 33261d05cddcSAtari911 33271d05cddcSAtari911 return formatTime(startTime) + ' - ' + formatTime(endTime); 33281d05cddcSAtari911}; 33291d05cddcSAtari911 33309ccd446eSAtari911// Track last known mouse position for tooltip positioning fallback 33319ccd446eSAtari911var _lastMouseX = 0, _lastMouseY = 0; 33329ccd446eSAtari911document.addEventListener('mousemove', function(e) { 33339ccd446eSAtari911 _lastMouseX = e.clientX; 33349ccd446eSAtari911 _lastMouseY = e.clientY; 33359ccd446eSAtari911}); 33369ccd446eSAtari911 33371d05cddcSAtari911// Show custom conflict tooltip 33381d05cddcSAtari911window.showConflictTooltip = function(badgeElement) { 33391d05cddcSAtari911 // Remove any existing tooltip 33401d05cddcSAtari911 hideConflictTooltip(); 33411d05cddcSAtari911 33429ccd446eSAtari911 // Get conflict data (base64-encoded JSON to avoid attribute quote issues) 33439ccd446eSAtari911 const conflictsRaw = badgeElement.getAttribute('data-conflicts'); 33449ccd446eSAtari911 if (!conflictsRaw) return; 33451d05cddcSAtari911 33461d05cddcSAtari911 let conflicts; 33471d05cddcSAtari911 try { 33489ccd446eSAtari911 conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw)))); 33491d05cddcSAtari911 } catch (e) { 33509ccd446eSAtari911 // Fallback: try parsing as plain JSON (for PHP-rendered badges) 33519ccd446eSAtari911 try { 33529ccd446eSAtari911 conflicts = JSON.parse(conflictsRaw); 33539ccd446eSAtari911 } catch (e2) { 33549ccd446eSAtari911 console.error('Failed to parse conflicts:', e2); 33551d05cddcSAtari911 return; 33561d05cddcSAtari911 } 33579ccd446eSAtari911 } 33589ccd446eSAtari911 33599ccd446eSAtari911 // Get theme from the calendar container via CSS variables 33609ccd446eSAtari911 // Try closest ancestor first, then fall back to any calendar on the page 33619ccd446eSAtari911 let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone'); 33629ccd446eSAtari911 if (!containerEl) { 33639ccd446eSAtari911 // Badge might be inside a day popup (appended to body) - find any calendar container 33649ccd446eSAtari911 containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]'); 33659ccd446eSAtari911 } 33669ccd446eSAtari911 const cs = containerEl ? getComputedStyle(containerEl) : null; 33679ccd446eSAtari911 33689ccd446eSAtari911 const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424'; 33699ccd446eSAtari911 const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07'; 33709ccd446eSAtari911 const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07'; 33719ccd446eSAtari911 const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00'; 33729ccd446eSAtari911 const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)'; 33731d05cddcSAtari911 33741d05cddcSAtari911 // Create tooltip 33751d05cddcSAtari911 const tooltip = document.createElement('div'); 33761d05cddcSAtari911 tooltip.id = 'conflict-tooltip'; 33771d05cddcSAtari911 tooltip.className = 'conflict-tooltip'; 33781d05cddcSAtari911 33799ccd446eSAtari911 // Apply theme styles 33809ccd446eSAtari911 tooltip.style.background = bg; 33819ccd446eSAtari911 tooltip.style.borderColor = border; 33829ccd446eSAtari911 tooltip.style.color = textPrimary; 33839ccd446eSAtari911 tooltip.style.boxShadow = '0 4px 12px ' + shadow; 33849ccd446eSAtari911 33859ccd446eSAtari911 // Build content with themed colors 33867e8ea635SAtari911 let html = '<div class="conflict-tooltip-header" style="background: ' + border + '; color: ' + bg + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>'; 33871d05cddcSAtari911 html += '<div class="conflict-tooltip-body">'; 33881d05cddcSAtari911 conflicts.forEach(conflict => { 33897e8ea635SAtari911 html += '<div class="conflict-item" style="color: ' + textDim + '; border-bottom-color: ' + border + ';">• ' + escapeHtml(conflict) + '</div>'; 33901d05cddcSAtari911 }); 33911d05cddcSAtari911 html += '</div>'; 33921d05cddcSAtari911 33931d05cddcSAtari911 tooltip.innerHTML = html; 33941d05cddcSAtari911 document.body.appendChild(tooltip); 33951d05cddcSAtari911 33961d05cddcSAtari911 // Position tooltip 33971d05cddcSAtari911 const rect = badgeElement.getBoundingClientRect(); 33981d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 33991d05cddcSAtari911 34001d05cddcSAtari911 // Position above the badge, centered 34011d05cddcSAtari911 let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); 34021d05cddcSAtari911 let top = rect.top - tooltipRect.height - 8; 34031d05cddcSAtari911 34041d05cddcSAtari911 // Keep tooltip within viewport 34051d05cddcSAtari911 if (left < 10) left = 10; 34061d05cddcSAtari911 if (left + tooltipRect.width > window.innerWidth - 10) { 34071d05cddcSAtari911 left = window.innerWidth - tooltipRect.width - 10; 34081d05cddcSAtari911 } 34091d05cddcSAtari911 if (top < 10) { 34101d05cddcSAtari911 // If not enough room above, show below 34111d05cddcSAtari911 top = rect.bottom + 8; 34121d05cddcSAtari911 } 34131d05cddcSAtari911 34141d05cddcSAtari911 tooltip.style.left = left + 'px'; 34151d05cddcSAtari911 tooltip.style.top = top + 'px'; 34161d05cddcSAtari911 tooltip.style.opacity = '1'; 34171d05cddcSAtari911}; 34181d05cddcSAtari911 34191d05cddcSAtari911// Hide conflict tooltip 34201d05cddcSAtari911window.hideConflictTooltip = function() { 34211d05cddcSAtari911 const tooltip = document.getElementById('conflict-tooltip'); 34221d05cddcSAtari911 if (tooltip) { 34231d05cddcSAtari911 tooltip.remove(); 34241d05cddcSAtari911 } 34251d05cddcSAtari911}; 34261d05cddcSAtari911 342796df7d3eSAtari911// Fuzzy search helper for event filtering - normalizes text for matching 342896df7d3eSAtari911function eventSearchNormalize(text) { 342996df7d3eSAtari911 if (typeof text !== 'string') { 343096df7d3eSAtari911 console.log('[eventSearchNormalize] WARNING: text is not a string:', typeof text, text); 343196df7d3eSAtari911 return ''; 343296df7d3eSAtari911 } 343396df7d3eSAtari911 return text 343496df7d3eSAtari911 .toLowerCase() 343596df7d3eSAtari911 .trim() 343696df7d3eSAtari911 // Remove common punctuation that might differ 343796df7d3eSAtari911 .replace(/[''\u2018\u2019]/g, '') // Remove apostrophes/quotes 343896df7d3eSAtari911 .replace(/["""\u201C\u201D]/g, '') // Remove smart quotes 343996df7d3eSAtari911 .replace(/[-–—]/g, ' ') // Dashes to spaces 344096df7d3eSAtari911 .replace(/[.,!?;:]/g, '') // Remove punctuation 344196df7d3eSAtari911 .replace(/\s+/g, ' ') // Normalize whitespace 344296df7d3eSAtari911 .trim(); 344396df7d3eSAtari911} 344496df7d3eSAtari911 344596df7d3eSAtari911// Check if search term matches text for event filtering 344696df7d3eSAtari911function eventSearchMatch(text, searchTerm) { 344796df7d3eSAtari911 const normalizedText = eventSearchNormalize(text); 344896df7d3eSAtari911 const normalizedSearch = eventSearchNormalize(searchTerm); 344996df7d3eSAtari911 345096df7d3eSAtari911 // Direct match after normalization 345196df7d3eSAtari911 if (normalizedText.includes(normalizedSearch)) { 345296df7d3eSAtari911 return true; 345396df7d3eSAtari911 } 345496df7d3eSAtari911 345596df7d3eSAtari911 // Split search into words and check if all words are present 345696df7d3eSAtari911 const searchWords = normalizedSearch.split(' ').filter(w => w.length > 0); 345796df7d3eSAtari911 if (searchWords.length > 1) { 345896df7d3eSAtari911 return searchWords.every(word => normalizedText.includes(word)); 345996df7d3eSAtari911 } 346096df7d3eSAtari911 346196df7d3eSAtari911 return false; 346296df7d3eSAtari911} 346396df7d3eSAtari911 34641d05cddcSAtari911// Filter events by search term 34651d05cddcSAtari911window.filterEvents = function(calId, searchTerm) { 34661d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 34671d05cddcSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 346896df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 34691d05cddcSAtari911 34701d05cddcSAtari911 if (!eventList) return; 34711d05cddcSAtari911 347296df7d3eSAtari911 // Check if we're in "all dates" mode 347396df7d3eSAtari911 const isAllDatesMode = searchMode && searchMode.classList.contains('all-dates'); 347496df7d3eSAtari911 34751d05cddcSAtari911 // Show/hide clear button 34761d05cddcSAtari911 if (searchClear) { 34771d05cddcSAtari911 searchClear.style.display = searchTerm ? 'block' : 'none'; 34781d05cddcSAtari911 } 34791d05cddcSAtari911 348096df7d3eSAtari911 searchTerm = searchTerm.trim(); 348196df7d3eSAtari911 348296df7d3eSAtari911 // If all-dates mode and we have a search term, do AJAX search 348396df7d3eSAtari911 if (isAllDatesMode && searchTerm.length >= 2) { 348496df7d3eSAtari911 searchAllDates(calId, searchTerm); 348596df7d3eSAtari911 return; 348696df7d3eSAtari911 } 348796df7d3eSAtari911 348896df7d3eSAtari911 // If all-dates mode but search cleared, restore normal view 348996df7d3eSAtari911 if (isAllDatesMode && !searchTerm) { 349096df7d3eSAtari911 // Remove search results container if exists 349196df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 349296df7d3eSAtari911 if (resultsContainer) { 349396df7d3eSAtari911 resultsContainer.remove(); 349496df7d3eSAtari911 } 349596df7d3eSAtari911 // Show normal event items 349696df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 349796df7d3eSAtari911 item.style.display = ''; 349896df7d3eSAtari911 }); 349996df7d3eSAtari911 // Show past events toggle if it exists 350096df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 350196df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 350296df7d3eSAtari911 } 35031d05cddcSAtari911 35041d05cddcSAtari911 // Get all event items 35051d05cddcSAtari911 const eventItems = eventList.querySelectorAll('.event-compact-item'); 35061d05cddcSAtari911 let visibleCount = 0; 35071d05cddcSAtari911 let hiddenPastCount = 0; 35081d05cddcSAtari911 35091d05cddcSAtari911 eventItems.forEach(item => { 35101d05cddcSAtari911 const title = item.querySelector('.event-title-compact'); 35111d05cddcSAtari911 const description = item.querySelector('.event-desc-compact'); 35121d05cddcSAtari911 const dateTime = item.querySelector('.event-date-time'); 35131d05cddcSAtari911 35141d05cddcSAtari911 // Build searchable text 35151d05cddcSAtari911 let searchableText = ''; 351696df7d3eSAtari911 if (title) searchableText += title.textContent + ' '; 351796df7d3eSAtari911 if (description) searchableText += description.textContent + ' '; 351896df7d3eSAtari911 if (dateTime) searchableText += dateTime.textContent + ' '; 35191d05cddcSAtari911 352096df7d3eSAtari911 // Check if matches search using fuzzy matching 352196df7d3eSAtari911 const matches = !searchTerm || eventSearchMatch(searchableText, searchTerm); 35221d05cddcSAtari911 35231d05cddcSAtari911 if (matches) { 35241d05cddcSAtari911 item.style.display = ''; 35251d05cddcSAtari911 visibleCount++; 35261d05cddcSAtari911 } else { 35271d05cddcSAtari911 item.style.display = 'none'; 35281d05cddcSAtari911 // Check if this is a past event 35291d05cddcSAtari911 if (item.classList.contains('event-past') || item.classList.contains('event-completed')) { 35301d05cddcSAtari911 hiddenPastCount++; 35311d05cddcSAtari911 } 35321d05cddcSAtari911 } 35331d05cddcSAtari911 }); 35341d05cddcSAtari911 35351d05cddcSAtari911 // Update past events toggle if it exists 35361d05cddcSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 35371d05cddcSAtari911 const pastLabel = eventList.querySelector('.past-events-label'); 35381d05cddcSAtari911 const pastContent = document.getElementById('past-events-' + calId); 35391d05cddcSAtari911 35401d05cddcSAtari911 if (pastToggle && pastLabel && pastContent) { 35411d05cddcSAtari911 const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])'); 35421d05cddcSAtari911 const totalPastVisible = visiblePastEvents.length; 35431d05cddcSAtari911 35441d05cddcSAtari911 if (totalPastVisible > 0) { 35451d05cddcSAtari911 pastLabel.textContent = `Past Events (${totalPastVisible})`; 35461d05cddcSAtari911 pastToggle.style.display = ''; 35471d05cddcSAtari911 } else { 35481d05cddcSAtari911 pastToggle.style.display = 'none'; 35491d05cddcSAtari911 } 35501d05cddcSAtari911 } 35511d05cddcSAtari911 355296df7d3eSAtari911 // Show "no results" message if nothing visible (only for month mode, not all-dates mode) 35531d05cddcSAtari911 let noResultsMsg = eventList.querySelector('.no-search-results'); 355496df7d3eSAtari911 if (visibleCount === 0 && searchTerm && !isAllDatesMode) { 35551d05cddcSAtari911 if (!noResultsMsg) { 35561d05cddcSAtari911 noResultsMsg = document.createElement('p'); 35571d05cddcSAtari911 noResultsMsg.className = 'no-search-results no-events-msg'; 35581d05cddcSAtari911 noResultsMsg.textContent = 'No events match your search'; 35591d05cddcSAtari911 eventList.appendChild(noResultsMsg); 35601d05cddcSAtari911 } 35611d05cddcSAtari911 noResultsMsg.style.display = 'block'; 35621d05cddcSAtari911 } else if (noResultsMsg) { 35631d05cddcSAtari911 noResultsMsg.style.display = 'none'; 35641d05cddcSAtari911 } 35651d05cddcSAtari911}; 35661d05cddcSAtari911 356796df7d3eSAtari911// Toggle search mode between "this month" and "all dates" 356896df7d3eSAtari911window.toggleSearchMode = function(calId, namespace) { 356996df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 357096df7d3eSAtari911 const searchInput = document.getElementById('event-search-' + calId); 357196df7d3eSAtari911 357296df7d3eSAtari911 if (!searchMode) return; 357396df7d3eSAtari911 357496df7d3eSAtari911 const isAllDates = searchMode.classList.toggle('all-dates'); 357596df7d3eSAtari911 357696df7d3eSAtari911 // Update button icon and title 357796df7d3eSAtari911 if (isAllDates) { 357896df7d3eSAtari911 searchMode.innerHTML = ''; 357996df7d3eSAtari911 searchMode.title = 'Searching all dates'; 358096df7d3eSAtari911 if (searchInput) { 358196df7d3eSAtari911 searchInput.placeholder = 'Search all dates...'; 358296df7d3eSAtari911 } 358396df7d3eSAtari911 } else { 358496df7d3eSAtari911 searchMode.innerHTML = ''; 358596df7d3eSAtari911 searchMode.title = 'Search this month only'; 358696df7d3eSAtari911 if (searchInput) { 358796df7d3eSAtari911 searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : ' Search...'; 358896df7d3eSAtari911 } 358996df7d3eSAtari911 } 359096df7d3eSAtari911 359196df7d3eSAtari911 // Re-run search with current term 359296df7d3eSAtari911 if (searchInput && searchInput.value) { 359396df7d3eSAtari911 filterEvents(calId, searchInput.value); 359496df7d3eSAtari911 } else { 359596df7d3eSAtari911 // Clear any all-dates results 359696df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 359796df7d3eSAtari911 if (eventList) { 359896df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 359996df7d3eSAtari911 if (resultsContainer) { 360096df7d3eSAtari911 resultsContainer.remove(); 360196df7d3eSAtari911 } 360296df7d3eSAtari911 // Show normal event items 360396df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 360496df7d3eSAtari911 item.style.display = ''; 360596df7d3eSAtari911 }); 360696df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 360796df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 360896df7d3eSAtari911 } 360996df7d3eSAtari911 } 361096df7d3eSAtari911}; 361196df7d3eSAtari911 361296df7d3eSAtari911// Search all dates via AJAX 361396df7d3eSAtari911window.searchAllDates = function(calId, searchTerm) { 361496df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 361596df7d3eSAtari911 if (!eventList) return; 361696df7d3eSAtari911 361796df7d3eSAtari911 // Get namespace from container 361896df7d3eSAtari911 const container = document.getElementById(calId); 361996df7d3eSAtari911 const namespace = container ? (container.dataset.namespace || '') : ''; 362096df7d3eSAtari911 362196df7d3eSAtari911 // Hide normal event items 362296df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 362396df7d3eSAtari911 item.style.display = 'none'; 362496df7d3eSAtari911 }); 362596df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 362696df7d3eSAtari911 if (pastToggle) pastToggle.style.display = 'none'; 362796df7d3eSAtari911 362896df7d3eSAtari911 // Remove old results container 362996df7d3eSAtari911 let resultsContainer = eventList.querySelector('.all-dates-results'); 363096df7d3eSAtari911 if (resultsContainer) { 363196df7d3eSAtari911 resultsContainer.remove(); 363296df7d3eSAtari911 } 363396df7d3eSAtari911 363496df7d3eSAtari911 // Create new results container 363596df7d3eSAtari911 resultsContainer = document.createElement('div'); 363696df7d3eSAtari911 resultsContainer.className = 'all-dates-results'; 363796df7d3eSAtari911 resultsContainer.innerHTML = '<p class="search-loading" style="text-align:center; padding:20px; color:var(--text-dim);"> Searching all dates...</p>'; 363896df7d3eSAtari911 eventList.appendChild(resultsContainer); 363996df7d3eSAtari911 364096df7d3eSAtari911 // Make AJAX request 364196df7d3eSAtari911 const params = new URLSearchParams({ 364296df7d3eSAtari911 call: 'plugin_calendar', 364396df7d3eSAtari911 action: 'search_all', 364496df7d3eSAtari911 search: searchTerm, 364596df7d3eSAtari911 namespace: namespace, 364696df7d3eSAtari911 _: new Date().getTime() 364796df7d3eSAtari911 }); 364896df7d3eSAtari911 364996df7d3eSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 365096df7d3eSAtari911 method: 'POST', 365196df7d3eSAtari911 headers: { 365296df7d3eSAtari911 'Content-Type': 'application/x-www-form-urlencoded' 365396df7d3eSAtari911 }, 365496df7d3eSAtari911 body: params.toString() 365596df7d3eSAtari911 }) 365696df7d3eSAtari911 .then(r => r.json()) 365796df7d3eSAtari911 .then(data => { 365896df7d3eSAtari911 if (data.success && data.results) { 365996df7d3eSAtari911 if (data.results.length === 0) { 366096df7d3eSAtari911 resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim); font-style:italic;">No events found matching "' + escapeHtml(searchTerm) + '"</p>'; 366196df7d3eSAtari911 } else { 366296df7d3eSAtari911 let html = '<div class="all-dates-header" style="padding:4px 8px; background:var(--cell-today-bg, #e8f5e9); font-size:10px; font-weight:600; color:var(--text-bright, #00cc07); border-bottom:1px solid var(--border-color);">Found ' + data.results.length + ' event(s) across all dates</div>'; 366396df7d3eSAtari911 366496df7d3eSAtari911 data.results.forEach(event => { 366596df7d3eSAtari911 const dateObj = new Date(event.date + 'T00:00:00'); 366696df7d3eSAtari911 const dateDisplay = dateObj.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); 366796df7d3eSAtari911 const color = event.color || 'var(--text-bright, #00cc07)'; 366896df7d3eSAtari911 366996df7d3eSAtari911 html += '<div class="event-compact-item search-result-item" style="display:flex; border-bottom:1px solid var(--border-color, #e0e0e0); padding:6px 8px; gap:6px; cursor:pointer;" onclick="jumpToDate(\'' + calId + '\', \'' + event.date + '\', \'' + namespace + '\')">'; 367096df7d3eSAtari911 html += '<div style="width:3px; background:' + color + '; border-radius:1px; flex-shrink:0;"></div>'; 367196df7d3eSAtari911 html += '<div style="flex:1; min-width:0;">'; 367296df7d3eSAtari911 html += '<div class="event-title-compact" style="font-weight:600; color:var(--text-primary); font-size:11px;">' + escapeHtml(event.title) + '</div>'; 367396df7d3eSAtari911 html += '<div class="event-date-time" style="font-size:10px; color:var(--text-dim);">' + dateDisplay; 367496df7d3eSAtari911 if (event.time) { 367596df7d3eSAtari911 html += ' • ' + formatTimeRange(event.time, event.endTime); 367696df7d3eSAtari911 } 367796df7d3eSAtari911 html += '</div>'; 367896df7d3eSAtari911 if (event.namespace) { 367996df7d3eSAtari911 html += '<span style="font-size:9px; background:var(--text-bright); color:var(--background-site); padding:1px 4px; border-radius:2px; margin-top:2px; display:inline-block;">' + escapeHtml(event.namespace) + '</span>'; 368096df7d3eSAtari911 } 368196df7d3eSAtari911 html += '</div></div>'; 368296df7d3eSAtari911 }); 368396df7d3eSAtari911 368496df7d3eSAtari911 resultsContainer.innerHTML = html; 368596df7d3eSAtari911 } 368696df7d3eSAtari911 } else { 368796df7d3eSAtari911 resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>'; 368896df7d3eSAtari911 } 368996df7d3eSAtari911 }) 369096df7d3eSAtari911 .catch(err => { 369196df7d3eSAtari911 console.error('Search error:', err); 369296df7d3eSAtari911 resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>'; 369396df7d3eSAtari911 }); 369496df7d3eSAtari911}; 369596df7d3eSAtari911 369696df7d3eSAtari911// Jump to a specific date (used by search results) 369796df7d3eSAtari911window.jumpToDate = function(calId, date, namespace) { 369896df7d3eSAtari911 const parts = date.split('-'); 369996df7d3eSAtari911 const year = parseInt(parts[0]); 370096df7d3eSAtari911 const month = parseInt(parts[1]); 370196df7d3eSAtari911 370296df7d3eSAtari911 // Get container to check current month 370396df7d3eSAtari911 const container = document.getElementById(calId); 370496df7d3eSAtari911 const currentYear = container ? parseInt(container.dataset.year) : year; 370596df7d3eSAtari911 const currentMonth = container ? parseInt(container.dataset.month) : month; 370696df7d3eSAtari911 370796df7d3eSAtari911 // Get search elements 370896df7d3eSAtari911 const searchInput = document.getElementById('event-search-' + calId); 370996df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 371096df7d3eSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 371196df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 371296df7d3eSAtari911 371396df7d3eSAtari911 // Remove the all-dates results container 371496df7d3eSAtari911 if (eventList) { 371596df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 371696df7d3eSAtari911 if (resultsContainer) { 371796df7d3eSAtari911 resultsContainer.remove(); 371896df7d3eSAtari911 } 371996df7d3eSAtari911 // Show normal event items again 372096df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 372196df7d3eSAtari911 item.style.display = ''; 372296df7d3eSAtari911 }); 372396df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 372496df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 372596df7d3eSAtari911 372696df7d3eSAtari911 // Hide any no-results message 372796df7d3eSAtari911 const noResults = eventList.querySelector('.no-search-results'); 372896df7d3eSAtari911 if (noResults) noResults.style.display = 'none'; 372996df7d3eSAtari911 } 373096df7d3eSAtari911 373196df7d3eSAtari911 // Clear search input 373296df7d3eSAtari911 if (searchInput) { 373396df7d3eSAtari911 searchInput.value = ''; 373496df7d3eSAtari911 } 373596df7d3eSAtari911 373696df7d3eSAtari911 // Hide clear button 373796df7d3eSAtari911 if (searchClear) { 373896df7d3eSAtari911 searchClear.style.display = 'none'; 373996df7d3eSAtari911 } 374096df7d3eSAtari911 374196df7d3eSAtari911 // Switch back to month mode 374296df7d3eSAtari911 if (searchMode && searchMode.classList.contains('all-dates')) { 374396df7d3eSAtari911 searchMode.classList.remove('all-dates'); 374496df7d3eSAtari911 searchMode.innerHTML = ''; 374596df7d3eSAtari911 searchMode.title = 'Search this month only'; 374696df7d3eSAtari911 if (searchInput) { 374796df7d3eSAtari911 searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : ' Search...'; 374896df7d3eSAtari911 } 374996df7d3eSAtari911 } 375096df7d3eSAtari911 375196df7d3eSAtari911 // Check if we need to navigate to a different month 375296df7d3eSAtari911 if (year !== currentYear || month !== currentMonth) { 375396df7d3eSAtari911 // Navigate to the target month, then show popup 375496df7d3eSAtari911 navCalendar(calId, year, month, namespace); 375596df7d3eSAtari911 375696df7d3eSAtari911 // After navigation completes, show the day popup 375796df7d3eSAtari911 setTimeout(() => { 375896df7d3eSAtari911 showDayPopup(calId, date, namespace); 375996df7d3eSAtari911 }, 400); 376096df7d3eSAtari911 } else { 376196df7d3eSAtari911 // Same month - just show the popup 376296df7d3eSAtari911 showDayPopup(calId, date, namespace); 376396df7d3eSAtari911 } 376496df7d3eSAtari911}; 376596df7d3eSAtari911 37661d05cddcSAtari911// Clear event search 37671d05cddcSAtari911window.clearEventSearch = function(calId) { 37681d05cddcSAtari911 const searchInput = document.getElementById('event-search-' + calId); 37691d05cddcSAtari911 if (searchInput) { 37701d05cddcSAtari911 searchInput.value = ''; 37711d05cddcSAtari911 filterEvents(calId, ''); 37721d05cddcSAtari911 searchInput.focus(); 37731d05cddcSAtari911 } 37741d05cddcSAtari911}; 37751d05cddcSAtari911 37769ccd446eSAtari911// ============================================ 37779ccd446eSAtari911// PINK THEME - GLOWING PARTICLE EFFECTS 37789ccd446eSAtari911// ============================================ 37799ccd446eSAtari911 37809ccd446eSAtari911// Create glowing pink particle effects for pink theme 37819ccd446eSAtari911(function() { 37829ccd446eSAtari911 let pinkThemeActive = false; 37839ccd446eSAtari911 let trailTimer = null; 37849ccd446eSAtari911 let pixelTimer = null; 37859ccd446eSAtari911 37869ccd446eSAtari911 // Check if pink theme is active 37879ccd446eSAtari911 function checkPinkTheme() { 37889ccd446eSAtari911 const pinkCalendars = document.querySelectorAll('.calendar-theme-pink'); 37899ccd446eSAtari911 pinkThemeActive = pinkCalendars.length > 0; 37909ccd446eSAtari911 return pinkThemeActive; 37919ccd446eSAtari911 } 37929ccd446eSAtari911 37939ccd446eSAtari911 // Create trail particle 37949ccd446eSAtari911 function createTrailParticle(clientX, clientY) { 37959ccd446eSAtari911 if (!pinkThemeActive) return; 37969ccd446eSAtari911 37979ccd446eSAtari911 const trail = document.createElement('div'); 37989ccd446eSAtari911 trail.className = 'pink-cursor-trail'; 37999ccd446eSAtari911 trail.style.left = clientX + 'px'; 38009ccd446eSAtari911 trail.style.top = clientY + 'px'; 38019ccd446eSAtari911 trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards'; 38029ccd446eSAtari911 38039ccd446eSAtari911 document.body.appendChild(trail); 38049ccd446eSAtari911 38059ccd446eSAtari911 setTimeout(function() { 38069ccd446eSAtari911 trail.remove(); 38079ccd446eSAtari911 }, 500); 38089ccd446eSAtari911 } 38099ccd446eSAtari911 38109ccd446eSAtari911 // Create pixel sparkles 38119ccd446eSAtari911 function createPixelSparkles(clientX, clientY) { 38129ccd446eSAtari911 if (!pinkThemeActive || pixelTimer) return; 38139ccd446eSAtari911 38149ccd446eSAtari911 const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels 38159ccd446eSAtari911 38169ccd446eSAtari911 for (let i = 0; i < pixelCount; i++) { 38179ccd446eSAtari911 const pixel = document.createElement('div'); 38189ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 38199ccd446eSAtari911 38209ccd446eSAtari911 // Random offset from cursor 38219ccd446eSAtari911 const offsetX = (Math.random() - 0.5) * 30; 38229ccd446eSAtari911 const offsetY = (Math.random() - 0.5) * 30; 38239ccd446eSAtari911 38249ccd446eSAtari911 pixel.style.left = (clientX + offsetX) + 'px'; 38259ccd446eSAtari911 pixel.style.top = (clientY + offsetY) + 'px'; 38269ccd446eSAtari911 38279ccd446eSAtari911 // Random color - bright neon pinks and whites 38289ccd446eSAtari911 const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 38299ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 38309ccd446eSAtari911 pixel.style.background = color; 38319ccd446eSAtari911 pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff'; 38329ccd446eSAtari911 38339ccd446eSAtari911 // Random animation 38349ccd446eSAtari911 if (Math.random() > 0.5) { 38359ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards'; 38369ccd446eSAtari911 } else { 38379ccd446eSAtari911 pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards'; 38389ccd446eSAtari911 } 38399ccd446eSAtari911 38409ccd446eSAtari911 document.body.appendChild(pixel); 38419ccd446eSAtari911 38429ccd446eSAtari911 setTimeout(function() { 38439ccd446eSAtari911 pixel.remove(); 38449ccd446eSAtari911 }, 800); 38459ccd446eSAtari911 } 38469ccd446eSAtari911 38479ccd446eSAtari911 pixelTimer = setTimeout(function() { 38489ccd446eSAtari911 pixelTimer = null; 38499ccd446eSAtari911 }, 40); 38509ccd446eSAtari911 } 38519ccd446eSAtari911 38529ccd446eSAtari911 // Create explosion 38539ccd446eSAtari911 function createExplosion(clientX, clientY) { 38549ccd446eSAtari911 if (!pinkThemeActive) return; 38559ccd446eSAtari911 38569ccd446eSAtari911 const particleCount = 25; 38579ccd446eSAtari911 const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff']; 38589ccd446eSAtari911 38599ccd446eSAtari911 // Add hearts to explosion (8-12 hearts) 38609ccd446eSAtari911 const heartCount = 8 + Math.floor(Math.random() * 5); 38619ccd446eSAtari911 for (let i = 0; i < heartCount; i++) { 38629ccd446eSAtari911 const heart = document.createElement('div'); 38639ccd446eSAtari911 heart.textContent = ''; 38649ccd446eSAtari911 heart.style.position = 'fixed'; 38659ccd446eSAtari911 heart.style.left = clientX + 'px'; 38669ccd446eSAtari911 heart.style.top = clientY + 'px'; 38679ccd446eSAtari911 heart.style.pointerEvents = 'none'; 38689ccd446eSAtari911 heart.style.zIndex = '9999999'; 38699ccd446eSAtari911 heart.style.fontSize = (12 + Math.random() * 16) + 'px'; 38709ccd446eSAtari911 38719ccd446eSAtari911 // Random direction 38729ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 38739ccd446eSAtari911 const velocity = 60 + Math.random() * 80; 38749ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 38759ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 38769ccd446eSAtari911 38779ccd446eSAtari911 heart.style.setProperty('--tx', tx + 'px'); 38789ccd446eSAtari911 heart.style.setProperty('--ty', ty + 'px'); 38799ccd446eSAtari911 38809ccd446eSAtari911 const duration = 0.8 + Math.random() * 0.4; 38819ccd446eSAtari911 heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 38829ccd446eSAtari911 38839ccd446eSAtari911 document.body.appendChild(heart); 38849ccd446eSAtari911 38859ccd446eSAtari911 setTimeout(function() { 38869ccd446eSAtari911 heart.remove(); 38879ccd446eSAtari911 }, duration * 1000); 38889ccd446eSAtari911 } 38899ccd446eSAtari911 38909ccd446eSAtari911 // Main explosion particles 38919ccd446eSAtari911 for (let i = 0; i < particleCount; i++) { 38929ccd446eSAtari911 const particle = document.createElement('div'); 38939ccd446eSAtari911 particle.className = 'pink-particle'; 38949ccd446eSAtari911 38959ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 38969ccd446eSAtari911 particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)'; 38979ccd446eSAtari911 particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color; 38989ccd446eSAtari911 38999ccd446eSAtari911 particle.style.left = clientX + 'px'; 39009ccd446eSAtari911 particle.style.top = clientY + 'px'; 39019ccd446eSAtari911 39029ccd446eSAtari911 const angle = (Math.PI * 2 * i) / particleCount; 39039ccd446eSAtari911 const velocity = 50 + Math.random() * 100; 39049ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 39059ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 39069ccd446eSAtari911 39079ccd446eSAtari911 particle.style.setProperty('--tx', tx + 'px'); 39089ccd446eSAtari911 particle.style.setProperty('--ty', ty + 'px'); 39099ccd446eSAtari911 39109ccd446eSAtari911 const size = 4 + Math.random() * 6; 39119ccd446eSAtari911 particle.style.width = size + 'px'; 39129ccd446eSAtari911 particle.style.height = size + 'px'; 39139ccd446eSAtari911 39149ccd446eSAtari911 const duration = 0.6 + Math.random() * 0.4; 39159ccd446eSAtari911 particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 39169ccd446eSAtari911 39179ccd446eSAtari911 document.body.appendChild(particle); 39189ccd446eSAtari911 39199ccd446eSAtari911 setTimeout(function() { 39209ccd446eSAtari911 particle.remove(); 39219ccd446eSAtari911 }, duration * 1000); 39229ccd446eSAtari911 } 39239ccd446eSAtari911 39249ccd446eSAtari911 // Pixel sparkles 39259ccd446eSAtari911 const pixelSparkleCount = 40; 39269ccd446eSAtari911 39279ccd446eSAtari911 for (let i = 0; i < pixelSparkleCount; i++) { 39289ccd446eSAtari911 const pixel = document.createElement('div'); 39299ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 39309ccd446eSAtari911 39319ccd446eSAtari911 const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 39329ccd446eSAtari911 const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)]; 39339ccd446eSAtari911 pixel.style.background = pixelColor; 39349ccd446eSAtari911 pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff'; 39359ccd446eSAtari911 39369ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 39379ccd446eSAtari911 const distance = 30 + Math.random() * 80; 39389ccd446eSAtari911 const offsetX = Math.cos(angle) * distance; 39399ccd446eSAtari911 const offsetY = Math.sin(angle) * distance; 39409ccd446eSAtari911 39419ccd446eSAtari911 pixel.style.left = clientX + 'px'; 39429ccd446eSAtari911 pixel.style.top = clientY + 'px'; 39439ccd446eSAtari911 pixel.style.setProperty('--tx', offsetX + 'px'); 39449ccd446eSAtari911 pixel.style.setProperty('--ty', offsetY + 'px'); 39459ccd446eSAtari911 39469ccd446eSAtari911 const pixelSize = 1 + Math.random() * 2; 39479ccd446eSAtari911 pixel.style.width = pixelSize + 'px'; 39489ccd446eSAtari911 pixel.style.height = pixelSize + 'px'; 39499ccd446eSAtari911 39509ccd446eSAtari911 const duration = 0.4 + Math.random() * 0.4; 39519ccd446eSAtari911 if (Math.random() > 0.5) { 39529ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards'; 39539ccd446eSAtari911 } else { 39549ccd446eSAtari911 pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 39559ccd446eSAtari911 } 39569ccd446eSAtari911 39579ccd446eSAtari911 document.body.appendChild(pixel); 39589ccd446eSAtari911 39599ccd446eSAtari911 setTimeout(function() { 39609ccd446eSAtari911 pixel.remove(); 39619ccd446eSAtari911 }, duration * 1000); 39629ccd446eSAtari911 } 39639ccd446eSAtari911 39649ccd446eSAtari911 // Flash 39659ccd446eSAtari911 const flash = document.createElement('div'); 39669ccd446eSAtari911 flash.style.position = 'fixed'; 39679ccd446eSAtari911 flash.style.left = clientX + 'px'; 39689ccd446eSAtari911 flash.style.top = clientY + 'px'; 39699ccd446eSAtari911 flash.style.width = '40px'; 39709ccd446eSAtari911 flash.style.height = '40px'; 39719ccd446eSAtari911 flash.style.borderRadius = '50%'; 39729ccd446eSAtari911 flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)'; 39739ccd446eSAtari911 flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4'; 39749ccd446eSAtari911 flash.style.pointerEvents = 'none'; 39759ccd446eSAtari911 flash.style.zIndex = '9999999'; // Above everything including dialogs 39769ccd446eSAtari911 flash.style.transform = 'translate(-50%, -50%)'; 39779ccd446eSAtari911 flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards'; 39789ccd446eSAtari911 39799ccd446eSAtari911 document.body.appendChild(flash); 39809ccd446eSAtari911 39819ccd446eSAtari911 setTimeout(function() { 39829ccd446eSAtari911 flash.remove(); 39839ccd446eSAtari911 }, 300); 39849ccd446eSAtari911 } 39859ccd446eSAtari911 39869ccd446eSAtari911 function initPinkParticles() { 39879ccd446eSAtari911 if (!checkPinkTheme()) return; 39889ccd446eSAtari911 39899ccd446eSAtari911 // Use capture phase to catch events before stopPropagation 39909ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 39919ccd446eSAtari911 if (!pinkThemeActive) return; 39929ccd446eSAtari911 39939ccd446eSAtari911 createTrailParticle(e.clientX, e.clientY); 39949ccd446eSAtari911 createPixelSparkles(e.clientX, e.clientY); 39959ccd446eSAtari911 }, true); // Capture phase! 39969ccd446eSAtari911 39979ccd446eSAtari911 // Throttle main trail 39989ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 39999ccd446eSAtari911 if (!pinkThemeActive || trailTimer) return; 40009ccd446eSAtari911 40019ccd446eSAtari911 trailTimer = setTimeout(function() { 40029ccd446eSAtari911 trailTimer = null; 40039ccd446eSAtari911 }, 30); 40049ccd446eSAtari911 }, true); // Capture phase! 40059ccd446eSAtari911 40069ccd446eSAtari911 // Click explosion - use capture phase 40079ccd446eSAtari911 document.addEventListener('click', function(e) { 40089ccd446eSAtari911 if (!pinkThemeActive) return; 40099ccd446eSAtari911 40109ccd446eSAtari911 createExplosion(e.clientX, e.clientY); 40119ccd446eSAtari911 }, true); // Capture phase! 40129ccd446eSAtari911 } 40139ccd446eSAtari911 40149ccd446eSAtari911 // Initialize on load 40159ccd446eSAtari911 if (document.readyState === 'loading') { 40169ccd446eSAtari911 document.addEventListener('DOMContentLoaded', initPinkParticles); 40179ccd446eSAtari911 } else { 40189ccd446eSAtari911 initPinkParticles(); 40199ccd446eSAtari911 } 40209ccd446eSAtari911 40219ccd446eSAtari911 // Re-check theme if calendar is dynamically added 402296df7d3eSAtari911 // Must wait for document.body to exist 402396df7d3eSAtari911 function setupMutationObserver() { 402496df7d3eSAtari911 if (typeof MutationObserver !== 'undefined' && document.body) { 40259ccd446eSAtari911 const observer = new MutationObserver(function(mutations) { 40269ccd446eSAtari911 mutations.forEach(function(mutation) { 40279ccd446eSAtari911 if (mutation.addedNodes.length > 0) { 40289ccd446eSAtari911 mutation.addedNodes.forEach(function(node) { 40299ccd446eSAtari911 if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) { 40309ccd446eSAtari911 checkPinkTheme(); 40319ccd446eSAtari911 initPinkParticles(); 40329ccd446eSAtari911 } 40339ccd446eSAtari911 }); 40349ccd446eSAtari911 } 40359ccd446eSAtari911 }); 40369ccd446eSAtari911 }); 40379ccd446eSAtari911 40389ccd446eSAtari911 observer.observe(document.body, { 40399ccd446eSAtari911 childList: true, 40409ccd446eSAtari911 subtree: true 40419ccd446eSAtari911 }); 40429ccd446eSAtari911 } 404396df7d3eSAtari911 } 404496df7d3eSAtari911 404596df7d3eSAtari911 // Setup observer when DOM is ready 404696df7d3eSAtari911 if (document.readyState === 'loading') { 404796df7d3eSAtari911 document.addEventListener('DOMContentLoaded', setupMutationObserver); 404896df7d3eSAtari911 } else { 404996df7d3eSAtari911 setupMutationObserver(); 405096df7d3eSAtari911 } 40519ccd446eSAtari911})(); 40529ccd446eSAtari911 4053da206178SAtari911// Mobile touch event delegation for edit/delete buttons 4054da206178SAtari911// This ensures buttons work on mobile where onclick may not fire reliably 4055da206178SAtari911(function() { 4056da206178SAtari911 function handleButtonTouch(e) { 4057da206178SAtari911 const btn = e.target.closest('.event-edit-btn, .event-delete-btn, .event-action-btn'); 4058da206178SAtari911 if (!btn) return; 4059da206178SAtari911 4060da206178SAtari911 // Prevent double-firing with onclick 4061da206178SAtari911 e.preventDefault(); 4062da206178SAtari911 4063da206178SAtari911 // Small delay to show visual feedback 4064da206178SAtari911 setTimeout(function() { 4065da206178SAtari911 btn.click(); 4066da206178SAtari911 }, 10); 4067da206178SAtari911 } 4068da206178SAtari911 4069da206178SAtari911 // Use touchend for more reliable mobile handling 4070da206178SAtari911 document.addEventListener('touchend', handleButtonTouch, { passive: false }); 4071da206178SAtari911})(); 4072da206178SAtari911 4073da206178SAtari911// Static calendar navigation 4074da206178SAtari911window.navStaticCalendar = function(calId, direction) { 4075da206178SAtari911 const container = document.getElementById(calId); 4076da206178SAtari911 if (!container) return; 4077da206178SAtari911 4078da206178SAtari911 let year = parseInt(container.dataset.year); 4079da206178SAtari911 let month = parseInt(container.dataset.month); 4080da206178SAtari911 const namespace = container.dataset.namespace || ''; 4081da206178SAtari911 4082da206178SAtari911 // Calculate new month 4083da206178SAtari911 month += direction; 4084da206178SAtari911 if (month < 1) { 4085da206178SAtari911 month = 12; 4086da206178SAtari911 year--; 4087da206178SAtari911 } else if (month > 12) { 4088da206178SAtari911 month = 1; 4089da206178SAtari911 year++; 4090da206178SAtari911 } 4091da206178SAtari911 4092da206178SAtari911 // Fetch new calendar content via AJAX 4093da206178SAtari911 const params = new URLSearchParams({ 4094da206178SAtari911 call: 'plugin_calendar', 4095da206178SAtari911 action: 'get_static_calendar', 4096da206178SAtari911 year: year, 4097da206178SAtari911 month: month, 4098da206178SAtari911 namespace: namespace 4099da206178SAtari911 }); 4100da206178SAtari911 4101da206178SAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 4102da206178SAtari911 method: 'POST', 4103da206178SAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 4104da206178SAtari911 body: params.toString() 4105da206178SAtari911 }) 4106da206178SAtari911 .then(r => r.json()) 4107da206178SAtari911 .then(data => { 4108da206178SAtari911 if (data.success && data.html) { 4109da206178SAtari911 // Replace the container content 4110da206178SAtari911 container.outerHTML = data.html; 4111da206178SAtari911 } 4112da206178SAtari911 }) 4113da206178SAtari911 .catch(err => console.error('Static calendar navigation error:', err)); 4114da206178SAtari911}; 4115da206178SAtari911 4116da206178SAtari911// Print static calendar - opens print dialog with only calendar content 4117da206178SAtari911window.printStaticCalendar = function(calId) { 4118da206178SAtari911 const container = document.getElementById(calId); 4119da206178SAtari911 if (!container) return; 4120da206178SAtari911 4121da206178SAtari911 // Get the print view content 4122da206178SAtari911 const printView = container.querySelector('.static-print-view'); 4123da206178SAtari911 if (!printView) return; 4124da206178SAtari911 4125da206178SAtari911 // Create a new window for printing 4126da206178SAtari911 const printWindow = window.open('', '_blank', 'width=800,height=600'); 4127da206178SAtari911 4128da206178SAtari911 // Build print document with inline margins for maximum compatibility 4129da206178SAtari911 const printContent = ` 4130da206178SAtari911<!DOCTYPE html> 4131da206178SAtari911<html> 4132da206178SAtari911<head> 4133da206178SAtari911 <title>Calendar - ${container.dataset.year}-${String(container.dataset.month).padStart(2, '0')}</title> 4134da206178SAtari911 <style> 4135da206178SAtari911 * { margin: 0; padding: 0; box-sizing: border-box; } 4136da206178SAtari911 body { font-family: Arial, sans-serif; color: #333; background: white; } 4137da206178SAtari911 table { border-collapse: collapse; font-size: 12px; } 4138da206178SAtari911 th { background: #2c3e50; color: white; padding: 8px; text-align: left; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 4139da206178SAtari911 td { padding: 6px 8px; border-bottom: 1px solid #ccc; vertical-align: top; } 4140da206178SAtari911 tr:nth-child(even) { background: #f0f0f0; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 4141da206178SAtari911 .static-itinerary-important { background: #fffde7 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 4142da206178SAtari911 .static-itinerary-date { font-weight: bold; white-space: nowrap; } 4143da206178SAtari911 .static-itinerary-time { white-space: nowrap; color: #555; } 4144da206178SAtari911 .static-itinerary-title { font-weight: 500; } 4145da206178SAtari911 .static-itinerary-desc { color: #555; font-size: 11px; } 4146da206178SAtari911 thead { display: table-header-group; } 4147da206178SAtari911 tr { page-break-inside: avoid; } 4148da206178SAtari911 h2 { font-size: 16px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #333; } 4149da206178SAtari911 p { font-size: 12px; color: #666; margin-bottom: 15px; } 4150da206178SAtari911 </style> 4151da206178SAtari911</head> 4152da206178SAtari911<body style="margin: 0; padding: 0;"> 4153da206178SAtari911 <div style="padding: 50px 60px; margin: 0 auto; max-width: 800px;"> 4154da206178SAtari911 ${printView.innerHTML} 4155da206178SAtari911 </div> 4156da206178SAtari911 <script> 4157da206178SAtari911 window.onload = function() { 4158da206178SAtari911 setTimeout(function() { 4159da206178SAtari911 window.print(); 4160da206178SAtari911 }, 300); 4161da206178SAtari911 window.onafterprint = function() { 4162da206178SAtari911 window.close(); 4163da206178SAtari911 }; 4164da206178SAtari911 }; 4165da206178SAtari911 </script> 4166da206178SAtari911</body> 4167da206178SAtari911</html>`; 4168da206178SAtari911 4169da206178SAtari911 printWindow.document.write(printContent); 4170da206178SAtari911 printWindow.document.close(); 4171da206178SAtari911}; 4172da206178SAtari911 4173*815440faSAtari911// ============================================================================ 4174*815440faSAtari911// ACCESSIBILITY - Screen reader announcements 4175*815440faSAtari911// ============================================================================ 4176*815440faSAtari911 4177*815440faSAtari911// Create ARIA live region for announcements 4178*815440faSAtari911if (!document.getElementById('calendar-aria-live')) { 4179*815440faSAtari911 var ariaLive = document.createElement('div'); 4180*815440faSAtari911 ariaLive.id = 'calendar-aria-live'; 4181*815440faSAtari911 ariaLive.setAttribute('role', 'status'); 4182*815440faSAtari911 ariaLive.setAttribute('aria-live', 'polite'); 4183*815440faSAtari911 ariaLive.setAttribute('aria-atomic', 'true'); 4184*815440faSAtari911 ariaLive.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;'; 4185*815440faSAtari911 document.body.appendChild(ariaLive); 4186*815440faSAtari911} 4187*815440faSAtari911 4188*815440faSAtari911// Announce message to screen readers 4189*815440faSAtari911window.announceToScreenReader = function(message) { 4190*815440faSAtari911 var ariaLive = document.getElementById('calendar-aria-live'); 4191*815440faSAtari911 if (ariaLive) { 4192*815440faSAtari911 ariaLive.textContent = ''; 4193*815440faSAtari911 // Small delay to ensure screen reader picks up the change 4194*815440faSAtari911 setTimeout(function() { 4195*815440faSAtari911 ariaLive.textContent = message; 4196*815440faSAtari911 }, 100); 4197*815440faSAtari911 } 4198*815440faSAtari911}; 4199*815440faSAtari911 42001d05cddcSAtari911// End of calendar plugin JavaScript 4201