1/**
2 * DokuWiki Compact Calendar Plugin JavaScript
3 * Loaded independently to avoid DokuWiki concatenation issues
4 * @version 7.0.8
5 */
6
7// Debug mode - set to true for console logging
8var CALENDAR_DEBUG = false;
9
10// Debug logging helper
11function calendarLog() {
12    if (CALENDAR_DEBUG && console && console.log) {
13        console.log.apply(console, ['[Calendar]'].concat(Array.prototype.slice.call(arguments)));
14    }
15}
16
17function calendarError() {
18    if (console && console.error) {
19        console.error.apply(console, ['[Calendar]'].concat(Array.prototype.slice.call(arguments)));
20    }
21}
22
23/**
24 * Format a Date object as YYYY-MM-DD in LOCAL time (not UTC)
25 * This avoids timezone issues where toISOString() shifts dates
26 * For example: In Prague (UTC+1), midnight local = 23:00 UTC previous day
27 * @param {Date} date - Date object to format
28 * @returns {string} Date string in YYYY-MM-DD format
29 */
30function formatLocalDate(date) {
31    var year = date.getFullYear();
32    var month = String(date.getMonth() + 1).padStart(2, '0');
33    var day = String(date.getDate()).padStart(2, '0');
34    return year + '-' + month + '-' + day;
35}
36
37// Ensure DOKU_BASE is defined - check multiple sources
38if (typeof DOKU_BASE === 'undefined') {
39    // Try to get from global jsinfo object (DokuWiki standard)
40    if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) {
41        window.DOKU_BASE = window.jsinfo.dokubase;
42    } else {
43        // Fallback: extract from script source path
44        var scripts = document.getElementsByTagName('script');
45        var pluginScriptPath = null;
46        for (var i = 0; i < scripts.length; i++) {
47            if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) {
48                pluginScriptPath = scripts[i].src;
49                break;
50            }
51        }
52
53        if (pluginScriptPath) {
54            // Extract base path from: .../lib/plugins/calendar/script.js
55            var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//);
56            window.DOKU_BASE = match ? match[1] : '/';
57        } else {
58            // Last resort: use root
59            window.DOKU_BASE = '/';
60        }
61    }
62}
63
64// Shorthand for convenience
65var DOKU_BASE = window.DOKU_BASE || '/';
66
67/**
68 * Get DokuWiki security token from multiple possible sources
69 * DokuWiki stores this in different places depending on version/config
70 */
71function getSecurityToken() {
72    // Try JSINFO.sectok (standard location)
73    if (typeof JSINFO !== 'undefined' && JSINFO.sectok) {
74        return JSINFO.sectok;
75    }
76    // Try window.JSINFO
77    if (typeof window.JSINFO !== 'undefined' && window.JSINFO.sectok) {
78        return window.JSINFO.sectok;
79    }
80    // Try finding it in a hidden form field (some templates/plugins add this)
81    var sectokInput = document.querySelector('input[name="sectok"]');
82    if (sectokInput && sectokInput.value) {
83        return sectokInput.value;
84    }
85    // Try meta tag (some DokuWiki setups)
86    var sectokMeta = document.querySelector('meta[name="sectok"]');
87    if (sectokMeta && sectokMeta.content) {
88        return sectokMeta.content;
89    }
90    // Return empty string if not found
91    console.warn('Calendar plugin: Security token not found');
92    return '';
93}
94
95// Helper: propagate CSS variables from a calendar container to a target element
96// This is needed for dialogs/popups that use position:fixed (they inherit CSS vars
97// from DOM parents per spec, but some DokuWiki templates break this inheritance)
98function propagateThemeVars(calId, targetEl) {
99    if (!targetEl) return;
100    // Find the calendar container (could be cal_, panel_, sidebar-widget-, etc.)
101    const container = document.getElementById(calId)
102        || document.getElementById('sidebar-widget-' + calId)
103        || document.querySelector('[id$="' + calId + '"]');
104    if (!container) return;
105    const cs = getComputedStyle(container);
106    const vars = [
107        '--background-site', '--background-alt', '--background-header',
108        '--text-primary', '--text-bright', '--text-dim',
109        '--border-color', '--border-main',
110        '--cell-bg', '--cell-today-bg', '--grid-bg',
111        '--shadow-color', '--header-border', '--header-shadow',
112        '--btn-text'
113    ];
114    vars.forEach(v => {
115        const val = cs.getPropertyValue(v).trim();
116        if (val) targetEl.style.setProperty(v, val);
117    });
118}
119
120// Filter calendar by namespace
121window.filterCalendarByNamespace = function(calId, namespace) {
122    // Get current year and month from calendar
123    const container = document.getElementById(calId);
124    if (!container) {
125        console.error('Calendar container not found:', calId);
126        return;
127    }
128
129    const year = parseInt(container.dataset.year) || new Date().getFullYear();
130    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
131
132    // Reload calendar with the filtered namespace
133    navCalendar(calId, year, month, namespace);
134};
135
136// Navigate to different month
137window.navCalendar = function(calId, year, month, namespace) {
138
139    const params = new URLSearchParams({
140        call: 'plugin_calendar',
141        action: 'load_month',
142        year: year,
143        month: month,
144        namespace: namespace,
145        _: new Date().getTime() // Cache buster
146    });
147
148    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
149        method: 'POST',
150        headers: {
151            'Content-Type': 'application/x-www-form-urlencoded',
152            'Cache-Control': 'no-cache, no-store, must-revalidate',
153            'Pragma': 'no-cache'
154        },
155        body: params.toString()
156    })
157    .then(r => r.json())
158    .then(data => {
159        if (data.success) {
160            rebuildCalendar(calId, data.year, data.month, data.events, namespace);
161        } else {
162            console.error('Failed to load month:', data.error);
163        }
164    })
165    .catch(err => {
166        console.error('Error loading month:', err);
167    });
168};
169
170// Jump to current month
171window.jumpToToday = function(calId, namespace) {
172    const today = new Date();
173    const year = today.getFullYear();
174    const month = today.getMonth() + 1; // JavaScript months are 0-indexed
175    navCalendar(calId, year, month, namespace);
176};
177
178// Jump to today for event panel
179window.jumpTodayPanel = function(calId, namespace) {
180    const today = new Date();
181    const year = today.getFullYear();
182    const month = today.getMonth() + 1;
183    navEventPanel(calId, year, month, namespace);
184};
185
186// Open month picker dialog
187window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) {
188
189    const overlay = document.getElementById('month-picker-overlay-' + calId);
190
191    const monthSelect = document.getElementById('month-picker-month-' + calId);
192
193    const yearSelect = document.getElementById('month-picker-year-' + calId);
194
195    if (!overlay) {
196        console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId);
197        return;
198    }
199
200    if (!monthSelect || !yearSelect) {
201        console.error('Select elements not found!');
202        return;
203    }
204
205    // Set current values
206    monthSelect.value = currentMonth;
207    yearSelect.value = currentYear;
208
209    // Show overlay
210    overlay.style.display = 'flex';
211};
212
213// Open month picker dialog for event panel
214window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) {
215    openMonthPicker(calId, currentYear, currentMonth, namespace);
216};
217
218// Close month picker dialog
219window.closeMonthPicker = function(calId) {
220    const overlay = document.getElementById('month-picker-overlay-' + calId);
221    overlay.style.display = 'none';
222};
223
224// Jump to selected month
225window.jumpToSelectedMonth = function(calId, namespace) {
226    const monthSelect = document.getElementById('month-picker-month-' + calId);
227    const yearSelect = document.getElementById('month-picker-year-' + calId);
228
229    const month = parseInt(monthSelect.value);
230    const year = parseInt(yearSelect.value);
231
232    closeMonthPicker(calId);
233
234    // Check if this is a calendar or event panel
235    const container = document.getElementById(calId);
236    if (container && container.classList.contains('event-panel-standalone')) {
237        navEventPanel(calId, year, month, namespace);
238    } else {
239        navCalendar(calId, year, month, namespace);
240    }
241};
242
243// Rebuild calendar grid after navigation
244window.rebuildCalendar = function(calId, year, month, events, namespace) {
245
246    const container = document.getElementById(calId);
247    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
248                       'July', 'August', 'September', 'October', 'November', 'December'];
249
250    // Get theme data from container
251    const theme = container.dataset.theme || 'matrix';
252    let themeStyles = {};
253    try {
254        themeStyles = JSON.parse(container.dataset.themeStyles || '{}');
255    } catch (e) {
256        console.error('Failed to parse theme styles:', e);
257        themeStyles = {};
258    }
259
260    // Preserve original namespace if not yet set
261    if (!container.dataset.originalNamespace) {
262        container.setAttribute('data-original-namespace', namespace || '');
263    }
264
265    // Update container data attributes for current month/year
266    container.setAttribute('data-year', year);
267    container.setAttribute('data-month', month);
268
269    // Update embedded events data
270    let eventsDataEl = document.getElementById('events-data-' + calId);
271    if (eventsDataEl) {
272        eventsDataEl.textContent = JSON.stringify(events);
273    } else {
274        eventsDataEl = document.createElement('script');
275        eventsDataEl.type = 'application/json';
276        eventsDataEl.id = 'events-data-' + calId;
277        eventsDataEl.textContent = JSON.stringify(events);
278        container.appendChild(eventsDataEl);
279    }
280
281    // Update header
282    const header = container.querySelector('.calendar-compact-header h3');
283    header.textContent = monthNames[month - 1] + ' ' + year;
284
285    // Update or create namespace filter indicator
286    let filterIndicator = container.querySelector('.calendar-namespace-filter');
287    const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' &&
288                            namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1;
289
290    if (shouldShowFilter) {
291        // Show/update filter indicator
292        if (!filterIndicator) {
293            // Create filter indicator if it doesn't exist
294            const headerDiv = container.querySelector('.calendar-compact-header');
295            if (headerDiv) {
296                filterIndicator = document.createElement('div');
297                filterIndicator.className = 'calendar-namespace-filter';
298                filterIndicator.id = 'namespace-filter-' + calId;
299                headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling);
300            }
301        }
302
303        if (filterIndicator) {
304            filterIndicator.innerHTML =
305                '<span class="namespace-filter-label">Filtering:</span>' +
306                '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' +
307                '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>';
308            filterIndicator.style.display = 'flex';
309        }
310    } else {
311        // Hide filter indicator
312        if (filterIndicator) {
313            filterIndicator.style.display = 'none';
314        }
315    }
316
317    // Update container's namespace attribute
318    container.setAttribute('data-namespace', namespace || '');
319
320    // Update nav buttons
321    let prevMonth = month - 1;
322    let prevYear = year;
323    if (prevMonth < 1) {
324        prevMonth = 12;
325        prevYear--;
326    }
327
328    let nextMonth = month + 1;
329    let nextYear = year;
330    if (nextMonth > 12) {
331        nextMonth = 1;
332        nextYear++;
333    }
334
335    const navBtns = container.querySelectorAll('.cal-nav-btn');
336    navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
337    navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
338
339    // Rebuild calendar grid
340    const tbody = container.querySelector('.calendar-compact-grid tbody');
341    const firstDay = new Date(year, month - 1, 1);
342    const daysInMonth = new Date(year, month, 0).getDate();
343    const dayOfWeek = firstDay.getDay();
344
345    // Calculate month boundaries
346    const monthStart = new Date(year, month - 1, 1);
347    const monthEnd = new Date(year, month - 1, daysInMonth);
348
349    // Build a map of all events with their date ranges
350    const eventRanges = {};
351    for (const [dateKey, dayEvents] of Object.entries(events)) {
352        // Defensive check: ensure dayEvents is an array
353        if (!Array.isArray(dayEvents)) {
354            console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents);
355            continue;
356        }
357
358        // Only process events that could possibly overlap with this month/year
359        const dateYear = parseInt(dateKey.split('-')[0]);
360
361        // Skip events from completely different years (unless they're very long multi-day events)
362        if (Math.abs(dateYear - year) > 1) {
363            continue;
364        }
365
366        for (const evt of dayEvents) {
367            const startDate = dateKey;
368            const endDate = evt.endDate || dateKey;
369
370            // Check if event overlaps with current month
371            const eventStart = new Date(startDate + 'T00:00:00');
372            const eventEnd = new Date(endDate + 'T00:00:00');
373
374            // Skip if event doesn't overlap with current month
375            if (eventEnd < monthStart || eventStart > monthEnd) {
376                continue;
377            }
378
379            // Create entry for each day the event spans
380            const start = new Date(startDate + 'T00:00:00');
381            const end = new Date(endDate + 'T00:00:00');
382            const current = new Date(start);
383
384            while (current <= end) {
385                // Use formatLocalDate to avoid timezone shift issues
386                const currentKey = formatLocalDate(current);
387
388                // Check if this date is in current month (use current Date object directly)
389                if (current.getFullYear() === year && current.getMonth() === month - 1) {
390                    if (!eventRanges[currentKey]) {
391                        eventRanges[currentKey] = [];
392                    }
393
394                    // Add event with span information
395                    const eventCopy = {...evt};
396                    eventCopy._span_start = startDate;
397                    eventCopy._span_end = endDate;
398                    eventCopy._is_first_day = (currentKey === startDate);
399                    eventCopy._is_last_day = (currentKey === endDate);
400                    eventCopy._original_date = dateKey;
401
402                    // Check if event continues from previous month or to next month
403                    eventCopy._continues_from_prev = (eventStart < monthStart);
404                    eventCopy._continues_to_next = (eventEnd > monthEnd);
405
406                    eventRanges[currentKey].push(eventCopy);
407                }
408
409                current.setDate(current.getDate() + 1);
410            }
411        }
412    }
413
414    let html = '';
415    let currentDay = 1;
416    const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
417
418    for (let row = 0; row < rowCount; row++) {
419        html += '<tr>';
420        for (let col = 0; col < 7; col++) {
421            if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
422                html += `<td class="cal-empty"></td>`;
423            } else {
424                const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
425
426                // Get today's date in local timezone
427                const todayObj = new Date();
428                const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`;
429
430                const isToday = dateKey === today;
431                const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0;
432
433                let classes = 'cal-day';
434                if (isToday) classes += ' cal-today';
435                if (hasEvents) classes += ' cal-has-events';
436
437                const dayNumClass = isToday ? 'day-num day-num-today' : 'day-num';
438
439                html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`;
440                html += `<span class="${dayNumClass}">${currentDay}</span>`;
441
442                if (hasEvents) {
443                    // Sort events by time (no time first, then by time)
444                    const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => {
445                        const timeA = a.time || '';
446                        const timeB = b.time || '';
447                        if (!timeA && timeB) return -1;
448                        if (timeA && !timeB) return 1;
449                        if (!timeA && !timeB) return 0;
450                        return timeA.localeCompare(timeB);
451                    });
452
453                    // Get important namespaces
454                    let importantNamespaces = ['important'];
455                    if (container.dataset.importantNamespaces) {
456                        try {
457                            importantNamespaces = JSON.parse(container.dataset.importantNamespaces);
458                        } catch (e) {}
459                    }
460
461                    // Show colored stacked bars for each event
462                    html += '<div class="event-indicators">';
463                    for (const evt of sortedEvents) {
464                        const eventId = evt.id || '';
465                        const eventColor = evt.color || '#3498db';
466                        const eventTitle = evt.title || 'Event';
467                        const eventTime = evt.time || '';
468                        const originalDate = evt._original_date || dateKey;
469                        const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true;
470                        const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true;
471
472                        // Check if important namespace
473                        let evtNs = evt.namespace || evt._namespace || '';
474                        let isImportant = false;
475                        for (const impNs of importantNamespaces) {
476                            if (evtNs === impNs || evtNs.startsWith(impNs + ':')) {
477                                isImportant = true;
478                                break;
479                            }
480                        }
481
482                        let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
483                        if (!isFirstDay) barClass += ' event-bar-continues';
484                        if (!isLastDay) barClass += ' event-bar-continuing';
485                        if (isImportant) {
486                            barClass += ' event-bar-important';
487                            if (isFirstDay) {
488                                barClass += ' event-bar-has-star';
489                            }
490                        }
491
492                        html += `<span class="event-bar ${barClass}" `;
493                        html += `style="background: ${eventColor};" `;
494                        html += `title="${isImportant ? '⭐ ' : ''}${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `;
495                        html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');">`;
496                        html += '</span>';
497                    }
498                    html += '</div>';
499                }
500
501                html += '</td>';
502                currentDay++;
503            }
504        }
505        html += '</tr>';
506    }
507
508    tbody.innerHTML = html;
509
510    // Update Today button with current namespace
511    const todayBtn = container.querySelector('.cal-today-btn');
512    if (todayBtn) {
513        todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`);
514    }
515
516    // Update month picker with current namespace
517    const monthPicker = container.querySelector('.calendar-month-picker');
518    if (monthPicker) {
519        monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`);
520    }
521
522    // Rebuild event list - server already filtered to current month
523    const eventList = container.querySelector('.event-list-compact');
524    eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
525
526    // Auto-scroll to first future event (past events will be above viewport)
527    setTimeout(() => {
528        const firstFuture = eventList.querySelector('[data-first-future="true"]');
529        if (firstFuture) {
530            firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' });
531        }
532    }, 100);
533
534    // Update title
535    const title = container.querySelector('#eventlist-title-' + calId);
536    title.textContent = 'Events';
537};
538
539// Render event list from data
540window.renderEventListFromData = function(events, calId, namespace, year, month) {
541    if (!events || Object.keys(events).length === 0) {
542        return '<p class="no-events-msg">No events this month</p>';
543    }
544
545    // Get theme data from container
546    const container = document.getElementById(calId);
547    let themeStyles = {};
548    if (container && container.dataset.themeStyles) {
549        try {
550            themeStyles = JSON.parse(container.dataset.themeStyles);
551        } catch (e) {
552            console.error('Failed to parse theme styles in renderEventListFromData:', e);
553        }
554    }
555
556    // Check for time conflicts
557    events = checkTimeConflicts(events, null);
558
559    let pastHtml = '';
560    let futureHtml = '';
561    let pastCount = 0;
562
563    const sortedDates = Object.keys(events).sort();
564    const today = new Date();
565    today.setHours(0, 0, 0, 0);
566    const todayStr = formatLocalDate(today);
567
568    // Helper function to check if event is past (with 15-minute grace period)
569    const isEventPast = function(dateKey, time) {
570        // If event is on a past date, it's definitely past
571        if (dateKey < todayStr) {
572            return true;
573        }
574
575        // If event is on a future date, it's definitely not past
576        if (dateKey > todayStr) {
577            return false;
578        }
579
580        // Event is today - check time with grace period
581        if (time && time.trim() !== '') {
582            try {
583                const now = new Date();
584                const eventDateTime = new Date(dateKey + 'T' + time);
585
586                // Add 15-minute grace period
587                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
588
589                // Event is past if current time > event time + 15 minutes
590                return now > gracePeriodEnd;
591            } catch (e) {
592                // If time parsing fails, treat as future
593                return false;
594            }
595        }
596
597        // No time specified for today's event, treat as future
598        return false;
599    };
600
601    // Filter events to only current month if year/month provided
602    const monthStart = year && month ? new Date(year, month - 1, 1) : null;
603    const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null;
604
605    for (const dateKey of sortedDates) {
606        // Skip events not in current month if filtering
607        if (monthStart && monthEnd) {
608            const eventDate = new Date(dateKey + 'T00:00:00');
609
610            if (eventDate < monthStart || eventDate > monthEnd) {
611                continue;
612            }
613        }
614
615        // Sort events within this day by time (all-day events at top)
616        const dayEvents = events[dateKey];
617        dayEvents.sort((a, b) => {
618            const timeA = a.time && a.time.trim() !== '' ? a.time : null;
619            const timeB = b.time && b.time.trim() !== '' ? b.time : null;
620
621            // All-day events (no time) go to the TOP
622            if (timeA === null && timeB !== null) return -1; // A before B
623            if (timeA !== null && timeB === null) return 1;  // A after B
624            if (timeA === null && timeB === null) return 0;  // Both all-day, equal
625
626            // Both have times, sort chronologically
627            return timeA.localeCompare(timeB);
628        });
629
630        for (const event of dayEvents) {
631            const isTask = event.isTask || false;
632            const completed = event.completed || false;
633
634            // Use helper function to determine if event is past (with grace period)
635            const isPast = isEventPast(dateKey, event.time);
636            const isPastDue = isPast && isTask && !completed;
637
638            // Determine if this goes in past section
639            const isPastOrCompleted = (isPast && (!isTask || completed)) || completed;
640
641            const eventHtml = renderEventItem(event, dateKey, calId, namespace);
642
643            if (isPastOrCompleted) {
644                pastCount++;
645                pastHtml += eventHtml;
646            } else {
647                futureHtml += eventHtml;
648            }
649        }
650    }
651
652    let html = '';
653
654    // Add collapsible past events section if any exist
655    if (pastCount > 0) {
656        html += '<div class="past-events-section">';
657        html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">';
658        html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> ';
659        html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>';
660        html += '</div>';
661        html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">';
662        html += pastHtml;
663        html += '</div>';
664        html += '</div>';
665    } else {
666    }
667
668    // Add future events
669    html += futureHtml;
670
671
672    if (!html) {
673        return '<p class="no-events-msg">No events this month</p>';
674    }
675
676    return html;
677};
678
679// Show day popup with events when clicking a date
680window.showDayPopup = function(calId, date, namespace) {
681    // Get events for this calendar
682    const eventsDataEl = document.getElementById('events-data-' + calId);
683    let events = {};
684
685    if (eventsDataEl) {
686        try {
687            events = JSON.parse(eventsDataEl.textContent);
688        } catch (e) {
689            console.error('Failed to parse events data:', e);
690        }
691    }
692
693    const dayEvents = events[date] || [];
694
695    // Check for conflicts on this day
696    const dayEventsObj = {[date]: dayEvents};
697    const checkedEvents = checkTimeConflicts(dayEventsObj, null);
698    const dayEventsWithConflicts = checkedEvents[date] || dayEvents;
699
700    // Sort events: all-day at top, then chronological by time
701    dayEventsWithConflicts.sort((a, b) => {
702        const timeA = a.time && a.time.trim() !== '' ? a.time : null;
703        const timeB = b.time && b.time.trim() !== '' ? b.time : null;
704
705        // All-day events (no time) go to the TOP
706        if (timeA === null && timeB !== null) return -1; // A before B
707        if (timeA !== null && timeB === null) return 1;  // A after B
708        if (timeA === null && timeB === null) return 0;  // Both all-day, equal
709
710        // Both have times, sort chronologically
711        return timeA.localeCompare(timeB);
712    });
713
714    const dateObj = new Date(date + 'T00:00:00');
715    const displayDate = dateObj.toLocaleDateString('en-US', {
716        weekday: 'long',
717        month: 'long',
718        day: 'numeric',
719        year: 'numeric'
720    });
721
722    // Create popup
723    let popup = document.getElementById('day-popup-' + calId);
724    if (!popup) {
725        popup = document.createElement('div');
726        popup.id = 'day-popup-' + calId;
727        popup.className = 'day-popup';
728        document.body.appendChild(popup);
729    }
730
731    // Get theme styles and important namespaces
732    const container = document.getElementById(calId);
733    const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {};
734    const theme = container ? container.dataset.theme : 'matrix';
735
736    // Get important namespaces
737    let importantNamespaces = ['important'];
738    if (container && container.dataset.importantNamespaces) {
739        try {
740            importantNamespaces = JSON.parse(container.dataset.importantNamespaces);
741        } catch (e) {
742            importantNamespaces = ['important'];
743        }
744    }
745
746    let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>';
747    html += '<div class="day-popup-content">';
748    html += '<div class="day-popup-header">';
749    html += '<h4>' + displayDate + '</h4>';
750    html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>';
751    html += '</div>';
752
753    html += '<div class="day-popup-body">';
754
755    if (dayEventsWithConflicts.length === 0) {
756        html += '<p class="no-events-msg">No events on this day</p>';
757    } else {
758        html += '<div class="popup-events-list">';
759        dayEventsWithConflicts.forEach(event => {
760            const color = event.color || '#3498db';
761
762            // Use individual event namespace if available (for multi-namespace support)
763            const eventNamespace = event._namespace !== undefined ? event._namespace : namespace;
764
765            // Check if this is an important namespace event
766            let isImportant = false;
767            if (eventNamespace) {
768                for (const impNs of importantNamespaces) {
769                    if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) {
770                        isImportant = true;
771                        break;
772                    }
773                }
774            }
775
776            // Check if this is a continuation (event started before this date)
777            const originalStartDate = event.originalStartDate || event._dateKey || date;
778            const isContinuation = originalStartDate < date;
779
780            // Convert to 12-hour format and handle time ranges
781            let displayTime = '';
782            if (event.time) {
783                displayTime = formatTimeRange(event.time, event.endTime);
784            }
785
786            // Multi-day indicator
787            let multiDay = '';
788            if (event.endDate && event.endDate !== date) {
789                const endObj = new Date(event.endDate + 'T00:00:00');
790                multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
791                    month: 'short',
792                    day: 'numeric'
793                });
794            }
795
796            // Continuation message
797            if (isContinuation) {
798                const startObj = new Date(originalStartDate + 'T00:00:00');
799                const startDisplay = startObj.toLocaleDateString('en-US', {
800                    weekday: 'short',
801                    month: 'short',
802                    day: 'numeric'
803                });
804                html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>';
805            }
806
807            const importantClass = isImportant ? ' popup-event-important' : '';
808            html += '<div class="popup-event-item' + importantClass + '" tabindex="0" role="listitem" aria-label="' + escapeHtml(event.title) + (displayTime ? ', ' + displayTime : '') + '">';
809            html += '<div class="event-color-bar" style="background: ' + color + ';"></div>';
810            html += '<div class="popup-event-content">';
811
812            // Single line with title, time, date range, namespace, and actions
813            html += '<div class="popup-event-main-row">';
814            html += '<div class="popup-event-info-inline">';
815
816            // Add star for important events
817            if (isImportant) {
818                html += '<span class="popup-event-star">⭐</span>';
819            }
820
821            html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>';
822            if (displayTime) {
823                html += '<span class="popup-event-time">�� ' + displayTime + '</span>';
824            }
825            if (multiDay) {
826                html += '<span class="popup-event-multiday">' + multiDay + '</span>';
827            }
828            if (eventNamespace) {
829                html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>';
830            }
831
832            // Add conflict warning badge if event has conflicts
833            if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
834                // Build conflict list for tooltip
835                let conflictList = [];
836                event.conflictsWith.forEach(conflict => {
837                    let conflictText = conflict.title;
838                    if (conflict.time) {
839                        conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
840                    }
841                    conflictList.push(conflictText);
842                });
843
844                html += '<span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
845            }
846
847            html += '</div>';
848            html += '<div class="popup-event-actions">';
849            html += '<button type="button" class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>';
850            html += '<button type="button" class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">��️</button>';
851            html += '</div>';
852            html += '</div>';
853
854            // Description on separate line if present
855            if (event.description) {
856                html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>';
857            }
858
859            html += '</div></div>';
860        });
861        html += '</div>';
862    }
863
864    html += '</div>';
865
866    html += '<div class="day-popup-footer">';
867    html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>';
868    html += '</div>';
869
870    html += '</div>';
871
872    popup.innerHTML = html;
873    popup.style.display = 'flex';
874
875    // Propagate CSS vars from calendar container to popup (popup is outside container in DOM)
876    if (container) {
877        propagateThemeVars(calId, popup.querySelector('.day-popup-content'));
878    }
879
880    // Make popup draggable by header
881    const popupContent = popup.querySelector('.day-popup-content');
882    const popupHeader = popup.querySelector('.day-popup-header');
883
884    if (popupContent && popupHeader) {
885        // Reset position to center
886        popupContent.style.position = 'relative';
887        popupContent.style.left = '0';
888        popupContent.style.top = '0';
889
890        // Store drag state on the element itself
891        popupHeader._isDragging = false;
892
893        popupHeader.onmousedown = function(e) {
894            // Ignore if clicking the close button
895            if (e.target.classList.contains('popup-close')) return;
896
897            popupHeader._isDragging = true;
898            popupHeader._dragStartX = e.clientX;
899            popupHeader._dragStartY = e.clientY;
900
901            const rect = popupContent.getBoundingClientRect();
902            const parentRect = popup.getBoundingClientRect();
903            popupHeader._initialLeft = rect.left - parentRect.left - (parentRect.width / 2 - rect.width / 2);
904            popupHeader._initialTop = rect.top - parentRect.top - (parentRect.height / 2 - rect.height / 2);
905
906            popupContent.style.transition = 'none';
907            e.preventDefault();
908        };
909
910        popup.onmousemove = function(e) {
911            if (!popupHeader._isDragging) return;
912
913            const deltaX = e.clientX - popupHeader._dragStartX;
914            const deltaY = e.clientY - popupHeader._dragStartY;
915
916            popupContent.style.left = (popupHeader._initialLeft + deltaX) + 'px';
917            popupContent.style.top = (popupHeader._initialTop + deltaY) + 'px';
918        };
919
920        popup.onmouseup = function() {
921            if (popupHeader._isDragging) {
922                popupHeader._isDragging = false;
923                popupContent.style.transition = '';
924            }
925        };
926
927        popup.onmouseleave = function() {
928            if (popupHeader._isDragging) {
929                popupHeader._isDragging = false;
930                popupContent.style.transition = '';
931            }
932        };
933    }
934};
935
936// Close day popup
937window.closeDayPopup = function(calId) {
938    const popup = document.getElementById('day-popup-' + calId);
939    if (popup) {
940        popup.style.display = 'none';
941    }
942};
943
944// Show events for a specific day (for event list panel)
945window.showDayEvents = function(calId, date, namespace) {
946    const params = new URLSearchParams({
947        call: 'plugin_calendar',
948        action: 'load_month',
949        year: date.split('-')[0],
950        month: parseInt(date.split('-')[1]),
951        namespace: namespace,
952        _: new Date().getTime() // Cache buster
953    });
954
955    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
956        method: 'POST',
957        headers: {
958            'Content-Type': 'application/x-www-form-urlencoded',
959            'Cache-Control': 'no-cache, no-store, must-revalidate',
960            'Pragma': 'no-cache'
961        },
962        body: params.toString()
963    })
964    .then(r => r.json())
965    .then(data => {
966        if (data.success) {
967            const eventList = document.getElementById('eventlist-' + calId);
968            const events = data.events;
969            const title = document.getElementById('eventlist-title-' + calId);
970
971            const dateObj = new Date(date + 'T00:00:00');
972            const displayDate = dateObj.toLocaleDateString('en-US', {
973                weekday: 'short',
974                month: 'short',
975                day: 'numeric'
976            });
977
978            title.textContent = 'Events - ' + displayDate;
979
980            // Filter events for this day
981            const dayEvents = events[date] || [];
982
983            if (dayEvents.length === 0) {
984                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>';
985            } else {
986                let html = '';
987                dayEvents.forEach(event => {
988                    html += renderEventItem(event, date, calId, namespace);
989                });
990                eventList.innerHTML = html;
991            }
992        }
993    })
994    .catch(err => console.error('Error:', err));
995};
996
997// Render a single event item
998window.renderEventItem = function(event, date, calId, namespace) {
999    // Get theme data from container
1000    const container = document.getElementById(calId);
1001    let themeStyles = {};
1002    let importantNamespaces = ['important']; // default
1003    if (container && container.dataset.themeStyles) {
1004        try {
1005            themeStyles = JSON.parse(container.dataset.themeStyles);
1006        } catch (e) {
1007            console.error('Failed to parse theme styles:', e);
1008        }
1009    }
1010    // Get important namespaces from container data attribute
1011    if (container && container.dataset.importantNamespaces) {
1012        try {
1013            importantNamespaces = JSON.parse(container.dataset.importantNamespaces);
1014        } catch (e) {
1015            importantNamespaces = ['important'];
1016        }
1017    }
1018
1019    // Check if this event is in the past or today (with 15-minute grace period)
1020    const today = new Date();
1021    today.setHours(0, 0, 0, 0);
1022    const todayStr = formatLocalDate(today);
1023    const eventDate = new Date(date + 'T00:00:00');
1024
1025    // Helper to determine if event is past with grace period
1026    let isPast;
1027    if (date < todayStr) {
1028        isPast = true; // Past date
1029    } else if (date > todayStr) {
1030        isPast = false; // Future date
1031    } else {
1032        // Today - check time with grace period
1033        if (event.time && event.time.trim() !== '') {
1034            try {
1035                const now = new Date();
1036                const eventDateTime = new Date(date + 'T' + event.time);
1037                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
1038                isPast = now > gracePeriodEnd;
1039            } catch (e) {
1040                isPast = false;
1041            }
1042        } else {
1043            isPast = false; // No time, treat as future
1044        }
1045    }
1046
1047    const isToday = eventDate.getTime() === today.getTime();
1048
1049    // Check if this is an important namespace event
1050    let eventNamespace = event.namespace || '';
1051    if (!eventNamespace && event._namespace !== undefined) {
1052        eventNamespace = event._namespace;
1053    }
1054    let isImportantNs = false;
1055    if (eventNamespace) {
1056        for (const impNs of importantNamespaces) {
1057            if (eventNamespace === impNs || eventNamespace.startsWith(impNs + ':')) {
1058                isImportantNs = true;
1059                break;
1060            }
1061        }
1062    }
1063
1064    // Format date display with day of week
1065    const displayDateKey = event.originalStartDate || date;
1066    const dateObj = new Date(displayDateKey + 'T00:00:00');
1067    const displayDate = dateObj.toLocaleDateString('en-US', {
1068        weekday: 'short',
1069        month: 'short',
1070        day: 'numeric'
1071    });
1072
1073    // Convert to 12-hour format and handle time ranges
1074    let displayTime = '';
1075    if (event.time) {
1076        displayTime = formatTimeRange(event.time, event.endTime);
1077    }
1078
1079    // Multi-day indicator
1080    let multiDay = '';
1081    if (event.endDate && event.endDate !== displayDateKey) {
1082        const endObj = new Date(event.endDate + 'T00:00:00');
1083        multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
1084            weekday: 'short',
1085            month: 'short',
1086            day: 'numeric'
1087        });
1088    }
1089
1090    const completedClass = event.completed ? ' event-completed' : '';
1091    const isTask = event.isTask || false;
1092    const completed = event.completed || false;
1093    const isPastDue = isPast && isTask && !completed;
1094    const pastClass = (isPast && !isPastDue) ? ' event-past' : '';
1095    const pastDueClass = isPastDue ? ' event-pastdue' : '';
1096    const importantClass = isImportantNs ? ' event-important' : '';
1097    const color = event.color || '#3498db';
1098
1099    // Only inline style needed: border-left-color for event color indicator
1100    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)' : '') + '">';
1101
1102    html += '<div class="event-info">';
1103    html += '<div class="event-title-row">';
1104    // Add star for important namespace events
1105    if (isImportantNs) {
1106        html += '<span class="event-important-star" title="Important">⭐</span> ';
1107    }
1108    html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>';
1109    html += '</div>';
1110
1111    // Show meta and description for non-past events AND past due tasks
1112    if (!isPast || isPastDue) {
1113        html += '<div class="event-meta-compact">';
1114        html += '<span class="event-date-time">' + displayDate + multiDay;
1115        if (displayTime) {
1116            html += ' • ' + displayTime;
1117        }
1118        // Add PAST DUE or TODAY badge
1119        if (isPastDue) {
1120            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>';
1121        } else if (isToday) {
1122            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>';
1123        }
1124        // Add namespace badge
1125        if (eventNamespace) {
1126            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>';
1127        }
1128        // Add conflict warning if event has time conflicts
1129        if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
1130            let conflictList = [];
1131            event.conflictsWith.forEach(conflict => {
1132                let conflictText = conflict.title;
1133                if (conflict.time) {
1134                    conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
1135                }
1136                conflictList.push(conflictText);
1137            });
1138
1139            html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
1140        }
1141        html += '</span>';
1142        html += '</div>';
1143
1144        if (event.description) {
1145            html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>';
1146        }
1147    } else {
1148        // For past events (not past due), store data in hidden divs for expand/collapse
1149        html += '<div class="event-meta-compact" style="display: none;">';
1150        html += '<span class="event-date-time">' + displayDate + multiDay;
1151        if (displayTime) {
1152            html += ' • ' + displayTime;
1153        }
1154        // Add namespace badge for past events too
1155        let eventNamespace = event.namespace || '';
1156        if (!eventNamespace && event._namespace !== undefined) {
1157            eventNamespace = event._namespace;
1158        }
1159        if (eventNamespace) {
1160            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>';
1161        }
1162        // Add conflict warning for past events too
1163        if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
1164            let conflictList = [];
1165            event.conflictsWith.forEach(conflict => {
1166                let conflictText = conflict.title;
1167                if (conflict.time) {
1168                    conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
1169                }
1170                conflictList.push(conflictText);
1171            });
1172
1173            html += ' <span class="event-conflict-badge" data-conflicts="' + btoa(unescape(encodeURIComponent(JSON.stringify(conflictList)))) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
1174        }
1175        html += '</span>';
1176        html += '</div>';
1177
1178        if (event.description) {
1179            html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>';
1180        }
1181    }
1182
1183    html += '</div>'; // event-info
1184
1185    // Use stored namespace from event, fallback to _namespace, then passed namespace
1186    let buttonNamespace = event.namespace || '';
1187    if (!buttonNamespace && event._namespace !== undefined) {
1188        buttonNamespace = event._namespace;
1189    }
1190    if (!buttonNamespace) {
1191        buttonNamespace = namespace;
1192    }
1193
1194    html += '<div class="event-actions-compact">';
1195    html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">��️</button>';
1196    html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>';
1197    html += '</div>';
1198
1199    // Checkbox for tasks - ON THE FAR RIGHT
1200    if (isTask) {
1201        const checked = completed ? 'checked' : '';
1202        html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">';
1203    }
1204
1205    html += '</div>';
1206
1207    return html;
1208};
1209
1210// Render description with rich content support
1211window.renderDescription = function(description) {
1212    if (!description) return '';
1213
1214    // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping)
1215    // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00
1216
1217    let rendered = description;
1218    const tokens = [];
1219    let tokenIndex = 0;
1220
1221    // Convert DokuWiki image syntax {{image.jpg}} to tokens
1222    rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
1223        imagePath = imagePath.trim();
1224        alt = alt ? alt.trim() : '';
1225
1226        let imageHtml;
1227        // Handle external URLs
1228        if (imagePath.match(/^https?:\/\//)) {
1229            imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
1230        } else {
1231            // Handle internal DokuWiki images
1232            const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
1233            imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
1234        }
1235
1236        const token = '\x00TOKEN' + tokenIndex + '\x00';
1237        tokens[tokenIndex] = imageHtml;
1238        tokenIndex++;
1239        return token;
1240    });
1241
1242    // Convert DokuWiki link syntax [[link|text]] to tokens
1243    rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
1244        link = link.trim();
1245        text = text ? text.trim() : link;
1246
1247        let linkHtml;
1248        // Handle external URLs
1249        if (link.match(/^https?:\/\//)) {
1250            linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
1251        } else {
1252            // Handle internal DokuWiki links with section anchors
1253            const hashIndex = link.indexOf('#');
1254            let pagePart = link;
1255            let sectionPart = '';
1256
1257            if (hashIndex !== -1) {
1258                pagePart = link.substring(0, hashIndex);
1259                sectionPart = link.substring(hashIndex); // Includes the #
1260            }
1261
1262            const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart;
1263            linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>';
1264        }
1265
1266        const token = '\x00TOKEN' + tokenIndex + '\x00';
1267        tokens[tokenIndex] = linkHtml;
1268        tokenIndex++;
1269        return token;
1270    });
1271
1272    // Convert markdown-style links [text](url) to tokens
1273    rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
1274        text = text.trim();
1275        url = url.trim();
1276
1277        let linkHtml;
1278        if (url.match(/^https?:\/\//)) {
1279            linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
1280        } else {
1281            linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>';
1282        }
1283
1284        const token = '\x00TOKEN' + tokenIndex + '\x00';
1285        tokens[tokenIndex] = linkHtml;
1286        tokenIndex++;
1287        return token;
1288    });
1289
1290    // Convert plain URLs to tokens
1291    rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
1292        const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>';
1293        const token = '\x00TOKEN' + tokenIndex + '\x00';
1294        tokens[tokenIndex] = linkHtml;
1295        tokenIndex++;
1296        return token;
1297    });
1298
1299    // NOW escape the remaining text (tokens are protected with null bytes)
1300    rendered = escapeHtml(rendered);
1301
1302    // Convert newlines to <br>
1303    rendered = rendered.replace(/\n/g, '<br>');
1304
1305    // DokuWiki text formatting (on escaped text)
1306    // Bold: **text** or __text__
1307    rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1308    rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>');
1309
1310    // Italic: //text//
1311    rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>');
1312
1313    // Strikethrough: <del>text</del>
1314    rendered = rendered.replace(/&lt;del&gt;(.+?)&lt;\/del&gt;/g, '<del>$1</del>');
1315
1316    // Monospace: ''text''
1317    rendered = rendered.replace(/&#39;&#39;(.+?)&#39;&#39;/g, '<code>$1</code>');
1318
1319    // Subscript: <sub>text</sub>
1320    rendered = rendered.replace(/&lt;sub&gt;(.+?)&lt;\/sub&gt;/g, '<sub>$1</sub>');
1321
1322    // Superscript: <sup>text</sup>
1323    rendered = rendered.replace(/&lt;sup&gt;(.+?)&lt;\/sup&gt;/g, '<sup>$1</sup>');
1324
1325    // Restore tokens (replace with actual HTML)
1326    for (let i = 0; i < tokens.length; i++) {
1327        const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g');
1328        rendered = rendered.replace(tokenPattern, tokens[i]);
1329    }
1330
1331    return rendered;
1332}
1333
1334// Open add event dialog
1335window.openAddEvent = function(calId, namespace, date) {
1336    const dialog = document.getElementById('dialog-' + calId);
1337    const form = document.getElementById('eventform-' + calId);
1338    const title = document.getElementById('dialog-title-' + calId);
1339    const dateField = document.getElementById('event-date-' + calId);
1340
1341    if (!dateField) {
1342        console.error('Date field not found! ID: event-date-' + calId);
1343        return;
1344    }
1345
1346    // Check if there's a filtered namespace active (only for regular calendars)
1347    const calendar = document.getElementById(calId);
1348    const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null;
1349
1350    // Use filtered namespace if available, otherwise use the passed namespace
1351    const effectiveNamespace = filteredNamespace || namespace;
1352
1353
1354    // Reset form
1355    form.reset();
1356    document.getElementById('event-id-' + calId).value = '';
1357
1358    // Store the effective namespace in a hidden field or data attribute
1359    form.dataset.effectiveNamespace = effectiveNamespace;
1360
1361    // Set namespace dropdown to effective namespace
1362    const namespaceSelect = document.getElementById('event-namespace-' + calId);
1363    if (namespaceSelect) {
1364        if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) {
1365            // Set to specific namespace if not wildcard or multi-namespace
1366            namespaceSelect.value = effectiveNamespace;
1367        } else {
1368            // Default to empty (default namespace) for wildcard/multi views
1369            namespaceSelect.value = '';
1370        }
1371    }
1372
1373    // Clear event namespace from previous edits
1374    delete form.dataset.eventNamespace;
1375
1376    // Set date - use local date, not UTC
1377    let defaultDate = date;
1378    if (!defaultDate) {
1379        // Get the currently displayed month from the calendar container
1380        const container = document.getElementById(calId);
1381        const displayedYear = parseInt(container.getAttribute('data-year'));
1382        const displayedMonth = parseInt(container.getAttribute('data-month'));
1383
1384
1385        if (displayedYear && displayedMonth) {
1386            // Use first day of the displayed month
1387            const year = displayedYear;
1388            const month = String(displayedMonth).padStart(2, '0');
1389            defaultDate = `${year}-${month}-01`;
1390        } else {
1391            // Fallback to today if attributes not found
1392            const today = new Date();
1393            const year = today.getFullYear();
1394            const month = String(today.getMonth() + 1).padStart(2, '0');
1395            const day = String(today.getDate()).padStart(2, '0');
1396            defaultDate = `${year}-${month}-${day}`;
1397        }
1398    }
1399    dateField.value = defaultDate;
1400    dateField.removeAttribute('data-original-date');
1401
1402    // Also set the end date field to the same default (user can change it)
1403    const endDateField = document.getElementById('event-end-date-' + calId);
1404    if (endDateField) {
1405        endDateField.value = ''; // Empty by default (single-day event)
1406    }
1407
1408    // Set default color
1409    document.getElementById('event-color-' + calId).value = '#3498db';
1410
1411    // Reset time pickers to default state
1412    setTimePicker(calId, false, ''); // Start time = All day
1413    setTimePicker(calId, true, '');  // End time = Same as start
1414
1415    // Set date pickers
1416    setDatePicker(calId, false, defaultDate); // Start date
1417    setDatePicker(calId, true, '');  // End date = Optional
1418
1419    // Initialize namespace search
1420    initNamespaceSearch(calId);
1421
1422    // Set title
1423    title.textContent = 'Add Event';
1424
1425    // Show dialog
1426    dialog.style.display = 'flex';
1427
1428    // Propagate CSS vars to dialog (position:fixed can break inheritance in some templates)
1429    propagateThemeVars(calId, dialog);
1430
1431    // Initialize custom pickers
1432    initCustomTimePickers(calId);
1433    initCustomDatePickers(calId);
1434
1435    // Make dialog draggable
1436    setTimeout(() => makeDialogDraggable(calId), 50);
1437
1438    // Focus title field
1439    setTimeout(() => {
1440        const titleField = document.getElementById('event-title-' + calId);
1441        if (titleField) titleField.focus();
1442    }, 100);
1443};
1444
1445// Edit event
1446window.editEvent = function(calId, eventId, date, namespace) {
1447    const params = new URLSearchParams({
1448        call: 'plugin_calendar',
1449        action: 'get_event',
1450        namespace: namespace,
1451        date: date,
1452        eventId: eventId
1453    });
1454
1455    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1456        method: 'POST',
1457        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1458        body: params.toString()
1459    })
1460    .then(r => r.json())
1461    .then(data => {
1462        if (data.success && data.event) {
1463            const event = data.event;
1464            const dialog = document.getElementById('dialog-' + calId);
1465            const title = document.getElementById('dialog-title-' + calId);
1466            const dateField = document.getElementById('event-date-' + calId);
1467            const form = document.getElementById('eventform-' + calId);
1468
1469            if (!dateField) {
1470                console.error('Date field not found when editing!');
1471                return;
1472            }
1473
1474            // Store the event's actual namespace for saving (important for namespace=* views)
1475            if (event.namespace !== undefined) {
1476                form.dataset.eventNamespace = event.namespace;
1477            }
1478
1479            // Populate form
1480            document.getElementById('event-id-' + calId).value = event.id;
1481            dateField.value = date;
1482            dateField.setAttribute('data-original-date', date);
1483
1484            const endDateField = document.getElementById('event-end-date-' + calId);
1485            endDateField.value = event.endDate || '';
1486
1487            document.getElementById('event-title-' + calId).value = event.title;
1488            document.getElementById('event-color-' + calId).value = event.color || '#3498db';
1489            document.getElementById('event-desc-' + calId).value = event.description || '';
1490            document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
1491
1492            // Set time picker values using custom picker API
1493            setTimePicker(calId, false, event.time || '');
1494            setTimePicker(calId, true, event.endTime || '');
1495
1496            // Set date picker values
1497            setDatePicker(calId, false, date);
1498            setDatePicker(calId, true, event.endDate || '');
1499
1500            // Initialize namespace search
1501            initNamespaceSearch(calId);
1502
1503            // Set namespace fields if available
1504            const namespaceHidden = document.getElementById('event-namespace-' + calId);
1505            const namespaceSearch = document.getElementById('event-namespace-search-' + calId);
1506            if (namespaceHidden && event.namespace !== undefined) {
1507                // Set the hidden input (this is what gets submitted)
1508                namespaceHidden.value = event.namespace || '';
1509                // Set the search input to display the namespace
1510                if (namespaceSearch) {
1511                    namespaceSearch.value = event.namespace || '(default)';
1512                }
1513            } else {
1514                // No namespace on event, set to default
1515                if (namespaceHidden) {
1516                    namespaceHidden.value = '';
1517                }
1518                if (namespaceSearch) {
1519                    namespaceSearch.value = '(default)';
1520                }
1521            }
1522
1523            title.textContent = 'Edit Event';
1524            dialog.style.display = 'flex';
1525
1526            // Propagate CSS vars to dialog
1527            propagateThemeVars(calId, dialog);
1528
1529            // Initialize custom pickers
1530            initCustomTimePickers(calId);
1531            initCustomDatePickers(calId);
1532
1533            // Make dialog draggable
1534            setTimeout(() => makeDialogDraggable(calId), 50);
1535        }
1536    })
1537    .catch(err => console.error('Error editing event:', err));
1538};
1539
1540// Delete event
1541window.deleteEvent = function(calId, eventId, date, namespace) {
1542    if (!confirm('Delete this event?')) return;
1543
1544    const params = new URLSearchParams({
1545        call: 'plugin_calendar',
1546        action: 'delete_event',
1547        namespace: namespace,
1548        date: date,
1549        eventId: eventId,
1550        sectok: getSecurityToken()
1551    });
1552
1553    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1554        method: 'POST',
1555        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1556        body: params.toString()
1557    })
1558    .then(r => r.json())
1559    .then(data => {
1560        if (data.success) {
1561            // Announce to screen readers
1562            announceToScreenReader('Event deleted');
1563
1564            // Extract year and month from date
1565            const [year, month] = date.split('-').map(Number);
1566
1567            // Get the calendar's ORIGINAL namespace setting (not the deleted event's namespace)
1568            // This preserves wildcard/multi-namespace views
1569            const container = document.getElementById(calId);
1570            const calendarNamespace = container ? (container.dataset.namespace || '') : namespace;
1571
1572            // Reload calendar data via AJAX with the calendar's original namespace
1573            reloadCalendarData(calId, year, month, calendarNamespace);
1574        }
1575    })
1576    .catch(err => console.error('Error:', err));
1577};
1578
1579// Save event (add or edit)
1580window.saveEventCompact = function(calId, namespace) {
1581    const form = document.getElementById('eventform-' + calId);
1582
1583    // Get namespace from dropdown - this is what the user selected
1584    const namespaceSelect = document.getElementById('event-namespace-' + calId);
1585    const selectedNamespace = namespaceSelect ? namespaceSelect.value : '';
1586
1587    // ALWAYS use what the user selected in the dropdown
1588    // This allows changing namespace when editing
1589    const finalNamespace = selectedNamespace;
1590
1591    const eventId = document.getElementById('event-id-' + calId).value;
1592
1593    // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event)
1594    const originalNamespace = form.dataset.eventNamespace;
1595
1596
1597    const dateInput = document.getElementById('event-date-' + calId);
1598    const date = dateInput.value;
1599    const oldDate = dateInput.getAttribute('data-original-date') || date;
1600    const endDate = document.getElementById('event-end-date-' + calId).value;
1601    const title = document.getElementById('event-title-' + calId).value;
1602    const time = document.getElementById('event-time-' + calId).value;
1603    const endTime = document.getElementById('event-end-time-' + calId).value;
1604    const colorSelect = document.getElementById('event-color-' + calId);
1605    let color = colorSelect.value;
1606
1607    // Handle custom color
1608    if (color === 'custom') {
1609        color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value;
1610    }
1611
1612    const description = document.getElementById('event-desc-' + calId).value;
1613    const isTask = document.getElementById('event-is-task-' + calId).checked;
1614    const completed = false; // New tasks are not completed
1615    const isRecurring = document.getElementById('event-recurring-' + calId).checked;
1616    const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value;
1617    const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value;
1618
1619    // New recurrence options
1620    const recurrenceIntervalInput = document.getElementById('event-recurrence-interval-' + calId);
1621    const recurrenceInterval = recurrenceIntervalInput ? parseInt(recurrenceIntervalInput.value) || 1 : 1;
1622
1623    // Weekly: collect selected days
1624    let weekDays = [];
1625    const weeklyOptions = document.getElementById('weekly-options-' + calId);
1626    if (weeklyOptions && recurrenceType === 'weekly') {
1627        const checkboxes = weeklyOptions.querySelectorAll('input[name="weekDays[]"]:checked');
1628        weekDays = Array.from(checkboxes).map(cb => cb.value);
1629    }
1630
1631    // Monthly: collect day-of-month or ordinal weekday
1632    let monthDay = '';
1633    let monthlyType = 'dayOfMonth';
1634    let ordinalWeek = '';
1635    let ordinalDay = '';
1636    const monthlyOptions = document.getElementById('monthly-options-' + calId);
1637    if (monthlyOptions && recurrenceType === 'monthly') {
1638        const monthlyTypeRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked');
1639        monthlyType = monthlyTypeRadio ? monthlyTypeRadio.value : 'dayOfMonth';
1640
1641        if (monthlyType === 'dayOfMonth') {
1642            const monthDayInput = document.getElementById('event-month-day-' + calId);
1643            monthDay = monthDayInput ? monthDayInput.value : '';
1644        } else {
1645            const ordinalSelect = document.getElementById('event-ordinal-' + calId);
1646            const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId);
1647            ordinalWeek = ordinalSelect ? ordinalSelect.value : '1';
1648            ordinalDay = ordinalDaySelect ? ordinalDaySelect.value : '0';
1649        }
1650    }
1651
1652    if (!title) {
1653        alert('Please enter a title');
1654        return;
1655    }
1656
1657    if (!date) {
1658        alert('Please select a date');
1659        return;
1660    }
1661
1662    const params = new URLSearchParams({
1663        call: 'plugin_calendar',
1664        action: 'save_event',
1665        namespace: finalNamespace,
1666        eventId: eventId,
1667        date: date,
1668        oldDate: oldDate,
1669        endDate: endDate,
1670        title: title,
1671        time: time,
1672        endTime: endTime,
1673        color: color,
1674        description: description,
1675        isTask: isTask ? '1' : '0',
1676        completed: completed ? '1' : '0',
1677        isRecurring: isRecurring ? '1' : '0',
1678        recurrenceType: recurrenceType,
1679        recurrenceInterval: recurrenceInterval,
1680        recurrenceEnd: recurrenceEnd,
1681        weekDays: weekDays.join(','),
1682        monthlyType: monthlyType,
1683        monthDay: monthDay,
1684        ordinalWeek: ordinalWeek,
1685        ordinalDay: ordinalDay,
1686        sectok: getSecurityToken()
1687    });
1688
1689    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1690        method: 'POST',
1691        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1692        body: params.toString()
1693    })
1694    .then(r => r.json())
1695    .then(data => {
1696        if (data.success) {
1697            // Announce to screen readers
1698            announceToScreenReader(eventId ? 'Event updated' : 'Event created');
1699
1700            closeEventDialog(calId);
1701
1702            // For recurring events, do a full page reload to show all occurrences
1703            if (isRecurring) {
1704                location.reload();
1705                return;
1706            }
1707
1708            // Extract year and month from the NEW date (in case date was changed)
1709            const [year, month] = date.split('-').map(Number);
1710
1711            // Get the calendar's ORIGINAL namespace setting from the container
1712            // This preserves wildcard/multi-namespace views after editing
1713            const container = document.getElementById(calId);
1714            const calendarNamespace = container ? (container.dataset.namespace || '') : namespace;
1715
1716            // Reload calendar data via AJAX to the month of the event
1717            reloadCalendarData(calId, year, month, calendarNamespace);
1718        } else {
1719            alert('Error: ' + (data.error || 'Unknown error'));
1720        }
1721    })
1722    .catch(err => {
1723        console.error('Error:', err);
1724        alert('Error saving event');
1725    });
1726};
1727
1728// Reload calendar data without page refresh
1729window.reloadCalendarData = function(calId, year, month, namespace) {
1730    const params = new URLSearchParams({
1731        call: 'plugin_calendar',
1732        action: 'load_month',
1733        year: year,
1734        month: month,
1735        namespace: namespace,
1736        _: new Date().getTime() // Cache buster
1737    });
1738
1739    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1740        method: 'POST',
1741        headers: {
1742            'Content-Type': 'application/x-www-form-urlencoded',
1743            'Cache-Control': 'no-cache, no-store, must-revalidate',
1744            'Pragma': 'no-cache'
1745        },
1746        body: params.toString()
1747    })
1748    .then(r => r.json())
1749    .then(data => {
1750        if (data.success) {
1751            const container = document.getElementById(calId);
1752
1753            // Check if this is a full calendar or just event panel
1754            if (container.classList.contains('calendar-compact-container')) {
1755                rebuildCalendar(calId, data.year, data.month, data.events, namespace);
1756            } else if (container.classList.contains('event-panel-standalone')) {
1757                rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
1758            }
1759        }
1760    })
1761    .catch(err => console.error('Error:', err));
1762};
1763
1764// Close event dialog
1765window.closeEventDialog = function(calId) {
1766    const dialog = document.getElementById('dialog-' + calId);
1767    dialog.style.display = 'none';
1768};
1769
1770// Escape HTML
1771window.escapeHtml = function(text) {
1772    const div = document.createElement('div');
1773    div.textContent = text;
1774    return div.innerHTML;
1775};
1776
1777// Highlight event when clicking on bar in calendar
1778window.highlightEvent = function(calId, eventId, date) {
1779
1780    // Find the event item in the event list
1781    const eventList = document.querySelector('#' + calId + ' .event-list-compact');
1782    if (!eventList) {
1783        return;
1784    }
1785
1786    const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
1787    if (!eventItem) {
1788        return;
1789    }
1790
1791
1792    // Get theme
1793    const container = document.getElementById(calId);
1794    const theme = container ? container.dataset.theme : 'matrix';
1795    const themeStyles = container ? JSON.parse(container.dataset.themeStyles || '{}') : {};
1796
1797
1798    // Theme-specific highlight colors
1799    let highlightBg, highlightShadow;
1800    if (theme === 'matrix') {
1801        highlightBg = '#1a3d1a';  // Darker green
1802        highlightShadow = '0 0 20px rgba(0, 204, 7, 0.8), 0 0 40px rgba(0, 204, 7, 0.4)';
1803    } else if (theme === 'purple') {
1804        highlightBg = '#3d2b4d';  // Darker purple
1805        highlightShadow = '0 0 20px rgba(155, 89, 182, 0.8), 0 0 40px rgba(155, 89, 182, 0.4)';
1806    } else if (theme === 'professional') {
1807        highlightBg = '#e3f2fd';  // Light blue
1808        highlightShadow = '0 0 20px rgba(74, 144, 226, 0.4)';
1809    } else if (theme === 'pink') {
1810        highlightBg = '#3d2030';  // Darker pink
1811        highlightShadow = '0 0 20px rgba(255, 20, 147, 0.8), 0 0 40px rgba(255, 20, 147, 0.4)';
1812    } else if (theme === 'wiki') {
1813        highlightBg = themeStyles.header_bg || '#e8e8e8';  // __background_alt__
1814        highlightShadow = '0 0 10px rgba(0, 0, 0, 0.15)';
1815    }
1816
1817
1818    // Store original styles
1819    const originalBg = eventItem.style.background;
1820    const originalShadow = eventItem.style.boxShadow;
1821
1822    // Remove previous highlights (restore their original styles)
1823    const previousHighlights = eventList.querySelectorAll('.event-highlighted');
1824    previousHighlights.forEach(el => {
1825        el.classList.remove('event-highlighted');
1826    });
1827
1828    // Add highlight class and apply theme-aware glow
1829    eventItem.classList.add('event-highlighted');
1830
1831    // Set CSS properties directly
1832    eventItem.style.setProperty('background', highlightBg, 'important');
1833    eventItem.style.setProperty('box-shadow', highlightShadow, 'important');
1834    eventItem.style.setProperty('transition', 'all 0.3s ease-in-out', 'important');
1835
1836
1837    // Scroll to event
1838    eventItem.scrollIntoView({
1839        behavior: 'smooth',
1840        block: 'nearest',
1841        inline: 'nearest'
1842    });
1843
1844    // Remove highlight after 3 seconds and restore original styles
1845    setTimeout(() => {
1846        eventItem.classList.remove('event-highlighted');
1847        eventItem.style.setProperty('background', originalBg);
1848        eventItem.style.setProperty('box-shadow', originalShadow);
1849        eventItem.style.setProperty('transition', '');
1850    }, 3000);
1851};
1852
1853// Toggle recurring event options
1854window.toggleRecurringOptions = function(calId) {
1855    const checkbox = document.getElementById('event-recurring-' + calId);
1856    const options = document.getElementById('recurring-options-' + calId);
1857
1858    if (checkbox && options) {
1859        options.style.display = checkbox.checked ? 'block' : 'none';
1860        if (checkbox.checked) {
1861            // Initialize the sub-options based on current selection
1862            updateRecurrenceOptions(calId);
1863        }
1864    }
1865};
1866
1867// Update visible recurrence options based on type (daily/weekly/monthly/yearly)
1868window.updateRecurrenceOptions = function(calId) {
1869    const typeSelect = document.getElementById('event-recurrence-type-' + calId);
1870    const weeklyOptions = document.getElementById('weekly-options-' + calId);
1871    const monthlyOptions = document.getElementById('monthly-options-' + calId);
1872
1873    if (!typeSelect) return;
1874
1875    const recurrenceType = typeSelect.value;
1876
1877    // Hide all conditional options first
1878    if (weeklyOptions) weeklyOptions.style.display = 'none';
1879    if (monthlyOptions) monthlyOptions.style.display = 'none';
1880
1881    // Show relevant options
1882    if (recurrenceType === 'weekly' && weeklyOptions) {
1883        weeklyOptions.style.display = 'block';
1884        // Auto-select today's day of week if nothing selected
1885        const checkboxes = weeklyOptions.querySelectorAll('input[type="checkbox"]');
1886        const anyChecked = Array.from(checkboxes).some(cb => cb.checked);
1887        if (!anyChecked) {
1888            const today = new Date().getDay();
1889            const todayCheckbox = weeklyOptions.querySelector('input[value="' + today + '"]');
1890            if (todayCheckbox) todayCheckbox.checked = true;
1891        }
1892    } else if (recurrenceType === 'monthly' && monthlyOptions) {
1893        monthlyOptions.style.display = 'block';
1894        // Set default day to current day of month
1895        const monthDayInput = document.getElementById('event-month-day-' + calId);
1896        if (monthDayInput && !monthDayInput.dataset.userSet) {
1897            monthDayInput.value = new Date().getDate();
1898        }
1899    }
1900};
1901
1902// Toggle between day-of-month and ordinal weekday for monthly recurrence
1903window.updateMonthlyType = function(calId) {
1904    const dayOfMonthDiv = document.getElementById('monthly-day-' + calId);
1905    const ordinalDiv = document.getElementById('monthly-ordinal-' + calId);
1906    const monthlyOptions = document.getElementById('monthly-options-' + calId);
1907
1908    if (!monthlyOptions) return;
1909
1910    const selectedRadio = monthlyOptions.querySelector('input[name="monthlyType"]:checked');
1911    if (!selectedRadio) return;
1912
1913    if (selectedRadio.value === 'dayOfMonth') {
1914        if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'flex';
1915        if (ordinalDiv) ordinalDiv.style.display = 'none';
1916    } else {
1917        if (dayOfMonthDiv) dayOfMonthDiv.style.display = 'none';
1918        if (ordinalDiv) ordinalDiv.style.display = 'block';
1919
1920        // Set defaults based on current date
1921        const now = new Date();
1922        const dayOfWeek = now.getDay();
1923        const weekOfMonth = Math.ceil(now.getDate() / 7);
1924
1925        const ordinalSelect = document.getElementById('event-ordinal-' + calId);
1926        const ordinalDaySelect = document.getElementById('event-ordinal-day-' + calId);
1927
1928        if (ordinalSelect && !ordinalSelect.dataset.userSet) {
1929            ordinalSelect.value = weekOfMonth;
1930        }
1931        if (ordinalDaySelect && !ordinalDaySelect.dataset.userSet) {
1932            ordinalDaySelect.value = dayOfWeek;
1933        }
1934    }
1935};
1936
1937// ============================================================
1938// Document-level event delegation (guarded - only attach once)
1939// These use event delegation so they work for AJAX-rebuilt content.
1940// ============================================================
1941if (!window._calendarDelegationInit) {
1942    window._calendarDelegationInit = true;
1943
1944    // Keyboard navigation for accessibility
1945    document.addEventListener('keydown', function(e) {
1946        // ESC closes dialogs, popups, tooltips, dropdowns
1947        if (e.key === 'Escape') {
1948            // Close dialogs
1949            document.querySelectorAll('.event-dialog-compact').forEach(function(d) {
1950                if (d.style.display === 'flex') d.style.display = 'none';
1951            });
1952            // Close day popups
1953            document.querySelectorAll('.day-popup').forEach(function(p) {
1954                p.style.display = 'none';
1955            });
1956            // Close custom pickers
1957            document.querySelectorAll('.time-dropdown.open, .date-dropdown.open').forEach(function(d) {
1958                d.classList.remove('open');
1959                d.innerHTML = '';
1960            });
1961            document.querySelectorAll('.custom-time-picker.open, .custom-date-picker.open').forEach(function(b) {
1962                b.classList.remove('open');
1963            });
1964            hideConflictTooltip();
1965            return;
1966        }
1967
1968        // Calendar grid navigation with arrow keys
1969        var focusedDay = document.activeElement;
1970        if (focusedDay && focusedDay.classList.contains('calendar-day')) {
1971            var calGrid = focusedDay.closest('.calendar-grid');
1972            if (!calGrid) return;
1973
1974            var days = Array.from(calGrid.querySelectorAll('.calendar-day:not(.empty)'));
1975            var currentIndex = days.indexOf(focusedDay);
1976            if (currentIndex === -1) return;
1977
1978            var newIndex = currentIndex;
1979
1980            if (e.key === 'ArrowRight') {
1981                newIndex = Math.min(currentIndex + 1, days.length - 1);
1982                e.preventDefault();
1983            } else if (e.key === 'ArrowLeft') {
1984                newIndex = Math.max(currentIndex - 1, 0);
1985                e.preventDefault();
1986            } else if (e.key === 'ArrowDown') {
1987                newIndex = Math.min(currentIndex + 7, days.length - 1);
1988                e.preventDefault();
1989            } else if (e.key === 'ArrowUp') {
1990                newIndex = Math.max(currentIndex - 7, 0);
1991                e.preventDefault();
1992            } else if (e.key === 'Enter' || e.key === ' ') {
1993                // Activate the day (click it)
1994                focusedDay.click();
1995                e.preventDefault();
1996                return;
1997            }
1998
1999            if (newIndex !== currentIndex && days[newIndex]) {
2000                days[newIndex].focus();
2001            }
2002        }
2003
2004        // Event item navigation with arrow keys
2005        var focusedEvent = document.activeElement;
2006        if (focusedEvent && focusedEvent.classList.contains('event-item')) {
2007            var eventList = focusedEvent.closest('.event-list-items, .day-popup-events');
2008            if (!eventList) return;
2009
2010            var events = Array.from(eventList.querySelectorAll('.event-item'));
2011            var currentIdx = events.indexOf(focusedEvent);
2012            if (currentIdx === -1) return;
2013
2014            if (e.key === 'ArrowDown') {
2015                var nextIdx = Math.min(currentIdx + 1, events.length - 1);
2016                events[nextIdx].focus();
2017                e.preventDefault();
2018            } else if (e.key === 'ArrowUp') {
2019                var prevIdx = Math.max(currentIdx - 1, 0);
2020                events[prevIdx].focus();
2021                e.preventDefault();
2022            } else if (e.key === 'Enter') {
2023                // Find and click the edit button
2024                var editBtn = focusedEvent.querySelector('.event-action-edit');
2025                if (editBtn) editBtn.click();
2026                e.preventDefault();
2027            } else if (e.key === 'Delete' || e.key === 'Backspace') {
2028                // Find and click the delete button
2029                var deleteBtn = focusedEvent.querySelector('.event-action-delete');
2030                if (deleteBtn) deleteBtn.click();
2031                e.preventDefault();
2032            }
2033        }
2034    });
2035
2036    // Conflict tooltip delegation (capture phase for mouseenter/leave)
2037    document.addEventListener('mouseenter', function(e) {
2038        if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) {
2039            showConflictTooltip(e.target);
2040        }
2041    }, true);
2042
2043    document.addEventListener('mouseleave', function(e) {
2044        if (e.target && e.target.classList && e.target.classList.contains('event-conflict-badge')) {
2045            hideConflictTooltip();
2046        }
2047    }, true);
2048} // end delegation guard
2049
2050// Event panel navigation
2051window.navEventPanel = function(calId, year, month, namespace) {
2052    const params = new URLSearchParams({
2053        call: 'plugin_calendar',
2054        action: 'load_month',
2055        year: year,
2056        month: month,
2057        namespace: namespace,
2058        _: new Date().getTime() // Cache buster
2059    });
2060
2061    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
2062        method: 'POST',
2063        headers: {
2064            'Content-Type': 'application/x-www-form-urlencoded',
2065            'Cache-Control': 'no-cache, no-store, must-revalidate',
2066            'Pragma': 'no-cache'
2067        },
2068        body: params.toString()
2069    })
2070    .then(r => r.json())
2071    .then(data => {
2072        if (data.success) {
2073            rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
2074        }
2075    })
2076    .catch(err => console.error('Error:', err));
2077};
2078
2079// Rebuild event panel only
2080window.rebuildEventPanel = function(calId, year, month, events, namespace) {
2081    const container = document.getElementById(calId);
2082    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
2083                       'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2084
2085    // Update month title in new compact header
2086    const monthTitle = container.querySelector('.panel-month-title');
2087    if (monthTitle) {
2088        monthTitle.textContent = monthNames[month - 1] + ' ' + year;
2089        monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
2090        monthTitle.setAttribute('title', 'Click to jump to month');
2091    }
2092
2093    // Fallback: Update old header format if exists
2094    const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker');
2095    if (oldHeader && !monthTitle) {
2096        oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events';
2097        oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
2098    }
2099
2100    // Update nav buttons
2101    let prevMonth = month - 1;
2102    let prevYear = year;
2103    if (prevMonth < 1) {
2104        prevMonth = 12;
2105        prevYear--;
2106    }
2107
2108    let nextMonth = month + 1;
2109    let nextYear = year;
2110    if (nextMonth > 12) {
2111        nextMonth = 1;
2112        nextYear++;
2113    }
2114
2115    // Update new compact nav buttons
2116    const navBtns = container.querySelectorAll('.panel-nav-btn');
2117    if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
2118    if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
2119
2120    // Fallback for old nav buttons
2121    const oldNavBtns = container.querySelectorAll('.cal-nav-btn');
2122    if (oldNavBtns.length > 0 && navBtns.length === 0) {
2123        if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
2124        if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
2125    }
2126
2127    // Update Today button (works for both old and new)
2128    const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact');
2129    if (todayBtn) {
2130        todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`);
2131    }
2132
2133    // Rebuild event list
2134    const eventList = container.querySelector('.event-list-compact');
2135    if (eventList) {
2136        eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
2137    }
2138};
2139
2140// Open add event for panel
2141window.openAddEventPanel = function(calId, namespace) {
2142    const today = new Date();
2143    const year = today.getFullYear();
2144    const month = String(today.getMonth() + 1).padStart(2, '0');
2145    const day = String(today.getDate()).padStart(2, '0');
2146    const localDate = `${year}-${month}-${day}`;
2147    openAddEvent(calId, namespace, localDate);
2148};
2149
2150// Toggle task completion
2151window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) {
2152    const params = new URLSearchParams({
2153        call: 'plugin_calendar',
2154        action: 'toggle_task',
2155        namespace: namespace,
2156        date: date,
2157        eventId: eventId,
2158        completed: completed ? '1' : '0',
2159        sectok: getSecurityToken()
2160    });
2161
2162    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
2163        method: 'POST',
2164        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
2165        body: params.toString()
2166    })
2167    .then(r => r.json())
2168    .then(data => {
2169        if (data.success) {
2170            // Announce to screen readers
2171            announceToScreenReader(completed ? 'Task marked complete' : 'Task marked incomplete');
2172
2173            const [year, month] = date.split('-').map(Number);
2174
2175            // Get the calendar's ORIGINAL namespace setting from the container
2176            const container = document.getElementById(calId);
2177            const calendarNamespace = container ? (container.dataset.namespace || '') : namespace;
2178
2179            reloadCalendarData(calId, year, month, calendarNamespace);
2180        }
2181    })
2182    .catch(err => console.error('Error toggling task:', err));
2183};
2184
2185// Make dialog draggable
2186window.makeDialogDraggable = function(calId) {
2187    const dialog = document.getElementById('dialog-content-' + calId);
2188    const handle = document.getElementById('drag-handle-' + calId);
2189
2190    if (!dialog || !handle) return;
2191
2192    // Remove any existing drag setup to prevent duplicate listeners
2193    if (handle._dragCleanup) {
2194        handle._dragCleanup();
2195    }
2196
2197    // Reset position when dialog opens
2198    dialog.style.transform = '';
2199
2200    let isDragging = false;
2201    let currentX = 0;
2202    let currentY = 0;
2203    let initialX;
2204    let initialY;
2205    let xOffset = 0;
2206    let yOffset = 0;
2207
2208    function dragStart(e) {
2209        // Only start drag if clicking on the handle itself, not buttons inside it
2210        if (e.target.tagName === 'BUTTON') return;
2211
2212        initialX = e.clientX - xOffset;
2213        initialY = e.clientY - yOffset;
2214        isDragging = true;
2215        handle.style.cursor = 'grabbing';
2216    }
2217
2218    function drag(e) {
2219        if (isDragging) {
2220            e.preventDefault();
2221            currentX = e.clientX - initialX;
2222            currentY = e.clientY - initialY;
2223            xOffset = currentX;
2224            yOffset = currentY;
2225            dialog.style.transform = `translate(${currentX}px, ${currentY}px)`;
2226        }
2227    }
2228
2229    function dragEnd(e) {
2230        if (isDragging) {
2231            initialX = currentX;
2232            initialY = currentY;
2233            isDragging = false;
2234            handle.style.cursor = 'move';
2235        }
2236    }
2237
2238    // Add listeners
2239    handle.addEventListener('mousedown', dragStart);
2240    document.addEventListener('mousemove', drag);
2241    document.addEventListener('mouseup', dragEnd);
2242
2243    // Store cleanup function to remove listeners later
2244    handle._dragCleanup = function() {
2245        handle.removeEventListener('mousedown', dragStart);
2246        document.removeEventListener('mousemove', drag);
2247        document.removeEventListener('mouseup', dragEnd);
2248    };
2249};
2250
2251// Toggle expand/collapse for past events
2252window.togglePastEventExpand = function(element) {
2253    // Stop propagation to prevent any parent click handlers
2254    event.stopPropagation();
2255
2256    const meta = element.querySelector(".event-meta-compact");
2257    const desc = element.querySelector(".event-desc-compact");
2258
2259    // Toggle visibility
2260    if (meta.style.display === "none") {
2261        // Expand
2262        meta.style.display = "block";
2263        if (desc) desc.style.display = "block";
2264        element.classList.add("event-past-expanded");
2265    } else {
2266        // Collapse
2267        meta.style.display = "none";
2268        if (desc) desc.style.display = "none";
2269        element.classList.remove("event-past-expanded");
2270    }
2271};
2272
2273// Filter calendar by namespace when clicking namespace badge (guarded)
2274if (!window._calendarClickDelegationInit) {
2275    window._calendarClickDelegationInit = true;
2276    document.addEventListener('click', function(e) {
2277    if (e.target.classList.contains('event-namespace-badge')) {
2278        const namespace = e.target.textContent;
2279        const calendar = e.target.closest('.calendar-compact-container');
2280
2281        if (!calendar) return;
2282
2283        const calId = calendar.id;
2284
2285        // Use AJAX reload to filter both calendar grid and event list
2286        filterCalendarByNamespace(calId, namespace);
2287    }
2288    });
2289} // end click delegation guard
2290
2291// Update the displayed filtered namespace in event list header
2292// Legacy badge removed - namespace filtering still works but badge no longer shown
2293window.updateFilteredNamespaceDisplay = function(calId, namespace) {
2294    const calendar = document.getElementById(calId);
2295    if (!calendar) return;
2296
2297    const headerContent = calendar.querySelector('.event-list-header-content');
2298    if (!headerContent) return;
2299
2300    // Remove any existing filter badge (cleanup)
2301    let filterBadge = headerContent.querySelector('.namespace-filter-badge');
2302    if (filterBadge) {
2303        filterBadge.remove();
2304    }
2305};
2306
2307// Clear namespace filter
2308window.clearNamespaceFilter = function(calId) {
2309
2310    const container = document.getElementById(calId);
2311    if (!container) {
2312        console.error('Calendar container not found:', calId);
2313        return;
2314    }
2315
2316    // Immediately hide/remove the filter badge
2317    const filterBadge = container.querySelector('.calendar-namespace-filter');
2318    if (filterBadge) {
2319        filterBadge.style.display = 'none';
2320        filterBadge.remove();
2321    }
2322
2323    // Get current year and month
2324    const year = parseInt(container.dataset.year) || new Date().getFullYear();
2325    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
2326
2327    // Get original namespace (what the calendar was initialized with)
2328    const originalNamespace = container.dataset.originalNamespace || '';
2329
2330    // Also check for sidebar widget
2331    const sidebarContainer = document.getElementById('sidebar-widget-' + calId);
2332    if (sidebarContainer) {
2333        // For sidebar widget, just reload the page without namespace filter
2334        // Remove the namespace from the URL and reload
2335        const url = new URL(window.location.href);
2336        url.searchParams.delete('namespace');
2337        window.location.href = url.toString();
2338        return;
2339    }
2340
2341    // For regular calendar, reload calendar with original namespace
2342    navCalendar(calId, year, month, originalNamespace);
2343};
2344
2345window.clearNamespaceFilterPanel = function(calId) {
2346
2347    const container = document.getElementById(calId);
2348    if (!container) {
2349        console.error('Event panel container not found:', calId);
2350        return;
2351    }
2352
2353    // Get current year and month from URL params or container
2354    const year = parseInt(container.dataset.year) || new Date().getFullYear();
2355    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
2356
2357    // Get original namespace (what the panel was initialized with)
2358    const originalNamespace = container.dataset.originalNamespace || '';
2359
2360
2361    // Reload event panel with original namespace
2362    navEventPanel(calId, year, month, originalNamespace);
2363};
2364
2365// Color picker functions
2366window.updateCustomColorPicker = function(calId) {
2367    const select = document.getElementById('event-color-' + calId);
2368    const picker = document.getElementById('event-color-custom-' + calId);
2369
2370    if (select.value === 'custom') {
2371        // Show color picker
2372        picker.style.display = 'inline-block';
2373        picker.click(); // Open color picker
2374    } else {
2375        // Hide color picker and sync value
2376        picker.style.display = 'none';
2377        picker.value = select.value;
2378    }
2379};
2380
2381function updateColorFromPicker(calId) {
2382    const select = document.getElementById('event-color-' + calId);
2383    const picker = document.getElementById('event-color-custom-' + calId);
2384
2385    // Set select to custom and update its underlying value
2386    select.value = 'custom';
2387    // Store the actual color value in a data attribute
2388    select.dataset.customColor = picker.value;
2389}
2390
2391// Toggle past events visibility
2392window.togglePastEvents = function(calId) {
2393    const content = document.getElementById('past-events-' + calId);
2394    const arrow = document.getElementById('past-arrow-' + calId);
2395
2396    if (!content || !arrow) {
2397        console.error('Past events elements not found for:', calId);
2398        return;
2399    }
2400
2401    // Check computed style instead of inline style
2402    const isHidden = window.getComputedStyle(content).display === 'none';
2403
2404    if (isHidden) {
2405        content.style.display = 'block';
2406        arrow.textContent = '▼';
2407    } else {
2408        content.style.display = 'none';
2409        arrow.textContent = '▶';
2410    }
2411};
2412
2413// Fuzzy match scoring function
2414window.fuzzyMatch = function(pattern, str) {
2415    pattern = pattern.toLowerCase();
2416    str = str.toLowerCase();
2417
2418    let patternIdx = 0;
2419    let score = 0;
2420    let consecutiveMatches = 0;
2421
2422    for (let i = 0; i < str.length; i++) {
2423        if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) {
2424            score += 1 + consecutiveMatches;
2425            consecutiveMatches++;
2426            patternIdx++;
2427        } else {
2428            consecutiveMatches = 0;
2429        }
2430    }
2431
2432    // Return null if not all characters matched
2433    if (patternIdx !== pattern.length) {
2434        return null;
2435    }
2436
2437    // Bonus for exact match
2438    if (str === pattern) {
2439        score += 100;
2440    }
2441
2442    // Bonus for starts with
2443    if (str.startsWith(pattern)) {
2444        score += 50;
2445    }
2446
2447    return score;
2448};
2449
2450// Initialize namespace search for a calendar
2451window.initNamespaceSearch = function(calId) {
2452    const searchInput = document.getElementById('event-namespace-search-' + calId);
2453    const hiddenInput = document.getElementById('event-namespace-' + calId);
2454    const dropdown = document.getElementById('event-namespace-dropdown-' + calId);
2455    const dataElement = document.getElementById('namespaces-data-' + calId);
2456
2457    if (!searchInput || !hiddenInput || !dropdown || !dataElement) {
2458        return; // Elements not found
2459    }
2460
2461    // PERFORMANCE FIX: Prevent re-binding event listeners on each dialog open
2462    if (searchInput.dataset.initialized === 'true') {
2463        return;
2464    }
2465    searchInput.dataset.initialized = 'true';
2466
2467    let namespaces = [];
2468    try {
2469        namespaces = JSON.parse(dataElement.textContent);
2470    } catch (e) {
2471        console.error('Failed to parse namespaces data:', e);
2472        return;
2473    }
2474
2475    let selectedIndex = -1;
2476
2477    // Filter and show dropdown
2478    function filterNamespaces(query) {
2479        if (!query || query.trim() === '') {
2480            // Show all namespaces when empty
2481            hiddenInput.value = '';
2482            const results = namespaces.slice(0, 20); // Limit to 20
2483            showDropdown(results);
2484            return;
2485        }
2486
2487        // Fuzzy match and score
2488        const matches = [];
2489        for (let i = 0; i < namespaces.length; i++) {
2490            const score = fuzzyMatch(query, namespaces[i]);
2491            if (score !== null) {
2492                matches.push({ namespace: namespaces[i], score: score });
2493            }
2494        }
2495
2496        // Sort by score (descending)
2497        matches.sort((a, b) => b.score - a.score);
2498
2499        // Take top 20 results
2500        const results = matches.slice(0, 20).map(m => m.namespace);
2501        showDropdown(results);
2502    }
2503
2504    function showDropdown(results) {
2505        dropdown.innerHTML = '';
2506        selectedIndex = -1;
2507
2508        if (results.length === 0) {
2509            dropdown.style.display = 'none';
2510            return;
2511        }
2512
2513        // Add (default) option
2514        const defaultOption = document.createElement('div');
2515        defaultOption.className = 'namespace-option';
2516        defaultOption.textContent = '(default)';
2517        defaultOption.dataset.value = '';
2518        dropdown.appendChild(defaultOption);
2519
2520        results.forEach(ns => {
2521            const option = document.createElement('div');
2522            option.className = 'namespace-option';
2523            option.textContent = ns;
2524            option.dataset.value = ns;
2525            dropdown.appendChild(option);
2526        });
2527
2528        dropdown.style.display = 'block';
2529    }
2530
2531    function hideDropdown() {
2532        dropdown.style.display = 'none';
2533        selectedIndex = -1;
2534    }
2535
2536    function selectOption(namespace) {
2537        hiddenInput.value = namespace;
2538        searchInput.value = namespace || '(default)';
2539        hideDropdown();
2540    }
2541
2542    // Event listeners - only bound once now
2543    searchInput.addEventListener('input', function(e) {
2544        filterNamespaces(e.target.value);
2545    });
2546
2547    searchInput.addEventListener('focus', function(e) {
2548        filterNamespaces(e.target.value);
2549    });
2550
2551    searchInput.addEventListener('blur', function(e) {
2552        // Delay to allow click on dropdown
2553        setTimeout(hideDropdown, 200);
2554    });
2555
2556    searchInput.addEventListener('keydown', function(e) {
2557        const options = dropdown.querySelectorAll('.namespace-option');
2558
2559        if (e.key === 'ArrowDown') {
2560            e.preventDefault();
2561            selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
2562            updateSelection(options);
2563        } else if (e.key === 'ArrowUp') {
2564            e.preventDefault();
2565            selectedIndex = Math.max(selectedIndex - 1, -1);
2566            updateSelection(options);
2567        } else if (e.key === 'Enter') {
2568            e.preventDefault();
2569            if (selectedIndex >= 0 && options[selectedIndex]) {
2570                selectOption(options[selectedIndex].dataset.value);
2571            }
2572        } else if (e.key === 'Escape') {
2573            hideDropdown();
2574        }
2575    });
2576
2577    function updateSelection(options) {
2578        options.forEach((opt, idx) => {
2579            if (idx === selectedIndex) {
2580                opt.classList.add('selected');
2581                opt.scrollIntoView({ block: 'nearest' });
2582            } else {
2583                opt.classList.remove('selected');
2584            }
2585        });
2586    }
2587
2588    // Click on dropdown option
2589    dropdown.addEventListener('mousedown', function(e) {
2590        if (e.target.classList.contains('namespace-option')) {
2591            selectOption(e.target.dataset.value);
2592        }
2593    });
2594};
2595
2596// Legacy function - kept for compatibility, now handled by custom pickers
2597window.updateEndTimeOptions = function(calId) {
2598    updateEndTimeButtonState(calId);
2599};
2600
2601// ============================================================================
2602// CUSTOM TIME PICKER - Fast, lightweight time selection
2603// ============================================================================
2604
2605// Time data - generated once, reused for all pickers
2606window._calendarTimeData = null;
2607window.getTimeData = function() {
2608    if (window._calendarTimeData) return window._calendarTimeData;
2609
2610    const periods = [
2611        { name: 'Morning', hours: [6, 7, 8, 9, 10, 11] },
2612        { name: 'Afternoon', hours: [12, 13, 14, 15, 16, 17] },
2613        { name: 'Evening', hours: [18, 19, 20, 21, 22, 23] },
2614        { name: 'Night', hours: [0, 1, 2, 3, 4, 5] }
2615    ];
2616
2617    const data = [];
2618    periods.forEach(period => {
2619        const times = [];
2620        period.hours.forEach(hour => {
2621            for (let minute = 0; minute < 60; minute += 15) {
2622                const value = String(hour).padStart(2, '0') + ':' + String(minute).padStart(2, '0');
2623                const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour);
2624                const ampm = hour < 12 ? 'AM' : 'PM';
2625                const display = displayHour + ':' + String(minute).padStart(2, '0') + ' ' + ampm;
2626                const minutes = hour * 60 + minute;
2627                times.push({ value, display, minutes });
2628            }
2629        });
2630        data.push({ name: period.name, times });
2631    });
2632
2633    window._calendarTimeData = data;
2634    return data;
2635};
2636
2637// Format time value to display string
2638window.formatTimeDisplay = function(value) {
2639    if (!value) return '';
2640    const [hour, minute] = value.split(':').map(Number);
2641    const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour);
2642    const ampm = hour < 12 ? 'AM' : 'PM';
2643    return displayHour + ':' + String(minute).padStart(2, '0') + ' ' + ampm;
2644};
2645
2646// Build dropdown HTML - called only when opening
2647window.buildTimeDropdown = function(calId, isEndTime, startTimeValue, isMultiDay) {
2648    const data = getTimeData();
2649    let html = '';
2650
2651    // Calculate start time minutes for filtering end time options
2652    let startMinutes = -1;
2653    if (isEndTime && startTimeValue && !isMultiDay) {
2654        const [h, m] = startTimeValue.split(':').map(Number);
2655        startMinutes = h * 60 + m;
2656    }
2657
2658    // Add "All day" / "Same as start" option
2659    const defaultText = isEndTime ? 'Same as start' : 'All day';
2660    html += '<div class="time-option" data-value="">' + defaultText + '</div>';
2661
2662    data.forEach(period => {
2663        html += '<div class="time-dropdown-section">';
2664        html += '<div class="time-dropdown-header">' + period.name + '</div>';
2665        period.times.forEach(time => {
2666            const disabled = (isEndTime && !isMultiDay && startMinutes >= 0 && time.minutes <= startMinutes);
2667            const disabledClass = disabled ? ' disabled' : '';
2668            html += '<div class="time-option' + disabledClass + '" data-value="' + time.value + '" data-minutes="' + time.minutes + '">' + time.display + '</div>';
2669        });
2670        html += '</div>';
2671    });
2672
2673    return html;
2674};
2675
2676// Open time dropdown
2677window.openTimeDropdown = function(calId, isEndTime) {
2678    const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId;
2679    const dropdownId = isEndTime ? 'end-time-dropdown-' + calId : 'time-dropdown-' + calId;
2680    const btn = document.getElementById(btnId);
2681    const dropdown = document.getElementById(dropdownId);
2682
2683    if (!btn || !dropdown) return;
2684
2685    // Close any other open dropdowns first
2686    document.querySelectorAll('.time-dropdown.open').forEach(d => {
2687        if (d.id !== dropdownId) {
2688            d.classList.remove('open');
2689            d.innerHTML = '';
2690        }
2691    });
2692    document.querySelectorAll('.custom-time-picker.open').forEach(b => {
2693        if (b.id !== btnId) b.classList.remove('open');
2694    });
2695
2696    // Toggle this dropdown
2697    if (dropdown.classList.contains('open')) {
2698        dropdown.classList.remove('open');
2699        btn.classList.remove('open');
2700        dropdown.innerHTML = '';
2701        return;
2702    }
2703
2704    // Get current state
2705    const startTimeInput = document.getElementById('event-time-' + calId);
2706    const startDateInput = document.getElementById('event-date-' + calId);
2707    const endDateInput = document.getElementById('event-end-date-' + calId);
2708
2709    const startTime = startTimeInput ? startTimeInput.value : '';
2710    const startDate = startDateInput ? startDateInput.value : '';
2711    const endDate = endDateInput ? endDateInput.value : '';
2712    const isMultiDay = endDate && endDate !== startDate;
2713
2714    // Build and show dropdown
2715    dropdown.innerHTML = buildTimeDropdown(calId, isEndTime, startTime, isMultiDay);
2716    dropdown.classList.add('open');
2717    btn.classList.add('open');
2718
2719    // Scroll to appropriate option
2720    const currentValue = isEndTime ?
2721        document.getElementById('event-end-time-' + calId).value :
2722        document.getElementById('event-time-' + calId).value;
2723
2724    if (currentValue) {
2725        // Scroll to selected option
2726        const selected = dropdown.querySelector('[data-value="' + currentValue + '"]');
2727        if (selected) {
2728            selected.classList.add('selected');
2729            selected.scrollIntoView({ block: 'center', behavior: 'instant' });
2730        }
2731    } else if (isEndTime && startTime) {
2732        // For end time with no selection, scroll to first available option after start time
2733        const firstAvailable = dropdown.querySelector('.time-option:not(.disabled):not([data-value=""])');
2734        if (firstAvailable) {
2735            firstAvailable.scrollIntoView({ block: 'center', behavior: 'instant' });
2736        }
2737    }
2738};
2739
2740// Select time option
2741window.selectTimeOption = function(calId, isEndTime, value) {
2742    const inputId = isEndTime ? 'event-end-time-' + calId : 'event-time-' + calId;
2743    const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId;
2744    const dropdownId = isEndTime ? 'end-time-dropdown-' + calId : 'time-dropdown-' + calId;
2745
2746    const input = document.getElementById(inputId);
2747    const btn = document.getElementById(btnId);
2748    const dropdown = document.getElementById(dropdownId);
2749
2750    if (input) {
2751        input.value = value;
2752    }
2753
2754    if (btn) {
2755        const display = btn.querySelector('.time-display');
2756        if (display) {
2757            if (value) {
2758                display.textContent = formatTimeDisplay(value);
2759            } else {
2760                display.textContent = isEndTime ? 'Same as start' : 'All day';
2761            }
2762        }
2763        btn.classList.remove('open');
2764    }
2765
2766    if (dropdown) {
2767        dropdown.classList.remove('open');
2768        dropdown.innerHTML = '';
2769    }
2770
2771    // If start time changed, update end time button state
2772    if (!isEndTime) {
2773        updateEndTimeButtonState(calId);
2774    }
2775};
2776
2777// Update end time button enabled/disabled state
2778window.updateEndTimeButtonState = function(calId) {
2779    const startTimeInput = document.getElementById('event-time-' + calId);
2780    const endTimeBtn = document.getElementById('end-time-picker-btn-' + calId);
2781    const endTimeInput = document.getElementById('event-end-time-' + calId);
2782
2783    if (!startTimeInput || !endTimeBtn) return;
2784
2785    const startTime = startTimeInput.value;
2786
2787    if (!startTime) {
2788        // All day - disable end time
2789        endTimeBtn.disabled = true;
2790        if (endTimeInput) endTimeInput.value = '';
2791        const display = endTimeBtn.querySelector('.time-display');
2792        if (display) display.textContent = 'Same as start';
2793    } else {
2794        endTimeBtn.disabled = false;
2795    }
2796};
2797
2798// Initialize custom time pickers for a dialog
2799window.initCustomTimePickers = function(calId) {
2800    const startBtn = document.getElementById('time-picker-btn-' + calId);
2801    const endBtn = document.getElementById('end-time-picker-btn-' + calId);
2802    const startDropdown = document.getElementById('time-dropdown-' + calId);
2803    const endDropdown = document.getElementById('end-time-dropdown-' + calId);
2804
2805    // Prevent re-initialization
2806    if (startBtn && startBtn.dataset.initialized) return;
2807
2808    if (startBtn) {
2809        startBtn.dataset.initialized = 'true';
2810        startBtn.addEventListener('click', function(e) {
2811            e.preventDefault();
2812            e.stopPropagation();
2813            openTimeDropdown(calId, false);
2814        });
2815    }
2816
2817    if (endBtn) {
2818        endBtn.addEventListener('click', function(e) {
2819            e.preventDefault();
2820            e.stopPropagation();
2821            if (!endBtn.disabled) {
2822                openTimeDropdown(calId, true);
2823            }
2824        });
2825    }
2826
2827    // Handle clicks on time options
2828    if (startDropdown) {
2829        startDropdown.addEventListener('click', function(e) {
2830            const option = e.target.closest('.time-option');
2831            if (option && !option.classList.contains('disabled')) {
2832                e.stopPropagation();
2833                selectTimeOption(calId, false, option.dataset.value);
2834            }
2835        });
2836    }
2837
2838    if (endDropdown) {
2839        endDropdown.addEventListener('click', function(e) {
2840            const option = e.target.closest('.time-option');
2841            if (option && !option.classList.contains('disabled')) {
2842                e.stopPropagation();
2843                selectTimeOption(calId, true, option.dataset.value);
2844            }
2845        });
2846    }
2847
2848    // Handle date changes - update end time options when dates change
2849    const startDateInput = document.getElementById('event-date-' + calId);
2850    const endDateInput = document.getElementById('event-end-date-' + calId);
2851
2852    if (startDateInput && !startDateInput.dataset.initialized) {
2853        startDateInput.dataset.initialized = 'true';
2854        startDateInput.addEventListener('change', function() {
2855            // Just close any open dropdowns - they'll rebuild with correct state when reopened
2856            const dropdown = document.getElementById('end-time-dropdown-' + calId);
2857            if (dropdown && dropdown.classList.contains('open')) {
2858                dropdown.classList.remove('open');
2859                dropdown.innerHTML = '';
2860            }
2861        });
2862    }
2863
2864    if (endDateInput && !endDateInput.dataset.initialized) {
2865        endDateInput.dataset.initialized = 'true';
2866        endDateInput.addEventListener('change', function() {
2867            const dropdown = document.getElementById('end-time-dropdown-' + calId);
2868            if (dropdown && dropdown.classList.contains('open')) {
2869                dropdown.classList.remove('open');
2870                dropdown.innerHTML = '';
2871            }
2872        });
2873    }
2874};
2875
2876// Close dropdowns when clicking outside
2877if (!window._calendarDropdownCloseInit) {
2878    window._calendarDropdownCloseInit = true;
2879    document.addEventListener('click', function(e) {
2880        // Don't close if clicking inside a picker button or dropdown
2881        if (e.target.closest('.custom-time-picker') || e.target.closest('.time-dropdown') ||
2882            e.target.closest('.custom-date-picker') || e.target.closest('.date-dropdown')) {
2883            return;
2884        }
2885
2886        // Close all open time dropdowns
2887        document.querySelectorAll('.time-dropdown.open').forEach(d => {
2888            d.classList.remove('open');
2889            d.innerHTML = '';
2890        });
2891        document.querySelectorAll('.custom-time-picker.open').forEach(b => {
2892            b.classList.remove('open');
2893        });
2894
2895        // Close all open date dropdowns
2896        document.querySelectorAll('.date-dropdown.open').forEach(d => {
2897            d.classList.remove('open');
2898            d.innerHTML = '';
2899        });
2900        document.querySelectorAll('.custom-date-picker.open').forEach(b => {
2901            b.classList.remove('open');
2902        });
2903    });
2904}
2905
2906// Set time picker value programmatically (for edit mode)
2907window.setTimePicker = function(calId, isEndTime, value) {
2908    const inputId = isEndTime ? 'event-end-time-' + calId : 'event-time-' + calId;
2909    const btnId = isEndTime ? 'end-time-picker-btn-' + calId : 'time-picker-btn-' + calId;
2910
2911    const input = document.getElementById(inputId);
2912    const btn = document.getElementById(btnId);
2913
2914    if (input) {
2915        input.value = value || '';
2916    }
2917
2918    if (btn) {
2919        const display = btn.querySelector('.time-display');
2920        if (display) {
2921            if (value) {
2922                display.textContent = formatTimeDisplay(value);
2923            } else {
2924                display.textContent = isEndTime ? 'Same as start' : 'All day';
2925            }
2926        }
2927
2928        // Update disabled state for end time
2929        if (isEndTime) {
2930            const startTimeInput = document.getElementById('event-time-' + calId);
2931            btn.disabled = !startTimeInput || !startTimeInput.value;
2932        }
2933    }
2934};
2935
2936// ============================================================================
2937// CUSTOM DATE PICKER - Fast, lightweight date selection
2938// ============================================================================
2939
2940// Format date for display
2941window.formatDateDisplay = function(dateStr) {
2942    if (!dateStr) return '';
2943    const date = new Date(dateStr + 'T00:00:00');
2944    return date.toLocaleDateString('en-US', {
2945        weekday: 'short',
2946        month: 'short',
2947        day: 'numeric',
2948        year: 'numeric'
2949    });
2950};
2951
2952// Build date picker calendar HTML
2953window.buildDateCalendar = function(calId, isEndDate, year, month, selectedDate, minDate) {
2954    const today = new Date();
2955    today.setHours(0, 0, 0, 0);
2956
2957    const firstDay = new Date(year, month, 1);
2958    const lastDay = new Date(year, month + 1, 0);
2959    const startDayOfWeek = firstDay.getDay();
2960    const daysInMonth = lastDay.getDate();
2961
2962    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
2963                        'July', 'August', 'September', 'October', 'November', 'December'];
2964
2965    let html = '<div class="date-picker-calendar">';
2966
2967    // Header with navigation
2968    html += '<div class="date-picker-header">';
2969    html += '<button type="button" class="date-picker-nav" data-action="prev">◀</button>';
2970    html += '<span class="date-picker-title">' + monthNames[month] + ' ' + year + '</span>';
2971    html += '<button type="button" class="date-picker-nav" data-action="next">▶</button>';
2972    html += '</div>';
2973
2974    // Weekday headers
2975    html += '<div class="date-picker-weekdays">';
2976    ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].forEach(d => {
2977        html += '<div class="date-picker-weekday">' + d + '</div>';
2978    });
2979    html += '</div>';
2980
2981    // Days grid
2982    html += '<div class="date-picker-days">';
2983
2984    // Previous month days
2985    const prevMonth = new Date(year, month, 0);
2986    const prevMonthDays = prevMonth.getDate();
2987    for (let i = startDayOfWeek - 1; i >= 0; i--) {
2988        const day = prevMonthDays - i;
2989        const dateStr = formatDateValue(year, month - 1, day);
2990        html += '<button type="button" class="date-picker-day other-month" data-date="' + dateStr + '">' + day + '</button>';
2991    }
2992
2993    // Current month days
2994    for (let day = 1; day <= daysInMonth; day++) {
2995        const dateStr = formatDateValue(year, month, day);
2996        const dateObj = new Date(year, month, day);
2997        dateObj.setHours(0, 0, 0, 0);
2998
2999        let classes = 'date-picker-day';
3000        if (dateObj.getTime() === today.getTime()) classes += ' today';
3001        if (dateStr === selectedDate) classes += ' selected';
3002
3003        // For end date, disable dates before start date
3004        if (isEndDate && minDate) {
3005            const minDateObj = new Date(minDate + 'T00:00:00');
3006            if (dateObj < minDateObj) classes += ' disabled';
3007        }
3008
3009        html += '<button type="button" class="' + classes + '" data-date="' + dateStr + '">' + day + '</button>';
3010    }
3011
3012    // Next month days to fill grid
3013    const totalCells = startDayOfWeek + daysInMonth;
3014    const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7);
3015    for (let i = 1; i <= remainingCells; i++) {
3016        const dateStr = formatDateValue(year, month + 1, i);
3017        html += '<button type="button" class="date-picker-day other-month" data-date="' + dateStr + '">' + i + '</button>';
3018    }
3019
3020    html += '</div>';
3021
3022    // Clear button for end date
3023    if (isEndDate) {
3024        html += '<button type="button" class="date-picker-clear" data-action="clear">Clear End Date</button>';
3025    }
3026
3027    html += '</div>';
3028    return html;
3029};
3030
3031// Format date value as YYYY-MM-DD
3032window.formatDateValue = function(year, month, day) {
3033    // Handle month overflow
3034    const date = new Date(year, month, day);
3035    const y = date.getFullYear();
3036    const m = String(date.getMonth() + 1).padStart(2, '0');
3037    const d = String(date.getDate()).padStart(2, '0');
3038    return y + '-' + m + '-' + d;
3039};
3040
3041// Open date dropdown
3042window.openDateDropdown = function(calId, isEndDate) {
3043    const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId;
3044    const dropdownId = isEndDate ? 'end-date-dropdown-' + calId : 'date-dropdown-' + calId;
3045    const btn = document.getElementById(btnId);
3046    const dropdown = document.getElementById(dropdownId);
3047
3048    if (!btn || !dropdown) return;
3049
3050    // Close any other open dropdowns first
3051    document.querySelectorAll('.date-dropdown.open, .time-dropdown.open').forEach(d => {
3052        if (d.id !== dropdownId) {
3053            d.classList.remove('open');
3054            d.innerHTML = '';
3055        }
3056    });
3057    document.querySelectorAll('.custom-date-picker.open, .custom-time-picker.open').forEach(b => {
3058        if (b.id !== btnId) b.classList.remove('open');
3059    });
3060
3061    // Toggle this dropdown
3062    if (dropdown.classList.contains('open')) {
3063        dropdown.classList.remove('open');
3064        btn.classList.remove('open');
3065        dropdown.innerHTML = '';
3066        return;
3067    }
3068
3069    // Get current value and min date
3070    const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId;
3071    const input = document.getElementById(inputId);
3072    const selectedDate = input ? input.value : '';
3073
3074    let minDate = null;
3075    if (isEndDate) {
3076        const startInput = document.getElementById('event-date-' + calId);
3077        minDate = startInput ? startInput.value : null;
3078    }
3079
3080    // Determine which month to show
3081    let year, month;
3082    if (selectedDate) {
3083        // If there's a selected date, show that month
3084        const d = new Date(selectedDate + 'T00:00:00');
3085        year = d.getFullYear();
3086        month = d.getMonth();
3087    } else if (isEndDate && minDate) {
3088        // For end date with no value, start on the start date's month
3089        const d = new Date(minDate + 'T00:00:00');
3090        year = d.getFullYear();
3091        month = d.getMonth();
3092    } else {
3093        // Fallback to current month
3094        const now = new Date();
3095        year = now.getFullYear();
3096        month = now.getMonth();
3097    }
3098
3099    // Store current view state
3100    dropdown.dataset.year = year;
3101    dropdown.dataset.month = month;
3102    dropdown.dataset.isEnd = isEndDate ? '1' : '0';
3103    dropdown.dataset.calId = calId;
3104
3105    // Build and show
3106    dropdown.innerHTML = buildDateCalendar(calId, isEndDate, year, month, selectedDate, minDate);
3107    dropdown.classList.add('open');
3108    btn.classList.add('open');
3109};
3110
3111// Select date
3112window.selectDate = function(calId, isEndDate, dateStr) {
3113    const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId;
3114    const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId;
3115    const dropdownId = isEndDate ? 'end-date-dropdown-' + calId : 'date-dropdown-' + calId;
3116
3117    const input = document.getElementById(inputId);
3118    const btn = document.getElementById(btnId);
3119    const dropdown = document.getElementById(dropdownId);
3120
3121    if (input) {
3122        input.value = dateStr || '';
3123    }
3124
3125    if (btn) {
3126        const display = btn.querySelector('.date-display');
3127        if (display) {
3128            display.textContent = dateStr ? formatDateDisplay(dateStr) : (isEndDate ? 'Optional' : 'Select date');
3129        }
3130        btn.classList.remove('open');
3131    }
3132
3133    if (dropdown) {
3134        dropdown.classList.remove('open');
3135        dropdown.innerHTML = '';
3136    }
3137};
3138
3139// Navigate date picker month
3140window.navigateDatePicker = function(dropdown, direction) {
3141    let year = parseInt(dropdown.dataset.year);
3142    let month = parseInt(dropdown.dataset.month);
3143    const isEndDate = dropdown.dataset.isEnd === '1';
3144    const calId = dropdown.dataset.calId;
3145
3146    month += direction;
3147    if (month < 0) { month = 11; year--; }
3148    if (month > 11) { month = 0; year++; }
3149
3150    dropdown.dataset.year = year;
3151    dropdown.dataset.month = month;
3152
3153    const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId;
3154    const input = document.getElementById(inputId);
3155    const selectedDate = input ? input.value : '';
3156
3157    let minDate = null;
3158    if (isEndDate) {
3159        const startInput = document.getElementById('event-date-' + calId);
3160        minDate = startInput ? startInput.value : null;
3161    }
3162
3163    dropdown.innerHTML = buildDateCalendar(calId, isEndDate, year, month, selectedDate, minDate);
3164};
3165
3166// Initialize custom date pickers for a dialog
3167window.initCustomDatePickers = function(calId) {
3168    const startBtn = document.getElementById('date-picker-btn-' + calId);
3169    const endBtn = document.getElementById('end-date-picker-btn-' + calId);
3170    const startDropdown = document.getElementById('date-dropdown-' + calId);
3171    const endDropdown = document.getElementById('end-date-dropdown-' + calId);
3172
3173    // Prevent re-initialization
3174    if (startBtn && startBtn.dataset.initialized) return;
3175
3176    if (startBtn) {
3177        startBtn.dataset.initialized = 'true';
3178        startBtn.addEventListener('click', function(e) {
3179            e.preventDefault();
3180            e.stopPropagation();
3181            openDateDropdown(calId, false);
3182        });
3183    }
3184
3185    if (endBtn) {
3186        endBtn.addEventListener('click', function(e) {
3187            e.preventDefault();
3188            e.stopPropagation();
3189            openDateDropdown(calId, true);
3190        });
3191    }
3192
3193    // Handle clicks inside date dropdowns
3194    [startDropdown, endDropdown].forEach((dropdown, idx) => {
3195        if (!dropdown) return;
3196        const isEnd = idx === 1;
3197
3198        dropdown.addEventListener('click', function(e) {
3199            e.stopPropagation();
3200
3201            const nav = e.target.closest('.date-picker-nav');
3202            if (nav) {
3203                const direction = nav.dataset.action === 'prev' ? -1 : 1;
3204                navigateDatePicker(dropdown, direction);
3205                return;
3206            }
3207
3208            const clear = e.target.closest('.date-picker-clear');
3209            if (clear) {
3210                selectDate(calId, true, '');
3211                return;
3212            }
3213
3214            const day = e.target.closest('.date-picker-day');
3215            if (day && !day.classList.contains('disabled')) {
3216                selectDate(calId, isEnd, day.dataset.date);
3217            }
3218        });
3219    });
3220};
3221
3222// Set date picker value programmatically
3223window.setDatePicker = function(calId, isEndDate, value) {
3224    const inputId = isEndDate ? 'event-end-date-' + calId : 'event-date-' + calId;
3225    const btnId = isEndDate ? 'end-date-picker-btn-' + calId : 'date-picker-btn-' + calId;
3226
3227    const input = document.getElementById(inputId);
3228    const btn = document.getElementById(btnId);
3229
3230    if (input) {
3231        input.value = value || '';
3232    }
3233
3234    if (btn) {
3235        const display = btn.querySelector('.date-display');
3236        if (display) {
3237            display.textContent = value ? formatDateDisplay(value) : (isEndDate ? 'Optional' : 'Select date');
3238        }
3239    }
3240};
3241
3242// Check for time conflicts between events on the same date
3243window.checkTimeConflicts = function(events, currentEventId) {
3244    const conflicts = [];
3245
3246    // Group events by date
3247    const eventsByDate = {};
3248    for (const [date, dateEvents] of Object.entries(events)) {
3249        if (!Array.isArray(dateEvents)) continue;
3250
3251        dateEvents.forEach(evt => {
3252            if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event
3253
3254            if (!eventsByDate[date]) eventsByDate[date] = [];
3255            eventsByDate[date].push(evt);
3256        });
3257    }
3258
3259    // Check for overlaps on each date
3260    for (const [date, dateEvents] of Object.entries(eventsByDate)) {
3261        for (let i = 0; i < dateEvents.length; i++) {
3262            for (let j = i + 1; j < dateEvents.length; j++) {
3263                const evt1 = dateEvents[i];
3264                const evt2 = dateEvents[j];
3265
3266                if (eventsOverlap(evt1, evt2)) {
3267                    // Mark both events as conflicting
3268                    if (!evt1.hasConflict) evt1.hasConflict = true;
3269                    if (!evt2.hasConflict) evt2.hasConflict = true;
3270
3271                    // Store conflict info
3272                    if (!evt1.conflictsWith) evt1.conflictsWith = [];
3273                    if (!evt2.conflictsWith) evt2.conflictsWith = [];
3274
3275                    evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime});
3276                    evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime});
3277                }
3278            }
3279        }
3280    }
3281
3282    return events;
3283};
3284
3285// Check if two events overlap in time
3286function eventsOverlap(evt1, evt2) {
3287    if (!evt1.time || !evt2.time) return false; // All-day events don't conflict
3288
3289    const start1 = evt1.time;
3290    const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start
3291
3292    const start2 = evt2.time;
3293    const end2 = evt2.endTime || evt2.time;
3294
3295    // Convert to minutes for easier comparison
3296    const start1Mins = timeToMinutes(start1);
3297    const end1Mins = timeToMinutes(end1);
3298    const start2Mins = timeToMinutes(start2);
3299    const end2Mins = timeToMinutes(end2);
3300
3301    // Check for overlap
3302    // Events overlap if: start1 < end2 AND start2 < end1
3303    return start1Mins < end2Mins && start2Mins < end1Mins;
3304}
3305
3306// Convert HH:MM time to minutes since midnight
3307function timeToMinutes(timeStr) {
3308    const [hours, minutes] = timeStr.split(':').map(Number);
3309    return hours * 60 + minutes;
3310}
3311
3312// Format time range for display
3313window.formatTimeRange = function(startTime, endTime) {
3314    if (!startTime) return '';
3315
3316    const formatTime = (timeStr) => {
3317        const [hour24, minute] = timeStr.split(':').map(Number);
3318        const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24);
3319        const ampm = hour24 < 12 ? 'AM' : 'PM';
3320        return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm;
3321    };
3322
3323    if (!endTime || endTime === startTime) {
3324        return formatTime(startTime);
3325    }
3326
3327    return formatTime(startTime) + ' - ' + formatTime(endTime);
3328};
3329
3330// Track last known mouse position for tooltip positioning fallback
3331var _lastMouseX = 0, _lastMouseY = 0;
3332document.addEventListener('mousemove', function(e) {
3333    _lastMouseX = e.clientX;
3334    _lastMouseY = e.clientY;
3335});
3336
3337// Show custom conflict tooltip
3338window.showConflictTooltip = function(badgeElement) {
3339    // Remove any existing tooltip
3340    hideConflictTooltip();
3341
3342    // Get conflict data (base64-encoded JSON to avoid attribute quote issues)
3343    const conflictsRaw = badgeElement.getAttribute('data-conflicts');
3344    if (!conflictsRaw) return;
3345
3346    let conflicts;
3347    try {
3348        conflicts = JSON.parse(decodeURIComponent(escape(atob(conflictsRaw))));
3349    } catch (e) {
3350        // Fallback: try parsing as plain JSON (for PHP-rendered badges)
3351        try {
3352            conflicts = JSON.parse(conflictsRaw);
3353        } catch (e2) {
3354            console.error('Failed to parse conflicts:', e2);
3355            return;
3356        }
3357    }
3358
3359    // Get theme from the calendar container via CSS variables
3360    // Try closest ancestor first, then fall back to any calendar on the page
3361    let containerEl = badgeElement.closest('[id^="cal_"], [id^="panel_"], [id^="sidebar-widget-"], .calendar-compact-container, .event-panel-standalone');
3362    if (!containerEl) {
3363        // Badge might be inside a day popup (appended to body) - find any calendar container
3364        containerEl = document.querySelector('.calendar-compact-container, .event-panel-standalone, [id^="sidebar-widget-"]');
3365    }
3366    const cs = containerEl ? getComputedStyle(containerEl) : null;
3367
3368    const bg = cs ? cs.getPropertyValue('--background-site').trim() || '#242424' : '#242424';
3369    const border = cs ? cs.getPropertyValue('--border-main').trim() || '#00cc07' : '#00cc07';
3370    const textPrimary = cs ? cs.getPropertyValue('--text-primary').trim() || '#00cc07' : '#00cc07';
3371    const textDim = cs ? cs.getPropertyValue('--text-dim').trim() || '#00aa00' : '#00aa00';
3372    const shadow = cs ? cs.getPropertyValue('--shadow-color').trim() || 'rgba(0, 204, 7, 0.3)' : 'rgba(0, 204, 7, 0.3)';
3373
3374    // Create tooltip
3375    const tooltip = document.createElement('div');
3376    tooltip.id = 'conflict-tooltip';
3377    tooltip.className = 'conflict-tooltip';
3378
3379    // Apply theme styles
3380    tooltip.style.background = bg;
3381    tooltip.style.borderColor = border;
3382    tooltip.style.color = textPrimary;
3383    tooltip.style.boxShadow = '0 4px 12px ' + shadow;
3384
3385    // Build content with themed colors
3386    let html = '<div class="conflict-tooltip-header" style="background: ' + border + '; color: ' + bg + '; border-bottom: 1px solid ' + border + ';">⚠️ Time Conflicts</div>';
3387    html += '<div class="conflict-tooltip-body">';
3388    conflicts.forEach(conflict => {
3389        html += '<div class="conflict-item" style="color: ' + textDim + '; border-bottom-color: ' + border + ';">• ' + escapeHtml(conflict) + '</div>';
3390    });
3391    html += '</div>';
3392
3393    tooltip.innerHTML = html;
3394    document.body.appendChild(tooltip);
3395
3396    // Position tooltip
3397    const rect = badgeElement.getBoundingClientRect();
3398    const tooltipRect = tooltip.getBoundingClientRect();
3399
3400    // Position above the badge, centered
3401    let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
3402    let top = rect.top - tooltipRect.height - 8;
3403
3404    // Keep tooltip within viewport
3405    if (left < 10) left = 10;
3406    if (left + tooltipRect.width > window.innerWidth - 10) {
3407        left = window.innerWidth - tooltipRect.width - 10;
3408    }
3409    if (top < 10) {
3410        // If not enough room above, show below
3411        top = rect.bottom + 8;
3412    }
3413
3414    tooltip.style.left = left + 'px';
3415    tooltip.style.top = top + 'px';
3416    tooltip.style.opacity = '1';
3417};
3418
3419// Hide conflict tooltip
3420window.hideConflictTooltip = function() {
3421    const tooltip = document.getElementById('conflict-tooltip');
3422    if (tooltip) {
3423        tooltip.remove();
3424    }
3425};
3426
3427// Fuzzy search helper for event filtering - normalizes text for matching
3428function eventSearchNormalize(text) {
3429    if (typeof text !== 'string') {
3430        console.log('[eventSearchNormalize] WARNING: text is not a string:', typeof text, text);
3431        return '';
3432    }
3433    return text
3434        .toLowerCase()
3435        .trim()
3436        // Remove common punctuation that might differ
3437        .replace(/[''\u2018\u2019]/g, '')  // Remove apostrophes/quotes
3438        .replace(/["""\u201C\u201D]/g, '') // Remove smart quotes
3439        .replace(/[-–—]/g, ' ')            // Dashes to spaces
3440        .replace(/[.,!?;:]/g, '')          // Remove punctuation
3441        .replace(/\s+/g, ' ')              // Normalize whitespace
3442        .trim();
3443}
3444
3445// Check if search term matches text for event filtering
3446function eventSearchMatch(text, searchTerm) {
3447    const normalizedText = eventSearchNormalize(text);
3448    const normalizedSearch = eventSearchNormalize(searchTerm);
3449
3450    // Direct match after normalization
3451    if (normalizedText.includes(normalizedSearch)) {
3452        return true;
3453    }
3454
3455    // Split search into words and check if all words are present
3456    const searchWords = normalizedSearch.split(' ').filter(w => w.length > 0);
3457    if (searchWords.length > 1) {
3458        return searchWords.every(word => normalizedText.includes(word));
3459    }
3460
3461    return false;
3462}
3463
3464// Filter events by search term
3465window.filterEvents = function(calId, searchTerm) {
3466    const eventList = document.getElementById('eventlist-' + calId);
3467    const searchClear = document.getElementById('search-clear-' + calId);
3468    const searchMode = document.getElementById('search-mode-' + calId);
3469
3470    if (!eventList) return;
3471
3472    // Check if we're in "all dates" mode
3473    const isAllDatesMode = searchMode && searchMode.classList.contains('all-dates');
3474
3475    // Show/hide clear button
3476    if (searchClear) {
3477        searchClear.style.display = searchTerm ? 'block' : 'none';
3478    }
3479
3480    searchTerm = searchTerm.trim();
3481
3482    // If all-dates mode and we have a search term, do AJAX search
3483    if (isAllDatesMode && searchTerm.length >= 2) {
3484        searchAllDates(calId, searchTerm);
3485        return;
3486    }
3487
3488    // If all-dates mode but search cleared, restore normal view
3489    if (isAllDatesMode && !searchTerm) {
3490        // Remove search results container if exists
3491        const resultsContainer = eventList.querySelector('.all-dates-results');
3492        if (resultsContainer) {
3493            resultsContainer.remove();
3494        }
3495        // Show normal event items
3496        eventList.querySelectorAll('.event-compact-item').forEach(item => {
3497            item.style.display = '';
3498        });
3499        // Show past events toggle if it exists
3500        const pastToggle = eventList.querySelector('.past-events-toggle');
3501        if (pastToggle) pastToggle.style.display = '';
3502    }
3503
3504    // Get all event items
3505    const eventItems = eventList.querySelectorAll('.event-compact-item');
3506    let visibleCount = 0;
3507    let hiddenPastCount = 0;
3508
3509    eventItems.forEach(item => {
3510        const title = item.querySelector('.event-title-compact');
3511        const description = item.querySelector('.event-desc-compact');
3512        const dateTime = item.querySelector('.event-date-time');
3513
3514        // Build searchable text
3515        let searchableText = '';
3516        if (title) searchableText += title.textContent + ' ';
3517        if (description) searchableText += description.textContent + ' ';
3518        if (dateTime) searchableText += dateTime.textContent + ' ';
3519
3520        // Check if matches search using fuzzy matching
3521        const matches = !searchTerm || eventSearchMatch(searchableText, searchTerm);
3522
3523        if (matches) {
3524            item.style.display = '';
3525            visibleCount++;
3526        } else {
3527            item.style.display = 'none';
3528            // Check if this is a past event
3529            if (item.classList.contains('event-past') || item.classList.contains('event-completed')) {
3530                hiddenPastCount++;
3531            }
3532        }
3533    });
3534
3535    // Update past events toggle if it exists
3536    const pastToggle = eventList.querySelector('.past-events-toggle');
3537    const pastLabel = eventList.querySelector('.past-events-label');
3538    const pastContent = document.getElementById('past-events-' + calId);
3539
3540    if (pastToggle && pastLabel && pastContent) {
3541        const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])');
3542        const totalPastVisible = visiblePastEvents.length;
3543
3544        if (totalPastVisible > 0) {
3545            pastLabel.textContent = `Past Events (${totalPastVisible})`;
3546            pastToggle.style.display = '';
3547        } else {
3548            pastToggle.style.display = 'none';
3549        }
3550    }
3551
3552    // Show "no results" message if nothing visible (only for month mode, not all-dates mode)
3553    let noResultsMsg = eventList.querySelector('.no-search-results');
3554    if (visibleCount === 0 && searchTerm && !isAllDatesMode) {
3555        if (!noResultsMsg) {
3556            noResultsMsg = document.createElement('p');
3557            noResultsMsg.className = 'no-search-results no-events-msg';
3558            noResultsMsg.textContent = 'No events match your search';
3559            eventList.appendChild(noResultsMsg);
3560        }
3561        noResultsMsg.style.display = 'block';
3562    } else if (noResultsMsg) {
3563        noResultsMsg.style.display = 'none';
3564    }
3565};
3566
3567// Toggle search mode between "this month" and "all dates"
3568window.toggleSearchMode = function(calId, namespace) {
3569    const searchMode = document.getElementById('search-mode-' + calId);
3570    const searchInput = document.getElementById('event-search-' + calId);
3571
3572    if (!searchMode) return;
3573
3574    const isAllDates = searchMode.classList.toggle('all-dates');
3575
3576    // Update button icon and title
3577    if (isAllDates) {
3578        searchMode.innerHTML = '��';
3579        searchMode.title = 'Searching all dates';
3580        if (searchInput) {
3581            searchInput.placeholder = 'Search all dates...';
3582        }
3583    } else {
3584        searchMode.innerHTML = '��';
3585        searchMode.title = 'Search this month only';
3586        if (searchInput) {
3587            searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : '�� Search...';
3588        }
3589    }
3590
3591    // Re-run search with current term
3592    if (searchInput && searchInput.value) {
3593        filterEvents(calId, searchInput.value);
3594    } else {
3595        // Clear any all-dates results
3596        const eventList = document.getElementById('eventlist-' + calId);
3597        if (eventList) {
3598            const resultsContainer = eventList.querySelector('.all-dates-results');
3599            if (resultsContainer) {
3600                resultsContainer.remove();
3601            }
3602            // Show normal event items
3603            eventList.querySelectorAll('.event-compact-item').forEach(item => {
3604                item.style.display = '';
3605            });
3606            const pastToggle = eventList.querySelector('.past-events-toggle');
3607            if (pastToggle) pastToggle.style.display = '';
3608        }
3609    }
3610};
3611
3612// Search all dates via AJAX
3613window.searchAllDates = function(calId, searchTerm) {
3614    const eventList = document.getElementById('eventlist-' + calId);
3615    if (!eventList) return;
3616
3617    // Get namespace from container
3618    const container = document.getElementById(calId);
3619    const namespace = container ? (container.dataset.namespace || '') : '';
3620
3621    // Hide normal event items
3622    eventList.querySelectorAll('.event-compact-item').forEach(item => {
3623        item.style.display = 'none';
3624    });
3625    const pastToggle = eventList.querySelector('.past-events-toggle');
3626    if (pastToggle) pastToggle.style.display = 'none';
3627
3628    // Remove old results container
3629    let resultsContainer = eventList.querySelector('.all-dates-results');
3630    if (resultsContainer) {
3631        resultsContainer.remove();
3632    }
3633
3634    // Create new results container
3635    resultsContainer = document.createElement('div');
3636    resultsContainer.className = 'all-dates-results';
3637    resultsContainer.innerHTML = '<p class="search-loading" style="text-align:center; padding:20px; color:var(--text-dim);">�� Searching all dates...</p>';
3638    eventList.appendChild(resultsContainer);
3639
3640    // Make AJAX request
3641    const params = new URLSearchParams({
3642        call: 'plugin_calendar',
3643        action: 'search_all',
3644        search: searchTerm,
3645        namespace: namespace,
3646        _: new Date().getTime()
3647    });
3648
3649    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
3650        method: 'POST',
3651        headers: {
3652            'Content-Type': 'application/x-www-form-urlencoded'
3653        },
3654        body: params.toString()
3655    })
3656    .then(r => r.json())
3657    .then(data => {
3658        if (data.success && data.results) {
3659            if (data.results.length === 0) {
3660                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>';
3661            } else {
3662                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>';
3663
3664                data.results.forEach(event => {
3665                    const dateObj = new Date(event.date + 'T00:00:00');
3666                    const dateDisplay = dateObj.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
3667                    const color = event.color || 'var(--text-bright, #00cc07)';
3668
3669                    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 + '\')">';
3670                    html += '<div style="width:3px; background:' + color + '; border-radius:1px; flex-shrink:0;"></div>';
3671                    html += '<div style="flex:1; min-width:0;">';
3672                    html += '<div class="event-title-compact" style="font-weight:600; color:var(--text-primary); font-size:11px;">' + escapeHtml(event.title) + '</div>';
3673                    html += '<div class="event-date-time" style="font-size:10px; color:var(--text-dim);">' + dateDisplay;
3674                    if (event.time) {
3675                        html += ' • ' + formatTimeRange(event.time, event.endTime);
3676                    }
3677                    html += '</div>';
3678                    if (event.namespace) {
3679                        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>';
3680                    }
3681                    html += '</div></div>';
3682                });
3683
3684                resultsContainer.innerHTML = html;
3685            }
3686        } else {
3687            resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>';
3688        }
3689    })
3690    .catch(err => {
3691        console.error('Search error:', err);
3692        resultsContainer.innerHTML = '<p class="no-search-results" style="text-align:center; padding:20px; color:var(--text-dim);">Search failed. Please try again.</p>';
3693    });
3694};
3695
3696// Jump to a specific date (used by search results)
3697window.jumpToDate = function(calId, date, namespace) {
3698    const parts = date.split('-');
3699    const year = parseInt(parts[0]);
3700    const month = parseInt(parts[1]);
3701
3702    // Get container to check current month
3703    const container = document.getElementById(calId);
3704    const currentYear = container ? parseInt(container.dataset.year) : year;
3705    const currentMonth = container ? parseInt(container.dataset.month) : month;
3706
3707    // Get search elements
3708    const searchInput = document.getElementById('event-search-' + calId);
3709    const searchMode = document.getElementById('search-mode-' + calId);
3710    const searchClear = document.getElementById('search-clear-' + calId);
3711    const eventList = document.getElementById('eventlist-' + calId);
3712
3713    // Remove the all-dates results container
3714    if (eventList) {
3715        const resultsContainer = eventList.querySelector('.all-dates-results');
3716        if (resultsContainer) {
3717            resultsContainer.remove();
3718        }
3719        // Show normal event items again
3720        eventList.querySelectorAll('.event-compact-item').forEach(item => {
3721            item.style.display = '';
3722        });
3723        const pastToggle = eventList.querySelector('.past-events-toggle');
3724        if (pastToggle) pastToggle.style.display = '';
3725
3726        // Hide any no-results message
3727        const noResults = eventList.querySelector('.no-search-results');
3728        if (noResults) noResults.style.display = 'none';
3729    }
3730
3731    // Clear search input
3732    if (searchInput) {
3733        searchInput.value = '';
3734    }
3735
3736    // Hide clear button
3737    if (searchClear) {
3738        searchClear.style.display = 'none';
3739    }
3740
3741    // Switch back to month mode
3742    if (searchMode && searchMode.classList.contains('all-dates')) {
3743        searchMode.classList.remove('all-dates');
3744        searchMode.innerHTML = '��';
3745        searchMode.title = 'Search this month only';
3746        if (searchInput) {
3747            searchInput.placeholder = searchInput.classList.contains('panel-search-input') ? 'Search this month...' : '�� Search...';
3748        }
3749    }
3750
3751    // Check if we need to navigate to a different month
3752    if (year !== currentYear || month !== currentMonth) {
3753        // Navigate to the target month, then show popup
3754        navCalendar(calId, year, month, namespace);
3755
3756        // After navigation completes, show the day popup
3757        setTimeout(() => {
3758            showDayPopup(calId, date, namespace);
3759        }, 400);
3760    } else {
3761        // Same month - just show the popup
3762        showDayPopup(calId, date, namespace);
3763    }
3764};
3765
3766// Clear event search
3767window.clearEventSearch = function(calId) {
3768    const searchInput = document.getElementById('event-search-' + calId);
3769    if (searchInput) {
3770        searchInput.value = '';
3771        filterEvents(calId, '');
3772        searchInput.focus();
3773    }
3774};
3775
3776// ============================================
3777// PINK THEME - GLOWING PARTICLE EFFECTS
3778// ============================================
3779
3780// Create glowing pink particle effects for pink theme
3781(function() {
3782    let pinkThemeActive = false;
3783    let trailTimer = null;
3784    let pixelTimer = null;
3785
3786    // Check if pink theme is active
3787    function checkPinkTheme() {
3788        const pinkCalendars = document.querySelectorAll('.calendar-theme-pink');
3789        pinkThemeActive = pinkCalendars.length > 0;
3790        return pinkThemeActive;
3791    }
3792
3793    // Create trail particle
3794    function createTrailParticle(clientX, clientY) {
3795        if (!pinkThemeActive) return;
3796
3797        const trail = document.createElement('div');
3798        trail.className = 'pink-cursor-trail';
3799        trail.style.left = clientX + 'px';
3800        trail.style.top = clientY + 'px';
3801        trail.style.animation = 'cursor-trail-fade 0.5s ease-out forwards';
3802
3803        document.body.appendChild(trail);
3804
3805        setTimeout(function() {
3806            trail.remove();
3807        }, 500);
3808    }
3809
3810    // Create pixel sparkles
3811    function createPixelSparkles(clientX, clientY) {
3812        if (!pinkThemeActive || pixelTimer) return;
3813
3814        const pixelCount = 3 + Math.floor(Math.random() * 4); // 3-6 pixels
3815
3816        for (let i = 0; i < pixelCount; i++) {
3817            const pixel = document.createElement('div');
3818            pixel.className = 'pink-pixel-sparkle';
3819
3820            // Random offset from cursor
3821            const offsetX = (Math.random() - 0.5) * 30;
3822            const offsetY = (Math.random() - 0.5) * 30;
3823
3824            pixel.style.left = (clientX + offsetX) + 'px';
3825            pixel.style.top = (clientY + offsetY) + 'px';
3826
3827            // Random color - bright neon pinks and whites
3828            const colors = ['#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1'];
3829            const color = colors[Math.floor(Math.random() * colors.length)];
3830            pixel.style.background = color;
3831            pixel.style.boxShadow = '0 0 2px ' + color + ', 0 0 4px ' + color + ', 0 0 6px #fff';
3832
3833            // Random animation
3834            if (Math.random() > 0.5) {
3835                pixel.style.animation = 'pixel-twinkle 0.6s ease-out forwards';
3836            } else {
3837                pixel.style.animation = 'pixel-float-away 0.8s ease-out forwards';
3838            }
3839
3840            document.body.appendChild(pixel);
3841
3842            setTimeout(function() {
3843                pixel.remove();
3844            }, 800);
3845        }
3846
3847        pixelTimer = setTimeout(function() {
3848            pixelTimer = null;
3849        }, 40);
3850    }
3851
3852    // Create explosion
3853    function createExplosion(clientX, clientY) {
3854        if (!pinkThemeActive) return;
3855
3856        const particleCount = 25;
3857        const colors = ['#ff1493', '#ff69b4', '#ff85c1', '#ffc0cb', '#fff'];
3858
3859        // Add hearts to explosion (8-12 hearts)
3860        const heartCount = 8 + Math.floor(Math.random() * 5);
3861        for (let i = 0; i < heartCount; i++) {
3862            const heart = document.createElement('div');
3863            heart.textContent = '��';
3864            heart.style.position = 'fixed';
3865            heart.style.left = clientX + 'px';
3866            heart.style.top = clientY + 'px';
3867            heart.style.pointerEvents = 'none';
3868            heart.style.zIndex = '9999999';
3869            heart.style.fontSize = (12 + Math.random() * 16) + 'px';
3870
3871            // Random direction
3872            const angle = Math.random() * Math.PI * 2;
3873            const velocity = 60 + Math.random() * 80;
3874            const tx = Math.cos(angle) * velocity;
3875            const ty = Math.sin(angle) * velocity;
3876
3877            heart.style.setProperty('--tx', tx + 'px');
3878            heart.style.setProperty('--ty', ty + 'px');
3879
3880            const duration = 0.8 + Math.random() * 0.4;
3881            heart.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
3882
3883            document.body.appendChild(heart);
3884
3885            setTimeout(function() {
3886                heart.remove();
3887            }, duration * 1000);
3888        }
3889
3890        // Main explosion particles
3891        for (let i = 0; i < particleCount; i++) {
3892            const particle = document.createElement('div');
3893            particle.className = 'pink-particle';
3894
3895            const color = colors[Math.floor(Math.random() * colors.length)];
3896            particle.style.background = 'radial-gradient(circle, ' + color + ', transparent)';
3897            particle.style.boxShadow = '0 0 10px ' + color + ', 0 0 20px ' + color;
3898
3899            particle.style.left = clientX + 'px';
3900            particle.style.top = clientY + 'px';
3901
3902            const angle = (Math.PI * 2 * i) / particleCount;
3903            const velocity = 50 + Math.random() * 100;
3904            const tx = Math.cos(angle) * velocity;
3905            const ty = Math.sin(angle) * velocity;
3906
3907            particle.style.setProperty('--tx', tx + 'px');
3908            particle.style.setProperty('--ty', ty + 'px');
3909
3910            const size = 4 + Math.random() * 6;
3911            particle.style.width = size + 'px';
3912            particle.style.height = size + 'px';
3913
3914            const duration = 0.6 + Math.random() * 0.4;
3915            particle.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
3916
3917            document.body.appendChild(particle);
3918
3919            setTimeout(function() {
3920                particle.remove();
3921            }, duration * 1000);
3922        }
3923
3924        // Pixel sparkles
3925        const pixelSparkleCount = 40;
3926
3927        for (let i = 0; i < pixelSparkleCount; i++) {
3928            const pixel = document.createElement('div');
3929            pixel.className = 'pink-pixel-sparkle';
3930
3931            const pixelColors = ['#fff', '#fff', '#ff1493', '#ff69b4', '#ffb6c1', '#ff85c1'];
3932            const pixelColor = pixelColors[Math.floor(Math.random() * pixelColors.length)];
3933            pixel.style.background = pixelColor;
3934            pixel.style.boxShadow = '0 0 3px ' + pixelColor + ', 0 0 6px ' + pixelColor + ', 0 0 9px #fff';
3935
3936            const angle = Math.random() * Math.PI * 2;
3937            const distance = 30 + Math.random() * 80;
3938            const offsetX = Math.cos(angle) * distance;
3939            const offsetY = Math.sin(angle) * distance;
3940
3941            pixel.style.left = clientX + 'px';
3942            pixel.style.top = clientY + 'px';
3943            pixel.style.setProperty('--tx', offsetX + 'px');
3944            pixel.style.setProperty('--ty', offsetY + 'px');
3945
3946            const pixelSize = 1 + Math.random() * 2;
3947            pixel.style.width = pixelSize + 'px';
3948            pixel.style.height = pixelSize + 'px';
3949
3950            const duration = 0.4 + Math.random() * 0.4;
3951            if (Math.random() > 0.5) {
3952                pixel.style.animation = 'pixel-twinkle ' + duration + 's ease-out forwards';
3953            } else {
3954                pixel.style.animation = 'particle-explode ' + duration + 's ease-out forwards';
3955            }
3956
3957            document.body.appendChild(pixel);
3958
3959            setTimeout(function() {
3960                pixel.remove();
3961            }, duration * 1000);
3962        }
3963
3964        // Flash
3965        const flash = document.createElement('div');
3966        flash.style.position = 'fixed';
3967        flash.style.left = clientX + 'px';
3968        flash.style.top = clientY + 'px';
3969        flash.style.width = '40px';
3970        flash.style.height = '40px';
3971        flash.style.borderRadius = '50%';
3972        flash.style.background = 'radial-gradient(circle, rgba(255, 255, 255, 0.9), rgba(255, 20, 147, 0.6), transparent)';
3973        flash.style.boxShadow = '0 0 40px #fff, 0 0 60px #ff1493, 0 0 80px #ff69b4';
3974        flash.style.pointerEvents = 'none';
3975        flash.style.zIndex = '9999999';  // Above everything including dialogs
3976        flash.style.transform = 'translate(-50%, -50%)';
3977        flash.style.animation = 'cursor-trail-fade 0.3s ease-out forwards';
3978
3979        document.body.appendChild(flash);
3980
3981        setTimeout(function() {
3982            flash.remove();
3983        }, 300);
3984    }
3985
3986    function initPinkParticles() {
3987        if (!checkPinkTheme()) return;
3988
3989        // Use capture phase to catch events before stopPropagation
3990        document.addEventListener('mousemove', function(e) {
3991            if (!pinkThemeActive) return;
3992
3993            createTrailParticle(e.clientX, e.clientY);
3994            createPixelSparkles(e.clientX, e.clientY);
3995        }, true); // Capture phase!
3996
3997        // Throttle main trail
3998        document.addEventListener('mousemove', function(e) {
3999            if (!pinkThemeActive || trailTimer) return;
4000
4001            trailTimer = setTimeout(function() {
4002                trailTimer = null;
4003            }, 30);
4004        }, true); // Capture phase!
4005
4006        // Click explosion - use capture phase
4007        document.addEventListener('click', function(e) {
4008            if (!pinkThemeActive) return;
4009
4010            createExplosion(e.clientX, e.clientY);
4011        }, true); // Capture phase!
4012    }
4013
4014    // Initialize on load
4015    if (document.readyState === 'loading') {
4016        document.addEventListener('DOMContentLoaded', initPinkParticles);
4017    } else {
4018        initPinkParticles();
4019    }
4020
4021    // Re-check theme if calendar is dynamically added
4022    // Must wait for document.body to exist
4023    function setupMutationObserver() {
4024        if (typeof MutationObserver !== 'undefined' && document.body) {
4025            const observer = new MutationObserver(function(mutations) {
4026                mutations.forEach(function(mutation) {
4027                    if (mutation.addedNodes.length > 0) {
4028                        mutation.addedNodes.forEach(function(node) {
4029                            if (node.nodeType === 1 && node.classList && node.classList.contains('calendar-theme-pink')) {
4030                                checkPinkTheme();
4031                                initPinkParticles();
4032                            }
4033                        });
4034                    }
4035                });
4036            });
4037
4038            observer.observe(document.body, {
4039                childList: true,
4040                subtree: true
4041            });
4042        }
4043    }
4044
4045    // Setup observer when DOM is ready
4046    if (document.readyState === 'loading') {
4047        document.addEventListener('DOMContentLoaded', setupMutationObserver);
4048    } else {
4049        setupMutationObserver();
4050    }
4051})();
4052
4053// Mobile touch event delegation for edit/delete buttons
4054// This ensures buttons work on mobile where onclick may not fire reliably
4055(function() {
4056    function handleButtonTouch(e) {
4057        const btn = e.target.closest('.event-edit-btn, .event-delete-btn, .event-action-btn');
4058        if (!btn) return;
4059
4060        // Prevent double-firing with onclick
4061        e.preventDefault();
4062
4063        // Small delay to show visual feedback
4064        setTimeout(function() {
4065            btn.click();
4066        }, 10);
4067    }
4068
4069    // Use touchend for more reliable mobile handling
4070    document.addEventListener('touchend', handleButtonTouch, { passive: false });
4071})();
4072
4073// Static calendar navigation
4074window.navStaticCalendar = function(calId, direction) {
4075    const container = document.getElementById(calId);
4076    if (!container) return;
4077
4078    let year = parseInt(container.dataset.year);
4079    let month = parseInt(container.dataset.month);
4080    const namespace = container.dataset.namespace || '';
4081
4082    // Calculate new month
4083    month += direction;
4084    if (month < 1) {
4085        month = 12;
4086        year--;
4087    } else if (month > 12) {
4088        month = 1;
4089        year++;
4090    }
4091
4092    // Fetch new calendar content via AJAX
4093    const params = new URLSearchParams({
4094        call: 'plugin_calendar',
4095        action: 'get_static_calendar',
4096        year: year,
4097        month: month,
4098        namespace: namespace
4099    });
4100
4101    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
4102        method: 'POST',
4103        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
4104        body: params.toString()
4105    })
4106    .then(r => r.json())
4107    .then(data => {
4108        if (data.success && data.html) {
4109            // Replace the container content
4110            container.outerHTML = data.html;
4111        }
4112    })
4113    .catch(err => console.error('Static calendar navigation error:', err));
4114};
4115
4116// Print static calendar - opens print dialog with only calendar content
4117window.printStaticCalendar = function(calId) {
4118    const container = document.getElementById(calId);
4119    if (!container) return;
4120
4121    // Get the print view content
4122    const printView = container.querySelector('.static-print-view');
4123    if (!printView) return;
4124
4125    // Create a new window for printing
4126    const printWindow = window.open('', '_blank', 'width=800,height=600');
4127
4128    // Build print document with inline margins for maximum compatibility
4129    const printContent = `
4130<!DOCTYPE html>
4131<html>
4132<head>
4133    <title>Calendar - ${container.dataset.year}-${String(container.dataset.month).padStart(2, '0')}</title>
4134    <style>
4135        * { margin: 0; padding: 0; box-sizing: border-box; }
4136        body { font-family: Arial, sans-serif; color: #333; background: white; }
4137        table { border-collapse: collapse; font-size: 12px; }
4138        th { background: #2c3e50; color: white; padding: 8px; text-align: left; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
4139        td { padding: 6px 8px; border-bottom: 1px solid #ccc; vertical-align: top; }
4140        tr:nth-child(even) { background: #f0f0f0; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
4141        .static-itinerary-important { background: #fffde7 !important; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
4142        .static-itinerary-date { font-weight: bold; white-space: nowrap; }
4143        .static-itinerary-time { white-space: nowrap; color: #555; }
4144        .static-itinerary-title { font-weight: 500; }
4145        .static-itinerary-desc { color: #555; font-size: 11px; }
4146        thead { display: table-header-group; }
4147        tr { page-break-inside: avoid; }
4148        h2 { font-size: 16px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #333; }
4149        p { font-size: 12px; color: #666; margin-bottom: 15px; }
4150    </style>
4151</head>
4152<body style="margin: 0; padding: 0;">
4153    <div style="padding: 50px 60px; margin: 0 auto; max-width: 800px;">
4154        ${printView.innerHTML}
4155    </div>
4156    <script>
4157        window.onload = function() {
4158            setTimeout(function() {
4159                window.print();
4160            }, 300);
4161            window.onafterprint = function() {
4162                window.close();
4163            };
4164        };
4165    </script>
4166</body>
4167</html>`;
4168
4169    printWindow.document.write(printContent);
4170    printWindow.document.close();
4171};
4172
4173// ============================================================================
4174// ACCESSIBILITY - Screen reader announcements
4175// ============================================================================
4176
4177// Create ARIA live region for announcements
4178if (!document.getElementById('calendar-aria-live')) {
4179    var ariaLive = document.createElement('div');
4180    ariaLive.id = 'calendar-aria-live';
4181    ariaLive.setAttribute('role', 'status');
4182    ariaLive.setAttribute('aria-live', 'polite');
4183    ariaLive.setAttribute('aria-atomic', 'true');
4184    ariaLive.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
4185    document.body.appendChild(ariaLive);
4186}
4187
4188// Announce message to screen readers
4189window.announceToScreenReader = function(message) {
4190    var ariaLive = document.getElementById('calendar-aria-live');
4191    if (ariaLive) {
4192        ariaLive.textContent = '';
4193        // Small delay to ensure screen reader picks up the change
4194        setTimeout(function() {
4195            ariaLive.textContent = message;
4196        }, 100);
4197    }
4198};
4199
4200// End of calendar plugin JavaScript
4201