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