11d05cddcSAtari911/** 21d05cddcSAtari911 * DokuWiki Compact Calendar Plugin JavaScript 31d05cddcSAtari911 * Loaded independently to avoid DokuWiki concatenation issues 41d05cddcSAtari911 */ 51d05cddcSAtari911 61d05cddcSAtari911// Ensure DOKU_BASE is defined - check multiple sources 71d05cddcSAtari911if (typeof DOKU_BASE === 'undefined') { 81d05cddcSAtari911 // Try to get from global jsinfo object (DokuWiki standard) 91d05cddcSAtari911 if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) { 101d05cddcSAtari911 window.DOKU_BASE = window.jsinfo.dokubase; 111d05cddcSAtari911 } else { 121d05cddcSAtari911 // Fallback: extract from script source path 131d05cddcSAtari911 var scripts = document.getElementsByTagName('script'); 141d05cddcSAtari911 var pluginScriptPath = null; 151d05cddcSAtari911 for (var i = 0; i < scripts.length; i++) { 161d05cddcSAtari911 if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) { 171d05cddcSAtari911 pluginScriptPath = scripts[i].src; 181d05cddcSAtari911 break; 191d05cddcSAtari911 } 201d05cddcSAtari911 } 211d05cddcSAtari911 221d05cddcSAtari911 if (pluginScriptPath) { 231d05cddcSAtari911 // Extract base path from: .../lib/plugins/calendar/script.js 241d05cddcSAtari911 var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//); 251d05cddcSAtari911 window.DOKU_BASE = match ? match[1] : '/'; 261d05cddcSAtari911 } else { 271d05cddcSAtari911 // Last resort: use root 281d05cddcSAtari911 window.DOKU_BASE = '/'; 291d05cddcSAtari911 } 301d05cddcSAtari911 } 311d05cddcSAtari911} 321d05cddcSAtari911 331d05cddcSAtari911// Shorthand for convenience 341d05cddcSAtari911var DOKU_BASE = window.DOKU_BASE || '/'; 351d05cddcSAtari911 36*b498f308SAtari911/** 37*b498f308SAtari911 * Get DokuWiki security token from multiple possible sources 38*b498f308SAtari911 * DokuWiki stores this in different places depending on version/config 39*b498f308SAtari911 */ 40*b498f308SAtari911function getSecurityToken() { 41*b498f308SAtari911 // Try JSINFO.sectok (standard location) 42*b498f308SAtari911 if (typeof JSINFO !== 'undefined' && JSINFO.sectok) { 43*b498f308SAtari911 return JSINFO.sectok; 44*b498f308SAtari911 } 45*b498f308SAtari911 // Try window.JSINFO 46*b498f308SAtari911 if (typeof window.JSINFO !== 'undefined' && window.JSINFO.sectok) { 47*b498f308SAtari911 return window.JSINFO.sectok; 48*b498f308SAtari911 } 49*b498f308SAtari911 // Try finding it in a hidden form field (some templates/plugins add this) 50*b498f308SAtari911 var sectokInput = document.querySelector('input[name="sectok"]'); 51*b498f308SAtari911 if (sectokInput && sectokInput.value) { 52*b498f308SAtari911 return sectokInput.value; 53*b498f308SAtari911 } 54*b498f308SAtari911 // Try meta tag (some DokuWiki setups) 55*b498f308SAtari911 var sectokMeta = document.querySelector('meta[name="sectok"]'); 56*b498f308SAtari911 if (sectokMeta && sectokMeta.content) { 57*b498f308SAtari911 return sectokMeta.content; 58*b498f308SAtari911 } 59*b498f308SAtari911 // Return empty string if not found 60*b498f308SAtari911 console.warn('Calendar plugin: Security token not found'); 61*b498f308SAtari911 return ''; 62*b498f308SAtari911} 63*b498f308SAtari911 649ccd446eSAtari911// Helper: propagate CSS variables from a calendar container to a target element 659ccd446eSAtari911// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars 669ccd446eSAtari911// from DOM parents per spec, but some DokuWiki templates break this inheritance) 679ccd446eSAtari911function propagateThemeVars(calId, targetEl) { 689ccd446eSAtari911 if (!targetEl) return; 699ccd446eSAtari911 // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.) 709ccd446eSAtari911 const container = document.getElementById(calId) 719ccd446eSAtari911 || document.getElementById('sidebar-widget-' + calId) 729ccd446eSAtari911 || document.querySelector('[id$="' + calId + '"]'); 739ccd446eSAtari911 if (!container) return; 749ccd446eSAtari911 const cs = getComputedStyle(container); 759ccd446eSAtari911 const vars = [ 769ccd446eSAtari911 '--background-site', '--background-alt', '--background-header', 779ccd446eSAtari911 '--text-primary', '--text-bright', '--text-dim', 789ccd446eSAtari911 '--border-color', '--border-main', 799ccd446eSAtari911 '--cell-bg', '--cell-today-bg', '--grid-bg', 809ccd446eSAtari911 '--shadow-color', '--header-border', '--header-shadow', 819ccd446eSAtari911 '--btn-text' 829ccd446eSAtari911 ]; 839ccd446eSAtari911 vars.forEach(v => { 849ccd446eSAtari911 const val = cs.getPropertyValue(v).trim(); 859ccd446eSAtari911 if (val) targetEl.style.setProperty(v, val); 869ccd446eSAtari911 }); 879ccd446eSAtari911} 889ccd446eSAtari911 891d05cddcSAtari911// Filter calendar by namespace 901d05cddcSAtari911window.filterCalendarByNamespace = function(calId, namespace) { 911d05cddcSAtari911 // Get current year and month from calendar 921d05cddcSAtari911 const container = document.getElementById(calId); 931d05cddcSAtari911 if (!container) { 941d05cddcSAtari911 console.error('Calendar container not found:', calId); 951d05cddcSAtari911 return; 961d05cddcSAtari911 } 971d05cddcSAtari911 981d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 991d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 1001d05cddcSAtari911 1011d05cddcSAtari911 // Reload calendar with the filtered namespace 1021d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1031d05cddcSAtari911}; 1041d05cddcSAtari911 1051d05cddcSAtari911// Navigate to different month 1061d05cddcSAtari911window.navCalendar = function(calId, year, month, namespace) { 1071d05cddcSAtari911 1081d05cddcSAtari911 const params = new URLSearchParams({ 1091d05cddcSAtari911 call: 'plugin_calendar', 1101d05cddcSAtari911 action: 'load_month', 1111d05cddcSAtari911 year: year, 1121d05cddcSAtari911 month: month, 1131d05cddcSAtari911 namespace: namespace, 1141d05cddcSAtari911 _: new Date().getTime() // Cache buster 1151d05cddcSAtari911 }); 1161d05cddcSAtari911 1171d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1181d05cddcSAtari911 method: 'POST', 1191d05cddcSAtari911 headers: { 1201d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 1211d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 1221d05cddcSAtari911 'Pragma': 'no-cache' 1231d05cddcSAtari911 }, 1241d05cddcSAtari911 body: params.toString() 1251d05cddcSAtari911 }) 1261d05cddcSAtari911 .then(r => r.json()) 1271d05cddcSAtari911 .then(data => { 1281d05cddcSAtari911 if (data.success) { 1291d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 1301d05cddcSAtari911 } else { 1311d05cddcSAtari911 console.error('Failed to load month:', data.error); 1321d05cddcSAtari911 } 1331d05cddcSAtari911 }) 1341d05cddcSAtari911 .catch(err => { 1351d05cddcSAtari911 console.error('Error loading month:', err); 1361d05cddcSAtari911 }); 1371d05cddcSAtari911}; 1381d05cddcSAtari911 1391d05cddcSAtari911// Jump to current month 1401d05cddcSAtari911window.jumpToToday = function(calId, namespace) { 1411d05cddcSAtari911 const today = new Date(); 1421d05cddcSAtari911 const year = today.getFullYear(); 1431d05cddcSAtari911 const month = today.getMonth() + 1; // JavaScript months are 0-indexed 1441d05cddcSAtari911 navCalendar(calId, year, month, namespace); 1451d05cddcSAtari911}; 1461d05cddcSAtari911 1471d05cddcSAtari911// Jump to today for event panel 1481d05cddcSAtari911window.jumpTodayPanel = function(calId, namespace) { 1491d05cddcSAtari911 const today = new Date(); 1501d05cddcSAtari911 const year = today.getFullYear(); 1511d05cddcSAtari911 const month = today.getMonth() + 1; 1521d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 1531d05cddcSAtari911}; 1541d05cddcSAtari911 1551d05cddcSAtari911// Open month picker dialog 1561d05cddcSAtari911window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) { 1571d05cddcSAtari911 1581d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 1591d05cddcSAtari911 1601d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 1611d05cddcSAtari911 1621d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 1631d05cddcSAtari911 1641d05cddcSAtari911 if (!overlay) { 1651d05cddcSAtari911 console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId); 1661d05cddcSAtari911 return; 1671d05cddcSAtari911 } 1681d05cddcSAtari911 1691d05cddcSAtari911 if (!monthSelect || !yearSelect) { 1701d05cddcSAtari911 console.error('Select elements not found!'); 1711d05cddcSAtari911 return; 1721d05cddcSAtari911 } 1731d05cddcSAtari911 1741d05cddcSAtari911 // Set current values 1751d05cddcSAtari911 monthSelect.value = currentMonth; 1761d05cddcSAtari911 yearSelect.value = currentYear; 1771d05cddcSAtari911 1781d05cddcSAtari911 // Show overlay 1791d05cddcSAtari911 overlay.style.display = 'flex'; 1801d05cddcSAtari911}; 1811d05cddcSAtari911 1821d05cddcSAtari911// Open month picker dialog for event panel 1831d05cddcSAtari911window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) { 1841d05cddcSAtari911 openMonthPicker(calId, currentYear, currentMonth, namespace); 1851d05cddcSAtari911}; 1861d05cddcSAtari911 1871d05cddcSAtari911// Close month picker dialog 1881d05cddcSAtari911window.closeMonthPicker = function(calId) { 1891d05cddcSAtari911 const overlay = document.getElementById('month-picker-overlay-' + calId); 1901d05cddcSAtari911 overlay.style.display = 'none'; 1911d05cddcSAtari911}; 1921d05cddcSAtari911 1931d05cddcSAtari911// Jump to selected month 1941d05cddcSAtari911window.jumpToSelectedMonth = function(calId, namespace) { 1951d05cddcSAtari911 const monthSelect = document.getElementById('month-picker-month-' + calId); 1961d05cddcSAtari911 const yearSelect = document.getElementById('month-picker-year-' + calId); 1971d05cddcSAtari911 1981d05cddcSAtari911 const month = parseInt(monthSelect.value); 1991d05cddcSAtari911 const year = parseInt(yearSelect.value); 2001d05cddcSAtari911 2011d05cddcSAtari911 closeMonthPicker(calId); 2021d05cddcSAtari911 2031d05cddcSAtari911 // Check if this is a calendar or event panel 2041d05cddcSAtari911 const container = document.getElementById(calId); 2051d05cddcSAtari911 if (container && container.classList.contains('event-panel-standalone')) { 2061d05cddcSAtari911 navEventPanel(calId, year, month, namespace); 2071d05cddcSAtari911 } else { 2081d05cddcSAtari911 navCalendar(calId, year, month, namespace); 2091d05cddcSAtari911 } 2101d05cddcSAtari911}; 2111d05cddcSAtari911 2121d05cddcSAtari911// Rebuild calendar grid after navigation 2131d05cddcSAtari911window.rebuildCalendar = function(calId, year, month, events, namespace) { 2141d05cddcSAtari911 2151d05cddcSAtari911 const container = document.getElementById(calId); 216da206178SAtari911 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 217da206178SAtari911 'July', 'August', 'September', 'October', 'November', 'December']; 2181d05cddcSAtari911 2199ccd446eSAtari911 // Get theme data from container 2209ccd446eSAtari911 const theme = container.dataset.theme || 'matrix'; 2219ccd446eSAtari911 let themeStyles = {}; 2229ccd446eSAtari911 try { 2239ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles || '{}'); 2249ccd446eSAtari911 } catch (e) { 2259ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 2269ccd446eSAtari911 themeStyles = {}; 2279ccd446eSAtari911 } 2289ccd446eSAtari911 2291d05cddcSAtari911 // Preserve original namespace if not yet set 2301d05cddcSAtari911 if (!container.dataset.originalNamespace) { 2311d05cddcSAtari911 container.setAttribute('data-original-namespace', namespace || ''); 2321d05cddcSAtari911 } 2331d05cddcSAtari911 2341d05cddcSAtari911 // Update container data attributes for current month/year 2351d05cddcSAtari911 container.setAttribute('data-year', year); 2361d05cddcSAtari911 container.setAttribute('data-month', month); 2371d05cddcSAtari911 2381d05cddcSAtari911 // Update embedded events data 2391d05cddcSAtari911 let eventsDataEl = document.getElementById('events-data-' + calId); 2401d05cddcSAtari911 if (eventsDataEl) { 2411d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2421d05cddcSAtari911 } else { 2431d05cddcSAtari911 eventsDataEl = document.createElement('script'); 2441d05cddcSAtari911 eventsDataEl.type = 'application/json'; 2451d05cddcSAtari911 eventsDataEl.id = 'events-data-' + calId; 2461d05cddcSAtari911 eventsDataEl.textContent = JSON.stringify(events); 2471d05cddcSAtari911 container.appendChild(eventsDataEl); 2481d05cddcSAtari911 } 2491d05cddcSAtari911 2501d05cddcSAtari911 // Update header 2511d05cddcSAtari911 const header = container.querySelector('.calendar-compact-header h3'); 2521d05cddcSAtari911 header.textContent = monthNames[month - 1] + ' ' + year; 2531d05cddcSAtari911 2541d05cddcSAtari911 // Update or create namespace filter indicator 2551d05cddcSAtari911 let filterIndicator = container.querySelector('.calendar-namespace-filter'); 2561d05cddcSAtari911 const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' && 2571d05cddcSAtari911 namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1; 2581d05cddcSAtari911 2591d05cddcSAtari911 if (shouldShowFilter) { 2601d05cddcSAtari911 // Show/update filter indicator 2611d05cddcSAtari911 if (!filterIndicator) { 2621d05cddcSAtari911 // Create filter indicator if it doesn't exist 2631d05cddcSAtari911 const headerDiv = container.querySelector('.calendar-compact-header'); 2649ccd446eSAtari911 if (headerDiv) { 2651d05cddcSAtari911 filterIndicator = document.createElement('div'); 2661d05cddcSAtari911 filterIndicator.className = 'calendar-namespace-filter'; 2671d05cddcSAtari911 filterIndicator.id = 'namespace-filter-' + calId; 2681d05cddcSAtari911 headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling); 2691d05cddcSAtari911 } 2701d05cddcSAtari911 } 2711d05cddcSAtari911 2721d05cddcSAtari911 if (filterIndicator) { 2731d05cddcSAtari911 filterIndicator.innerHTML = 2741d05cddcSAtari911 '<span class="namespace-filter-label">Filtering:</span>' + 2751d05cddcSAtari911 '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' + 2761d05cddcSAtari911 '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>'; 2771d05cddcSAtari911 filterIndicator.style.display = 'flex'; 2781d05cddcSAtari911 } 2791d05cddcSAtari911 } else { 2801d05cddcSAtari911 // Hide filter indicator 2811d05cddcSAtari911 if (filterIndicator) { 2821d05cddcSAtari911 filterIndicator.style.display = 'none'; 2831d05cddcSAtari911 } 2841d05cddcSAtari911 } 2851d05cddcSAtari911 2861d05cddcSAtari911 // Update container's namespace attribute 2871d05cddcSAtari911 container.setAttribute('data-namespace', namespace || ''); 2881d05cddcSAtari911 2891d05cddcSAtari911 // Update nav buttons 2901d05cddcSAtari911 let prevMonth = month - 1; 2911d05cddcSAtari911 let prevYear = year; 2921d05cddcSAtari911 if (prevMonth < 1) { 2931d05cddcSAtari911 prevMonth = 12; 2941d05cddcSAtari911 prevYear--; 2951d05cddcSAtari911 } 2961d05cddcSAtari911 2971d05cddcSAtari911 let nextMonth = month + 1; 2981d05cddcSAtari911 let nextYear = year; 2991d05cddcSAtari911 if (nextMonth > 12) { 3001d05cddcSAtari911 nextMonth = 1; 3011d05cddcSAtari911 nextYear++; 3021d05cddcSAtari911 } 3031d05cddcSAtari911 3041d05cddcSAtari911 const navBtns = container.querySelectorAll('.cal-nav-btn'); 3051d05cddcSAtari911 navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 3061d05cddcSAtari911 navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 3071d05cddcSAtari911 3081d05cddcSAtari911 // Rebuild calendar grid 3091d05cddcSAtari911 const tbody = container.querySelector('.calendar-compact-grid tbody'); 3101d05cddcSAtari911 const firstDay = new Date(year, month - 1, 1); 3111d05cddcSAtari911 const daysInMonth = new Date(year, month, 0).getDate(); 3121d05cddcSAtari911 const dayOfWeek = firstDay.getDay(); 3131d05cddcSAtari911 3141d05cddcSAtari911 // Calculate month boundaries 3151d05cddcSAtari911 const monthStart = new Date(year, month - 1, 1); 3161d05cddcSAtari911 const monthEnd = new Date(year, month - 1, daysInMonth); 3171d05cddcSAtari911 3181d05cddcSAtari911 // Build a map of all events with their date ranges 3191d05cddcSAtari911 const eventRanges = {}; 3201d05cddcSAtari911 for (const [dateKey, dayEvents] of Object.entries(events)) { 3211d05cddcSAtari911 // Defensive check: ensure dayEvents is an array 3221d05cddcSAtari911 if (!Array.isArray(dayEvents)) { 3231d05cddcSAtari911 console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents); 3241d05cddcSAtari911 continue; 3251d05cddcSAtari911 } 3261d05cddcSAtari911 3271d05cddcSAtari911 // Only process events that could possibly overlap with this month/year 3281d05cddcSAtari911 const dateYear = parseInt(dateKey.split('-')[0]); 3291d05cddcSAtari911 3301d05cddcSAtari911 // Skip events from completely different years (unless they're very long multi-day events) 3311d05cddcSAtari911 if (Math.abs(dateYear - year) > 1) { 3321d05cddcSAtari911 continue; 3331d05cddcSAtari911 } 3341d05cddcSAtari911 3351d05cddcSAtari911 for (const evt of dayEvents) { 3361d05cddcSAtari911 const startDate = dateKey; 3371d05cddcSAtari911 const endDate = evt.endDate || dateKey; 3381d05cddcSAtari911 3391d05cddcSAtari911 // Check if event overlaps with current month 3401d05cddcSAtari911 const eventStart = new Date(startDate + 'T00:00:00'); 3411d05cddcSAtari911 const eventEnd = new Date(endDate + 'T00:00:00'); 3421d05cddcSAtari911 3431d05cddcSAtari911 // Skip if event doesn't overlap with current month 3441d05cddcSAtari911 if (eventEnd < monthStart || eventStart > monthEnd) { 3451d05cddcSAtari911 continue; 3461d05cddcSAtari911 } 3471d05cddcSAtari911 3481d05cddcSAtari911 // Create entry for each day the event spans 3491d05cddcSAtari911 const start = new Date(startDate + 'T00:00:00'); 3501d05cddcSAtari911 const end = new Date(endDate + 'T00:00:00'); 3511d05cddcSAtari911 const current = new Date(start); 3521d05cddcSAtari911 3531d05cddcSAtari911 while (current <= end) { 3541d05cddcSAtari911 const currentKey = current.toISOString().split('T')[0]; 3551d05cddcSAtari911 3561d05cddcSAtari911 // Check if this date is in current month 3571d05cddcSAtari911 const currentDate = new Date(currentKey + 'T00:00:00'); 3581d05cddcSAtari911 if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) { 3591d05cddcSAtari911 if (!eventRanges[currentKey]) { 3601d05cddcSAtari911 eventRanges[currentKey] = []; 3611d05cddcSAtari911 } 3621d05cddcSAtari911 3631d05cddcSAtari911 // Add event with span information 3641d05cddcSAtari911 const eventCopy = {...evt}; 3651d05cddcSAtari911 eventCopy._span_start = startDate; 3661d05cddcSAtari911 eventCopy._span_end = endDate; 3671d05cddcSAtari911 eventCopy._is_first_day = (currentKey === startDate); 3681d05cddcSAtari911 eventCopy._is_last_day = (currentKey === endDate); 3691d05cddcSAtari911 eventCopy._original_date = dateKey; 3701d05cddcSAtari911 3711d05cddcSAtari911 // Check if event continues from previous month or to next month 3721d05cddcSAtari911 eventCopy._continues_from_prev = (eventStart < monthStart); 3731d05cddcSAtari911 eventCopy._continues_to_next = (eventEnd > monthEnd); 3741d05cddcSAtari911 3751d05cddcSAtari911 eventRanges[currentKey].push(eventCopy); 3761d05cddcSAtari911 } 3771d05cddcSAtari911 3781d05cddcSAtari911 current.setDate(current.getDate() + 1); 3791d05cddcSAtari911 } 3801d05cddcSAtari911 } 3811d05cddcSAtari911 } 3821d05cddcSAtari911 3831d05cddcSAtari911 let html = ''; 3841d05cddcSAtari911 let currentDay = 1; 3851d05cddcSAtari911 const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7); 3861d05cddcSAtari911 3871d05cddcSAtari911 for (let row = 0; row < rowCount; row++) { 3881d05cddcSAtari911 html += '<tr>'; 3891d05cddcSAtari911 for (let col = 0; col < 7; col++) { 3901d05cddcSAtari911 if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) { 3919ccd446eSAtari911 html += `<td class="cal-empty"></td>`; 3921d05cddcSAtari911 } else { 3931d05cddcSAtari911 const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 3941d05cddcSAtari911 3951d05cddcSAtari911 // Get today's date in local timezone 3961d05cddcSAtari911 const todayObj = new Date(); 3971d05cddcSAtari911 const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`; 3981d05cddcSAtari911 3991d05cddcSAtari911 const isToday = dateKey === today; 4001d05cddcSAtari911 const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0; 4011d05cddcSAtari911 4021d05cddcSAtari911 let classes = 'cal-day'; 4031d05cddcSAtari911 if (isToday) classes += ' cal-today'; 4041d05cddcSAtari911 if (hasEvents) classes += ' cal-has-events'; 4051d05cddcSAtari911 4069ccd446eSAtari911 const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num'; 4079ccd446eSAtari911 4081d05cddcSAtari911 html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`; 4099ccd446eSAtari911 html += `<span class="${dayNumClass}">${currentDay}</span>`; 4101d05cddcSAtari911 4111d05cddcSAtari911 if (hasEvents) { 4121d05cddcSAtari911 // Sort events by time (no time first, then by time) 4131d05cddcSAtari911 const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => { 4141d05cddcSAtari911 const timeA = a.time || ''; 4151d05cddcSAtari911 const timeB = b.time || ''; 4161d05cddcSAtari911 if (!timeA && timeB) return -1; 4171d05cddcSAtari911 if (timeA && !timeB) return 1; 4181d05cddcSAtari911 if (!timeA && !timeB) return 0; 4191d05cddcSAtari911 return timeA.localeCompare(timeB); 4201d05cddcSAtari911 }); 4211d05cddcSAtari911 42296df7d3eSAtari911 // Get important namespaces 42396df7d3eSAtari911 let importantNamespaces = ['important']; 42496df7d3eSAtari911 if (container.dataset.importantNamespaces) { 42596df7d3eSAtari911 try { 42696df7d3eSAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 42796df7d3eSAtari911 } catch (e) {} 42896df7d3eSAtari911 } 42996df7d3eSAtari911 4301d05cddcSAtari911 // Show colored stacked bars for each event 4311d05cddcSAtari911 html += '<div class="event-indicators">'; 4321d05cddcSAtari911 for (const evt of sortedEvents) { 4331d05cddcSAtari911 const eventId = evt.id || ''; 4341d05cddcSAtari911 const eventColor = evt.color || '#3498db'; 4351d05cddcSAtari911 const eventTitle = evt.title || 'Event'; 4369ccd446eSAtari911 const eventTime = evt.time || ''; 4371d05cddcSAtari911 const originalDate = evt._original_date || dateKey; 4381d05cddcSAtari911 const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true; 4391d05cddcSAtari911 const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true; 4401d05cddcSAtari911 44196df7d3eSAtari911 // Check if important namespace 44296df7d3eSAtari911 let evtNs = evt.namespace || evt._namespace || ''; 44396df7d3eSAtari911 let isImportant = false; 44496df7d3eSAtari911 for (const impNs of importantNamespaces) { 44596df7d3eSAtari911 if (evtNs === impNs || evtNs.startsWith(impNs + ':')) { 44696df7d3eSAtari911 isImportant = true; 44796df7d3eSAtari911 break; 44896df7d3eSAtari911 } 44996df7d3eSAtari911 } 45096df7d3eSAtari911 4511d05cddcSAtari911 let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed'; 4521d05cddcSAtari911 if (!isFirstDay) barClass += ' event-bar-continues'; 4531d05cddcSAtari911 if (!isLastDay) barClass += ' event-bar-continuing'; 45496df7d3eSAtari911 if (isImportant) { 45596df7d3eSAtari911 barClass += ' event-bar-important'; 45696df7d3eSAtari911 if (isFirstDay) { 45796df7d3eSAtari911 barClass += ' event-bar-has-star'; 45896df7d3eSAtari911 } 45996df7d3eSAtari911 } 4601d05cddcSAtari911 4611d05cddcSAtari911 html += `<span class="event-bar ${barClass}" `; 4621d05cddcSAtari911 html += `style="background: ${eventColor};" `; 46396df7d3eSAtari911 html += `title="${isImportant ? '⭐ ' : ''}${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `; 46496df7d3eSAtari911 html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');">`; 46596df7d3eSAtari911 html += '</span>'; 4661d05cddcSAtari911 } 4671d05cddcSAtari911 html += '</div>'; 4681d05cddcSAtari911 } 4691d05cddcSAtari911 4701d05cddcSAtari911 html += '</td>'; 4711d05cddcSAtari911 currentDay++; 4721d05cddcSAtari911 } 4731d05cddcSAtari911 } 4741d05cddcSAtari911 html += '</tr>'; 4751d05cddcSAtari911 } 4761d05cddcSAtari911 4771d05cddcSAtari911 tbody.innerHTML = html; 4781d05cddcSAtari911 4791d05cddcSAtari911 // Update Today button with current namespace 4801d05cddcSAtari911 const todayBtn = container.querySelector('.cal-today-btn'); 4811d05cddcSAtari911 if (todayBtn) { 4821d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`); 4831d05cddcSAtari911 } 4841d05cddcSAtari911 4851d05cddcSAtari911 // Update month picker with current namespace 4861d05cddcSAtari911 const monthPicker = container.querySelector('.calendar-month-picker'); 4871d05cddcSAtari911 if (monthPicker) { 4881d05cddcSAtari911 monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`); 4891d05cddcSAtari911 } 4901d05cddcSAtari911 4911d05cddcSAtari911 // Rebuild event list - server already filtered to current month 4921d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 4931d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 4941d05cddcSAtari911 4951d05cddcSAtari911 // Auto-scroll to first future event (past events will be above viewport) 4961d05cddcSAtari911 setTimeout(() => { 4971d05cddcSAtari911 const firstFuture = eventList.querySelector('[data-first-future="true"]'); 4981d05cddcSAtari911 if (firstFuture) { 4991d05cddcSAtari911 firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' }); 5001d05cddcSAtari911 } 5011d05cddcSAtari911 }, 100); 5021d05cddcSAtari911 5031d05cddcSAtari911 // Update title 5041d05cddcSAtari911 const title = container.querySelector('#eventlist-title-' + calId); 505da206178SAtari911 title.textContent = 'Events'; 5061d05cddcSAtari911}; 5071d05cddcSAtari911 5081d05cddcSAtari911// Render event list from data 5091d05cddcSAtari911window.renderEventListFromData = function(events, calId, namespace, year, month) { 5101d05cddcSAtari911 if (!events || Object.keys(events).length === 0) { 511da206178SAtari911 return '<p class="no-events-msg">No events this month</p>'; 5121d05cddcSAtari911 } 5131d05cddcSAtari911 5149ccd446eSAtari911 // Get theme data from container 5159ccd446eSAtari911 const container = document.getElementById(calId); 5169ccd446eSAtari911 let themeStyles = {}; 5179ccd446eSAtari911 if (container && container.dataset.themeStyles) { 5189ccd446eSAtari911 try { 5199ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 5209ccd446eSAtari911 } catch (e) { 5219ccd446eSAtari911 console.error('Failed to parse theme styles in renderEventListFromData:', e); 5229ccd446eSAtari911 } 5239ccd446eSAtari911 } 5249ccd446eSAtari911 5251d05cddcSAtari911 // Check for time conflicts 5261d05cddcSAtari911 events = checkTimeConflicts(events, null); 5271d05cddcSAtari911 5281d05cddcSAtari911 let pastHtml = ''; 5291d05cddcSAtari911 let futureHtml = ''; 5301d05cddcSAtari911 let pastCount = 0; 5311d05cddcSAtari911 5321d05cddcSAtari911 const sortedDates = Object.keys(events).sort(); 5331d05cddcSAtari911 const today = new Date(); 5341d05cddcSAtari911 today.setHours(0, 0, 0, 0); 5351d05cddcSAtari911 const todayStr = today.toISOString().split('T')[0]; 5361d05cddcSAtari911 5371d05cddcSAtari911 // Helper function to check if event is past (with 15-minute grace period) 5381d05cddcSAtari911 const isEventPast = function(dateKey, time) { 5391d05cddcSAtari911 // If event is on a past date, it's definitely past 5401d05cddcSAtari911 if (dateKey < todayStr) { 5411d05cddcSAtari911 return true; 5421d05cddcSAtari911 } 5431d05cddcSAtari911 5441d05cddcSAtari911 // If event is on a future date, it's definitely not past 5451d05cddcSAtari911 if (dateKey > todayStr) { 5461d05cddcSAtari911 return false; 5471d05cddcSAtari911 } 5481d05cddcSAtari911 5491d05cddcSAtari911 // Event is today - check time with grace period 5501d05cddcSAtari911 if (time && time.trim() !== '') { 5511d05cddcSAtari911 try { 5521d05cddcSAtari911 const now = new Date(); 5531d05cddcSAtari911 const eventDateTime = new Date(dateKey + 'T' + time); 5541d05cddcSAtari911 5551d05cddcSAtari911 // Add 15-minute grace period 5561d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 5571d05cddcSAtari911 5581d05cddcSAtari911 // Event is past if current time > event time + 15 minutes 5591d05cddcSAtari911 return now > gracePeriodEnd; 5601d05cddcSAtari911 } catch (e) { 5611d05cddcSAtari911 // If time parsing fails, treat as future 5621d05cddcSAtari911 return false; 5631d05cddcSAtari911 } 5641d05cddcSAtari911 } 5651d05cddcSAtari911 5661d05cddcSAtari911 // No time specified for today's event, treat as future 5671d05cddcSAtari911 return false; 5681d05cddcSAtari911 }; 5691d05cddcSAtari911 5701d05cddcSAtari911 // Filter events to only current month if year/month provided 5711d05cddcSAtari911 const monthStart = year && month ? new Date(year, month - 1, 1) : null; 5721d05cddcSAtari911 const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null; 5731d05cddcSAtari911 5741d05cddcSAtari911 for (const dateKey of sortedDates) { 5751d05cddcSAtari911 // Skip events not in current month if filtering 5761d05cddcSAtari911 if (monthStart && monthEnd) { 5771d05cddcSAtari911 const eventDate = new Date(dateKey + 'T00:00:00'); 5781d05cddcSAtari911 5791d05cddcSAtari911 if (eventDate < monthStart || eventDate > monthEnd) { 5801d05cddcSAtari911 continue; 5811d05cddcSAtari911 } 5821d05cddcSAtari911 } 5831d05cddcSAtari911 5841d05cddcSAtari911 // Sort events within this day by time (all-day events at top) 5851d05cddcSAtari911 const dayEvents = events[dateKey]; 5861d05cddcSAtari911 dayEvents.sort((a, b) => { 5871d05cddcSAtari911 const timeA = a.time && a.time.trim() !== '' ? a.time : null; 5881d05cddcSAtari911 const timeB = b.time && b.time.trim() !== '' ? b.time : null; 5891d05cddcSAtari911 5901d05cddcSAtari911 // All-day events (no time) go to the TOP 5911d05cddcSAtari911 if (timeA === null && timeB !== null) return -1; // A before B 5921d05cddcSAtari911 if (timeA !== null && timeB === null) return 1; // A after B 5931d05cddcSAtari911 if (timeA === null && timeB === null) return 0; // Both all-day, equal 5941d05cddcSAtari911 5951d05cddcSAtari911 // Both have times, sort chronologically 5961d05cddcSAtari911 return timeA.localeCompare(timeB); 5971d05cddcSAtari911 }); 5981d05cddcSAtari911 5991d05cddcSAtari911 for (const event of dayEvents) { 6001d05cddcSAtari911 const isTask = event.isTask || false; 6011d05cddcSAtari911 const completed = event.completed || false; 6021d05cddcSAtari911 6031d05cddcSAtari911 // Use helper function to determine if event is past (with grace period) 6041d05cddcSAtari911 const isPast = isEventPast(dateKey, event.time); 6051d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 6061d05cddcSAtari911 6071d05cddcSAtari911 // Determine if this goes in past section 6081d05cddcSAtari911 const isPastOrCompleted = (isPast && (!isTask || completed)) || completed; 6091d05cddcSAtari911 6101d05cddcSAtari911 const eventHtml = renderEventItem(event, dateKey, calId, namespace); 6111d05cddcSAtari911 6121d05cddcSAtari911 if (isPastOrCompleted) { 6131d05cddcSAtari911 pastCount++; 6141d05cddcSAtari911 pastHtml += eventHtml; 6151d05cddcSAtari911 } else { 6161d05cddcSAtari911 futureHtml += eventHtml; 6171d05cddcSAtari911 } 6181d05cddcSAtari911 } 6191d05cddcSAtari911 } 6201d05cddcSAtari911 6211d05cddcSAtari911 let html = ''; 6221d05cddcSAtari911 6231d05cddcSAtari911 // Add collapsible past events section if any exist 6241d05cddcSAtari911 if (pastCount > 0) { 6251d05cddcSAtari911 html += '<div class="past-events-section">'; 6261d05cddcSAtari911 html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">'; 6271d05cddcSAtari911 html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> '; 628da206178SAtari911 html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>'; 6291d05cddcSAtari911 html += '</div>'; 6301d05cddcSAtari911 html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">'; 6311d05cddcSAtari911 html += pastHtml; 6321d05cddcSAtari911 html += '</div>'; 6331d05cddcSAtari911 html += '</div>'; 6341d05cddcSAtari911 } else { 6351d05cddcSAtari911 } 6361d05cddcSAtari911 6371d05cddcSAtari911 // Add future events 6381d05cddcSAtari911 html += futureHtml; 6391d05cddcSAtari911 6401d05cddcSAtari911 6411d05cddcSAtari911 if (!html) { 642da206178SAtari911 return '<p class="no-events-msg">No events this month</p>'; 6431d05cddcSAtari911 } 6441d05cddcSAtari911 6451d05cddcSAtari911 return html; 6461d05cddcSAtari911}; 6471d05cddcSAtari911 6481d05cddcSAtari911// Show day popup with events when clicking a date 6491d05cddcSAtari911window.showDayPopup = function(calId, date, namespace) { 6501d05cddcSAtari911 // Get events for this calendar 6511d05cddcSAtari911 const eventsDataEl = document.getElementById('events-data-' + calId); 6521d05cddcSAtari911 let events = {}; 6531d05cddcSAtari911 6541d05cddcSAtari911 if (eventsDataEl) { 6551d05cddcSAtari911 try { 6561d05cddcSAtari911 events = JSON.parse(eventsDataEl.textContent); 6571d05cddcSAtari911 } catch (e) { 6581d05cddcSAtari911 console.error('Failed to parse events data:', e); 6591d05cddcSAtari911 } 6601d05cddcSAtari911 } 6611d05cddcSAtari911 6621d05cddcSAtari911 const dayEvents = events[date] || []; 6631d05cddcSAtari911 6641d05cddcSAtari911 // Check for conflicts on this day 6651d05cddcSAtari911 const dayEventsObj = {[date]: dayEvents}; 6661d05cddcSAtari911 const checkedEvents = checkTimeConflicts(dayEventsObj, null); 6671d05cddcSAtari911 const dayEventsWithConflicts = checkedEvents[date] || dayEvents; 6681d05cddcSAtari911 6691d05cddcSAtari911 // Sort events: all-day at top, then chronological by time 6701d05cddcSAtari911 dayEventsWithConflicts.sort((a, b) => { 6711d05cddcSAtari911 const timeA = a.time && a.time.trim() !== '' ? a.time : null; 6721d05cddcSAtari911 const timeB = b.time && b.time.trim() !== '' ? b.time : null; 6731d05cddcSAtari911 6741d05cddcSAtari911 // All-day events (no time) go to the TOP 6751d05cddcSAtari911 if (timeA === null && timeB !== null) return -1; // A before B 6761d05cddcSAtari911 if (timeA !== null && timeB === null) return 1; // A after B 6771d05cddcSAtari911 if (timeA === null && timeB === null) return 0; // Both all-day, equal 6781d05cddcSAtari911 6791d05cddcSAtari911 // Both have times, sort chronologically 6801d05cddcSAtari911 return timeA.localeCompare(timeB); 6811d05cddcSAtari911 }); 6821d05cddcSAtari911 6831d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 6841d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 6851d05cddcSAtari911 weekday: 'long', 6861d05cddcSAtari911 month: 'long', 6871d05cddcSAtari911 day: 'numeric', 6881d05cddcSAtari911 year: 'numeric' 6891d05cddcSAtari911 }); 6901d05cddcSAtari911 6911d05cddcSAtari911 // Create popup 6921d05cddcSAtari911 let popup = document.getElementById('day-popup-' + calId); 6931d05cddcSAtari911 if (!popup) { 6941d05cddcSAtari911 popup = document.createElement('div'); 6951d05cddcSAtari911 popup.id = 'day-popup-' + calId; 6961d05cddcSAtari911 popup.className = 'day-popup'; 6971d05cddcSAtari911 document.body.appendChild(popup); 6981d05cddcSAtari911 } 6991d05cddcSAtari911 700da206178SAtari911 // Get theme styles and important namespaces 7019ccd446eSAtari911 const container = document.getElementById(calId); 7029ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 7039ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 7049ccd446eSAtari911 705da206178SAtari911 // Get important namespaces 706da206178SAtari911 let importantNamespaces = ['important']; 707da206178SAtari911 if (container && container.dataset.importantNamespaces) { 708da206178SAtari911 try { 709da206178SAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 710da206178SAtari911 } catch (e) { 711da206178SAtari911 importantNamespaces = ['important']; 712da206178SAtari911 } 713da206178SAtari911 } 714da206178SAtari911 7151d05cddcSAtari911 let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>'; 7161d05cddcSAtari911 html += '<div class="day-popup-content">'; 7171d05cddcSAtari911 html += '<div class="day-popup-header">'; 7181d05cddcSAtari911 html += '<h4>' + displayDate + '</h4>'; 7191d05cddcSAtari911 html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>'; 7201d05cddcSAtari911 html += '</div>'; 7211d05cddcSAtari911 7221d05cddcSAtari911 html += '<div class="day-popup-body">'; 7231d05cddcSAtari911 7241d05cddcSAtari911 if (dayEventsWithConflicts.length === 0) { 7251d05cddcSAtari911 html += '<p class="no-events-msg">No events on this day</p>'; 7261d05cddcSAtari911 } else { 7271d05cddcSAtari911 html += '<div class="popup-events-list">'; 7281d05cddcSAtari911 dayEventsWithConflicts.forEach(event => { 7291d05cddcSAtari911 const color = event.color || '#3498db'; 7301d05cddcSAtari911 7311d05cddcSAtari911 // Use individual event namespace if available (for multi-namespace support) 7321d05cddcSAtari911 const eventNamespace = event._namespace !== undefined ? event._namespace : namespace; 7331d05cddcSAtari911 734da206178SAtari911 // Check if this is an important namespace event 735da206178SAtari911 let isImportant = false; 736da206178SAtari911 if (eventNamespace) { 737da206178SAtari911 for (const impNs of importantNamespaces) { 738da206178SAtari911 if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) { 739da206178SAtari911 isImportant = true; 740da206178SAtari911 break; 741da206178SAtari911 } 742da206178SAtari911 } 743da206178SAtari911 } 744da206178SAtari911 7451d05cddcSAtari911 // Check if this is a continuation (event started before this date) 7461d05cddcSAtari911 const originalStartDate = event.originalStartDate || event._dateKey || date; 7471d05cddcSAtari911 const isContinuation = originalStartDate < date; 7481d05cddcSAtari911 7491d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 7501d05cddcSAtari911 let displayTime = ''; 7511d05cddcSAtari911 if (event.time) { 7521d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 7531d05cddcSAtari911 } 7541d05cddcSAtari911 7551d05cddcSAtari911 // Multi-day indicator 7561d05cddcSAtari911 let multiDay = ''; 7571d05cddcSAtari911 if (event.endDate && event.endDate !== date) { 7581d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 7591d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 7601d05cddcSAtari911 month: 'short', 7611d05cddcSAtari911 day: 'numeric' 7621d05cddcSAtari911 }); 7631d05cddcSAtari911 } 7641d05cddcSAtari911 7651d05cddcSAtari911 // Continuation message 7661d05cddcSAtari911 if (isContinuation) { 7671d05cddcSAtari911 const startObj = new Date(originalStartDate + 'T00:00:00'); 7681d05cddcSAtari911 const startDisplay = startObj.toLocaleDateString('en-US', { 7691d05cddcSAtari911 weekday: 'short', 7701d05cddcSAtari911 month: 'short', 7711d05cddcSAtari911 day: 'numeric' 7721d05cddcSAtari911 }); 7731d05cddcSAtari911 html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>'; 7741d05cddcSAtari911 } 7751d05cddcSAtari911 776da206178SAtari911 const importantClass = isImportant ? ' popup-event-important' : ''; 777da206178SAtari911 html += '<div class="popup-event-item' + importantClass + '">'; 7781d05cddcSAtari911 html += '<div class="event-color-bar" style="background: ' + color + ';"></div>'; 7791d05cddcSAtari911 html += '<div class="popup-event-content">'; 7801d05cddcSAtari911 7811d05cddcSAtari911 // Single line with title, time, date range, namespace, and actions 7821d05cddcSAtari911 html += '<div class="popup-event-main-row">'; 7831d05cddcSAtari911 html += '<div class="popup-event-info-inline">'; 784da206178SAtari911 785da206178SAtari911 // Add star for important events 786da206178SAtari911 if (isImportant) { 787da206178SAtari911 html += '<span class="popup-event-star">⭐</span>'; 788da206178SAtari911 } 789da206178SAtari911 7901d05cddcSAtari911 html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>'; 7911d05cddcSAtari911 if (displayTime) { 7921d05cddcSAtari911 html += '<span class="popup-event-time"> ' + displayTime + '</span>'; 7931d05cddcSAtari911 } 7941d05cddcSAtari911 if (multiDay) { 7951d05cddcSAtari911 html += '<span class="popup-event-multiday">' + multiDay + '</span>'; 7961d05cddcSAtari911 } 7971d05cddcSAtari911 if (eventNamespace) { 7981d05cddcSAtari911 html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>'; 7991d05cddcSAtari911 } 8001d05cddcSAtari911 8011d05cddcSAtari911 // Add conflict warning badge if event has conflicts 8021d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 8031d05cddcSAtari911 // Build conflict list for tooltip 8041d05cddcSAtari911 let conflictList = []; 8051d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 8061d05cddcSAtari911 let conflictText = conflict.title; 8071d05cddcSAtari911 if (conflict.time) { 8081d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 8091d05cddcSAtari911 } 8101d05cddcSAtari911 conflictList.push(conflictText); 8111d05cddcSAtari911 }); 8121d05cddcSAtari911 8139ccd446eSAtari911 html += '<span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 8141d05cddcSAtari911 } 8151d05cddcSAtari911 8161d05cddcSAtari911 html += '</div>'; 8171d05cddcSAtari911 html += '<div class="popup-event-actions">'; 818da206178SAtari911 html += '<button type="button" class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>'; 819da206178SAtari911 html += '<button type="button" class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">️</button>'; 8201d05cddcSAtari911 html += '</div>'; 8211d05cddcSAtari911 html += '</div>'; 8221d05cddcSAtari911 8231d05cddcSAtari911 // Description on separate line if present 8241d05cddcSAtari911 if (event.description) { 8251d05cddcSAtari911 html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>'; 8261d05cddcSAtari911 } 8271d05cddcSAtari911 8281d05cddcSAtari911 html += '</div></div>'; 8291d05cddcSAtari911 }); 8301d05cddcSAtari911 html += '</div>'; 8311d05cddcSAtari911 } 8321d05cddcSAtari911 8331d05cddcSAtari911 html += '</div>'; 8341d05cddcSAtari911 8351d05cddcSAtari911 html += '<div class="day-popup-footer">'; 836da206178SAtari911 html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>'; 8371d05cddcSAtari911 html += '</div>'; 8381d05cddcSAtari911 8391d05cddcSAtari911 html += '</div>'; 8401d05cddcSAtari911 8411d05cddcSAtari911 popup.innerHTML = html; 8421d05cddcSAtari911 popup.style.display = 'flex'; 8439ccd446eSAtari911 8449ccd446eSAtari911 // Propagate CSS vars from calendar container to popup (popup is outside container in DOM) 8459ccd446eSAtari911 if (container) { 8469ccd446eSAtari911 propagateThemeVars(calId, popup.querySelector('.day-popup-content')); 8479ccd446eSAtari911 } 84896df7d3eSAtari911 84996df7d3eSAtari911 // Make popup draggable by header 85096df7d3eSAtari911 const popupContent = popup.querySelector('.day-popup-content'); 85196df7d3eSAtari911 const popupHeader = popup.querySelector('.day-popup-header'); 85296df7d3eSAtari911 85396df7d3eSAtari911 if (popupContent && popupHeader) { 85496df7d3eSAtari911 // Reset position to center 85596df7d3eSAtari911 popupContent.style.position = 'relative'; 85696df7d3eSAtari911 popupContent.style.left = '0'; 85796df7d3eSAtari911 popupContent.style.top = '0'; 85896df7d3eSAtari911 85996df7d3eSAtari911 // Store drag state on the element itself 86096df7d3eSAtari911 popupHeader._isDragging = false; 86196df7d3eSAtari911 86296df7d3eSAtari911 popupHeader.onmousedown = function(e) { 86396df7d3eSAtari911 // Ignore if clicking the close button 86496df7d3eSAtari911 if (e.target.classList.contains('popup-close')) return; 86596df7d3eSAtari911 86696df7d3eSAtari911 popupHeader._isDragging = true; 86796df7d3eSAtari911 popupHeader._dragStartX = e.clientX; 86896df7d3eSAtari911 popupHeader._dragStartY = e.clientY; 86996df7d3eSAtari911 87096df7d3eSAtari911 const rect = popupContent.getBoundingClientRect(); 87196df7d3eSAtari911 const parentRect = popup.getBoundingClientRect(); 87296df7d3eSAtari911 popupHeader._initialLeft = rect.left - parentRect.left - (parentRect.width / 2 - rect.width / 2); 87396df7d3eSAtari911 popupHeader._initialTop = rect.top - parentRect.top - (parentRect.height / 2 - rect.height / 2); 87496df7d3eSAtari911 87596df7d3eSAtari911 popupContent.style.transition = 'none'; 87696df7d3eSAtari911 e.preventDefault(); 87796df7d3eSAtari911 }; 87896df7d3eSAtari911 87996df7d3eSAtari911 popup.onmousemove = function(e) { 88096df7d3eSAtari911 if (!popupHeader._isDragging) return; 88196df7d3eSAtari911 88296df7d3eSAtari911 const deltaX = e.clientX - popupHeader._dragStartX; 88396df7d3eSAtari911 const deltaY = e.clientY - popupHeader._dragStartY; 88496df7d3eSAtari911 88596df7d3eSAtari911 popupContent.style.left = (popupHeader._initialLeft + deltaX) + 'px'; 88696df7d3eSAtari911 popupContent.style.top = (popupHeader._initialTop + deltaY) + 'px'; 88796df7d3eSAtari911 }; 88896df7d3eSAtari911 88996df7d3eSAtari911 popup.onmouseup = function() { 89096df7d3eSAtari911 if (popupHeader._isDragging) { 89196df7d3eSAtari911 popupHeader._isDragging = false; 89296df7d3eSAtari911 popupContent.style.transition = ''; 89396df7d3eSAtari911 } 89496df7d3eSAtari911 }; 89596df7d3eSAtari911 89696df7d3eSAtari911 popup.onmouseleave = function() { 89796df7d3eSAtari911 if (popupHeader._isDragging) { 89896df7d3eSAtari911 popupHeader._isDragging = false; 89996df7d3eSAtari911 popupContent.style.transition = ''; 90096df7d3eSAtari911 } 90196df7d3eSAtari911 }; 90296df7d3eSAtari911 } 9031d05cddcSAtari911}; 9041d05cddcSAtari911 9051d05cddcSAtari911// Close day popup 9061d05cddcSAtari911window.closeDayPopup = function(calId) { 9071d05cddcSAtari911 const popup = document.getElementById('day-popup-' + calId); 9081d05cddcSAtari911 if (popup) { 9091d05cddcSAtari911 popup.style.display = 'none'; 9101d05cddcSAtari911 } 9111d05cddcSAtari911}; 9121d05cddcSAtari911 9131d05cddcSAtari911// Show events for a specific day (for event list panel) 9141d05cddcSAtari911window.showDayEvents = function(calId, date, namespace) { 9151d05cddcSAtari911 const params = new URLSearchParams({ 9161d05cddcSAtari911 call: 'plugin_calendar', 9171d05cddcSAtari911 action: 'load_month', 9181d05cddcSAtari911 year: date.split('-')[0], 9191d05cddcSAtari911 month: parseInt(date.split('-')[1]), 9201d05cddcSAtari911 namespace: namespace, 9211d05cddcSAtari911 _: new Date().getTime() // Cache buster 9221d05cddcSAtari911 }); 9231d05cddcSAtari911 9241d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 9251d05cddcSAtari911 method: 'POST', 9261d05cddcSAtari911 headers: { 9271d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 9281d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 9291d05cddcSAtari911 'Pragma': 'no-cache' 9301d05cddcSAtari911 }, 9311d05cddcSAtari911 body: params.toString() 9321d05cddcSAtari911 }) 9331d05cddcSAtari911 .then(r => r.json()) 9341d05cddcSAtari911 .then(data => { 9351d05cddcSAtari911 if (data.success) { 9361d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 9371d05cddcSAtari911 const events = data.events; 9381d05cddcSAtari911 const title = document.getElementById('eventlist-title-' + calId); 9391d05cddcSAtari911 9401d05cddcSAtari911 const dateObj = new Date(date + 'T00:00:00'); 941da206178SAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 9421d05cddcSAtari911 weekday: 'short', 9431d05cddcSAtari911 month: 'short', 9441d05cddcSAtari911 day: 'numeric' 9451d05cddcSAtari911 }); 9461d05cddcSAtari911 947da206178SAtari911 title.textContent = 'Events - ' + displayDate; 9481d05cddcSAtari911 9491d05cddcSAtari911 // Filter events for this day 9501d05cddcSAtari911 const dayEvents = events[date] || []; 9511d05cddcSAtari911 9521d05cddcSAtari911 if (dayEvents.length === 0) { 953da206178SAtari911 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>'; 9541d05cddcSAtari911 } else { 9551d05cddcSAtari911 let html = ''; 9561d05cddcSAtari911 dayEvents.forEach(event => { 9571d05cddcSAtari911 html += renderEventItem(event, date, calId, namespace); 9581d05cddcSAtari911 }); 9591d05cddcSAtari911 eventList.innerHTML = html; 9601d05cddcSAtari911 } 9611d05cddcSAtari911 } 9621d05cddcSAtari911 }) 9631d05cddcSAtari911 .catch(err => console.error('Error:', err)); 9641d05cddcSAtari911}; 9651d05cddcSAtari911 9661d05cddcSAtari911// Render a single event item 9671d05cddcSAtari911window.renderEventItem = function(event, date, calId, namespace) { 9689ccd446eSAtari911 // Get theme data from container 9699ccd446eSAtari911 const container = document.getElementById(calId); 9709ccd446eSAtari911 let themeStyles = {}; 97196df7d3eSAtari911 let importantNamespaces = ['important']; // default 9729ccd446eSAtari911 if (container && container.dataset.themeStyles) { 9739ccd446eSAtari911 try { 9749ccd446eSAtari911 themeStyles = JSON.parse(container.dataset.themeStyles); 9759ccd446eSAtari911 } catch (e) { 9769ccd446eSAtari911 console.error('Failed to parse theme styles:', e); 9779ccd446eSAtari911 } 9789ccd446eSAtari911 } 97996df7d3eSAtari911 // Get important namespaces from container data attribute 98096df7d3eSAtari911 if (container && container.dataset.importantNamespaces) { 98196df7d3eSAtari911 try { 98296df7d3eSAtari911 importantNamespaces = JSON.parse(container.dataset.importantNamespaces); 98396df7d3eSAtari911 } catch (e) { 98496df7d3eSAtari911 importantNamespaces = ['important']; 98596df7d3eSAtari911 } 98696df7d3eSAtari911 } 9879ccd446eSAtari911 9881d05cddcSAtari911 // Check if this event is in the past or today (with 15-minute grace period) 9891d05cddcSAtari911 const today = new Date(); 9901d05cddcSAtari911 today.setHours(0, 0, 0, 0); 9911d05cddcSAtari911 const todayStr = today.toISOString().split('T')[0]; 9921d05cddcSAtari911 const eventDate = new Date(date + 'T00:00:00'); 9931d05cddcSAtari911 9941d05cddcSAtari911 // Helper to determine if event is past with grace period 9951d05cddcSAtari911 let isPast; 9961d05cddcSAtari911 if (date < todayStr) { 9971d05cddcSAtari911 isPast = true; // Past date 9981d05cddcSAtari911 } else if (date > todayStr) { 9991d05cddcSAtari911 isPast = false; // Future date 10001d05cddcSAtari911 } else { 10011d05cddcSAtari911 // Today - check time with grace period 10021d05cddcSAtari911 if (event.time && event.time.trim() !== '') { 10031d05cddcSAtari911 try { 10041d05cddcSAtari911 const now = new Date(); 10051d05cddcSAtari911 const eventDateTime = new Date(date + 'T' + event.time); 10061d05cddcSAtari911 const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000); 10071d05cddcSAtari911 isPast = now > gracePeriodEnd; 10081d05cddcSAtari911 } catch (e) { 10091d05cddcSAtari911 isPast = false; 10101d05cddcSAtari911 } 10111d05cddcSAtari911 } else { 10121d05cddcSAtari911 isPast = false; // No time, treat as future 10131d05cddcSAtari911 } 10141d05cddcSAtari911 } 10151d05cddcSAtari911 10161d05cddcSAtari911 const isToday = eventDate.getTime() === today.getTime(); 10171d05cddcSAtari911 101896df7d3eSAtari911 // Check if this is an important namespace event 101996df7d3eSAtari911 let eventNamespace = event.namespace || ''; 102096df7d3eSAtari911 if (!eventNamespace && event._namespace !== undefined) { 102196df7d3eSAtari911 eventNamespace = event._namespace; 102296df7d3eSAtari911 } 102396df7d3eSAtari911 let isImportantNs = false; 102496df7d3eSAtari911 if (eventNamespace) { 102596df7d3eSAtari911 for (const impNs of importantNamespaces) { 102696df7d3eSAtari911 if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) { 102796df7d3eSAtari911 isImportantNs = true; 102896df7d3eSAtari911 break; 102996df7d3eSAtari911 } 103096df7d3eSAtari911 } 103196df7d3eSAtari911 } 103296df7d3eSAtari911 10331d05cddcSAtari911 // Format date display with day of week 10341d05cddcSAtari911 const displayDateKey = event.originalStartDate || date; 10351d05cddcSAtari911 const dateObj = new Date(displayDateKey + 'T00:00:00'); 10361d05cddcSAtari911 const displayDate = dateObj.toLocaleDateString('en-US', { 10371d05cddcSAtari911 weekday: 'short', 10381d05cddcSAtari911 month: 'short', 10391d05cddcSAtari911 day: 'numeric' 10401d05cddcSAtari911 }); 10411d05cddcSAtari911 10421d05cddcSAtari911 // Convert to 12-hour format and handle time ranges 10431d05cddcSAtari911 let displayTime = ''; 10441d05cddcSAtari911 if (event.time) { 10451d05cddcSAtari911 displayTime = formatTimeRange(event.time, event.endTime); 10461d05cddcSAtari911 } 10471d05cddcSAtari911 10481d05cddcSAtari911 // Multi-day indicator 10491d05cddcSAtari911 let multiDay = ''; 10501d05cddcSAtari911 if (event.endDate && event.endDate !== displayDateKey) { 10511d05cddcSAtari911 const endObj = new Date(event.endDate + 'T00:00:00'); 10521d05cddcSAtari911 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 10531d05cddcSAtari911 weekday: 'short', 10541d05cddcSAtari911 month: 'short', 10551d05cddcSAtari911 day: 'numeric' 10561d05cddcSAtari911 }); 10571d05cddcSAtari911 } 10581d05cddcSAtari911 10591d05cddcSAtari911 const completedClass = event.completed ? ' event-completed' : ''; 10601d05cddcSAtari911 const isTask = event.isTask || false; 10611d05cddcSAtari911 const completed = event.completed || false; 10621d05cddcSAtari911 const isPastDue = isPast && isTask && !completed; 10631d05cddcSAtari911 const pastClass = (isPast && !isPastDue) ? ' event-past' : ''; 10641d05cddcSAtari911 const pastDueClass = isPastDue ? ' event-pastdue' : ''; 106596df7d3eSAtari911 const importantClass = isImportantNs ? ' event-important' : ''; 10661d05cddcSAtari911 const color = event.color || '#3498db'; 10671d05cddcSAtari911 10689ccd446eSAtari911 // Only inline style needed: border-left-color for event color indicator 106996df7d3eSAtari911 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)' : '') + '">'; 10701d05cddcSAtari911 10711d05cddcSAtari911 html += '<div class="event-info">'; 10721d05cddcSAtari911 html += '<div class="event-title-row">'; 107396df7d3eSAtari911 // Add star for important namespace events 107496df7d3eSAtari911 if (isImportantNs) { 1075da206178SAtari911 html += '<span class="event-important-star" title="Important">⭐</span> '; 107696df7d3eSAtari911 } 10771d05cddcSAtari911 html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>'; 10781d05cddcSAtari911 html += '</div>'; 10791d05cddcSAtari911 10801d05cddcSAtari911 // Show meta and description for non-past events AND past due tasks 10811d05cddcSAtari911 if (!isPast || isPastDue) { 10821d05cddcSAtari911 html += '<div class="event-meta-compact">'; 10831d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 10841d05cddcSAtari911 if (displayTime) { 10851d05cddcSAtari911 html += ' • ' + displayTime; 10861d05cddcSAtari911 } 10871d05cddcSAtari911 // Add PAST DUE or TODAY badge 10881d05cddcSAtari911 if (isPastDue) { 1089da206178SAtari911 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>'; 10901d05cddcSAtari911 } else if (isToday) { 1091da206178SAtari911 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>'; 10921d05cddcSAtari911 } 10939ccd446eSAtari911 // Add namespace badge 10941d05cddcSAtari911 if (eventNamespace) { 10957e8ea635SAtari911 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>'; 10961d05cddcSAtari911 } 10971d05cddcSAtari911 // Add conflict warning if event has time conflicts 10981d05cddcSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 10991d05cddcSAtari911 let conflictList = []; 11001d05cddcSAtari911 event.conflictsWith.forEach(conflict => { 11011d05cddcSAtari911 let conflictText = conflict.title; 11021d05cddcSAtari911 if (conflict.time) { 11031d05cddcSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 11041d05cddcSAtari911 } 11051d05cddcSAtari911 conflictList.push(conflictText); 11061d05cddcSAtari911 }); 11071d05cddcSAtari911 11089ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 11091d05cddcSAtari911 } 11101d05cddcSAtari911 html += '</span>'; 11111d05cddcSAtari911 html += '</div>'; 11121d05cddcSAtari911 11131d05cddcSAtari911 if (event.description) { 11141d05cddcSAtari911 html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>'; 11151d05cddcSAtari911 } 11161d05cddcSAtari911 } else { 11171d05cddcSAtari911 // For past events (not past due), store data in hidden divs for expand/collapse 11181d05cddcSAtari911 html += '<div class="event-meta-compact" style="display: none;">'; 11191d05cddcSAtari911 html += '<span class="event-date-time">' + displayDate + multiDay; 11201d05cddcSAtari911 if (displayTime) { 11211d05cddcSAtari911 html += ' • ' + displayTime; 11221d05cddcSAtari911 } 11231d05cddcSAtari911 // Add namespace badge for past events too 11241d05cddcSAtari911 let eventNamespace = event.namespace || ''; 11251d05cddcSAtari911 if (!eventNamespace && event._namespace !== undefined) { 11261d05cddcSAtari911 eventNamespace = event._namespace; 11271d05cddcSAtari911 } 11281d05cddcSAtari911 if (eventNamespace) { 11297e8ea635SAtari911 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>'; 11309ccd446eSAtari911 } 11319ccd446eSAtari911 // Add conflict warning for past events too 11329ccd446eSAtari911 if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) { 11339ccd446eSAtari911 let conflictList = []; 11349ccd446eSAtari911 event.conflictsWith.forEach(conflict => { 11359ccd446eSAtari911 let conflictText = conflict.title; 11369ccd446eSAtari911 if (conflict.time) { 11379ccd446eSAtari911 conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')'; 11389ccd446eSAtari911 } 11399ccd446eSAtari911 conflictList.push(conflictText); 11409ccd446eSAtari911 }); 11419ccd446eSAtari911 11429ccd446eSAtari911 html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>'; 11431d05cddcSAtari911 } 11441d05cddcSAtari911 html += '</span>'; 11451d05cddcSAtari911 html += '</div>'; 11461d05cddcSAtari911 11471d05cddcSAtari911 if (event.description) { 11481d05cddcSAtari911 html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>'; 11491d05cddcSAtari911 } 11501d05cddcSAtari911 } 11511d05cddcSAtari911 11521d05cddcSAtari911 html += '</div>'; // event-info 11531d05cddcSAtari911 11541d05cddcSAtari911 // Use stored namespace from event, fallback to _namespace, then passed namespace 11551d05cddcSAtari911 let buttonNamespace = event.namespace || ''; 11561d05cddcSAtari911 if (!buttonNamespace && event._namespace !== undefined) { 11571d05cddcSAtari911 buttonNamespace = event._namespace; 11581d05cddcSAtari911 } 11591d05cddcSAtari911 if (!buttonNamespace) { 11601d05cddcSAtari911 buttonNamespace = namespace; 11611d05cddcSAtari911 } 11621d05cddcSAtari911 11631d05cddcSAtari911 html += '<div class="event-actions-compact">'; 11641d05cddcSAtari911 html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">️</button>'; 11651d05cddcSAtari911 html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>'; 11661d05cddcSAtari911 html += '</div>'; 11671d05cddcSAtari911 11681d05cddcSAtari911 // Checkbox for tasks - ON THE FAR RIGHT 11691d05cddcSAtari911 if (isTask) { 11701d05cddcSAtari911 const checked = completed ? 'checked' : ''; 11711d05cddcSAtari911 html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">'; 11721d05cddcSAtari911 } 11731d05cddcSAtari911 11741d05cddcSAtari911 html += '</div>'; 11751d05cddcSAtari911 11761d05cddcSAtari911 return html; 11771d05cddcSAtari911}; 11781d05cddcSAtari911 11791d05cddcSAtari911// Render description with rich content support 11801d05cddcSAtari911window.renderDescription = function(description) { 11811d05cddcSAtari911 if (!description) return ''; 11821d05cddcSAtari911 11831d05cddcSAtari911 // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping) 11841d05cddcSAtari911 // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00 11851d05cddcSAtari911 11861d05cddcSAtari911 let rendered = description; 11871d05cddcSAtari911 const tokens = []; 11881d05cddcSAtari911 let tokenIndex = 0; 11891d05cddcSAtari911 11901d05cddcSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 11911d05cddcSAtari911 rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) { 11921d05cddcSAtari911 imagePath = imagePath.trim(); 11931d05cddcSAtari911 alt = alt ? alt.trim() : ''; 11941d05cddcSAtari911 11951d05cddcSAtari911 let imageHtml; 11961d05cddcSAtari911 // Handle external URLs 11971d05cddcSAtari911 if (imagePath.match(/^https?:\/\//)) { 11981d05cddcSAtari911 imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 11991d05cddcSAtari911 } else { 12001d05cddcSAtari911 // Handle internal DokuWiki images 12011d05cddcSAtari911 const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath); 12021d05cddcSAtari911 imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 12031d05cddcSAtari911 } 12041d05cddcSAtari911 12051d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12061d05cddcSAtari911 tokens[tokenIndex] = imageHtml; 12071d05cddcSAtari911 tokenIndex++; 12081d05cddcSAtari911 return token; 12091d05cddcSAtari911 }); 12101d05cddcSAtari911 12111d05cddcSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 12121d05cddcSAtari911 rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) { 12131d05cddcSAtari911 link = link.trim(); 12141d05cddcSAtari911 text = text ? text.trim() : link; 12151d05cddcSAtari911 12161d05cddcSAtari911 let linkHtml; 12171d05cddcSAtari911 // Handle external URLs 12181d05cddcSAtari911 if (link.match(/^https?:\/\//)) { 12191d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 12201d05cddcSAtari911 } else { 12211d05cddcSAtari911 // Handle internal DokuWiki links with section anchors 12221d05cddcSAtari911 const hashIndex = link.indexOf('#'); 12231d05cddcSAtari911 let pagePart = link; 12241d05cddcSAtari911 let sectionPart = ''; 12251d05cddcSAtari911 12261d05cddcSAtari911 if (hashIndex !== -1) { 12271d05cddcSAtari911 pagePart = link.substring(0, hashIndex); 12281d05cddcSAtari911 sectionPart = link.substring(hashIndex); // Includes the # 12291d05cddcSAtari911 } 12301d05cddcSAtari911 12311d05cddcSAtari911 const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart; 12321d05cddcSAtari911 linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>'; 12331d05cddcSAtari911 } 12341d05cddcSAtari911 12351d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12361d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12371d05cddcSAtari911 tokenIndex++; 12381d05cddcSAtari911 return token; 12391d05cddcSAtari911 }); 12401d05cddcSAtari911 12411d05cddcSAtari911 // Convert markdown-style links [text](url) to tokens 12421d05cddcSAtari911 rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) { 12431d05cddcSAtari911 text = text.trim(); 12441d05cddcSAtari911 url = url.trim(); 12451d05cddcSAtari911 12461d05cddcSAtari911 let linkHtml; 12471d05cddcSAtari911 if (url.match(/^https?:\/\//)) { 12481d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 12491d05cddcSAtari911 } else { 12501d05cddcSAtari911 linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>'; 12511d05cddcSAtari911 } 12521d05cddcSAtari911 12531d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12541d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12551d05cddcSAtari911 tokenIndex++; 12561d05cddcSAtari911 return token; 12571d05cddcSAtari911 }); 12581d05cddcSAtari911 12591d05cddcSAtari911 // Convert plain URLs to tokens 12601d05cddcSAtari911 rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) { 12611d05cddcSAtari911 const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>'; 12621d05cddcSAtari911 const token = '\x00TOKEN' + tokenIndex + '\x00'; 12631d05cddcSAtari911 tokens[tokenIndex] = linkHtml; 12641d05cddcSAtari911 tokenIndex++; 12651d05cddcSAtari911 return token; 12661d05cddcSAtari911 }); 12671d05cddcSAtari911 12681d05cddcSAtari911 // NOW escape the remaining text (tokens are protected with null bytes) 12691d05cddcSAtari911 rendered = escapeHtml(rendered); 12701d05cddcSAtari911 12711d05cddcSAtari911 // Convert newlines to <br> 12721d05cddcSAtari911 rendered = rendered.replace(/\n/g, '<br>'); 12731d05cddcSAtari911 12741d05cddcSAtari911 // DokuWiki text formatting (on escaped text) 12751d05cddcSAtari911 // Bold: **text** or __text__ 12761d05cddcSAtari911 rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); 12771d05cddcSAtari911 rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>'); 12781d05cddcSAtari911 12791d05cddcSAtari911 // Italic: //text// 12801d05cddcSAtari911 rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>'); 12811d05cddcSAtari911 12821d05cddcSAtari911 // Strikethrough: <del>text</del> 12831d05cddcSAtari911 rendered = rendered.replace(/<del>(.+?)<\/del>/g, '<del>$1</del>'); 12841d05cddcSAtari911 12851d05cddcSAtari911 // Monospace: ''text'' 12861d05cddcSAtari911 rendered = rendered.replace(/''(.+?)''/g, '<code>$1</code>'); 12871d05cddcSAtari911 12881d05cddcSAtari911 // Subscript: <sub>text</sub> 12891d05cddcSAtari911 rendered = rendered.replace(/<sub>(.+?)<\/sub>/g, '<sub>$1</sub>'); 12901d05cddcSAtari911 12911d05cddcSAtari911 // Superscript: <sup>text</sup> 12921d05cddcSAtari911 rendered = rendered.replace(/<sup>(.+?)<\/sup>/g, '<sup>$1</sup>'); 12931d05cddcSAtari911 12941d05cddcSAtari911 // Restore tokens (replace with actual HTML) 12951d05cddcSAtari911 for (let i = 0; i < tokens.length; i++) { 12961d05cddcSAtari911 const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g'); 12971d05cddcSAtari911 rendered = rendered.replace(tokenPattern, tokens[i]); 12981d05cddcSAtari911 } 12991d05cddcSAtari911 13001d05cddcSAtari911 return rendered; 13011d05cddcSAtari911} 13021d05cddcSAtari911 13031d05cddcSAtari911// Open add event dialog 13041d05cddcSAtari911window.openAddEvent = function(calId, namespace, date) { 13051d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 13061d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 13071d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 13081d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 13091d05cddcSAtari911 13101d05cddcSAtari911 if (!dateField) { 13111d05cddcSAtari911 console.error('Date field not found! ID: event-date-' + calId); 13121d05cddcSAtari911 return; 13131d05cddcSAtari911 } 13141d05cddcSAtari911 1315231d0edbSAtari911 // Check if there's a filtered namespace active (only for regular calendars) 13161d05cddcSAtari911 const calendar = document.getElementById(calId); 1317231d0edbSAtari911 const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null; 13181d05cddcSAtari911 13191d05cddcSAtari911 // Use filtered namespace if available, otherwise use the passed namespace 13201d05cddcSAtari911 const effectiveNamespace = filteredNamespace || namespace; 13211d05cddcSAtari911 13221d05cddcSAtari911 13231d05cddcSAtari911 // Reset form 13241d05cddcSAtari911 form.reset(); 13251d05cddcSAtari911 document.getElementById('event-id-' + calId).value = ''; 13261d05cddcSAtari911 13271d05cddcSAtari911 // Store the effective namespace in a hidden field or data attribute 13281d05cddcSAtari911 form.dataset.effectiveNamespace = effectiveNamespace; 13291d05cddcSAtari911 13301d05cddcSAtari911 // Set namespace dropdown to effective namespace 13311d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 13321d05cddcSAtari911 if (namespaceSelect) { 13331d05cddcSAtari911 if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) { 13341d05cddcSAtari911 // Set to specific namespace if not wildcard or multi-namespace 13351d05cddcSAtari911 namespaceSelect.value = effectiveNamespace; 13361d05cddcSAtari911 } else { 13371d05cddcSAtari911 // Default to empty (default namespace) for wildcard/multi views 13381d05cddcSAtari911 namespaceSelect.value = ''; 13391d05cddcSAtari911 } 13401d05cddcSAtari911 } 13411d05cddcSAtari911 13421d05cddcSAtari911 // Clear event namespace from previous edits 13431d05cddcSAtari911 delete form.dataset.eventNamespace; 13441d05cddcSAtari911 13451d05cddcSAtari911 // Set date - use local date, not UTC 13461d05cddcSAtari911 let defaultDate = date; 13471d05cddcSAtari911 if (!defaultDate) { 13481d05cddcSAtari911 // Get the currently displayed month from the calendar container 13491d05cddcSAtari911 const container = document.getElementById(calId); 13501d05cddcSAtari911 const displayedYear = parseInt(container.getAttribute('data-year')); 13511d05cddcSAtari911 const displayedMonth = parseInt(container.getAttribute('data-month')); 13521d05cddcSAtari911 13531d05cddcSAtari911 13541d05cddcSAtari911 if (displayedYear && displayedMonth) { 13551d05cddcSAtari911 // Use first day of the displayed month 13561d05cddcSAtari911 const year = displayedYear; 13571d05cddcSAtari911 const month = String(displayedMonth).padStart(2, '0'); 13581d05cddcSAtari911 defaultDate = `${year}-${month}-01`; 13591d05cddcSAtari911 } else { 13601d05cddcSAtari911 // Fallback to today if attributes not found 13611d05cddcSAtari911 const today = new Date(); 13621d05cddcSAtari911 const year = today.getFullYear(); 13631d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 13641d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 13651d05cddcSAtari911 defaultDate = `${year}-${month}-${day}`; 13661d05cddcSAtari911 } 13671d05cddcSAtari911 } 13681d05cddcSAtari911 dateField.value = defaultDate; 13691d05cddcSAtari911 dateField.removeAttribute('data-original-date'); 13701d05cddcSAtari911 13711d05cddcSAtari911 // Also set the end date field to the same default (user can change it) 13721d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 13731d05cddcSAtari911 if (endDateField) { 13741d05cddcSAtari911 endDateField.value = ''; // Empty by default (single-day event) 13751d05cddcSAtari911 // Set min attribute to help the date picker open on the right month 13761d05cddcSAtari911 endDateField.setAttribute('min', defaultDate); 13771d05cddcSAtari911 } 13781d05cddcSAtari911 13791d05cddcSAtari911 // Set default color 13801d05cddcSAtari911 document.getElementById('event-color-' + calId).value = '#3498db'; 13811d05cddcSAtari911 13821d05cddcSAtari911 // Initialize end time dropdown (disabled by default since no start time set) 13831d05cddcSAtari911 const endTimeField = document.getElementById('event-end-time-' + calId); 13841d05cddcSAtari911 if (endTimeField) { 13851d05cddcSAtari911 endTimeField.disabled = true; 13861d05cddcSAtari911 endTimeField.value = ''; 13871d05cddcSAtari911 } 13881d05cddcSAtari911 13891d05cddcSAtari911 // Initialize namespace search 13901d05cddcSAtari911 initNamespaceSearch(calId); 13911d05cddcSAtari911 13921d05cddcSAtari911 // Set title 1393da206178SAtari911 title.textContent = 'Add Event'; 13941d05cddcSAtari911 13951d05cddcSAtari911 // Show dialog 13961d05cddcSAtari911 dialog.style.display = 'flex'; 13971d05cddcSAtari911 13989ccd446eSAtari911 // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates) 13999ccd446eSAtari911 propagateThemeVars(calId, dialog); 14009ccd446eSAtari911 1401da206178SAtari911 // Make dialog draggable 1402da206178SAtari911 setTimeout(() => makeDialogDraggable(calId), 50); 1403da206178SAtari911 14041d05cddcSAtari911 // Focus title field 14051d05cddcSAtari911 setTimeout(() => { 14061d05cddcSAtari911 const titleField = document.getElementById('event-title-' + calId); 14071d05cddcSAtari911 if (titleField) titleField.focus(); 14081d05cddcSAtari911 }, 100); 14091d05cddcSAtari911}; 14101d05cddcSAtari911 14111d05cddcSAtari911// Edit event 14121d05cddcSAtari911window.editEvent = function(calId, eventId, date, namespace) { 14131d05cddcSAtari911 const params = new URLSearchParams({ 14141d05cddcSAtari911 call: 'plugin_calendar', 14151d05cddcSAtari911 action: 'get_event', 14161d05cddcSAtari911 namespace: namespace, 14171d05cddcSAtari911 date: date, 14181d05cddcSAtari911 eventId: eventId 14191d05cddcSAtari911 }); 14201d05cddcSAtari911 14211d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 14221d05cddcSAtari911 method: 'POST', 14231d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 14241d05cddcSAtari911 body: params.toString() 14251d05cddcSAtari911 }) 14261d05cddcSAtari911 .then(r => r.json()) 14271d05cddcSAtari911 .then(data => { 14281d05cddcSAtari911 if (data.success && data.event) { 14291d05cddcSAtari911 const event = data.event; 14301d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 14311d05cddcSAtari911 const title = document.getElementById('dialog-title-' + calId); 14321d05cddcSAtari911 const dateField = document.getElementById('event-date-' + calId); 14331d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 14341d05cddcSAtari911 14351d05cddcSAtari911 if (!dateField) { 14361d05cddcSAtari911 console.error('Date field not found when editing!'); 14371d05cddcSAtari911 return; 14381d05cddcSAtari911 } 14391d05cddcSAtari911 14401d05cddcSAtari911 // Store the event's actual namespace for saving (important for namespace=* views) 14411d05cddcSAtari911 if (event.namespace !== undefined) { 14421d05cddcSAtari911 form.dataset.eventNamespace = event.namespace; 14431d05cddcSAtari911 } 14441d05cddcSAtari911 14451d05cddcSAtari911 // Populate form 14461d05cddcSAtari911 document.getElementById('event-id-' + calId).value = event.id; 14471d05cddcSAtari911 dateField.value = date; 14481d05cddcSAtari911 dateField.setAttribute('data-original-date', date); 14491d05cddcSAtari911 14501d05cddcSAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 14511d05cddcSAtari911 endDateField.value = event.endDate || ''; 14521d05cddcSAtari911 // Set min attribute to help date picker open on the start date's month 14531d05cddcSAtari911 endDateField.setAttribute('min', date); 14541d05cddcSAtari911 14551d05cddcSAtari911 document.getElementById('event-title-' + calId).value = event.title; 14561d05cddcSAtari911 document.getElementById('event-time-' + calId).value = event.time || ''; 14571d05cddcSAtari911 document.getElementById('event-end-time-' + calId).value = event.endTime || ''; 14581d05cddcSAtari911 document.getElementById('event-color-' + calId).value = event.color || '#3498db'; 14591d05cddcSAtari911 document.getElementById('event-desc-' + calId).value = event.description || ''; 14601d05cddcSAtari911 document.getElementById('event-is-task-' + calId).checked = event.isTask || false; 14611d05cddcSAtari911 14621d05cddcSAtari911 // Update end time options based on start time 14631d05cddcSAtari911 if (event.time) { 14641d05cddcSAtari911 updateEndTimeOptions(calId); 14651d05cddcSAtari911 } 14661d05cddcSAtari911 14671d05cddcSAtari911 // Initialize namespace search 14681d05cddcSAtari911 initNamespaceSearch(calId); 14691d05cddcSAtari911 14701d05cddcSAtari911 // Set namespace fields if available 14711d05cddcSAtari911 const namespaceHidden = document.getElementById('event-namespace-' + calId); 14721d05cddcSAtari911 const namespaceSearch = document.getElementById('event-namespace-search-' + calId); 14731d05cddcSAtari911 if (namespaceHidden && event.namespace !== undefined) { 14749ccd446eSAtari911 // Set the hidden input (this is what gets submitted) 14759ccd446eSAtari911 namespaceHidden.value = event.namespace || ''; 14769ccd446eSAtari911 // Set the search input to display the namespace 14771d05cddcSAtari911 if (namespaceSearch) { 14781d05cddcSAtari911 namespaceSearch.value = event.namespace || '(default)'; 14791d05cddcSAtari911 } 14809ccd446eSAtari911 } else { 14819ccd446eSAtari911 // No namespace on event, set to default 14829ccd446eSAtari911 if (namespaceHidden) { 14839ccd446eSAtari911 namespaceHidden.value = ''; 14849ccd446eSAtari911 } 14859ccd446eSAtari911 if (namespaceSearch) { 14869ccd446eSAtari911 namespaceSearch.value = '(default)'; 14879ccd446eSAtari911 } 14881d05cddcSAtari911 } 14891d05cddcSAtari911 1490da206178SAtari911 title.textContent = 'Edit Event'; 14911d05cddcSAtari911 dialog.style.display = 'flex'; 14929ccd446eSAtari911 14939ccd446eSAtari911 // Propagate CSS vars to dialog 14949ccd446eSAtari911 propagateThemeVars(calId, dialog); 1495da206178SAtari911 1496da206178SAtari911 // Make dialog draggable 1497da206178SAtari911 setTimeout(() => makeDialogDraggable(calId), 50); 14981d05cddcSAtari911 } 14991d05cddcSAtari911 }) 15001d05cddcSAtari911 .catch(err => console.error('Error editing event:', err)); 15011d05cddcSAtari911}; 15021d05cddcSAtari911 15031d05cddcSAtari911// Delete event 15041d05cddcSAtari911window.deleteEvent = function(calId, eventId, date, namespace) { 1505da206178SAtari911 if (!confirm('Delete this event?')) return; 15061d05cddcSAtari911 15071d05cddcSAtari911 const params = new URLSearchParams({ 15081d05cddcSAtari911 call: 'plugin_calendar', 15091d05cddcSAtari911 action: 'delete_event', 15101d05cddcSAtari911 namespace: namespace, 15111d05cddcSAtari911 date: date, 15127e8ea635SAtari911 eventId: eventId, 1513*b498f308SAtari911 sectok: getSecurityToken() 15141d05cddcSAtari911 }); 15151d05cddcSAtari911 15161d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 15171d05cddcSAtari911 method: 'POST', 15181d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 15191d05cddcSAtari911 body: params.toString() 15201d05cddcSAtari911 }) 15211d05cddcSAtari911 .then(r => r.json()) 15221d05cddcSAtari911 .then(data => { 15231d05cddcSAtari911 if (data.success) { 15241d05cddcSAtari911 // Extract year and month from date 15251d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 15261d05cddcSAtari911 152796df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting (not the deleted event's namespace) 152896df7d3eSAtari911 // This preserves wildcard/multi-namespace views 152996df7d3eSAtari911 const container = document.getElementById(calId); 153096df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 153196df7d3eSAtari911 153296df7d3eSAtari911 // Reload calendar data via AJAX with the calendar's original namespace 153396df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 15341d05cddcSAtari911 } 15351d05cddcSAtari911 }) 15361d05cddcSAtari911 .catch(err => console.error('Error:', err)); 15371d05cddcSAtari911}; 15381d05cddcSAtari911 15391d05cddcSAtari911// Save event (add or edit) 15401d05cddcSAtari911window.saveEventCompact = function(calId, namespace) { 15411d05cddcSAtari911 const form = document.getElementById('eventform-' + calId); 15421d05cddcSAtari911 15431d05cddcSAtari911 // Get namespace from dropdown - this is what the user selected 15441d05cddcSAtari911 const namespaceSelect = document.getElementById('event-namespace-' + calId); 15451d05cddcSAtari911 const selectedNamespace = namespaceSelect ? namespaceSelect.value : ''; 15461d05cddcSAtari911 15471d05cddcSAtari911 // ALWAYS use what the user selected in the dropdown 15481d05cddcSAtari911 // This allows changing namespace when editing 15491d05cddcSAtari911 const finalNamespace = selectedNamespace; 15501d05cddcSAtari911 15511d05cddcSAtari911 const eventId = document.getElementById('event-id-' + calId).value; 15521d05cddcSAtari911 15531d05cddcSAtari911 // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event) 15541d05cddcSAtari911 const originalNamespace = form.dataset.eventNamespace; 15551d05cddcSAtari911 15561d05cddcSAtari911 15571d05cddcSAtari911 const dateInput = document.getElementById('event-date-' + calId); 15581d05cddcSAtari911 const date = dateInput.value; 15591d05cddcSAtari911 const oldDate = dateInput.getAttribute('data-original-date') || date; 15601d05cddcSAtari911 const endDate = document.getElementById('event-end-date-' + calId).value; 15611d05cddcSAtari911 const title = document.getElementById('event-title-' + calId).value; 15621d05cddcSAtari911 const time = document.getElementById('event-time-' + calId).value; 15631d05cddcSAtari911 const endTime = document.getElementById('event-end-time-' + calId).value; 15641d05cddcSAtari911 const colorSelect = document.getElementById('event-color-' + calId); 15651d05cddcSAtari911 let color = colorSelect.value; 15661d05cddcSAtari911 15671d05cddcSAtari911 // Handle custom color 15681d05cddcSAtari911 if (color === 'custom') { 15691d05cddcSAtari911 color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value; 15701d05cddcSAtari911 } 15711d05cddcSAtari911 15721d05cddcSAtari911 const description = document.getElementById('event-desc-' + calId).value; 15731d05cddcSAtari911 const isTask = document.getElementById('event-is-task-' + calId).checked; 15741d05cddcSAtari911 const completed = false; // New tasks are not completed 15751d05cddcSAtari911 const isRecurring = document.getElementById('event-recurring-' + calId).checked; 15761d05cddcSAtari911 const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value; 15771d05cddcSAtari911 const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value; 15781d05cddcSAtari911 157996df7d3eSAtari911 // New recurrence options 158096df7d3eSAtari911 const recurrenceIntervalInput = document.getElementById('event-recurrence-interval-' + calId); 158196df7d3eSAtari911 const recurrenceInterval = recurrenceIntervalInput ? parseInt(recurrenceIntervalInput.value) || 1 : 1; 158296df7d3eSAtari911 158396df7d3eSAtari911 // Weekly: collect selected days 158496df7d3eSAtari911 let weekDays = []; 158596df7d3eSAtari911 const weeklyOptions = document.getElementById('weekly-options-' + calId); 158696df7d3eSAtari911 if (weeklyOptions && recurrenceType === 'weekly') { 158796df7d3eSAtari911 const checkboxes = weeklyOptions.querySelectorAll('input[name="weekDays[]"]:checked'); 158896df7d3eSAtari911 weekDays = Array.from(checkboxes).map(cb => cb.value); 158996df7d3eSAtari911 } 159096df7d3eSAtari911 159196df7d3eSAtari911 // Monthly: collect day-of-month or ordinal weekday 159296df7d3eSAtari911 let monthDay = ''; 159396df7d3eSAtari911 let monthlyType = 'dayOfMonth'; 159496df7d3eSAtari911 let ordinalWeek = ''; 159596df7d3eSAtari911 let ordinalDay = ''; 159696df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 159796df7d3eSAtari911 if (monthlyOptions && recurrenceType === 'monthly') { 159896df7d3eSAtari911 const monthlyTypeRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked'); 159996df7d3eSAtari911 monthlyType = monthlyTypeRadio ? monthlyTypeRadio.value : 'dayOfMonth'; 160096df7d3eSAtari911 160196df7d3eSAtari911 if (monthlyType === 'dayOfMonth') { 160296df7d3eSAtari911 const monthDayInput = document.getElementById('event-month-day-' + calId); 160396df7d3eSAtari911 monthDay = monthDayInput ? monthDayInput.value : ''; 160496df7d3eSAtari911 } else { 160596df7d3eSAtari911 const ordinalSelect = document.getElementById('event-ordinal-' + calId); 160696df7d3eSAtari911 const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId); 160796df7d3eSAtari911 ordinalWeek = ordinalSelect ? ordinalSelect.value : '1'; 160896df7d3eSAtari911 ordinalDay = ordinalDaySelect ? ordinalDaySelect.value : '0'; 160996df7d3eSAtari911 } 161096df7d3eSAtari911 } 161196df7d3eSAtari911 16121d05cddcSAtari911 if (!title) { 16131d05cddcSAtari911 alert('Please enter a title'); 16141d05cddcSAtari911 return; 16151d05cddcSAtari911 } 16161d05cddcSAtari911 16171d05cddcSAtari911 if (!date) { 16181d05cddcSAtari911 alert('Please select a date'); 16191d05cddcSAtari911 return; 16201d05cddcSAtari911 } 16211d05cddcSAtari911 16221d05cddcSAtari911 const params = new URLSearchParams({ 16231d05cddcSAtari911 call: 'plugin_calendar', 16241d05cddcSAtari911 action: 'save_event', 16251d05cddcSAtari911 namespace: finalNamespace, 16261d05cddcSAtari911 eventId: eventId, 16271d05cddcSAtari911 date: date, 16281d05cddcSAtari911 oldDate: oldDate, 16291d05cddcSAtari911 endDate: endDate, 16301d05cddcSAtari911 title: title, 16311d05cddcSAtari911 time: time, 16321d05cddcSAtari911 endTime: endTime, 16331d05cddcSAtari911 color: color, 16341d05cddcSAtari911 description: description, 16351d05cddcSAtari911 isTask: isTask ? '1' : '0', 16361d05cddcSAtari911 completed: completed ? '1' : '0', 16371d05cddcSAtari911 isRecurring: isRecurring ? '1' : '0', 16381d05cddcSAtari911 recurrenceType: recurrenceType, 163996df7d3eSAtari911 recurrenceInterval: recurrenceInterval, 16407e8ea635SAtari911 recurrenceEnd: recurrenceEnd, 164196df7d3eSAtari911 weekDays: weekDays.join(','), 164296df7d3eSAtari911 monthlyType: monthlyType, 164396df7d3eSAtari911 monthDay: monthDay, 164496df7d3eSAtari911 ordinalWeek: ordinalWeek, 164596df7d3eSAtari911 ordinalDay: ordinalDay, 1646*b498f308SAtari911 sectok: getSecurityToken() 16471d05cddcSAtari911 }); 16481d05cddcSAtari911 16491d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 16501d05cddcSAtari911 method: 'POST', 16511d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 16521d05cddcSAtari911 body: params.toString() 16531d05cddcSAtari911 }) 16541d05cddcSAtari911 .then(r => r.json()) 16551d05cddcSAtari911 .then(data => { 16561d05cddcSAtari911 if (data.success) { 16571d05cddcSAtari911 closeEventDialog(calId); 16581d05cddcSAtari911 16591d05cddcSAtari911 // For recurring events, do a full page reload to show all occurrences 16601d05cddcSAtari911 if (isRecurring) { 16611d05cddcSAtari911 location.reload(); 16621d05cddcSAtari911 return; 16631d05cddcSAtari911 } 16641d05cddcSAtari911 16651d05cddcSAtari911 // Extract year and month from the NEW date (in case date was changed) 16661d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 16671d05cddcSAtari911 166896df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting from the container 166996df7d3eSAtari911 // This preserves wildcard/multi-namespace views after editing 167096df7d3eSAtari911 const container = document.getElementById(calId); 167196df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 167296df7d3eSAtari911 16731d05cddcSAtari911 // Reload calendar data via AJAX to the month of the event 167496df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 16751d05cddcSAtari911 } else { 16761d05cddcSAtari911 alert('Error: ' + (data.error || 'Unknown error')); 16771d05cddcSAtari911 } 16781d05cddcSAtari911 }) 16791d05cddcSAtari911 .catch(err => { 16801d05cddcSAtari911 console.error('Error:', err); 16811d05cddcSAtari911 alert('Error saving event'); 16821d05cddcSAtari911 }); 16831d05cddcSAtari911}; 16841d05cddcSAtari911 16851d05cddcSAtari911// Reload calendar data without page refresh 16861d05cddcSAtari911window.reloadCalendarData = function(calId, year, month, namespace) { 16871d05cddcSAtari911 const params = new URLSearchParams({ 16881d05cddcSAtari911 call: 'plugin_calendar', 16891d05cddcSAtari911 action: 'load_month', 16901d05cddcSAtari911 year: year, 16911d05cddcSAtari911 month: month, 16921d05cddcSAtari911 namespace: namespace, 16931d05cddcSAtari911 _: new Date().getTime() // Cache buster 16941d05cddcSAtari911 }); 16951d05cddcSAtari911 16961d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 16971d05cddcSAtari911 method: 'POST', 16981d05cddcSAtari911 headers: { 16991d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 17001d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 17011d05cddcSAtari911 'Pragma': 'no-cache' 17021d05cddcSAtari911 }, 17031d05cddcSAtari911 body: params.toString() 17041d05cddcSAtari911 }) 17051d05cddcSAtari911 .then(r => r.json()) 17061d05cddcSAtari911 .then(data => { 17071d05cddcSAtari911 if (data.success) { 17081d05cddcSAtari911 const container = document.getElementById(calId); 17091d05cddcSAtari911 17101d05cddcSAtari911 // Check if this is a full calendar or just event panel 17111d05cddcSAtari911 if (container.classList.contains('calendar-compact-container')) { 17121d05cddcSAtari911 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 17131d05cddcSAtari911 } else if (container.classList.contains('event-panel-standalone')) { 17141d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 17151d05cddcSAtari911 } 17161d05cddcSAtari911 } 17171d05cddcSAtari911 }) 17181d05cddcSAtari911 .catch(err => console.error('Error:', err)); 17191d05cddcSAtari911}; 17201d05cddcSAtari911 17211d05cddcSAtari911// Close event dialog 17221d05cddcSAtari911window.closeEventDialog = function(calId) { 17231d05cddcSAtari911 const dialog = document.getElementById('dialog-' + calId); 17241d05cddcSAtari911 dialog.style.display = 'none'; 17251d05cddcSAtari911}; 17261d05cddcSAtari911 17271d05cddcSAtari911// Escape HTML 17281d05cddcSAtari911window.escapeHtml = function(text) { 17291d05cddcSAtari911 const div = document.createElement('div'); 17301d05cddcSAtari911 div.textContent = text; 17311d05cddcSAtari911 return div.innerHTML; 17321d05cddcSAtari911}; 17331d05cddcSAtari911 17341d05cddcSAtari911// Highlight event when clicking on bar in calendar 17351d05cddcSAtari911window.highlightEvent = function(calId, eventId, date) { 17369ccd446eSAtari911 17371d05cddcSAtari911 // Find the event item in the event list 17381d05cddcSAtari911 const eventList = document.querySelector('#' + calId + ' .event-list-compact'); 17399ccd446eSAtari911 if (!eventList) { 17409ccd446eSAtari911 return; 17419ccd446eSAtari911 } 17421d05cddcSAtari911 17431d05cddcSAtari911 const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]'); 17449ccd446eSAtari911 if (!eventItem) { 17459ccd446eSAtari911 return; 17469ccd446eSAtari911 } 17471d05cddcSAtari911 17489ccd446eSAtari911 17499ccd446eSAtari911 // Get theme 17509ccd446eSAtari911 const container = document.getElementById(calId); 17519ccd446eSAtari911 const theme = container ? container.dataset.theme : 'matrix'; 17529ccd446eSAtari911 const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {}; 17539ccd446eSAtari911 17549ccd446eSAtari911 17559ccd446eSAtari911 // Theme-specific highlight colors 17569ccd446eSAtari911 let highlightBg, highlightShadow; 17579ccd446eSAtari911 if (theme === 'matrix') { 17589ccd446eSAtari911 highlightBg = '#1a3d1a'; // Darker green 17599ccd446eSAtari911 highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)'; 17609ccd446eSAtari911 } else if (theme === 'purple') { 17619ccd446eSAtari911 highlightBg = '#3d2b4d'; // Darker purple 17629ccd446eSAtari911 highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)'; 17639ccd446eSAtari911 } else if (theme === 'professional') { 17649ccd446eSAtari911 highlightBg = '#e3f2fd'; // Light blue 17659ccd446eSAtari911 highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)'; 17669ccd446eSAtari911 } else if (theme === 'pink') { 17679ccd446eSAtari911 highlightBg = '#3d2030'; // Darker pink 17689ccd446eSAtari911 highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)'; 17699ccd446eSAtari911 } else if (theme === 'wiki') { 17707e8ea635SAtari911 highlightBg = themeStyles.header_bg || '#e8e8e8'; // __background_alt__ 17717e8ea635SAtari911 highlightShadow = '0 0 10px rgba(0, 0, 0, 0.15)'; 17729ccd446eSAtari911 } 17739ccd446eSAtari911 17749ccd446eSAtari911 17759ccd446eSAtari911 // Store original styles 17769ccd446eSAtari911 const originalBg = eventItem.style.background; 17779ccd446eSAtari911 const originalShadow = eventItem.style.boxShadow; 17789ccd446eSAtari911 17799ccd446eSAtari911 // Remove previous highlights (restore their original styles) 17801d05cddcSAtari911 const previousHighlights = eventList.querySelectorAll('.event-highlighted'); 17819ccd446eSAtari911 previousHighlights.forEach(el => { 17829ccd446eSAtari911 el.classList.remove('event-highlighted'); 17839ccd446eSAtari911 }); 17841d05cddcSAtari911 17859ccd446eSAtari911 // Add highlight class and apply theme-aware glow 17861d05cddcSAtari911 eventItem.classList.add('event-highlighted'); 17871d05cddcSAtari911 17889ccd446eSAtari911 // Set CSS properties directly 17899ccd446eSAtari911 eventItem.style.setProperty('background', highlightBg, 'important'); 17909ccd446eSAtari911 eventItem.style.setProperty('box-shadow', highlightShadow, 'important'); 17919ccd446eSAtari911 eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important'); 17929ccd446eSAtari911 17939ccd446eSAtari911 17941d05cddcSAtari911 // Scroll to event 17951d05cddcSAtari911 eventItem.scrollIntoView({ 17961d05cddcSAtari911 behavior: 'smooth', 17971d05cddcSAtari911 block: 'nearest', 17981d05cddcSAtari911 inline: 'nearest' 17991d05cddcSAtari911 }); 18001d05cddcSAtari911 18019ccd446eSAtari911 // Remove highlight after 3 seconds and restore original styles 18021d05cddcSAtari911 setTimeout(() => { 18031d05cddcSAtari911 eventItem.classList.remove('event-highlighted'); 18049ccd446eSAtari911 eventItem.style.setProperty('background', originalBg); 18059ccd446eSAtari911 eventItem.style.setProperty('box-shadow', originalShadow); 18069ccd446eSAtari911 eventItem.style.setProperty('transition', ''); 18071d05cddcSAtari911 }, 3000); 18081d05cddcSAtari911}; 18091d05cddcSAtari911 18101d05cddcSAtari911// Toggle recurring event options 18111d05cddcSAtari911window.toggleRecurringOptions = function(calId) { 18121d05cddcSAtari911 const checkbox = document.getElementById('event-recurring-' + calId); 18131d05cddcSAtari911 const options = document.getElementById('recurring-options-' + calId); 18141d05cddcSAtari911 18151d05cddcSAtari911 if (checkbox && options) { 18161d05cddcSAtari911 options.style.display = checkbox.checked ? 'block' : 'none'; 181796df7d3eSAtari911 if (checkbox.checked) { 181896df7d3eSAtari911 // Initialize the sub-options based on current selection 181996df7d3eSAtari911 updateRecurrenceOptions(calId); 182096df7d3eSAtari911 } 182196df7d3eSAtari911 } 182296df7d3eSAtari911}; 182396df7d3eSAtari911 182496df7d3eSAtari911// Update visible recurrence options based on type (daily/weekly/monthly/yearly) 182596df7d3eSAtari911window.updateRecurrenceOptions = function(calId) { 182696df7d3eSAtari911 const typeSelect = document.getElementById('event-recurrence-type-' + calId); 182796df7d3eSAtari911 const weeklyOptions = document.getElementById('weekly-options-' + calId); 182896df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 182996df7d3eSAtari911 183096df7d3eSAtari911 if (!typeSelect) return; 183196df7d3eSAtari911 183296df7d3eSAtari911 const recurrenceType = typeSelect.value; 183396df7d3eSAtari911 183496df7d3eSAtari911 // Hide all conditional options first 183596df7d3eSAtari911 if (weeklyOptions) weeklyOptions.style.display = 'none'; 183696df7d3eSAtari911 if (monthlyOptions) monthlyOptions.style.display = 'none'; 183796df7d3eSAtari911 183896df7d3eSAtari911 // Show relevant options 183996df7d3eSAtari911 if (recurrenceType === 'weekly' && weeklyOptions) { 184096df7d3eSAtari911 weeklyOptions.style.display = 'block'; 184196df7d3eSAtari911 // Auto-select today's day of week if nothing selected 184296df7d3eSAtari911 const checkboxes = weeklyOptions.querySelectorAll('input[type="checkbox"]'); 184396df7d3eSAtari911 const anyChecked = Array.from(checkboxes).some(cb => cb.checked); 184496df7d3eSAtari911 if (!anyChecked) { 184596df7d3eSAtari911 const today = new Date().getDay(); 184696df7d3eSAtari911 const todayCheckbox = weeklyOptions.querySelector('input[value="' + today + '"]'); 184796df7d3eSAtari911 if (todayCheckbox) todayCheckbox.checked = true; 184896df7d3eSAtari911 } 184996df7d3eSAtari911 } else if (recurrenceType === 'monthly' && monthlyOptions) { 185096df7d3eSAtari911 monthlyOptions.style.display = 'block'; 185196df7d3eSAtari911 // Set default day to current day of month 185296df7d3eSAtari911 const monthDayInput = document.getElementById('event-month-day-' + calId); 185396df7d3eSAtari911 if (monthDayInput && !monthDayInput.dataset.userSet) { 185496df7d3eSAtari911 monthDayInput.value = new Date().getDate(); 185596df7d3eSAtari911 } 185696df7d3eSAtari911 } 185796df7d3eSAtari911}; 185896df7d3eSAtari911 185996df7d3eSAtari911// Toggle between day-of-month and ordinal weekday for monthly recurrence 186096df7d3eSAtari911window.updateMonthlyType = function(calId) { 186196df7d3eSAtari911 const dayOfMonthDiv = document.getElementById('monthly-day-' + calId); 186296df7d3eSAtari911 const ordinalDiv = document.getElementById('monthly-ordinal-' + calId); 186396df7d3eSAtari911 const monthlyOptions = document.getElementById('monthly-options-' + calId); 186496df7d3eSAtari911 186596df7d3eSAtari911 if (!monthlyOptions) return; 186696df7d3eSAtari911 186796df7d3eSAtari911 const selectedRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked'); 186896df7d3eSAtari911 if (!selectedRadio) return; 186996df7d3eSAtari911 187096df7d3eSAtari911 if (selectedRadio.value === 'dayOfMonth') { 187196df7d3eSAtari911 if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'flex'; 187296df7d3eSAtari911 if (ordinalDiv) ordinalDiv.style.display = 'none'; 187396df7d3eSAtari911 } else { 187496df7d3eSAtari911 if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'none'; 187596df7d3eSAtari911 if (ordinalDiv) ordinalDiv.style.display = 'block'; 187696df7d3eSAtari911 187796df7d3eSAtari911 // Set defaults based on current date 187896df7d3eSAtari911 const now = new Date(); 187996df7d3eSAtari911 const dayOfWeek = now.getDay(); 188096df7d3eSAtari911 const weekOfMonth = Math.ceil(now.getDate() / 7); 188196df7d3eSAtari911 188296df7d3eSAtari911 const ordinalSelect = document.getElementById('event-ordinal-' + calId); 188396df7d3eSAtari911 const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId); 188496df7d3eSAtari911 188596df7d3eSAtari911 if (ordinalSelect && !ordinalSelect.dataset.userSet) { 188696df7d3eSAtari911 ordinalSelect.value = weekOfMonth; 188796df7d3eSAtari911 } 188896df7d3eSAtari911 if (ordinalDaySelect && !ordinalDaySelect.dataset.userSet) { 188996df7d3eSAtari911 ordinalDaySelect.value = dayOfWeek; 189096df7d3eSAtari911 } 18911d05cddcSAtari911 } 18921d05cddcSAtari911}; 18931d05cddcSAtari911 18949ccd446eSAtari911// ============================================================ 18959ccd446eSAtari911// Document-level event delegation (guarded - only attach once) 18969ccd446eSAtari911// These use event delegation so they work for AJAX-rebuilt content. 18979ccd446eSAtari911// ============================================================ 18989ccd446eSAtari911if (!window._calendarDelegationInit) { 18999ccd446eSAtari911 window._calendarDelegationInit = true; 19009ccd446eSAtari911 19019ccd446eSAtari911 // ESC closes dialogs, popups, tooltips 19021d05cddcSAtari911 document.addEventListener('keydown', function(e) { 19031d05cddcSAtari911 if (e.key === 'Escape') { 19049ccd446eSAtari911 document.querySelectorAll('.event-dialog-compact').forEach(function(d) { 19059ccd446eSAtari911 if (d.style.display === 'flex') d.style.display = 'none'; 19069ccd446eSAtari911 }); 19079ccd446eSAtari911 document.querySelectorAll('.day-popup').forEach(function(p) { 19089ccd446eSAtari911 p.style.display = 'none'; 19099ccd446eSAtari911 }); 19109ccd446eSAtari911 hideConflictTooltip(); 19111d05cddcSAtari911 } 19121d05cddcSAtari911 }); 19139ccd446eSAtari911 19149ccd446eSAtari911 // Conflict tooltip delegation (capture phase for mouseenter/leave) 19159ccd446eSAtari911 document.addEventListener('mouseenter', function(e) { 19169ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 19179ccd446eSAtari911 showConflictTooltip(e.target); 19181d05cddcSAtari911 } 19199ccd446eSAtari911 }, true); 19209ccd446eSAtari911 19219ccd446eSAtari911 document.addEventListener('mouseleave', function(e) { 19229ccd446eSAtari911 if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) { 19239ccd446eSAtari911 hideConflictTooltip(); 19249ccd446eSAtari911 } 19259ccd446eSAtari911 }, true); 19269ccd446eSAtari911} // end delegation guard 19271d05cddcSAtari911 19281d05cddcSAtari911// Event panel navigation 19291d05cddcSAtari911window.navEventPanel = function(calId, year, month, namespace) { 19301d05cddcSAtari911 const params = new URLSearchParams({ 19311d05cddcSAtari911 call: 'plugin_calendar', 19321d05cddcSAtari911 action: 'load_month', 19331d05cddcSAtari911 year: year, 19341d05cddcSAtari911 month: month, 19351d05cddcSAtari911 namespace: namespace, 19361d05cddcSAtari911 _: new Date().getTime() // Cache buster 19371d05cddcSAtari911 }); 19381d05cddcSAtari911 19391d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 19401d05cddcSAtari911 method: 'POST', 19411d05cddcSAtari911 headers: { 19421d05cddcSAtari911 'Content-Type': 'application/x-www-form-urlencoded', 19431d05cddcSAtari911 'Cache-Control': 'no-cache, no-store, must-revalidate', 19441d05cddcSAtari911 'Pragma': 'no-cache' 19451d05cddcSAtari911 }, 19461d05cddcSAtari911 body: params.toString() 19471d05cddcSAtari911 }) 19481d05cddcSAtari911 .then(r => r.json()) 19491d05cddcSAtari911 .then(data => { 19501d05cddcSAtari911 if (data.success) { 19511d05cddcSAtari911 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 19521d05cddcSAtari911 } 19531d05cddcSAtari911 }) 19541d05cddcSAtari911 .catch(err => console.error('Error:', err)); 19551d05cddcSAtari911}; 19561d05cddcSAtari911 19571d05cddcSAtari911// Rebuild event panel only 19581d05cddcSAtari911window.rebuildEventPanel = function(calId, year, month, events, namespace) { 19591d05cddcSAtari911 const container = document.getElementById(calId); 1960da206178SAtari911 const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 1961da206178SAtari911 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 19621d05cddcSAtari911 19631d05cddcSAtari911 // Update month title in new compact header 19641d05cddcSAtari911 const monthTitle = container.querySelector('.panel-month-title'); 19651d05cddcSAtari911 if (monthTitle) { 19661d05cddcSAtari911 monthTitle.textContent = monthNames[month - 1] + ' ' + year; 19671d05cddcSAtari911 monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 1968da206178SAtari911 monthTitle.setAttribute('title', 'Click to jump to month'); 19691d05cddcSAtari911 } 19701d05cddcSAtari911 19711d05cddcSAtari911 // Fallback: Update old header format if exists 19721d05cddcSAtari911 const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker'); 19731d05cddcSAtari911 if (oldHeader && !monthTitle) { 1974da206178SAtari911 oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events'; 19751d05cddcSAtari911 oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 19761d05cddcSAtari911 } 19771d05cddcSAtari911 19781d05cddcSAtari911 // Update nav buttons 19791d05cddcSAtari911 let prevMonth = month - 1; 19801d05cddcSAtari911 let prevYear = year; 19811d05cddcSAtari911 if (prevMonth < 1) { 19821d05cddcSAtari911 prevMonth = 12; 19831d05cddcSAtari911 prevYear--; 19841d05cddcSAtari911 } 19851d05cddcSAtari911 19861d05cddcSAtari911 let nextMonth = month + 1; 19871d05cddcSAtari911 let nextYear = year; 19881d05cddcSAtari911 if (nextMonth > 12) { 19891d05cddcSAtari911 nextMonth = 1; 19901d05cddcSAtari911 nextYear++; 19911d05cddcSAtari911 } 19921d05cddcSAtari911 19931d05cddcSAtari911 // Update new compact nav buttons 19941d05cddcSAtari911 const navBtns = container.querySelectorAll('.panel-nav-btn'); 19951d05cddcSAtari911 if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 19961d05cddcSAtari911 if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 19971d05cddcSAtari911 19981d05cddcSAtari911 // Fallback for old nav buttons 19991d05cddcSAtari911 const oldNavBtns = container.querySelectorAll('.cal-nav-btn'); 20001d05cddcSAtari911 if (oldNavBtns.length > 0 && navBtns.length === 0) { 20011d05cddcSAtari911 if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 20021d05cddcSAtari911 if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 20031d05cddcSAtari911 } 20041d05cddcSAtari911 20051d05cddcSAtari911 // Update Today button (works for both old and new) 20061d05cddcSAtari911 const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact'); 20071d05cddcSAtari911 if (todayBtn) { 20081d05cddcSAtari911 todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`); 20091d05cddcSAtari911 } 20101d05cddcSAtari911 20111d05cddcSAtari911 // Rebuild event list 20121d05cddcSAtari911 const eventList = container.querySelector('.event-list-compact'); 20131d05cddcSAtari911 if (eventList) { 20141d05cddcSAtari911 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 20151d05cddcSAtari911 } 20161d05cddcSAtari911}; 20171d05cddcSAtari911 20181d05cddcSAtari911// Open add event for panel 20191d05cddcSAtari911window.openAddEventPanel = function(calId, namespace) { 20201d05cddcSAtari911 const today = new Date(); 20211d05cddcSAtari911 const year = today.getFullYear(); 20221d05cddcSAtari911 const month = String(today.getMonth() + 1).padStart(2, '0'); 20231d05cddcSAtari911 const day = String(today.getDate()).padStart(2, '0'); 20241d05cddcSAtari911 const localDate = `${year}-${month}-${day}`; 20251d05cddcSAtari911 openAddEvent(calId, namespace, localDate); 20261d05cddcSAtari911}; 20271d05cddcSAtari911 20281d05cddcSAtari911// Toggle task completion 20291d05cddcSAtari911window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) { 20301d05cddcSAtari911 const params = new URLSearchParams({ 20311d05cddcSAtari911 call: 'plugin_calendar', 20321d05cddcSAtari911 action: 'toggle_task', 20331d05cddcSAtari911 namespace: namespace, 20341d05cddcSAtari911 date: date, 20351d05cddcSAtari911 eventId: eventId, 20367e8ea635SAtari911 completed: completed ? '1' : '0', 2037*b498f308SAtari911 sectok: getSecurityToken() 20381d05cddcSAtari911 }); 20391d05cddcSAtari911 20401d05cddcSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 20411d05cddcSAtari911 method: 'POST', 20421d05cddcSAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 20431d05cddcSAtari911 body: params.toString() 20441d05cddcSAtari911 }) 20451d05cddcSAtari911 .then(r => r.json()) 20461d05cddcSAtari911 .then(data => { 20471d05cddcSAtari911 if (data.success) { 20481d05cddcSAtari911 const [year, month] = date.split('-').map(Number); 204996df7d3eSAtari911 205096df7d3eSAtari911 // Get the calendar's ORIGINAL namespace setting from the container 205196df7d3eSAtari911 const container = document.getElementById(calId); 205296df7d3eSAtari911 const calendarNamespace = container ? (container.dataset.namespace || '') : namespace; 205396df7d3eSAtari911 205496df7d3eSAtari911 reloadCalendarData(calId, year, month, calendarNamespace); 20551d05cddcSAtari911 } 20561d05cddcSAtari911 }) 20571d05cddcSAtari911 .catch(err => console.error('Error toggling task:', err)); 20581d05cddcSAtari911}; 20591d05cddcSAtari911 20601d05cddcSAtari911// Make dialog draggable 20611d05cddcSAtari911window.makeDialogDraggable = function(calId) { 20621d05cddcSAtari911 const dialog = document.getElementById('dialog-content-' + calId); 20631d05cddcSAtari911 const handle = document.getElementById('drag-handle-' + calId); 20641d05cddcSAtari911 20651d05cddcSAtari911 if (!dialog || !handle) return; 20661d05cddcSAtari911 2067da206178SAtari911 // Remove any existing drag setup to prevent duplicate listeners 2068da206178SAtari911 if (handle._dragCleanup) { 2069da206178SAtari911 handle._dragCleanup(); 2070da206178SAtari911 } 2071da206178SAtari911 2072da206178SAtari911 // Reset position when dialog opens 2073da206178SAtari911 dialog.style.transform = ''; 2074da206178SAtari911 20751d05cddcSAtari911 let isDragging = false; 2076da206178SAtari911 let currentX = 0; 2077da206178SAtari911 let currentY = 0; 20781d05cddcSAtari911 let initialX; 20791d05cddcSAtari911 let initialY; 20801d05cddcSAtari911 let xOffset = 0; 20811d05cddcSAtari911 let yOffset = 0; 20821d05cddcSAtari911 20831d05cddcSAtari911 function dragStart(e) { 2084da206178SAtari911 // Only start drag if clicking on the handle itself, not buttons inside it 2085da206178SAtari911 if (e.target.tagName === 'BUTTON') return; 2086da206178SAtari911 20871d05cddcSAtari911 initialX = e.clientX - xOffset; 20881d05cddcSAtari911 initialY = e.clientY - yOffset; 20891d05cddcSAtari911 isDragging = true; 2090da206178SAtari911 handle.style.cursor = 'grabbing'; 20911d05cddcSAtari911 } 20921d05cddcSAtari911 20931d05cddcSAtari911 function drag(e) { 20941d05cddcSAtari911 if (isDragging) { 20951d05cddcSAtari911 e.preventDefault(); 20961d05cddcSAtari911 currentX = e.clientX - initialX; 20971d05cddcSAtari911 currentY = e.clientY - initialY; 20981d05cddcSAtari911 xOffset = currentX; 20991d05cddcSAtari911 yOffset = currentY; 2100da206178SAtari911 dialog.style.transform = `translate(${currentX}px, ${currentY}px)`; 21011d05cddcSAtari911 } 21021d05cddcSAtari911 } 21031d05cddcSAtari911 21041d05cddcSAtari911 function dragEnd(e) { 2105da206178SAtari911 if (isDragging) { 21061d05cddcSAtari911 initialX = currentX; 21071d05cddcSAtari911 initialY = currentY; 21081d05cddcSAtari911 isDragging = false; 2109da206178SAtari911 handle.style.cursor = 'move'; 2110da206178SAtari911 } 21111d05cddcSAtari911 } 21121d05cddcSAtari911 2113da206178SAtari911 // Add listeners 2114da206178SAtari911 handle.addEventListener('mousedown', dragStart); 2115da206178SAtari911 document.addEventListener('mousemove', drag); 2116da206178SAtari911 document.addEventListener('mouseup', dragEnd); 21171d05cddcSAtari911 2118da206178SAtari911 // Store cleanup function to remove listeners later 2119da206178SAtari911 handle._dragCleanup = function() { 2120da206178SAtari911 handle.removeEventListener('mousedown', dragStart); 2121da206178SAtari911 document.removeEventListener('mousemove', drag); 2122da206178SAtari911 document.removeEventListener('mouseup', dragEnd); 21231d05cddcSAtari911 }; 21241d05cddcSAtari911}; 21251d05cddcSAtari911 21261d05cddcSAtari911// Toggle expand/collapse for past events 21271d05cddcSAtari911window.togglePastEventExpand = function(element) { 21281d05cddcSAtari911 // Stop propagation to prevent any parent click handlers 21291d05cddcSAtari911 event.stopPropagation(); 21301d05cddcSAtari911 21311d05cddcSAtari911 const meta = element.querySelector(".event-meta-compact"); 21321d05cddcSAtari911 const desc = element.querySelector(".event-desc-compact"); 21331d05cddcSAtari911 21341d05cddcSAtari911 // Toggle visibility 21351d05cddcSAtari911 if (meta.style.display === "none") { 21361d05cddcSAtari911 // Expand 21371d05cddcSAtari911 meta.style.display = "block"; 21381d05cddcSAtari911 if (desc) desc.style.display = "block"; 21391d05cddcSAtari911 element.classList.add("event-past-expanded"); 21401d05cddcSAtari911 } else { 21411d05cddcSAtari911 // Collapse 21421d05cddcSAtari911 meta.style.display = "none"; 21431d05cddcSAtari911 if (desc) desc.style.display = "none"; 21441d05cddcSAtari911 element.classList.remove("event-past-expanded"); 21451d05cddcSAtari911 } 21461d05cddcSAtari911}; 21471d05cddcSAtari911 21489ccd446eSAtari911// Filter calendar by namespace when clicking namespace badge (guarded) 21499ccd446eSAtari911if (!window._calendarClickDelegationInit) { 21509ccd446eSAtari911 window._calendarClickDelegationInit = true; 21511d05cddcSAtari911 document.addEventListener('click', function(e) { 21521d05cddcSAtari911 if (e.target.classList.contains('event-namespace-badge')) { 21531d05cddcSAtari911 const namespace = e.target.textContent; 21541d05cddcSAtari911 const calendar = e.target.closest('.calendar-compact-container'); 21551d05cddcSAtari911 21567e8ea635SAtari911 if (!calendar) return; 21571d05cddcSAtari911 21581d05cddcSAtari911 const calId = calendar.id; 21591d05cddcSAtari911 21607e8ea635SAtari911 // Use AJAX reload to filter both calendar grid and event list 21617e8ea635SAtari911 filterCalendarByNamespace(calId, namespace); 21621d05cddcSAtari911 } 21631d05cddcSAtari911 }); 21649ccd446eSAtari911} // end click delegation guard 21651d05cddcSAtari911 21661d05cddcSAtari911// Update the displayed filtered namespace in event list header 21677e8ea635SAtari911// Legacy badge removed - namespace filtering still works but badge no longer shown 21681d05cddcSAtari911window.updateFilteredNamespaceDisplay = function(calId, namespace) { 21691d05cddcSAtari911 const calendar = document.getElementById(calId); 21701d05cddcSAtari911 if (!calendar) return; 21711d05cddcSAtari911 21721d05cddcSAtari911 const headerContent = calendar.querySelector('.event-list-header-content'); 21731d05cddcSAtari911 if (!headerContent) return; 21741d05cddcSAtari911 21757e8ea635SAtari911 // Remove any existing filter badge (cleanup) 21761d05cddcSAtari911 let filterBadge = headerContent.querySelector('.namespace-filter-badge'); 21771d05cddcSAtari911 if (filterBadge) { 21781d05cddcSAtari911 filterBadge.remove(); 21791d05cddcSAtari911 } 21801d05cddcSAtari911}; 21811d05cddcSAtari911 21821d05cddcSAtari911// Clear namespace filter 21831d05cddcSAtari911window.clearNamespaceFilter = function(calId) { 21841d05cddcSAtari911 21851d05cddcSAtari911 const container = document.getElementById(calId); 21861d05cddcSAtari911 if (!container) { 21871d05cddcSAtari911 console.error('Calendar container not found:', calId); 21881d05cddcSAtari911 return; 21891d05cddcSAtari911 } 21901d05cddcSAtari911 21919ccd446eSAtari911 // Immediately hide/remove the filter badge 21929ccd446eSAtari911 const filterBadge = container.querySelector('.calendar-namespace-filter'); 21939ccd446eSAtari911 if (filterBadge) { 21949ccd446eSAtari911 filterBadge.style.display = 'none'; 21959ccd446eSAtari911 filterBadge.remove(); 21969ccd446eSAtari911 } 21979ccd446eSAtari911 21981d05cddcSAtari911 // Get current year and month 21991d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 22001d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 22011d05cddcSAtari911 22021d05cddcSAtari911 // Get original namespace (what the calendar was initialized with) 22031d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 22041d05cddcSAtari911 22059ccd446eSAtari911 // Also check for sidebar widget 22069ccd446eSAtari911 const sidebarContainer = document.getElementById('sidebar-widget-' + calId); 22079ccd446eSAtari911 if (sidebarContainer) { 22089ccd446eSAtari911 // For sidebar widget, just reload the page without namespace filter 22099ccd446eSAtari911 // Remove the namespace from the URL and reload 22109ccd446eSAtari911 const url = new URL(window.location.href); 22119ccd446eSAtari911 url.searchParams.delete('namespace'); 22129ccd446eSAtari911 window.location.href = url.toString(); 22139ccd446eSAtari911 return; 22149ccd446eSAtari911 } 22151d05cddcSAtari911 22169ccd446eSAtari911 // For regular calendar, reload calendar with original namespace 22171d05cddcSAtari911 navCalendar(calId, year, month, originalNamespace); 22181d05cddcSAtari911}; 22191d05cddcSAtari911 22201d05cddcSAtari911window.clearNamespaceFilterPanel = function(calId) { 22211d05cddcSAtari911 22221d05cddcSAtari911 const container = document.getElementById(calId); 22231d05cddcSAtari911 if (!container) { 22241d05cddcSAtari911 console.error('Event panel container not found:', calId); 22251d05cddcSAtari911 return; 22261d05cddcSAtari911 } 22271d05cddcSAtari911 22281d05cddcSAtari911 // Get current year and month from URL params or container 22291d05cddcSAtari911 const year = parseInt(container.dataset.year) || new Date().getFullYear(); 22301d05cddcSAtari911 const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1); 22311d05cddcSAtari911 22321d05cddcSAtari911 // Get original namespace (what the panel was initialized with) 22331d05cddcSAtari911 const originalNamespace = container.dataset.originalNamespace || ''; 22341d05cddcSAtari911 22351d05cddcSAtari911 22361d05cddcSAtari911 // Reload event panel with original namespace 22371d05cddcSAtari911 navEventPanel(calId, year, month, originalNamespace); 22381d05cddcSAtari911}; 22391d05cddcSAtari911 22401d05cddcSAtari911// Color picker functions 22411d05cddcSAtari911window.updateCustomColorPicker = function(calId) { 22421d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 22431d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 22441d05cddcSAtari911 22451d05cddcSAtari911 if (select.value === 'custom') { 22461d05cddcSAtari911 // Show color picker 22471d05cddcSAtari911 picker.style.display = 'inline-block'; 22481d05cddcSAtari911 picker.click(); // Open color picker 22491d05cddcSAtari911 } else { 22501d05cddcSAtari911 // Hide color picker and sync value 22511d05cddcSAtari911 picker.style.display = 'none'; 22521d05cddcSAtari911 picker.value = select.value; 22531d05cddcSAtari911 } 22541d05cddcSAtari911}; 22551d05cddcSAtari911 22561d05cddcSAtari911function updateColorFromPicker(calId) { 22571d05cddcSAtari911 const select = document.getElementById('event-color-' + calId); 22581d05cddcSAtari911 const picker = document.getElementById('event-color-custom-' + calId); 22591d05cddcSAtari911 22601d05cddcSAtari911 // Set select to custom and update its underlying value 22611d05cddcSAtari911 select.value = 'custom'; 22621d05cddcSAtari911 // Store the actual color value in a data attribute 22631d05cddcSAtari911 select.dataset.customColor = picker.value; 22641d05cddcSAtari911} 22651d05cddcSAtari911 22661d05cddcSAtari911// Toggle past events visibility 22671d05cddcSAtari911window.togglePastEvents = function(calId) { 22681d05cddcSAtari911 const content = document.getElementById('past-events-' + calId); 22691d05cddcSAtari911 const arrow = document.getElementById('past-arrow-' + calId); 22701d05cddcSAtari911 22711d05cddcSAtari911 if (!content || !arrow) { 22721d05cddcSAtari911 console.error('Past events elements not found for:', calId); 22731d05cddcSAtari911 return; 22741d05cddcSAtari911 } 22751d05cddcSAtari911 22761d05cddcSAtari911 // Check computed style instead of inline style 22771d05cddcSAtari911 const isHidden = window.getComputedStyle(content).display === 'none'; 22781d05cddcSAtari911 22791d05cddcSAtari911 if (isHidden) { 22801d05cddcSAtari911 content.style.display = 'block'; 22811d05cddcSAtari911 arrow.textContent = '▼'; 22821d05cddcSAtari911 } else { 22831d05cddcSAtari911 content.style.display = 'none'; 22841d05cddcSAtari911 arrow.textContent = '▶'; 22851d05cddcSAtari911 } 22861d05cddcSAtari911}; 22871d05cddcSAtari911 22881d05cddcSAtari911// Fuzzy match scoring function 22891d05cddcSAtari911window.fuzzyMatch = function(pattern, str) { 22901d05cddcSAtari911 pattern = pattern.toLowerCase(); 22911d05cddcSAtari911 str = str.toLowerCase(); 22921d05cddcSAtari911 22931d05cddcSAtari911 let patternIdx = 0; 22941d05cddcSAtari911 let score = 0; 22951d05cddcSAtari911 let consecutiveMatches = 0; 22961d05cddcSAtari911 22971d05cddcSAtari911 for (let i = 0; i < str.length; i++) { 22981d05cddcSAtari911 if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) { 22991d05cddcSAtari911 score += 1 + consecutiveMatches; 23001d05cddcSAtari911 consecutiveMatches++; 23011d05cddcSAtari911 patternIdx++; 23021d05cddcSAtari911 } else { 23031d05cddcSAtari911 consecutiveMatches = 0; 23041d05cddcSAtari911 } 23051d05cddcSAtari911 } 23061d05cddcSAtari911 23071d05cddcSAtari911 // Return null if not all characters matched 23081d05cddcSAtari911 if (patternIdx !== pattern.length) { 23091d05cddcSAtari911 return null; 23101d05cddcSAtari911 } 23111d05cddcSAtari911 23121d05cddcSAtari911 // Bonus for exact match 23131d05cddcSAtari911 if (str === pattern) { 23141d05cddcSAtari911 score += 100; 23151d05cddcSAtari911 } 23161d05cddcSAtari911 23171d05cddcSAtari911 // Bonus for starts with 23181d05cddcSAtari911 if (str.startsWith(pattern)) { 23191d05cddcSAtari911 score += 50; 23201d05cddcSAtari911 } 23211d05cddcSAtari911 23221d05cddcSAtari911 return score; 23231d05cddcSAtari911}; 23241d05cddcSAtari911 23251d05cddcSAtari911// Initialize namespace search for a calendar 23261d05cddcSAtari911window.initNamespaceSearch = function(calId) { 23271d05cddcSAtari911 const searchInput = document.getElementById('event-namespace-search-' + calId); 23281d05cddcSAtari911 const hiddenInput = document.getElementById('event-namespace-' + calId); 23291d05cddcSAtari911 const dropdown = document.getElementById('event-namespace-dropdown-' + calId); 23301d05cddcSAtari911 const dataElement = document.getElementById('namespaces-data-' + calId); 23311d05cddcSAtari911 23321d05cddcSAtari911 if (!searchInput || !hiddenInput || !dropdown || !dataElement) { 23331d05cddcSAtari911 return; // Elements not found 23341d05cddcSAtari911 } 23351d05cddcSAtari911 23361d05cddcSAtari911 let namespaces = []; 23371d05cddcSAtari911 try { 23381d05cddcSAtari911 namespaces = JSON.parse(dataElement.textContent); 23391d05cddcSAtari911 } catch (e) { 23401d05cddcSAtari911 console.error('Failed to parse namespaces data:', e); 23411d05cddcSAtari911 return; 23421d05cddcSAtari911 } 23431d05cddcSAtari911 23441d05cddcSAtari911 let selectedIndex = -1; 23451d05cddcSAtari911 23461d05cddcSAtari911 // Filter and show dropdown 23471d05cddcSAtari911 function filterNamespaces(query) { 23481d05cddcSAtari911 if (!query || query.trim() === '') { 23491d05cddcSAtari911 // Show all namespaces when empty 23501d05cddcSAtari911 hiddenInput.value = ''; 23511d05cddcSAtari911 const results = namespaces.slice(0, 20); // Limit to 20 23521d05cddcSAtari911 showDropdown(results); 23531d05cddcSAtari911 return; 23541d05cddcSAtari911 } 23551d05cddcSAtari911 23561d05cddcSAtari911 // Fuzzy match and score 23571d05cddcSAtari911 const matches = []; 23581d05cddcSAtari911 for (let i = 0; i < namespaces.length; i++) { 23591d05cddcSAtari911 const score = fuzzyMatch(query, namespaces[i]); 23601d05cddcSAtari911 if (score !== null) { 23611d05cddcSAtari911 matches.push({ namespace: namespaces[i], score: score }); 23621d05cddcSAtari911 } 23631d05cddcSAtari911 } 23641d05cddcSAtari911 23651d05cddcSAtari911 // Sort by score (descending) 23661d05cddcSAtari911 matches.sort((a, b) => b.score - a.score); 23671d05cddcSAtari911 23681d05cddcSAtari911 // Take top 20 results 23691d05cddcSAtari911 const results = matches.slice(0, 20).map(m => m.namespace); 23701d05cddcSAtari911 showDropdown(results); 23711d05cddcSAtari911 } 23721d05cddcSAtari911 23731d05cddcSAtari911 function showDropdown(results) { 23741d05cddcSAtari911 dropdown.innerHTML = ''; 23751d05cddcSAtari911 selectedIndex = -1; 23761d05cddcSAtari911 23771d05cddcSAtari911 if (results.length === 0) { 23781d05cddcSAtari911 dropdown.style.display = 'none'; 23791d05cddcSAtari911 return; 23801d05cddcSAtari911 } 23811d05cddcSAtari911 23821d05cddcSAtari911 // Add (default) option 23831d05cddcSAtari911 const defaultOption = document.createElement('div'); 23841d05cddcSAtari911 defaultOption.className = 'namespace-option'; 23851d05cddcSAtari911 defaultOption.textContent = '(default)'; 23861d05cddcSAtari911 defaultOption.dataset.value = ''; 23871d05cddcSAtari911 dropdown.appendChild(defaultOption); 23881d05cddcSAtari911 23891d05cddcSAtari911 results.forEach(ns => { 23901d05cddcSAtari911 const option = document.createElement('div'); 23911d05cddcSAtari911 option.className = 'namespace-option'; 23921d05cddcSAtari911 option.textContent = ns; 23931d05cddcSAtari911 option.dataset.value = ns; 23941d05cddcSAtari911 dropdown.appendChild(option); 23951d05cddcSAtari911 }); 23961d05cddcSAtari911 23971d05cddcSAtari911 dropdown.style.display = 'block'; 23981d05cddcSAtari911 } 23991d05cddcSAtari911 24001d05cddcSAtari911 function hideDropdown() { 24011d05cddcSAtari911 dropdown.style.display = 'none'; 24021d05cddcSAtari911 selectedIndex = -1; 24031d05cddcSAtari911 } 24041d05cddcSAtari911 24051d05cddcSAtari911 function selectOption(namespace) { 24061d05cddcSAtari911 hiddenInput.value = namespace; 24071d05cddcSAtari911 searchInput.value = namespace || '(default)'; 24081d05cddcSAtari911 hideDropdown(); 24091d05cddcSAtari911 } 24101d05cddcSAtari911 24111d05cddcSAtari911 // Event listeners 24121d05cddcSAtari911 searchInput.addEventListener('input', function(e) { 24131d05cddcSAtari911 filterNamespaces(e.target.value); 24141d05cddcSAtari911 }); 24151d05cddcSAtari911 24161d05cddcSAtari911 searchInput.addEventListener('focus', function(e) { 24171d05cddcSAtari911 filterNamespaces(e.target.value); 24181d05cddcSAtari911 }); 24191d05cddcSAtari911 24201d05cddcSAtari911 searchInput.addEventListener('blur', function(e) { 24211d05cddcSAtari911 // Delay to allow click on dropdown 24221d05cddcSAtari911 setTimeout(hideDropdown, 200); 24231d05cddcSAtari911 }); 24241d05cddcSAtari911 24251d05cddcSAtari911 searchInput.addEventListener('keydown', function(e) { 24261d05cddcSAtari911 const options = dropdown.querySelectorAll('.namespace-option'); 24271d05cddcSAtari911 24281d05cddcSAtari911 if (e.key === 'ArrowDown') { 24291d05cddcSAtari911 e.preventDefault(); 24301d05cddcSAtari911 selectedIndex = Math.min(selectedIndex + 1, options.length - 1); 24311d05cddcSAtari911 updateSelection(options); 24321d05cddcSAtari911 } else if (e.key === 'ArrowUp') { 24331d05cddcSAtari911 e.preventDefault(); 24341d05cddcSAtari911 selectedIndex = Math.max(selectedIndex - 1, -1); 24351d05cddcSAtari911 updateSelection(options); 24361d05cddcSAtari911 } else if (e.key === 'Enter') { 24371d05cddcSAtari911 e.preventDefault(); 24381d05cddcSAtari911 if (selectedIndex >= 0 && options[selectedIndex]) { 24391d05cddcSAtari911 selectOption(options[selectedIndex].dataset.value); 24401d05cddcSAtari911 } 24411d05cddcSAtari911 } else if (e.key === 'Escape') { 24421d05cddcSAtari911 hideDropdown(); 24431d05cddcSAtari911 } 24441d05cddcSAtari911 }); 24451d05cddcSAtari911 24461d05cddcSAtari911 function updateSelection(options) { 24471d05cddcSAtari911 options.forEach((opt, idx) => { 24481d05cddcSAtari911 if (idx === selectedIndex) { 24491d05cddcSAtari911 opt.classList.add('selected'); 24501d05cddcSAtari911 opt.scrollIntoView({ block: 'nearest' }); 24511d05cddcSAtari911 } else { 24521d05cddcSAtari911 opt.classList.remove('selected'); 24531d05cddcSAtari911 } 24541d05cddcSAtari911 }); 24551d05cddcSAtari911 } 24561d05cddcSAtari911 24571d05cddcSAtari911 // Click on dropdown option 24581d05cddcSAtari911 dropdown.addEventListener('mousedown', function(e) { 24591d05cddcSAtari911 if (e.target.classList.contains('namespace-option')) { 24601d05cddcSAtari911 selectOption(e.target.dataset.value); 24611d05cddcSAtari911 } 24621d05cddcSAtari911 }); 24631d05cddcSAtari911}; 24641d05cddcSAtari911 24651d05cddcSAtari911// Update end time options based on start time selection 24661d05cddcSAtari911window.updateEndTimeOptions = function(calId) { 24671d05cddcSAtari911 const startTimeSelect = document.getElementById('event-time-' + calId); 24681d05cddcSAtari911 const endTimeSelect = document.getElementById('event-end-time-' + calId); 2469da206178SAtari911 const startDateField = document.getElementById('event-date-' + calId); 2470da206178SAtari911 const endDateField = document.getElementById('event-end-date-' + calId); 24711d05cddcSAtari911 24721d05cddcSAtari911 if (!startTimeSelect || !endTimeSelect) return; 24731d05cddcSAtari911 24741d05cddcSAtari911 const startTime = startTimeSelect.value; 2475da206178SAtari911 const startDate = startDateField ? startDateField.value : ''; 2476da206178SAtari911 const endDate = endDateField ? endDateField.value : ''; 24771d05cddcSAtari911 2478da206178SAtari911 // Check if end date is different from start date (multi-day event) 2479da206178SAtari911 const isMultiDay = endDate && endDate !== startDate; 2480da206178SAtari911 2481da206178SAtari911 // If start time is empty (all day), disable end time and reset 24821d05cddcSAtari911 if (!startTime) { 24831d05cddcSAtari911 endTimeSelect.disabled = true; 24841d05cddcSAtari911 endTimeSelect.value = ''; 2485da206178SAtari911 // Show all options again 2486da206178SAtari911 Array.from(endTimeSelect.options).forEach(opt => { 2487da206178SAtari911 opt.disabled = false; 2488da206178SAtari911 opt.style.display = ''; 2489da206178SAtari911 }); 24901d05cddcSAtari911 return; 24911d05cddcSAtari911 } 24921d05cddcSAtari911 24931d05cddcSAtari911 // Enable end time select 24941d05cddcSAtari911 endTimeSelect.disabled = false; 24951d05cddcSAtari911 2496da206178SAtari911 // If multi-day event, allow all end times (event can end at any time on the end date) 2497da206178SAtari911 if (isMultiDay) { 2498da206178SAtari911 Array.from(endTimeSelect.options).forEach(opt => { 2499da206178SAtari911 opt.disabled = false; 2500da206178SAtari911 opt.style.display = ''; 2501da206178SAtari911 }); 2502da206178SAtari911 return; 2503da206178SAtari911 } 25041d05cddcSAtari911 2505da206178SAtari911 // Same-day event: Convert start time to minutes and filter options 2506da206178SAtari911 const [startHour, startMinute] = startTime.split(':').map(Number); 2507da206178SAtari911 const startMinutes = startHour * 60 + startMinute; 2508da206178SAtari911 2509da206178SAtari911 // Get current end time value 25101d05cddcSAtari911 const currentEndTime = endTimeSelect.value; 2511da206178SAtari911 let currentEndMinutes = 0; 2512da206178SAtari911 if (currentEndTime) { 2513da206178SAtari911 const [h, m] = currentEndTime.split(':').map(Number); 2514da206178SAtari911 currentEndMinutes = h * 60 + m; 2515da206178SAtari911 } 25161d05cddcSAtari911 2517da206178SAtari911 // Disable/hide options before or equal to start time 25181d05cddcSAtari911 let firstValidOption = null; 2519da206178SAtari911 Array.from(endTimeSelect.options).forEach(opt => { 2520da206178SAtari911 if (opt.value === '') { 2521da206178SAtari911 // Keep "Same as start" option enabled 2522da206178SAtari911 opt.disabled = false; 2523da206178SAtari911 opt.style.display = ''; 2524da206178SAtari911 return; 25251d05cddcSAtari911 } 25261d05cddcSAtari911 2527da206178SAtari911 const [h, m] = opt.value.split(':').map(Number); 2528da206178SAtari911 const optMinutes = h * 60 + m; 25291d05cddcSAtari911 2530da206178SAtari911 if (optMinutes <= startMinutes) { 2531da206178SAtari911 // Disable and hide times at or before start 2532da206178SAtari911 opt.disabled = true; 2533da206178SAtari911 opt.style.display = 'none'; 25341d05cddcSAtari911 } else { 2535da206178SAtari911 // Enable and show times after start 2536da206178SAtari911 opt.disabled = false; 2537da206178SAtari911 opt.style.display = ''; 2538da206178SAtari911 if (!firstValidOption) { 2539da206178SAtari911 firstValidOption = opt.value; 25401d05cddcSAtari911 } 25411d05cddcSAtari911 } 2542da206178SAtari911 }); 25431d05cddcSAtari911 25441d05cddcSAtari911 // If current end time is now invalid, set a new one 2545da206178SAtari911 if (currentEndTime && currentEndMinutes <= startMinutes) { 25461d05cddcSAtari911 // Try to set to 1 hour after start 25471d05cddcSAtari911 let endHour = startHour + 1; 25481d05cddcSAtari911 let endMinute = startMinute; 25491d05cddcSAtari911 25501d05cddcSAtari911 if (endHour >= 24) { 25511d05cddcSAtari911 endHour = 23; 25521d05cddcSAtari911 endMinute = 45; 25531d05cddcSAtari911 } 25541d05cddcSAtari911 25551d05cddcSAtari911 const suggestedEndTime = String(endHour).padStart(2, '0') + ':' + String(endMinute).padStart(2, '0'); 25561d05cddcSAtari911 2557da206178SAtari911 // Check if suggested time exists and is valid 2558da206178SAtari911 const suggestedOpt = Array.from(endTimeSelect.options).find(opt => opt.value === suggestedEndTime && !opt.disabled); 25591d05cddcSAtari911 2560da206178SAtari911 if (suggestedOpt) { 25611d05cddcSAtari911 endTimeSelect.value = suggestedEndTime; 25621d05cddcSAtari911 } else if (firstValidOption) { 25631d05cddcSAtari911 endTimeSelect.value = firstValidOption; 25641d05cddcSAtari911 } else { 25651d05cddcSAtari911 endTimeSelect.value = ''; 25661d05cddcSAtari911 } 25671d05cddcSAtari911 } 25681d05cddcSAtari911}; 25691d05cddcSAtari911 25701d05cddcSAtari911// Check for time conflicts between events on the same date 25711d05cddcSAtari911window.checkTimeConflicts = function(events, currentEventId) { 25721d05cddcSAtari911 const conflicts = []; 25731d05cddcSAtari911 25741d05cddcSAtari911 // Group events by date 25751d05cddcSAtari911 const eventsByDate = {}; 25761d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(events)) { 25771d05cddcSAtari911 if (!Array.isArray(dateEvents)) continue; 25781d05cddcSAtari911 25791d05cddcSAtari911 dateEvents.forEach(evt => { 25801d05cddcSAtari911 if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event 25811d05cddcSAtari911 25821d05cddcSAtari911 if (!eventsByDate[date]) eventsByDate[date] = []; 25831d05cddcSAtari911 eventsByDate[date].push(evt); 25841d05cddcSAtari911 }); 25851d05cddcSAtari911 } 25861d05cddcSAtari911 25871d05cddcSAtari911 // Check for overlaps on each date 25881d05cddcSAtari911 for (const [date, dateEvents] of Object.entries(eventsByDate)) { 25891d05cddcSAtari911 for (let i = 0; i < dateEvents.length; i++) { 25901d05cddcSAtari911 for (let j = i + 1; j < dateEvents.length; j++) { 25911d05cddcSAtari911 const evt1 = dateEvents[i]; 25921d05cddcSAtari911 const evt2 = dateEvents[j]; 25931d05cddcSAtari911 25941d05cddcSAtari911 if (eventsOverlap(evt1, evt2)) { 25951d05cddcSAtari911 // Mark both events as conflicting 25961d05cddcSAtari911 if (!evt1.hasConflict) evt1.hasConflict = true; 25971d05cddcSAtari911 if (!evt2.hasConflict) evt2.hasConflict = true; 25981d05cddcSAtari911 25991d05cddcSAtari911 // Store conflict info 26001d05cddcSAtari911 if (!evt1.conflictsWith) evt1.conflictsWith = []; 26011d05cddcSAtari911 if (!evt2.conflictsWith) evt2.conflictsWith = []; 26021d05cddcSAtari911 26031d05cddcSAtari911 evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime}); 26041d05cddcSAtari911 evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime}); 26051d05cddcSAtari911 } 26061d05cddcSAtari911 } 26071d05cddcSAtari911 } 26081d05cddcSAtari911 } 26091d05cddcSAtari911 26101d05cddcSAtari911 return events; 26111d05cddcSAtari911}; 26121d05cddcSAtari911 26131d05cddcSAtari911// Check if two events overlap in time 26141d05cddcSAtari911function eventsOverlap(evt1, evt2) { 26151d05cddcSAtari911 if (!evt1.time || !evt2.time) return false; // All-day events don't conflict 26161d05cddcSAtari911 26171d05cddcSAtari911 const start1 = evt1.time; 26181d05cddcSAtari911 const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start 26191d05cddcSAtari911 26201d05cddcSAtari911 const start2 = evt2.time; 26211d05cddcSAtari911 const end2 = evt2.endTime || evt2.time; 26221d05cddcSAtari911 26231d05cddcSAtari911 // Convert to minutes for easier comparison 26241d05cddcSAtari911 const start1Mins = timeToMinutes(start1); 26251d05cddcSAtari911 const end1Mins = timeToMinutes(end1); 26261d05cddcSAtari911 const start2Mins = timeToMinutes(start2); 26271d05cddcSAtari911 const end2Mins = timeToMinutes(end2); 26281d05cddcSAtari911 26291d05cddcSAtari911 // Check for overlap 26301d05cddcSAtari911 // Events overlap if: start1 < end2 AND start2 < end1 26311d05cddcSAtari911 return start1Mins < end2Mins && start2Mins < end1Mins; 26321d05cddcSAtari911} 26331d05cddcSAtari911 26341d05cddcSAtari911// Convert HH:MM time to minutes since midnight 26351d05cddcSAtari911function timeToMinutes(timeStr) { 26361d05cddcSAtari911 const [hours, minutes] = timeStr.split(':').map(Number); 26371d05cddcSAtari911 return hours * 60 + minutes; 26381d05cddcSAtari911} 26391d05cddcSAtari911 26401d05cddcSAtari911// Format time range for display 26411d05cddcSAtari911window.formatTimeRange = function(startTime, endTime) { 26421d05cddcSAtari911 if (!startTime) return ''; 26431d05cddcSAtari911 26441d05cddcSAtari911 const formatTime = (timeStr) => { 26451d05cddcSAtari911 const [hour24, minute] = timeStr.split(':').map(Number); 26461d05cddcSAtari911 const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24); 26471d05cddcSAtari911 const ampm = hour24 < 12 ? 'AM' : 'PM'; 26481d05cddcSAtari911 return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm; 26491d05cddcSAtari911 }; 26501d05cddcSAtari911 26511d05cddcSAtari911 if (!endTime || endTime === startTime) { 26521d05cddcSAtari911 return formatTime(startTime); 26531d05cddcSAtari911 } 26541d05cddcSAtari911 26551d05cddcSAtari911 return formatTime(startTime) + ' - ' + formatTime(endTime); 26561d05cddcSAtari911}; 26571d05cddcSAtari911 26589ccd446eSAtari911// Track last known mouse position for tooltip positioning fallback 26599ccd446eSAtari911var _lastMouseX = 0, _lastMouseY = 0; 26609ccd446eSAtari911document.addEventListener('mousemove', function(e) { 26619ccd446eSAtari911 _lastMouseX = e.clientX; 26629ccd446eSAtari911 _lastMouseY = e.clientY; 26639ccd446eSAtari911}); 26649ccd446eSAtari911 26651d05cddcSAtari911// Show custom conflict tooltip 26661d05cddcSAtari911window.showConflictTooltip = function(badgeElement) { 26671d05cddcSAtari911 // Remove any existing tooltip 26681d05cddcSAtari911 hideConflictTooltip(); 26691d05cddcSAtari911 26709ccd446eSAtari911 // Get conflict data (base64-encoded JSON to avoid attribute quote issues) 26719ccd446eSAtari911 const conflictsRaw = badgeElement.getAttribute('data-conflicts'); 26729ccd446eSAtari911 if (!conflictsRaw) return; 26731d05cddcSAtari911 26741d05cddcSAtari911 let conflicts; 26751d05cddcSAtari911 try { 26769ccd446eSAtari911 conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw)))); 26771d05cddcSAtari911 } catch (e) { 26789ccd446eSAtari911 // Fallback: try parsing as plain JSON (for PHP-rendered badges) 26799ccd446eSAtari911 try { 26809ccd446eSAtari911 conflicts = JSON.parse(conflictsRaw); 26819ccd446eSAtari911 } catch (e2) { 26829ccd446eSAtari911 console.error('Failed to parse conflicts:', e2); 26831d05cddcSAtari911 return; 26841d05cddcSAtari911 } 26859ccd446eSAtari911 } 26869ccd446eSAtari911 26879ccd446eSAtari911 // Get theme from the calendar container via CSS variables 26889ccd446eSAtari911 // Try closest ancestor first, then fall back to any calendar on the page 26899ccd446eSAtari911 let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone'); 26909ccd446eSAtari911 if (!containerEl) { 26919ccd446eSAtari911 // Badge might be inside a day popup (appended to body) - find any calendar container 26929ccd446eSAtari911 containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]'); 26939ccd446eSAtari911 } 26949ccd446eSAtari911 const cs = containerEl ? getComputedStyle(containerEl) : null; 26959ccd446eSAtari911 26969ccd446eSAtari911 const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424'; 26979ccd446eSAtari911 const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07'; 26989ccd446eSAtari911 const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07'; 26999ccd446eSAtari911 const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00'; 27009ccd446eSAtari911 const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)'; 27011d05cddcSAtari911 27021d05cddcSAtari911 // Create tooltip 27031d05cddcSAtari911 const tooltip = document.createElement('div'); 27041d05cddcSAtari911 tooltip.id = 'conflict-tooltip'; 27051d05cddcSAtari911 tooltip.className = 'conflict-tooltip'; 27061d05cddcSAtari911 27079ccd446eSAtari911 // Apply theme styles 27089ccd446eSAtari911 tooltip.style.background = bg; 27099ccd446eSAtari911 tooltip.style.borderColor = border; 27109ccd446eSAtari911 tooltip.style.color = textPrimary; 27119ccd446eSAtari911 tooltip.style.boxShadow = '0 4px 12px ' + shadow; 27129ccd446eSAtari911 27139ccd446eSAtari911 // Build content with themed colors 27147e8ea635SAtari911 let html = '<div class="conflict-tooltip-header" style="background: ' + border + '; color: ' + bg + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>'; 27151d05cddcSAtari911 html += '<div class="conflict-tooltip-body">'; 27161d05cddcSAtari911 conflicts.forEach(conflict => { 27177e8ea635SAtari911 html += '<div class="conflict-item" style="color: ' + textDim + '; border-bottom-color: ' + border + ';">• ' + escapeHtml(conflict) + '</div>'; 27181d05cddcSAtari911 }); 27191d05cddcSAtari911 html += '</div>'; 27201d05cddcSAtari911 27211d05cddcSAtari911 tooltip.innerHTML = html; 27221d05cddcSAtari911 document.body.appendChild(tooltip); 27231d05cddcSAtari911 27241d05cddcSAtari911 // Position tooltip 27251d05cddcSAtari911 const rect = badgeElement.getBoundingClientRect(); 27261d05cddcSAtari911 const tooltipRect = tooltip.getBoundingClientRect(); 27271d05cddcSAtari911 27281d05cddcSAtari911 // Position above the badge, centered 27291d05cddcSAtari911 let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2); 27301d05cddcSAtari911 let top = rect.top - tooltipRect.height - 8; 27311d05cddcSAtari911 27321d05cddcSAtari911 // Keep tooltip within viewport 27331d05cddcSAtari911 if (left < 10) left = 10; 27341d05cddcSAtari911 if (left + tooltipRect.width > window.innerWidth - 10) { 27351d05cddcSAtari911 left = window.innerWidth - tooltipRect.width - 10; 27361d05cddcSAtari911 } 27371d05cddcSAtari911 if (top < 10) { 27381d05cddcSAtari911 // If not enough room above, show below 27391d05cddcSAtari911 top = rect.bottom + 8; 27401d05cddcSAtari911 } 27411d05cddcSAtari911 27421d05cddcSAtari911 tooltip.style.left = left + 'px'; 27431d05cddcSAtari911 tooltip.style.top = top + 'px'; 27441d05cddcSAtari911 tooltip.style.opacity = '1'; 27451d05cddcSAtari911}; 27461d05cddcSAtari911 27471d05cddcSAtari911// Hide conflict tooltip 27481d05cddcSAtari911window.hideConflictTooltip = function() { 27491d05cddcSAtari911 const tooltip = document.getElementById('conflict-tooltip'); 27501d05cddcSAtari911 if (tooltip) { 27511d05cddcSAtari911 tooltip.remove(); 27521d05cddcSAtari911 } 27531d05cddcSAtari911}; 27541d05cddcSAtari911 275596df7d3eSAtari911// Fuzzy search helper for event filtering - normalizes text for matching 275696df7d3eSAtari911function eventSearchNormalize(text) { 275796df7d3eSAtari911 if (typeof text !== 'string') { 275896df7d3eSAtari911 console.log('[eventSearchNormalize] WARNING: text is not a string:', typeof text, text); 275996df7d3eSAtari911 return ''; 276096df7d3eSAtari911 } 276196df7d3eSAtari911 return text 276296df7d3eSAtari911 .toLowerCase() 276396df7d3eSAtari911 .trim() 276496df7d3eSAtari911 // Remove common punctuation that might differ 276596df7d3eSAtari911 .replace(/[''\u2018\u2019]/g, '') // Remove apostrophes/quotes 276696df7d3eSAtari911 .replace(/["""\u201C\u201D]/g, '') // Remove smart quotes 276796df7d3eSAtari911 .replace(/[-–—]/g, ' ') // Dashes to spaces 276896df7d3eSAtari911 .replace(/[.,!?;:]/g, '') // Remove punctuation 276996df7d3eSAtari911 .replace(/\s+/g, ' ') // Normalize whitespace 277096df7d3eSAtari911 .trim(); 277196df7d3eSAtari911} 277296df7d3eSAtari911 277396df7d3eSAtari911// Check if search term matches text for event filtering 277496df7d3eSAtari911function eventSearchMatch(text, searchTerm) { 277596df7d3eSAtari911 const normalizedText = eventSearchNormalize(text); 277696df7d3eSAtari911 const normalizedSearch = eventSearchNormalize(searchTerm); 277796df7d3eSAtari911 277896df7d3eSAtari911 // Direct match after normalization 277996df7d3eSAtari911 if (normalizedText.includes(normalizedSearch)) { 278096df7d3eSAtari911 return true; 278196df7d3eSAtari911 } 278296df7d3eSAtari911 278396df7d3eSAtari911 // Split search into words and check if all words are present 278496df7d3eSAtari911 const searchWords = normalizedSearch.split(' ').filter(w => w.length > 0); 278596df7d3eSAtari911 if (searchWords.length > 1) { 278696df7d3eSAtari911 return searchWords.every(word => normalizedText.includes(word)); 278796df7d3eSAtari911 } 278896df7d3eSAtari911 278996df7d3eSAtari911 return false; 279096df7d3eSAtari911} 279196df7d3eSAtari911 27921d05cddcSAtari911// Filter events by search term 27931d05cddcSAtari911window.filterEvents = function(calId, searchTerm) { 27941d05cddcSAtari911 const eventList = document.getElementById('eventlist-' + calId); 27951d05cddcSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 279696df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 27971d05cddcSAtari911 27981d05cddcSAtari911 if (!eventList) return; 27991d05cddcSAtari911 280096df7d3eSAtari911 // Check if we're in "all dates" mode 280196df7d3eSAtari911 const isAllDatesMode = searchMode && searchMode.classList.contains('all-dates'); 280296df7d3eSAtari911 28031d05cddcSAtari911 // Show/hide clear button 28041d05cddcSAtari911 if (searchClear) { 28051d05cddcSAtari911 searchClear.style.display = searchTerm ? 'block' : 'none'; 28061d05cddcSAtari911 } 28071d05cddcSAtari911 280896df7d3eSAtari911 searchTerm = searchTerm.trim(); 280996df7d3eSAtari911 281096df7d3eSAtari911 // If all-dates mode and we have a search term, do AJAX search 281196df7d3eSAtari911 if (isAllDatesMode && searchTerm.length >= 2) { 281296df7d3eSAtari911 searchAllDates(calId, searchTerm); 281396df7d3eSAtari911 return; 281496df7d3eSAtari911 } 281596df7d3eSAtari911 281696df7d3eSAtari911 // If all-dates mode but search cleared, restore normal view 281796df7d3eSAtari911 if (isAllDatesMode && !searchTerm) { 281896df7d3eSAtari911 // Remove search results container if exists 281996df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 282096df7d3eSAtari911 if (resultsContainer) { 282196df7d3eSAtari911 resultsContainer.remove(); 282296df7d3eSAtari911 } 282396df7d3eSAtari911 // Show normal event items 282496df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 282596df7d3eSAtari911 item.style.display = ''; 282696df7d3eSAtari911 }); 282796df7d3eSAtari911 // Show past events toggle if it exists 282896df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 282996df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 283096df7d3eSAtari911 } 28311d05cddcSAtari911 28321d05cddcSAtari911 // Get all event items 28331d05cddcSAtari911 const eventItems = eventList.querySelectorAll('.event-compact-item'); 28341d05cddcSAtari911 let visibleCount = 0; 28351d05cddcSAtari911 let hiddenPastCount = 0; 28361d05cddcSAtari911 28371d05cddcSAtari911 eventItems.forEach(item => { 28381d05cddcSAtari911 const title = item.querySelector('.event-title-compact'); 28391d05cddcSAtari911 const description = item.querySelector('.event-desc-compact'); 28401d05cddcSAtari911 const dateTime = item.querySelector('.event-date-time'); 28411d05cddcSAtari911 28421d05cddcSAtari911 // Build searchable text 28431d05cddcSAtari911 let searchableText = ''; 284496df7d3eSAtari911 if (title) searchableText += title.textContent + ' '; 284596df7d3eSAtari911 if (description) searchableText += description.textContent + ' '; 284696df7d3eSAtari911 if (dateTime) searchableText += dateTime.textContent + ' '; 28471d05cddcSAtari911 284896df7d3eSAtari911 // Check if matches search using fuzzy matching 284996df7d3eSAtari911 const matches = !searchTerm || eventSearchMatch(searchableText, searchTerm); 28501d05cddcSAtari911 28511d05cddcSAtari911 if (matches) { 28521d05cddcSAtari911 item.style.display = ''; 28531d05cddcSAtari911 visibleCount++; 28541d05cddcSAtari911 } else { 28551d05cddcSAtari911 item.style.display = 'none'; 28561d05cddcSAtari911 // Check if this is a past event 28571d05cddcSAtari911 if (item.classList.contains('event-past') || item.classList.contains('event-completed')) { 28581d05cddcSAtari911 hiddenPastCount++; 28591d05cddcSAtari911 } 28601d05cddcSAtari911 } 28611d05cddcSAtari911 }); 28621d05cddcSAtari911 28631d05cddcSAtari911 // Update past events toggle if it exists 28641d05cddcSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 28651d05cddcSAtari911 const pastLabel = eventList.querySelector('.past-events-label'); 28661d05cddcSAtari911 const pastContent = document.getElementById('past-events-' + calId); 28671d05cddcSAtari911 28681d05cddcSAtari911 if (pastToggle && pastLabel && pastContent) { 28691d05cddcSAtari911 const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])'); 28701d05cddcSAtari911 const totalPastVisible = visiblePastEvents.length; 28711d05cddcSAtari911 28721d05cddcSAtari911 if (totalPastVisible > 0) { 28731d05cddcSAtari911 pastLabel.textContent = `Past Events (${totalPastVisible})`; 28741d05cddcSAtari911 pastToggle.style.display = ''; 28751d05cddcSAtari911 } else { 28761d05cddcSAtari911 pastToggle.style.display = 'none'; 28771d05cddcSAtari911 } 28781d05cddcSAtari911 } 28791d05cddcSAtari911 288096df7d3eSAtari911 // Show "no results" message if nothing visible (only for month mode, not all-dates mode) 28811d05cddcSAtari911 let noResultsMsg = eventList.querySelector('.no-search-results'); 288296df7d3eSAtari911 if (visibleCount === 0 && searchTerm && !isAllDatesMode) { 28831d05cddcSAtari911 if (!noResultsMsg) { 28841d05cddcSAtari911 noResultsMsg = document.createElement('p'); 28851d05cddcSAtari911 noResultsMsg.className = 'no-search-results no-events-msg'; 28861d05cddcSAtari911 noResultsMsg.textContent = 'No events match your search'; 28871d05cddcSAtari911 eventList.appendChild(noResultsMsg); 28881d05cddcSAtari911 } 28891d05cddcSAtari911 noResultsMsg.style.display = 'block'; 28901d05cddcSAtari911 } else if (noResultsMsg) { 28911d05cddcSAtari911 noResultsMsg.style.display = 'none'; 28921d05cddcSAtari911 } 28931d05cddcSAtari911}; 28941d05cddcSAtari911 289596df7d3eSAtari911// Toggle search mode between "this month" and "all dates" 289696df7d3eSAtari911window.toggleSearchMode = function(calId, namespace) { 289796df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 289896df7d3eSAtari911 const searchInput = document.getElementById('event-search-' + calId); 289996df7d3eSAtari911 290096df7d3eSAtari911 if (!searchMode) return; 290196df7d3eSAtari911 290296df7d3eSAtari911 const isAllDates = searchMode.classList.toggle('all-dates'); 290396df7d3eSAtari911 290496df7d3eSAtari911 // Update button icon and title 290596df7d3eSAtari911 if (isAllDates) { 290696df7d3eSAtari911 searchMode.innerHTML = ''; 290796df7d3eSAtari911 searchMode.title = 'Searching all dates'; 290896df7d3eSAtari911 if (searchInput) { 290996df7d3eSAtari911 searchInput.placeholder = 'Search all dates...'; 291096df7d3eSAtari911 } 291196df7d3eSAtari911 } else { 291296df7d3eSAtari911 searchMode.innerHTML = ''; 291396df7d3eSAtari911 searchMode.title = 'Search this month only'; 291496df7d3eSAtari911 if (searchInput) { 291596df7d3eSAtari911 searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : ' Search...'; 291696df7d3eSAtari911 } 291796df7d3eSAtari911 } 291896df7d3eSAtari911 291996df7d3eSAtari911 // Re-run search with current term 292096df7d3eSAtari911 if (searchInput && searchInput.value) { 292196df7d3eSAtari911 filterEvents(calId, searchInput.value); 292296df7d3eSAtari911 } else { 292396df7d3eSAtari911 // Clear any all-dates results 292496df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 292596df7d3eSAtari911 if (eventList) { 292696df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 292796df7d3eSAtari911 if (resultsContainer) { 292896df7d3eSAtari911 resultsContainer.remove(); 292996df7d3eSAtari911 } 293096df7d3eSAtari911 // Show normal event items 293196df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 293296df7d3eSAtari911 item.style.display = ''; 293396df7d3eSAtari911 }); 293496df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 293596df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 293696df7d3eSAtari911 } 293796df7d3eSAtari911 } 293896df7d3eSAtari911}; 293996df7d3eSAtari911 294096df7d3eSAtari911// Search all dates via AJAX 294196df7d3eSAtari911window.searchAllDates = function(calId, searchTerm) { 294296df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 294396df7d3eSAtari911 if (!eventList) return; 294496df7d3eSAtari911 294596df7d3eSAtari911 // Get namespace from container 294696df7d3eSAtari911 const container = document.getElementById(calId); 294796df7d3eSAtari911 const namespace = container ? (container.dataset.namespace || '') : ''; 294896df7d3eSAtari911 294996df7d3eSAtari911 // Hide normal event items 295096df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 295196df7d3eSAtari911 item.style.display = 'none'; 295296df7d3eSAtari911 }); 295396df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 295496df7d3eSAtari911 if (pastToggle) pastToggle.style.display = 'none'; 295596df7d3eSAtari911 295696df7d3eSAtari911 // Remove old results container 295796df7d3eSAtari911 let resultsContainer = eventList.querySelector('.all-dates-results'); 295896df7d3eSAtari911 if (resultsContainer) { 295996df7d3eSAtari911 resultsContainer.remove(); 296096df7d3eSAtari911 } 296196df7d3eSAtari911 296296df7d3eSAtari911 // Create new results container 296396df7d3eSAtari911 resultsContainer = document.createElement('div'); 296496df7d3eSAtari911 resultsContainer.className = 'all-dates-results'; 296596df7d3eSAtari911 resultsContainer.innerHTML = '<p class="search-loading" style="text-align:center; padding:20px; color:var(--text-dim);"> Searching all dates...</p>'; 296696df7d3eSAtari911 eventList.appendChild(resultsContainer); 296796df7d3eSAtari911 296896df7d3eSAtari911 // Make AJAX request 296996df7d3eSAtari911 const params = new URLSearchParams({ 297096df7d3eSAtari911 call: 'plugin_calendar', 297196df7d3eSAtari911 action: 'search_all', 297296df7d3eSAtari911 search: searchTerm, 297396df7d3eSAtari911 namespace: namespace, 297496df7d3eSAtari911 _: new Date().getTime() 297596df7d3eSAtari911 }); 297696df7d3eSAtari911 297796df7d3eSAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 297896df7d3eSAtari911 method: 'POST', 297996df7d3eSAtari911 headers: { 298096df7d3eSAtari911 'Content-Type': 'application/x-www-form-urlencoded' 298196df7d3eSAtari911 }, 298296df7d3eSAtari911 body: params.toString() 298396df7d3eSAtari911 }) 298496df7d3eSAtari911 .then(r => r.json()) 298596df7d3eSAtari911 .then(data => { 298696df7d3eSAtari911 if (data.success && data.results) { 298796df7d3eSAtari911 if (data.results.length === 0) { 298896df7d3eSAtari911 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>'; 298996df7d3eSAtari911 } else { 299096df7d3eSAtari911 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>'; 299196df7d3eSAtari911 299296df7d3eSAtari911 data.results.forEach(event => { 299396df7d3eSAtari911 const dateObj = new Date(event.date + 'T00:00:00'); 299496df7d3eSAtari911 const dateDisplay = dateObj.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); 299596df7d3eSAtari911 const color = event.color || 'var(--text-bright, #00cc07)'; 299696df7d3eSAtari911 299796df7d3eSAtari911 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 + '\')">'; 299896df7d3eSAtari911 html += '<div style="width:3px; background:' + color + '; border-radius:1px; flex-shrink:0;"></div>'; 299996df7d3eSAtari911 html += '<div style="flex:1; min-width:0;">'; 300096df7d3eSAtari911 html += '<div class="event-title-compact" style="font-weight:600; color:var(--text-primary); font-size:11px;">' + escapeHtml(event.title) + '</div>'; 300196df7d3eSAtari911 html += '<div class="event-date-time" style="font-size:10px; color:var(--text-dim);">' + dateDisplay; 300296df7d3eSAtari911 if (event.time) { 300396df7d3eSAtari911 html += ' • ' + formatTimeRange(event.time, event.endTime); 300496df7d3eSAtari911 } 300596df7d3eSAtari911 html += '</div>'; 300696df7d3eSAtari911 if (event.namespace) { 300796df7d3eSAtari911 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>'; 300896df7d3eSAtari911 } 300996df7d3eSAtari911 html += '</div></div>'; 301096df7d3eSAtari911 }); 301196df7d3eSAtari911 301296df7d3eSAtari911 resultsContainer.innerHTML = html; 301396df7d3eSAtari911 } 301496df7d3eSAtari911 } else { 301596df7d3eSAtari911 resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>'; 301696df7d3eSAtari911 } 301796df7d3eSAtari911 }) 301896df7d3eSAtari911 .catch(err => { 301996df7d3eSAtari911 console.error('Search error:', err); 302096df7d3eSAtari911 resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>'; 302196df7d3eSAtari911 }); 302296df7d3eSAtari911}; 302396df7d3eSAtari911 302496df7d3eSAtari911// Jump to a specific date (used by search results) 302596df7d3eSAtari911window.jumpToDate = function(calId, date, namespace) { 302696df7d3eSAtari911 const parts = date.split('-'); 302796df7d3eSAtari911 const year = parseInt(parts[0]); 302896df7d3eSAtari911 const month = parseInt(parts[1]); 302996df7d3eSAtari911 303096df7d3eSAtari911 // Get container to check current month 303196df7d3eSAtari911 const container = document.getElementById(calId); 303296df7d3eSAtari911 const currentYear = container ? parseInt(container.dataset.year) : year; 303396df7d3eSAtari911 const currentMonth = container ? parseInt(container.dataset.month) : month; 303496df7d3eSAtari911 303596df7d3eSAtari911 // Get search elements 303696df7d3eSAtari911 const searchInput = document.getElementById('event-search-' + calId); 303796df7d3eSAtari911 const searchMode = document.getElementById('search-mode-' + calId); 303896df7d3eSAtari911 const searchClear = document.getElementById('search-clear-' + calId); 303996df7d3eSAtari911 const eventList = document.getElementById('eventlist-' + calId); 304096df7d3eSAtari911 304196df7d3eSAtari911 // Remove the all-dates results container 304296df7d3eSAtari911 if (eventList) { 304396df7d3eSAtari911 const resultsContainer = eventList.querySelector('.all-dates-results'); 304496df7d3eSAtari911 if (resultsContainer) { 304596df7d3eSAtari911 resultsContainer.remove(); 304696df7d3eSAtari911 } 304796df7d3eSAtari911 // Show normal event items again 304896df7d3eSAtari911 eventList.querySelectorAll('.event-compact-item').forEach(item => { 304996df7d3eSAtari911 item.style.display = ''; 305096df7d3eSAtari911 }); 305196df7d3eSAtari911 const pastToggle = eventList.querySelector('.past-events-toggle'); 305296df7d3eSAtari911 if (pastToggle) pastToggle.style.display = ''; 305396df7d3eSAtari911 305496df7d3eSAtari911 // Hide any no-results message 305596df7d3eSAtari911 const noResults = eventList.querySelector('.no-search-results'); 305696df7d3eSAtari911 if (noResults) noResults.style.display = 'none'; 305796df7d3eSAtari911 } 305896df7d3eSAtari911 305996df7d3eSAtari911 // Clear search input 306096df7d3eSAtari911 if (searchInput) { 306196df7d3eSAtari911 searchInput.value = ''; 306296df7d3eSAtari911 } 306396df7d3eSAtari911 306496df7d3eSAtari911 // Hide clear button 306596df7d3eSAtari911 if (searchClear) { 306696df7d3eSAtari911 searchClear.style.display = 'none'; 306796df7d3eSAtari911 } 306896df7d3eSAtari911 306996df7d3eSAtari911 // Switch back to month mode 307096df7d3eSAtari911 if (searchMode && searchMode.classList.contains('all-dates')) { 307196df7d3eSAtari911 searchMode.classList.remove('all-dates'); 307296df7d3eSAtari911 searchMode.innerHTML = ''; 307396df7d3eSAtari911 searchMode.title = 'Search this month only'; 307496df7d3eSAtari911 if (searchInput) { 307596df7d3eSAtari911 searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : ' Search...'; 307696df7d3eSAtari911 } 307796df7d3eSAtari911 } 307896df7d3eSAtari911 307996df7d3eSAtari911 // Check if we need to navigate to a different month 308096df7d3eSAtari911 if (year !== currentYear || month !== currentMonth) { 308196df7d3eSAtari911 // Navigate to the target month, then show popup 308296df7d3eSAtari911 navCalendar(calId, year, month, namespace); 308396df7d3eSAtari911 308496df7d3eSAtari911 // After navigation completes, show the day popup 308596df7d3eSAtari911 setTimeout(() => { 308696df7d3eSAtari911 showDayPopup(calId, date, namespace); 308796df7d3eSAtari911 }, 400); 308896df7d3eSAtari911 } else { 308996df7d3eSAtari911 // Same month - just show the popup 309096df7d3eSAtari911 showDayPopup(calId, date, namespace); 309196df7d3eSAtari911 } 309296df7d3eSAtari911}; 309396df7d3eSAtari911 30941d05cddcSAtari911// Clear event search 30951d05cddcSAtari911window.clearEventSearch = function(calId) { 30961d05cddcSAtari911 const searchInput = document.getElementById('event-search-' + calId); 30971d05cddcSAtari911 if (searchInput) { 30981d05cddcSAtari911 searchInput.value = ''; 30991d05cddcSAtari911 filterEvents(calId, ''); 31001d05cddcSAtari911 searchInput.focus(); 31011d05cddcSAtari911 } 31021d05cddcSAtari911}; 31031d05cddcSAtari911 31049ccd446eSAtari911// ============================================ 31059ccd446eSAtari911// PINK THEME - GLOWING PARTICLE EFFECTS 31069ccd446eSAtari911// ============================================ 31079ccd446eSAtari911 31089ccd446eSAtari911// Create glowing pink particle effects for pink theme 31099ccd446eSAtari911(function() { 31109ccd446eSAtari911 let pinkThemeActive = false; 31119ccd446eSAtari911 let trailTimer = null; 31129ccd446eSAtari911 let pixelTimer = null; 31139ccd446eSAtari911 31149ccd446eSAtari911 // Check if pink theme is active 31159ccd446eSAtari911 function checkPinkTheme() { 31169ccd446eSAtari911 const pinkCalendars = document.querySelectorAll('.calendar-theme-pink'); 31179ccd446eSAtari911 pinkThemeActive = pinkCalendars.length > 0; 31189ccd446eSAtari911 return pinkThemeActive; 31199ccd446eSAtari911 } 31209ccd446eSAtari911 31219ccd446eSAtari911 // Create trail particle 31229ccd446eSAtari911 function createTrailParticle(clientX, clientY) { 31239ccd446eSAtari911 if (!pinkThemeActive) return; 31249ccd446eSAtari911 31259ccd446eSAtari911 const trail = document.createElement('div'); 31269ccd446eSAtari911 trail.className = 'pink-cursor-trail'; 31279ccd446eSAtari911 trail.style.left = clientX + 'px'; 31289ccd446eSAtari911 trail.style.top = clientY + 'px'; 31299ccd446eSAtari911 trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards'; 31309ccd446eSAtari911 31319ccd446eSAtari911 document.body.appendChild(trail); 31329ccd446eSAtari911 31339ccd446eSAtari911 setTimeout(function() { 31349ccd446eSAtari911 trail.remove(); 31359ccd446eSAtari911 }, 500); 31369ccd446eSAtari911 } 31379ccd446eSAtari911 31389ccd446eSAtari911 // Create pixel sparkles 31399ccd446eSAtari911 function createPixelSparkles(clientX, clientY) { 31409ccd446eSAtari911 if (!pinkThemeActive || pixelTimer) return; 31419ccd446eSAtari911 31429ccd446eSAtari911 const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels 31439ccd446eSAtari911 31449ccd446eSAtari911 for (let i = 0; i < pixelCount; i++) { 31459ccd446eSAtari911 const pixel = document.createElement('div'); 31469ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 31479ccd446eSAtari911 31489ccd446eSAtari911 // Random offset from cursor 31499ccd446eSAtari911 const offsetX = (Math.random() - 0.5) * 30; 31509ccd446eSAtari911 const offsetY = (Math.random() - 0.5) * 30; 31519ccd446eSAtari911 31529ccd446eSAtari911 pixel.style.left = (clientX + offsetX) + 'px'; 31539ccd446eSAtari911 pixel.style.top = (clientY + offsetY) + 'px'; 31549ccd446eSAtari911 31559ccd446eSAtari911 // Random color - bright neon pinks and whites 31569ccd446eSAtari911 const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 31579ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 31589ccd446eSAtari911 pixel.style.background = color; 31599ccd446eSAtari911 pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff'; 31609ccd446eSAtari911 31619ccd446eSAtari911 // Random animation 31629ccd446eSAtari911 if (Math.random() > 0.5) { 31639ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards'; 31649ccd446eSAtari911 } else { 31659ccd446eSAtari911 pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards'; 31669ccd446eSAtari911 } 31679ccd446eSAtari911 31689ccd446eSAtari911 document.body.appendChild(pixel); 31699ccd446eSAtari911 31709ccd446eSAtari911 setTimeout(function() { 31719ccd446eSAtari911 pixel.remove(); 31729ccd446eSAtari911 }, 800); 31739ccd446eSAtari911 } 31749ccd446eSAtari911 31759ccd446eSAtari911 pixelTimer = setTimeout(function() { 31769ccd446eSAtari911 pixelTimer = null; 31779ccd446eSAtari911 }, 40); 31789ccd446eSAtari911 } 31799ccd446eSAtari911 31809ccd446eSAtari911 // Create explosion 31819ccd446eSAtari911 function createExplosion(clientX, clientY) { 31829ccd446eSAtari911 if (!pinkThemeActive) return; 31839ccd446eSAtari911 31849ccd446eSAtari911 const particleCount = 25; 31859ccd446eSAtari911 const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff']; 31869ccd446eSAtari911 31879ccd446eSAtari911 // Add hearts to explosion (8-12 hearts) 31889ccd446eSAtari911 const heartCount = 8 + Math.floor(Math.random() * 5); 31899ccd446eSAtari911 for (let i = 0; i < heartCount; i++) { 31909ccd446eSAtari911 const heart = document.createElement('div'); 31919ccd446eSAtari911 heart.textContent = ''; 31929ccd446eSAtari911 heart.style.position = 'fixed'; 31939ccd446eSAtari911 heart.style.left = clientX + 'px'; 31949ccd446eSAtari911 heart.style.top = clientY + 'px'; 31959ccd446eSAtari911 heart.style.pointerEvents = 'none'; 31969ccd446eSAtari911 heart.style.zIndex = '9999999'; 31979ccd446eSAtari911 heart.style.fontSize = (12 + Math.random() * 16) + 'px'; 31989ccd446eSAtari911 31999ccd446eSAtari911 // Random direction 32009ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 32019ccd446eSAtari911 const velocity = 60 + Math.random() * 80; 32029ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 32039ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 32049ccd446eSAtari911 32059ccd446eSAtari911 heart.style.setProperty('--tx', tx + 'px'); 32069ccd446eSAtari911 heart.style.setProperty('--ty', ty + 'px'); 32079ccd446eSAtari911 32089ccd446eSAtari911 const duration = 0.8 + Math.random() * 0.4; 32099ccd446eSAtari911 heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 32109ccd446eSAtari911 32119ccd446eSAtari911 document.body.appendChild(heart); 32129ccd446eSAtari911 32139ccd446eSAtari911 setTimeout(function() { 32149ccd446eSAtari911 heart.remove(); 32159ccd446eSAtari911 }, duration * 1000); 32169ccd446eSAtari911 } 32179ccd446eSAtari911 32189ccd446eSAtari911 // Main explosion particles 32199ccd446eSAtari911 for (let i = 0; i < particleCount; i++) { 32209ccd446eSAtari911 const particle = document.createElement('div'); 32219ccd446eSAtari911 particle.className = 'pink-particle'; 32229ccd446eSAtari911 32239ccd446eSAtari911 const color = colors[Math.floor(Math.random() * colors.length)]; 32249ccd446eSAtari911 particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)'; 32259ccd446eSAtari911 particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color; 32269ccd446eSAtari911 32279ccd446eSAtari911 particle.style.left = clientX + 'px'; 32289ccd446eSAtari911 particle.style.top = clientY + 'px'; 32299ccd446eSAtari911 32309ccd446eSAtari911 const angle = (Math.PI * 2 * i) / particleCount; 32319ccd446eSAtari911 const velocity = 50 + Math.random() * 100; 32329ccd446eSAtari911 const tx = Math.cos(angle) * velocity; 32339ccd446eSAtari911 const ty = Math.sin(angle) * velocity; 32349ccd446eSAtari911 32359ccd446eSAtari911 particle.style.setProperty('--tx', tx + 'px'); 32369ccd446eSAtari911 particle.style.setProperty('--ty', ty + 'px'); 32379ccd446eSAtari911 32389ccd446eSAtari911 const size = 4 + Math.random() * 6; 32399ccd446eSAtari911 particle.style.width = size + 'px'; 32409ccd446eSAtari911 particle.style.height = size + 'px'; 32419ccd446eSAtari911 32429ccd446eSAtari911 const duration = 0.6 + Math.random() * 0.4; 32439ccd446eSAtari911 particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 32449ccd446eSAtari911 32459ccd446eSAtari911 document.body.appendChild(particle); 32469ccd446eSAtari911 32479ccd446eSAtari911 setTimeout(function() { 32489ccd446eSAtari911 particle.remove(); 32499ccd446eSAtari911 }, duration * 1000); 32509ccd446eSAtari911 } 32519ccd446eSAtari911 32529ccd446eSAtari911 // Pixel sparkles 32539ccd446eSAtari911 const pixelSparkleCount = 40; 32549ccd446eSAtari911 32559ccd446eSAtari911 for (let i = 0; i < pixelSparkleCount; i++) { 32569ccd446eSAtari911 const pixel = document.createElement('div'); 32579ccd446eSAtari911 pixel.className = 'pink-pixel-sparkle'; 32589ccd446eSAtari911 32599ccd446eSAtari911 const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1']; 32609ccd446eSAtari911 const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)]; 32619ccd446eSAtari911 pixel.style.background = pixelColor; 32629ccd446eSAtari911 pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff'; 32639ccd446eSAtari911 32649ccd446eSAtari911 const angle = Math.random() * Math.PI * 2; 32659ccd446eSAtari911 const distance = 30 + Math.random() * 80; 32669ccd446eSAtari911 const offsetX = Math.cos(angle) * distance; 32679ccd446eSAtari911 const offsetY = Math.sin(angle) * distance; 32689ccd446eSAtari911 32699ccd446eSAtari911 pixel.style.left = clientX + 'px'; 32709ccd446eSAtari911 pixel.style.top = clientY + 'px'; 32719ccd446eSAtari911 pixel.style.setProperty('--tx', offsetX + 'px'); 32729ccd446eSAtari911 pixel.style.setProperty('--ty', offsetY + 'px'); 32739ccd446eSAtari911 32749ccd446eSAtari911 const pixelSize = 1 + Math.random() * 2; 32759ccd446eSAtari911 pixel.style.width = pixelSize + 'px'; 32769ccd446eSAtari911 pixel.style.height = pixelSize + 'px'; 32779ccd446eSAtari911 32789ccd446eSAtari911 const duration = 0.4 + Math.random() * 0.4; 32799ccd446eSAtari911 if (Math.random() > 0.5) { 32809ccd446eSAtari911 pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards'; 32819ccd446eSAtari911 } else { 32829ccd446eSAtari911 pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards'; 32839ccd446eSAtari911 } 32849ccd446eSAtari911 32859ccd446eSAtari911 document.body.appendChild(pixel); 32869ccd446eSAtari911 32879ccd446eSAtari911 setTimeout(function() { 32889ccd446eSAtari911 pixel.remove(); 32899ccd446eSAtari911 }, duration * 1000); 32909ccd446eSAtari911 } 32919ccd446eSAtari911 32929ccd446eSAtari911 // Flash 32939ccd446eSAtari911 const flash = document.createElement('div'); 32949ccd446eSAtari911 flash.style.position = 'fixed'; 32959ccd446eSAtari911 flash.style.left = clientX + 'px'; 32969ccd446eSAtari911 flash.style.top = clientY + 'px'; 32979ccd446eSAtari911 flash.style.width = '40px'; 32989ccd446eSAtari911 flash.style.height = '40px'; 32999ccd446eSAtari911 flash.style.borderRadius = '50%'; 33009ccd446eSAtari911 flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)'; 33019ccd446eSAtari911 flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4'; 33029ccd446eSAtari911 flash.style.pointerEvents = 'none'; 33039ccd446eSAtari911 flash.style.zIndex = '9999999'; // Above everything including dialogs 33049ccd446eSAtari911 flash.style.transform = 'translate(-50%, -50%)'; 33059ccd446eSAtari911 flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards'; 33069ccd446eSAtari911 33079ccd446eSAtari911 document.body.appendChild(flash); 33089ccd446eSAtari911 33099ccd446eSAtari911 setTimeout(function() { 33109ccd446eSAtari911 flash.remove(); 33119ccd446eSAtari911 }, 300); 33129ccd446eSAtari911 } 33139ccd446eSAtari911 33149ccd446eSAtari911 function initPinkParticles() { 33159ccd446eSAtari911 if (!checkPinkTheme()) return; 33169ccd446eSAtari911 33179ccd446eSAtari911 // Use capture phase to catch events before stopPropagation 33189ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 33199ccd446eSAtari911 if (!pinkThemeActive) return; 33209ccd446eSAtari911 33219ccd446eSAtari911 createTrailParticle(e.clientX, e.clientY); 33229ccd446eSAtari911 createPixelSparkles(e.clientX, e.clientY); 33239ccd446eSAtari911 }, true); // Capture phase! 33249ccd446eSAtari911 33259ccd446eSAtari911 // Throttle main trail 33269ccd446eSAtari911 document.addEventListener('mousemove', function(e) { 33279ccd446eSAtari911 if (!pinkThemeActive || trailTimer) return; 33289ccd446eSAtari911 33299ccd446eSAtari911 trailTimer = setTimeout(function() { 33309ccd446eSAtari911 trailTimer = null; 33319ccd446eSAtari911 }, 30); 33329ccd446eSAtari911 }, true); // Capture phase! 33339ccd446eSAtari911 33349ccd446eSAtari911 // Click explosion - use capture phase 33359ccd446eSAtari911 document.addEventListener('click', function(e) { 33369ccd446eSAtari911 if (!pinkThemeActive) return; 33379ccd446eSAtari911 33389ccd446eSAtari911 createExplosion(e.clientX, e.clientY); 33399ccd446eSAtari911 }, true); // Capture phase! 33409ccd446eSAtari911 } 33419ccd446eSAtari911 33429ccd446eSAtari911 // Initialize on load 33439ccd446eSAtari911 if (document.readyState === 'loading') { 33449ccd446eSAtari911 document.addEventListener('DOMContentLoaded', initPinkParticles); 33459ccd446eSAtari911 } else { 33469ccd446eSAtari911 initPinkParticles(); 33479ccd446eSAtari911 } 33489ccd446eSAtari911 33499ccd446eSAtari911 // Re-check theme if calendar is dynamically added 335096df7d3eSAtari911 // Must wait for document.body to exist 335196df7d3eSAtari911 function setupMutationObserver() { 335296df7d3eSAtari911 if (typeof MutationObserver !== 'undefined' && document.body) { 33539ccd446eSAtari911 const observer = new MutationObserver(function(mutations) { 33549ccd446eSAtari911 mutations.forEach(function(mutation) { 33559ccd446eSAtari911 if (mutation.addedNodes.length > 0) { 33569ccd446eSAtari911 mutation.addedNodes.forEach(function(node) { 33579ccd446eSAtari911 if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) { 33589ccd446eSAtari911 checkPinkTheme(); 33599ccd446eSAtari911 initPinkParticles(); 33609ccd446eSAtari911 } 33619ccd446eSAtari911 }); 33629ccd446eSAtari911 } 33639ccd446eSAtari911 }); 33649ccd446eSAtari911 }); 33659ccd446eSAtari911 33669ccd446eSAtari911 observer.observe(document.body, { 33679ccd446eSAtari911 childList: true, 33689ccd446eSAtari911 subtree: true 33699ccd446eSAtari911 }); 33709ccd446eSAtari911 } 337196df7d3eSAtari911 } 337296df7d3eSAtari911 337396df7d3eSAtari911 // Setup observer when DOM is ready 337496df7d3eSAtari911 if (document.readyState === 'loading') { 337596df7d3eSAtari911 document.addEventListener('DOMContentLoaded', setupMutationObserver); 337696df7d3eSAtari911 } else { 337796df7d3eSAtari911 setupMutationObserver(); 337896df7d3eSAtari911 } 33799ccd446eSAtari911})(); 33809ccd446eSAtari911 3381da206178SAtari911// Mobile touch event delegation for edit/delete buttons 3382da206178SAtari911// This ensures buttons work on mobile where onclick may not fire reliably 3383da206178SAtari911(function() { 3384da206178SAtari911 function handleButtonTouch(e) { 3385da206178SAtari911 const btn = e.target.closest('.event-edit-btn, .event-delete-btn, .event-action-btn'); 3386da206178SAtari911 if (!btn) return; 3387da206178SAtari911 3388da206178SAtari911 // Prevent double-firing with onclick 3389da206178SAtari911 e.preventDefault(); 3390da206178SAtari911 3391da206178SAtari911 // Small delay to show visual feedback 3392da206178SAtari911 setTimeout(function() { 3393da206178SAtari911 btn.click(); 3394da206178SAtari911 }, 10); 3395da206178SAtari911 } 3396da206178SAtari911 3397da206178SAtari911 // Use touchend for more reliable mobile handling 3398da206178SAtari911 document.addEventListener('touchend', handleButtonTouch, { passive: false }); 3399da206178SAtari911})(); 3400da206178SAtari911 3401da206178SAtari911// Static calendar navigation 3402da206178SAtari911window.navStaticCalendar = function(calId, direction) { 3403da206178SAtari911 const container = document.getElementById(calId); 3404da206178SAtari911 if (!container) return; 3405da206178SAtari911 3406da206178SAtari911 let year = parseInt(container.dataset.year); 3407da206178SAtari911 let month = parseInt(container.dataset.month); 3408da206178SAtari911 const namespace = container.dataset.namespace || ''; 3409da206178SAtari911 3410da206178SAtari911 // Calculate new month 3411da206178SAtari911 month += direction; 3412da206178SAtari911 if (month < 1) { 3413da206178SAtari911 month = 12; 3414da206178SAtari911 year--; 3415da206178SAtari911 } else if (month > 12) { 3416da206178SAtari911 month = 1; 3417da206178SAtari911 year++; 3418da206178SAtari911 } 3419da206178SAtari911 3420da206178SAtari911 // Fetch new calendar content via AJAX 3421da206178SAtari911 const params = new URLSearchParams({ 3422da206178SAtari911 call: 'plugin_calendar', 3423da206178SAtari911 action: 'get_static_calendar', 3424da206178SAtari911 year: year, 3425da206178SAtari911 month: month, 3426da206178SAtari911 namespace: namespace 3427da206178SAtari911 }); 3428da206178SAtari911 3429da206178SAtari911 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 3430da206178SAtari911 method: 'POST', 3431da206178SAtari911 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 3432da206178SAtari911 body: params.toString() 3433da206178SAtari911 }) 3434da206178SAtari911 .then(r => r.json()) 3435da206178SAtari911 .then(data => { 3436da206178SAtari911 if (data.success && data.html) { 3437da206178SAtari911 // Replace the container content 3438da206178SAtari911 container.outerHTML = data.html; 3439da206178SAtari911 } 3440da206178SAtari911 }) 3441da206178SAtari911 .catch(err => console.error('Static calendar navigation error:', err)); 3442da206178SAtari911}; 3443da206178SAtari911 3444da206178SAtari911// Print static calendar - opens print dialog with only calendar content 3445da206178SAtari911window.printStaticCalendar = function(calId) { 3446da206178SAtari911 const container = document.getElementById(calId); 3447da206178SAtari911 if (!container) return; 3448da206178SAtari911 3449da206178SAtari911 // Get the print view content 3450da206178SAtari911 const printView = container.querySelector('.static-print-view'); 3451da206178SAtari911 if (!printView) return; 3452da206178SAtari911 3453da206178SAtari911 // Create a new window for printing 3454da206178SAtari911 const printWindow = window.open('', '_blank', 'width=800,height=600'); 3455da206178SAtari911 3456da206178SAtari911 // Build print document with inline margins for maximum compatibility 3457da206178SAtari911 const printContent = ` 3458da206178SAtari911<!DOCTYPE html> 3459da206178SAtari911<html> 3460da206178SAtari911<head> 3461da206178SAtari911 <title>Calendar - ${container.dataset.year}-${String(container.dataset.month).padStart(2, '0')}</title> 3462da206178SAtari911 <style> 3463da206178SAtari911 * { margin: 0; padding: 0; box-sizing: border-box; } 3464da206178SAtari911 body { font-family: Arial, sans-serif; color: #333; background: white; } 3465da206178SAtari911 table { border-collapse: collapse; font-size: 12px; } 3466da206178SAtari911 th { background: #2c3e50; color: white; padding: 8px; text-align: left; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 3467da206178SAtari911 td { padding: 6px 8px; border-bottom: 1px solid #ccc; vertical-align: top; } 3468da206178SAtari911 tr:nth-child(even) { background: #f0f0f0; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 3469da206178SAtari911 .static-itinerary-important { background: #fffde7 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; } 3470da206178SAtari911 .static-itinerary-date { font-weight: bold; white-space: nowrap; } 3471da206178SAtari911 .static-itinerary-time { white-space: nowrap; color: #555; } 3472da206178SAtari911 .static-itinerary-title { font-weight: 500; } 3473da206178SAtari911 .static-itinerary-desc { color: #555; font-size: 11px; } 3474da206178SAtari911 thead { display: table-header-group; } 3475da206178SAtari911 tr { page-break-inside: avoid; } 3476da206178SAtari911 h2 { font-size: 16px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #333; } 3477da206178SAtari911 p { font-size: 12px; color: #666; margin-bottom: 15px; } 3478da206178SAtari911 </style> 3479da206178SAtari911</head> 3480da206178SAtari911<body style="margin: 0; padding: 0;"> 3481da206178SAtari911 <div style="padding: 50px 60px; margin: 0 auto; max-width: 800px;"> 3482da206178SAtari911 ${printView.innerHTML} 3483da206178SAtari911 </div> 3484da206178SAtari911 <script> 3485da206178SAtari911 window.onload = function() { 3486da206178SAtari911 setTimeout(function() { 3487da206178SAtari911 window.print(); 3488da206178SAtari911 }, 300); 3489da206178SAtari911 window.onafterprint = function() { 3490da206178SAtari911 window.close(); 3491da206178SAtari911 }; 3492da206178SAtari911 }; 3493da206178SAtari911 </script> 3494da206178SAtari911</body> 3495da206178SAtari911</html>`; 3496da206178SAtari911 3497da206178SAtari911 printWindow.document.write(printContent); 3498da206178SAtari911 printWindow.document.close(); 3499da206178SAtari911}; 3500da206178SAtari911 35011d05cddcSAtari911// End of calendar plugin JavaScript 3502