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