1/**
2 * DokuWiki Compact Calendar Plugin JavaScript
3 * Loaded independently to avoid DokuWiki concatenation issues
4 */
5
6// Ensure DOKU_BASE is defined - check multiple sources
7if (typeof DOKU_BASE === 'undefined') {
8    // Try to get from global jsinfo object (DokuWiki standard)
9    if (typeof window.jsinfo !== 'undefined' && window.jsinfo.dokubase) {
10        window.DOKU_BASE = window.jsinfo.dokubase;
11    } else {
12        // Fallback: extract from script source path
13        var scripts = document.getElementsByTagName('script');
14        var pluginScriptPath = null;
15        for (var i = 0; i < scripts.length; i++) {
16            if (scripts[i].src && scripts[i].src.indexOf('calendar/script.js') !== -1) {
17                pluginScriptPath = scripts[i].src;
18                break;
19            }
20        }
21
22        if (pluginScriptPath) {
23            // Extract base path from: .../lib/plugins/calendar/script.js
24            var match = pluginScriptPath.match(/^(.*?)lib\/plugins\//);
25            window.DOKU_BASE = match ? match[1] : '/';
26        } else {
27            // Last resort: use root
28            window.DOKU_BASE = '/';
29        }
30    }
31}
32
33// Shorthand for convenience
34var DOKU_BASE = window.DOKU_BASE || '/';
35
36// Filter calendar by namespace
37window.filterCalendarByNamespace = function(calId, namespace) {
38    // Get current year and month from calendar
39    const container = document.getElementById(calId);
40    if (!container) {
41        console.error('Calendar container not found:', calId);
42        return;
43    }
44
45    const year = parseInt(container.dataset.year) || new Date().getFullYear();
46    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
47
48    // Reload calendar with the filtered namespace
49    navCalendar(calId, year, month, namespace);
50};
51
52// Navigate to different month
53window.navCalendar = function(calId, year, month, namespace) {
54
55    const params = new URLSearchParams({
56        call: 'plugin_calendar',
57        action: 'load_month',
58        year: year,
59        month: month,
60        namespace: namespace,
61        _: new Date().getTime() // Cache buster
62    });
63
64    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
65        method: 'POST',
66        headers: {
67            'Content-Type': 'application/x-www-form-urlencoded',
68            'Cache-Control': 'no-cache, no-store, must-revalidate',
69            'Pragma': 'no-cache'
70        },
71        body: params.toString()
72    })
73    .then(r => r.json())
74    .then(data => {
75        if (data.success) {
76            rebuildCalendar(calId, data.year, data.month, data.events, namespace);
77        } else {
78            console.error('Failed to load month:', data.error);
79        }
80    })
81    .catch(err => {
82        console.error('Error loading month:', err);
83    });
84};
85
86// Jump to current month
87window.jumpToToday = function(calId, namespace) {
88    const today = new Date();
89    const year = today.getFullYear();
90    const month = today.getMonth() + 1; // JavaScript months are 0-indexed
91    navCalendar(calId, year, month, namespace);
92};
93
94// Jump to today for event panel
95window.jumpTodayPanel = function(calId, namespace) {
96    const today = new Date();
97    const year = today.getFullYear();
98    const month = today.getMonth() + 1;
99    navEventPanel(calId, year, month, namespace);
100};
101
102// Open month picker dialog
103window.openMonthPicker = function(calId, currentYear, currentMonth, namespace) {
104
105    const overlay = document.getElementById('month-picker-overlay-' + calId);
106
107    const monthSelect = document.getElementById('month-picker-month-' + calId);
108
109    const yearSelect = document.getElementById('month-picker-year-' + calId);
110
111    if (!overlay) {
112        console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId);
113        return;
114    }
115
116    if (!monthSelect || !yearSelect) {
117        console.error('Select elements not found!');
118        return;
119    }
120
121    // Set current values
122    monthSelect.value = currentMonth;
123    yearSelect.value = currentYear;
124
125    // Show overlay
126    overlay.style.display = 'flex';
127};
128
129// Open month picker dialog for event panel
130window.openMonthPickerPanel = function(calId, currentYear, currentMonth, namespace) {
131    openMonthPicker(calId, currentYear, currentMonth, namespace);
132};
133
134// Close month picker dialog
135window.closeMonthPicker = function(calId) {
136    const overlay = document.getElementById('month-picker-overlay-' + calId);
137    overlay.style.display = 'none';
138};
139
140// Jump to selected month
141window.jumpToSelectedMonth = function(calId, namespace) {
142    const monthSelect = document.getElementById('month-picker-month-' + calId);
143    const yearSelect = document.getElementById('month-picker-year-' + calId);
144
145    const month = parseInt(monthSelect.value);
146    const year = parseInt(yearSelect.value);
147
148    closeMonthPicker(calId);
149
150    // Check if this is a calendar or event panel
151    const container = document.getElementById(calId);
152    if (container && container.classList.contains('event-panel-standalone')) {
153        navEventPanel(calId, year, month, namespace);
154    } else {
155        navCalendar(calId, year, month, namespace);
156    }
157};
158
159// Rebuild calendar grid after navigation
160window.rebuildCalendar = function(calId, year, month, events, namespace) {
161
162    const container = document.getElementById(calId);
163    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
164                       'July', 'August', 'September', 'October', 'November', 'December'];
165
166    // Preserve original namespace if not yet set
167    if (!container.dataset.originalNamespace) {
168        container.setAttribute('data-original-namespace', namespace || '');
169    }
170
171    // Update container data attributes for current month/year
172    container.setAttribute('data-year', year);
173    container.setAttribute('data-month', month);
174
175    // Update embedded events data
176    let eventsDataEl = document.getElementById('events-data-' + calId);
177    if (eventsDataEl) {
178        eventsDataEl.textContent = JSON.stringify(events);
179    } else {
180        eventsDataEl = document.createElement('script');
181        eventsDataEl.type = 'application/json';
182        eventsDataEl.id = 'events-data-' + calId;
183        eventsDataEl.textContent = JSON.stringify(events);
184        container.appendChild(eventsDataEl);
185    }
186
187    // Update header
188    const header = container.querySelector('.calendar-compact-header h3');
189    header.textContent = monthNames[month - 1] + ' ' + year;
190
191    // Update or create namespace filter indicator
192    let filterIndicator = container.querySelector('.calendar-namespace-filter');
193    const shouldShowFilter = namespace && namespace !== '' && namespace !== '*' &&
194                            namespace.indexOf('*') === -1 && namespace.indexOf(';') === -1;
195
196    if (shouldShowFilter) {
197        // Show/update filter indicator
198        if (!filterIndicator) {
199            // Create filter indicator if it doesn't exist
200            const headerDiv = container.querySelector('.calendar-compact-header');
201            if (!headerDiv) {
202                console.error('Header div not found!');
203            } else {
204                filterIndicator = document.createElement('div');
205                filterIndicator.className = 'calendar-namespace-filter';
206                filterIndicator.id = 'namespace-filter-' + calId;
207                headerDiv.parentNode.insertBefore(filterIndicator, headerDiv.nextSibling);
208            }
209        } else {
210        }
211
212        if (filterIndicator) {
213            filterIndicator.innerHTML =
214                '<span class="namespace-filter-label">Filtering:</span>' +
215                '<span class="namespace-filter-name">' + escapeHtml(namespace) + '</span>' +
216                '<button class="namespace-filter-clear" onclick="clearNamespaceFilter(\'' + calId + '\')" title="Clear filter and show all namespaces">✕</button>';
217            filterIndicator.style.display = 'flex';
218        }
219    } else {
220        // Hide filter indicator
221        if (filterIndicator) {
222            filterIndicator.style.display = 'none';
223        }
224    }
225
226    // Update container's namespace attribute
227    container.setAttribute('data-namespace', namespace || '');
228
229    // Update nav buttons
230    let prevMonth = month - 1;
231    let prevYear = year;
232    if (prevMonth < 1) {
233        prevMonth = 12;
234        prevYear--;
235    }
236
237    let nextMonth = month + 1;
238    let nextYear = year;
239    if (nextMonth > 12) {
240        nextMonth = 1;
241        nextYear++;
242    }
243
244    const navBtns = container.querySelectorAll('.cal-nav-btn');
245    navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
246    navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
247
248    // Rebuild calendar grid
249    const tbody = container.querySelector('.calendar-compact-grid tbody');
250    const firstDay = new Date(year, month - 1, 1);
251    const daysInMonth = new Date(year, month, 0).getDate();
252    const dayOfWeek = firstDay.getDay();
253
254    // Calculate month boundaries
255    const monthStart = new Date(year, month - 1, 1);
256    const monthEnd = new Date(year, month - 1, daysInMonth);
257
258    // Build a map of all events with their date ranges
259    const eventRanges = {};
260    for (const [dateKey, dayEvents] of Object.entries(events)) {
261        // Defensive check: ensure dayEvents is an array
262        if (!Array.isArray(dayEvents)) {
263            console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents);
264            continue;
265        }
266
267        // Only process events that could possibly overlap with this month/year
268        const dateYear = parseInt(dateKey.split('-')[0]);
269        const dateMonth = parseInt(dateKey.split('-')[1]);
270
271        // Skip events from completely different years (unless they're very long multi-day events)
272        if (Math.abs(dateYear - year) > 1) {
273            continue;
274        }
275
276        for (const evt of dayEvents) {
277            const startDate = dateKey;
278            const endDate = evt.endDate || dateKey;
279
280            // Check if event overlaps with current month
281            const eventStart = new Date(startDate + 'T00:00:00');
282            const eventEnd = new Date(endDate + 'T00:00:00');
283
284            // Skip if event doesn't overlap with current month
285            if (eventEnd < monthStart || eventStart > monthEnd) {
286                continue;
287            }
288
289            // Create entry for each day the event spans
290            const start = new Date(startDate + 'T00:00:00');
291            const end = new Date(endDate + 'T00:00:00');
292            const current = new Date(start);
293
294            while (current <= end) {
295                const currentKey = current.toISOString().split('T')[0];
296
297                // Check if this date is in current month
298                const currentDate = new Date(currentKey + 'T00:00:00');
299                if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) {
300                    if (!eventRanges[currentKey]) {
301                        eventRanges[currentKey] = [];
302                    }
303
304                    // Add event with span information
305                    const eventCopy = {...evt};
306                    eventCopy._span_start = startDate;
307                    eventCopy._span_end = endDate;
308                    eventCopy._is_first_day = (currentKey === startDate);
309                    eventCopy._is_last_day = (currentKey === endDate);
310                    eventCopy._original_date = dateKey;
311
312                    // Check if event continues from previous month or to next month
313                    eventCopy._continues_from_prev = (eventStart < monthStart);
314                    eventCopy._continues_to_next = (eventEnd > monthEnd);
315
316                    eventRanges[currentKey].push(eventCopy);
317                }
318
319                current.setDate(current.getDate() + 1);
320            }
321        }
322    }
323
324    let html = '';
325    let currentDay = 1;
326    const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
327
328    for (let row = 0; row < rowCount; row++) {
329        html += '<tr>';
330        for (let col = 0; col < 7; col++) {
331            if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
332                html += '<td class="cal-empty"></td>';
333            } else {
334                const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
335
336                // Get today's date in local timezone
337                const todayObj = new Date();
338                const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`;
339
340                const isToday = dateKey === today;
341                const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0;
342
343                let classes = 'cal-day';
344                if (isToday) classes += ' cal-today';
345                if (hasEvents) classes += ' cal-has-events';
346
347                html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`;
348                html += `<span class="day-num">${currentDay}</span>`;
349
350                if (hasEvents) {
351                    // Sort events by time (no time first, then by time)
352                    const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => {
353                        const timeA = a.time || '';
354                        const timeB = b.time || '';
355
356                        // Events without time go first
357                        if (!timeA && timeB) return -1;
358                        if (timeA && !timeB) return 1;
359                        if (!timeA && !timeB) return 0;
360
361                        // Sort by time
362                        return timeA.localeCompare(timeB);
363                    });
364
365                    // Show colored stacked bars for each event
366                    html += '<div class="event-indicators">';
367                    for (const evt of sortedEvents) {
368                        const eventId = evt.id || '';
369                        const eventColor = evt.color || '#3498db';
370                        const eventTime = evt.time || '';
371                        const eventTitle = evt.title || 'Event';
372                        const originalDate = evt._original_date || dateKey;
373                        const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true;
374                        const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true;
375
376                        let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
377
378                        // Add classes for multi-day spanning
379                        if (!isFirstDay) barClass += ' event-bar-continues';
380                        if (!isLastDay) barClass += ' event-bar-continuing';
381
382                        html += `<span class="event-bar ${barClass}" `;
383                        html += `style="background: ${eventColor};" `;
384                        html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `;
385                        html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');"></span>`;
386                    }
387                    html += '</div>';
388                }
389
390                html += '</td>';
391                currentDay++;
392            }
393        }
394        html += '</tr>';
395    }
396
397    tbody.innerHTML = html;
398
399    // Update Today button with current namespace
400    const todayBtn = container.querySelector('.cal-today-btn');
401    if (todayBtn) {
402        todayBtn.setAttribute('onclick', `jumpToToday('${calId}', '${namespace}')`);
403    }
404
405    // Update month picker with current namespace
406    const monthPicker = container.querySelector('.calendar-month-picker');
407    if (monthPicker) {
408        monthPicker.setAttribute('onclick', `openMonthPicker('${calId}', ${year}, ${month}, '${namespace}')`);
409    }
410
411    // Rebuild event list - server already filtered to current month
412    const eventList = container.querySelector('.event-list-compact');
413    eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
414
415    // Auto-scroll to first future event (past events will be above viewport)
416    setTimeout(() => {
417        const firstFuture = eventList.querySelector('[data-first-future="true"]');
418        if (firstFuture) {
419            firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' });
420        }
421    }, 100);
422
423    // Update title
424    const title = container.querySelector('#eventlist-title-' + calId);
425    title.textContent = 'Events';
426};
427
428// Render event list from data
429window.renderEventListFromData = function(events, calId, namespace, year, month) {
430    if (!events || Object.keys(events).length === 0) {
431        return '<p class="no-events-msg">No events this month</p>';
432    }
433
434    // Check for time conflicts
435    events = checkTimeConflicts(events, null);
436
437    let pastHtml = '';
438    let futureHtml = '';
439    let pastCount = 0;
440
441    const sortedDates = Object.keys(events).sort();
442    const today = new Date();
443    today.setHours(0, 0, 0, 0);
444    const todayStr = today.toISOString().split('T')[0];
445
446    // Helper function to check if event is past (with 15-minute grace period)
447    const isEventPast = function(dateKey, time) {
448        // If event is on a past date, it's definitely past
449        if (dateKey < todayStr) {
450            return true;
451        }
452
453        // If event is on a future date, it's definitely not past
454        if (dateKey > todayStr) {
455            return false;
456        }
457
458        // Event is today - check time with grace period
459        if (time && time.trim() !== '') {
460            try {
461                const now = new Date();
462                const eventDateTime = new Date(dateKey + 'T' + time);
463
464                // Add 15-minute grace period
465                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
466
467                // Event is past if current time > event time + 15 minutes
468                return now > gracePeriodEnd;
469            } catch (e) {
470                // If time parsing fails, treat as future
471                return false;
472            }
473        }
474
475        // No time specified for today's event, treat as future
476        return false;
477    };
478
479    // Filter events to only current month if year/month provided
480    const monthStart = year && month ? new Date(year, month - 1, 1) : null;
481    const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null;
482
483    for (const dateKey of sortedDates) {
484        // Skip events not in current month if filtering
485        if (monthStart && monthEnd) {
486            const eventDate = new Date(dateKey + 'T00:00:00');
487
488            if (eventDate < monthStart || eventDate > monthEnd) {
489                continue;
490            }
491        }
492
493        // Sort events within this day by time (all-day events at top)
494        const dayEvents = events[dateKey];
495        dayEvents.sort((a, b) => {
496            const timeA = a.time && a.time.trim() !== '' ? a.time : null;
497            const timeB = b.time && b.time.trim() !== '' ? b.time : null;
498
499            // All-day events (no time) go to the TOP
500            if (timeA === null && timeB !== null) return -1; // A before B
501            if (timeA !== null && timeB === null) return 1;  // A after B
502            if (timeA === null && timeB === null) return 0;  // Both all-day, equal
503
504            // Both have times, sort chronologically
505            return timeA.localeCompare(timeB);
506        });
507
508        for (const event of dayEvents) {
509            const isTask = event.isTask || false;
510            const completed = event.completed || false;
511
512            // Use helper function to determine if event is past (with grace period)
513            const isPast = isEventPast(dateKey, event.time);
514            const isPastDue = isPast && isTask && !completed;
515
516            // Determine if this goes in past section
517            const isPastOrCompleted = (isPast && (!isTask || completed)) || completed;
518
519            const eventHtml = renderEventItem(event, dateKey, calId, namespace);
520
521            if (isPastOrCompleted) {
522                pastCount++;
523                pastHtml += eventHtml;
524            } else {
525                futureHtml += eventHtml;
526            }
527        }
528    }
529
530    let html = '';
531
532    // Add collapsible past events section if any exist
533    if (pastCount > 0) {
534        html += '<div class="past-events-section">';
535        html += '<div class="past-events-toggle" onclick="togglePastEvents(\'' + calId + '\')">';
536        html += '<span class="past-events-arrow" id="past-arrow-' + calId + '">▶</span> ';
537        html += '<span class="past-events-label">Past Events (' + pastCount + ')</span>';
538        html += '</div>';
539        html += '<div class="past-events-content" id="past-events-' + calId + '" style="display:none;">';
540        html += pastHtml;
541        html += '</div>';
542        html += '</div>';
543    } else {
544    }
545
546    // Add future events
547    html += futureHtml;
548
549
550    if (!html) {
551        return '<p class="no-events-msg">No events this month</p>';
552    }
553
554    return html;
555};
556
557// Show day popup with events when clicking a date
558window.showDayPopup = function(calId, date, namespace) {
559    // Get events for this calendar
560    const eventsDataEl = document.getElementById('events-data-' + calId);
561    let events = {};
562
563    if (eventsDataEl) {
564        try {
565            events = JSON.parse(eventsDataEl.textContent);
566        } catch (e) {
567            console.error('Failed to parse events data:', e);
568        }
569    }
570
571    const dayEvents = events[date] || [];
572
573    // Check for conflicts on this day
574    const dayEventsObj = {[date]: dayEvents};
575    const checkedEvents = checkTimeConflicts(dayEventsObj, null);
576    const dayEventsWithConflicts = checkedEvents[date] || dayEvents;
577
578    // Sort events: all-day at top, then chronological by time
579    dayEventsWithConflicts.sort((a, b) => {
580        const timeA = a.time && a.time.trim() !== '' ? a.time : null;
581        const timeB = b.time && b.time.trim() !== '' ? b.time : null;
582
583        // All-day events (no time) go to the TOP
584        if (timeA === null && timeB !== null) return -1; // A before B
585        if (timeA !== null && timeB === null) return 1;  // A after B
586        if (timeA === null && timeB === null) return 0;  // Both all-day, equal
587
588        // Both have times, sort chronologically
589        return timeA.localeCompare(timeB);
590    });
591
592    const dateObj = new Date(date + 'T00:00:00');
593    const displayDate = dateObj.toLocaleDateString('en-US', {
594        weekday: 'long',
595        month: 'long',
596        day: 'numeric',
597        year: 'numeric'
598    });
599
600    // Create popup
601    let popup = document.getElementById('day-popup-' + calId);
602    if (!popup) {
603        popup = document.createElement('div');
604        popup.id = 'day-popup-' + calId;
605        popup.className = 'day-popup';
606        document.body.appendChild(popup);
607    }
608
609    let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>';
610    html += '<div class="day-popup-content">';
611    html += '<div class="day-popup-header">';
612    html += '<h4>' + displayDate + '</h4>';
613    html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>';
614    html += '</div>';
615
616    html += '<div class="day-popup-body">';
617
618    if (dayEventsWithConflicts.length === 0) {
619        html += '<p class="no-events-msg">No events on this day</p>';
620    } else {
621        html += '<div class="popup-events-list">';
622        dayEventsWithConflicts.forEach(event => {
623            const color = event.color || '#3498db';
624
625            // Use individual event namespace if available (for multi-namespace support)
626            const eventNamespace = event._namespace !== undefined ? event._namespace : namespace;
627
628            // Check if this is a continuation (event started before this date)
629            const originalStartDate = event.originalStartDate || event._dateKey || date;
630            const isContinuation = originalStartDate < date;
631
632            // Convert to 12-hour format and handle time ranges
633            let displayTime = '';
634            if (event.time) {
635                displayTime = formatTimeRange(event.time, event.endTime);
636            }
637
638            // Multi-day indicator
639            let multiDay = '';
640            if (event.endDate && event.endDate !== date) {
641                const endObj = new Date(event.endDate + 'T00:00:00');
642                multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
643                    month: 'short',
644                    day: 'numeric'
645                });
646            }
647
648            // Continuation message
649            if (isContinuation) {
650                const startObj = new Date(originalStartDate + 'T00:00:00');
651                const startDisplay = startObj.toLocaleDateString('en-US', {
652                    weekday: 'short',
653                    month: 'short',
654                    day: 'numeric'
655                });
656                html += '<div class="popup-continuation-notice">↪ Continues from ' + startDisplay + '</div>';
657            }
658
659            html += '<div class="popup-event-item">';
660            html += '<div class="event-color-bar" style="background: ' + color + ';"></div>';
661            html += '<div class="popup-event-content">';
662
663            // Single line with title, time, date range, namespace, and actions
664            html += '<div class="popup-event-main-row">';
665            html += '<div class="popup-event-info-inline">';
666            html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>';
667            if (displayTime) {
668                html += '<span class="popup-event-time">�� ' + displayTime + '</span>';
669            }
670            if (multiDay) {
671                html += '<span class="popup-event-multiday">' + multiDay + '</span>';
672            }
673            if (eventNamespace) {
674                html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>';
675            }
676
677            // Add conflict warning badge if event has conflicts
678            if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
679                // Build conflict list for tooltip
680                let conflictList = [];
681                event.conflictsWith.forEach(conflict => {
682                    let conflictText = conflict.title;
683                    if (conflict.time) {
684                        conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
685                    }
686                    conflictList.push(conflictText);
687                });
688
689                html += '<span class="event-conflict-badge" data-conflicts="' + escapeHtml(JSON.stringify(conflictList)) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
690            }
691
692            html += '</div>';
693            html += '<div class="popup-event-actions">';
694            html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>';
695            html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">��️</button>';
696            html += '</div>';
697            html += '</div>';
698
699            // Description on separate line if present
700            if (event.description) {
701                html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>';
702            }
703
704            html += '</div></div>';
705        });
706        html += '</div>';
707    }
708
709    html += '</div>';
710
711    html += '<div class="day-popup-footer">';
712    html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>';
713    html += '</div>';
714
715    html += '</div>';
716
717    popup.innerHTML = html;
718    popup.style.display = 'flex';
719};
720
721// Close day popup
722window.closeDayPopup = function(calId) {
723    const popup = document.getElementById('day-popup-' + calId);
724    if (popup) {
725        popup.style.display = 'none';
726    }
727};
728
729// Show events for a specific day (for event list panel)
730window.showDayEvents = function(calId, date, namespace) {
731    const params = new URLSearchParams({
732        call: 'plugin_calendar',
733        action: 'load_month',
734        year: date.split('-')[0],
735        month: parseInt(date.split('-')[1]),
736        namespace: namespace,
737        _: new Date().getTime() // Cache buster
738    });
739
740    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
741        method: 'POST',
742        headers: {
743            'Content-Type': 'application/x-www-form-urlencoded',
744            'Cache-Control': 'no-cache, no-store, must-revalidate',
745            'Pragma': 'no-cache'
746        },
747        body: params.toString()
748    })
749    .then(r => r.json())
750    .then(data => {
751        if (data.success) {
752            const eventList = document.getElementById('eventlist-' + calId);
753            const events = data.events;
754            const title = document.getElementById('eventlist-title-' + calId);
755
756            const dateObj = new Date(date + 'T00:00:00');
757            const displayDate = dateObj.toLocaleDateString('en-US', {
758                weekday: 'short',
759                month: 'short',
760                day: 'numeric'
761            });
762
763            title.textContent = 'Events - ' + displayDate;
764
765            // Filter events for this day
766            const dayEvents = events[date] || [];
767
768            if (dayEvents.length === 0) {
769                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>';
770            } else {
771                let html = '';
772                dayEvents.forEach(event => {
773                    html += renderEventItem(event, date, calId, namespace);
774                });
775                eventList.innerHTML = html;
776            }
777        }
778    })
779    .catch(err => console.error('Error:', err));
780};
781
782// Render a single event item
783window.renderEventItem = function(event, date, calId, namespace) {
784    // Check if this event is in the past or today (with 15-minute grace period)
785    const today = new Date();
786    today.setHours(0, 0, 0, 0);
787    const todayStr = today.toISOString().split('T')[0];
788    const eventDate = new Date(date + 'T00:00:00');
789
790    // Helper to determine if event is past with grace period
791    let isPast;
792    if (date < todayStr) {
793        isPast = true; // Past date
794    } else if (date > todayStr) {
795        isPast = false; // Future date
796    } else {
797        // Today - check time with grace period
798        if (event.time && event.time.trim() !== '') {
799            try {
800                const now = new Date();
801                const eventDateTime = new Date(date + 'T' + event.time);
802                const gracePeriodEnd = new Date(eventDateTime.getTime() + 15 * 60 * 1000);
803                isPast = now > gracePeriodEnd;
804            } catch (e) {
805                isPast = false;
806            }
807        } else {
808            isPast = false; // No time, treat as future
809        }
810    }
811
812    const isToday = eventDate.getTime() === today.getTime();
813
814    // Format date display with day of week
815    // Use originalStartDate if this is a multi-month event continuation
816    const displayDateKey = event.originalStartDate || date;
817    const dateObj = new Date(displayDateKey + 'T00:00:00');
818    const displayDate = dateObj.toLocaleDateString('en-US', {
819        weekday: 'short',
820        month: 'short',
821        day: 'numeric'
822    });
823
824    // Convert to 12-hour format and handle time ranges
825    let displayTime = '';
826    if (event.time) {
827        displayTime = formatTimeRange(event.time, event.endTime);
828    }
829
830    // Multi-day indicator
831    let multiDay = '';
832    if (event.endDate && event.endDate !== displayDateKey) {
833        const endObj = new Date(event.endDate + 'T00:00:00');
834        multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
835            weekday: 'short',
836            month: 'short',
837            day: 'numeric'
838        });
839    }
840
841    const completedClass = event.completed ? ' event-completed' : '';
842    const isTask = event.isTask || false;
843    const completed = event.completed || false;
844    const isPastDue = isPast && isTask && !completed;
845    const pastClass = (isPast && !isPastDue) ? ' event-past' : '';
846    const pastDueClass = isPastDue ? ' event-pastdue' : '';
847    const color = event.color || '#3498db';
848
849    let html = '<div class="event-compact-item' + completedClass + pastClass + pastDueClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';" onclick="' + (isPast && !isPastDue ? 'togglePastEventExpand(this)' : '') + '">';
850
851    html += '<div class="event-info">';
852    html += '<div class="event-title-row">';
853    html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>';
854    html += '</div>';
855
856    // Show meta and description for non-past events AND past due tasks
857    if (!isPast || isPastDue) {
858        html += '<div class="event-meta-compact">';
859        html += '<span class="event-date-time">' + displayDate + multiDay;
860        if (displayTime) {
861            html += ' • ' + displayTime;
862        }
863        // Add PAST DUE or TODAY badge
864        if (isPastDue) {
865            html += ' <span class="event-pastdue-badge">PAST DUE</span>';
866        } else if (isToday) {
867            html += ' <span class="event-today-badge">TODAY</span>';
868        }
869        // Add namespace badge (stored namespace or _namespace for multi-namespace)
870        let eventNamespace = event.namespace || '';
871        if (!eventNamespace && event._namespace !== undefined) {
872            eventNamespace = event._namespace; // Fallback to _namespace for multi-namespace loading
873        }
874        if (eventNamespace) {
875            html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" style="cursor:pointer;" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>';
876        }
877        // Add conflict warning if event has time conflicts
878        if (event.hasConflict && event.conflictsWith && event.conflictsWith.length > 0) {
879            // Build conflict list for data attribute
880            let conflictList = [];
881            event.conflictsWith.forEach(conflict => {
882                let conflictText = conflict.title;
883                if (conflict.time) {
884                    conflictText += ' (' + formatTimeRange(conflict.time, conflict.endTime) + ')';
885                }
886                conflictList.push(conflictText);
887            });
888
889            html += ' <span class="event-conflict-badge" data-conflicts="' + escapeHtml(JSON.stringify(conflictList)) + '" onmouseenter="showConflictTooltip(this)" onmouseleave="hideConflictTooltip()">⚠️ ' + event.conflictsWith.length + '</span>';
890        }
891        html += '</span>';
892        html += '</div>';
893
894        if (event.description) {
895            html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>';
896        }
897    } else {
898        // For past events (not past due), store data in hidden divs for expand/collapse
899        html += '<div class="event-meta-compact" style="display: none;">';
900        html += '<span class="event-date-time">' + displayDate + multiDay;
901        if (displayTime) {
902            html += ' • ' + displayTime;
903        }
904        // Add namespace badge for past events too
905        let eventNamespace = event.namespace || '';
906        if (!eventNamespace && event._namespace !== undefined) {
907            eventNamespace = event._namespace;
908        }
909        if (eventNamespace) {
910            html += ' <span class="event-namespace-badge" onclick="filterCalendarByNamespace(\'' + calId + '\', \'' + escapeHtml(eventNamespace) + '\')" style="cursor:pointer;" title="Click to filter by this namespace">' + escapeHtml(eventNamespace) + '</span>';
911        }
912        html += '</span>';
913        html += '</div>';
914
915        if (event.description) {
916            html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>';
917        }
918    }
919
920    html += '</div>'; // event-info
921
922    // Use stored namespace from event, fallback to _namespace, then passed namespace
923    let buttonNamespace = event.namespace || '';
924    if (!buttonNamespace && event._namespace !== undefined) {
925        buttonNamespace = event._namespace;
926    }
927    if (!buttonNamespace) {
928        buttonNamespace = namespace;
929    }
930
931    html += '<div class="event-actions-compact">';
932    html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">��️</button>';
933    html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>';
934    html += '</div>';
935
936    // Checkbox for tasks - ON THE FAR RIGHT
937    if (isTask) {
938        const checked = completed ? 'checked' : '';
939        html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">';
940    }
941
942    html += '</div>';
943
944    return html;
945};
946
947// Render description with rich content support
948window.renderDescription = function(description) {
949    if (!description) return '';
950
951    // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping)
952    // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00
953
954    let rendered = description;
955    const tokens = [];
956    let tokenIndex = 0;
957
958    // Convert DokuWiki image syntax {{image.jpg}} to tokens
959    rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
960        imagePath = imagePath.trim();
961        alt = alt ? alt.trim() : '';
962
963        let imageHtml;
964        // Handle external URLs
965        if (imagePath.match(/^https?:\/\//)) {
966            imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
967        } else {
968            // Handle internal DokuWiki images
969            const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
970            imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />';
971        }
972
973        const token = '\x00TOKEN' + tokenIndex + '\x00';
974        tokens[tokenIndex] = imageHtml;
975        tokenIndex++;
976        return token;
977    });
978
979    // Convert DokuWiki link syntax [[link|text]] to tokens
980    rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
981        link = link.trim();
982        text = text ? text.trim() : link;
983
984        let linkHtml;
985        // Handle external URLs
986        if (link.match(/^https?:\/\//)) {
987            linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
988        } else {
989            // Handle internal DokuWiki links with section anchors
990            const hashIndex = link.indexOf('#');
991            let pagePart = link;
992            let sectionPart = '';
993
994            if (hashIndex !== -1) {
995                pagePart = link.substring(0, hashIndex);
996                sectionPart = link.substring(hashIndex); // Includes the #
997            }
998
999            const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart;
1000            linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>';
1001        }
1002
1003        const token = '\x00TOKEN' + tokenIndex + '\x00';
1004        tokens[tokenIndex] = linkHtml;
1005        tokenIndex++;
1006        return token;
1007    });
1008
1009    // Convert markdown-style links [text](url) to tokens
1010    rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
1011        text = text.trim();
1012        url = url.trim();
1013
1014        let linkHtml;
1015        if (url.match(/^https?:\/\//)) {
1016            linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>';
1017        } else {
1018            linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>';
1019        }
1020
1021        const token = '\x00TOKEN' + tokenIndex + '\x00';
1022        tokens[tokenIndex] = linkHtml;
1023        tokenIndex++;
1024        return token;
1025    });
1026
1027    // Convert plain URLs to tokens
1028    rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
1029        const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>';
1030        const token = '\x00TOKEN' + tokenIndex + '\x00';
1031        tokens[tokenIndex] = linkHtml;
1032        tokenIndex++;
1033        return token;
1034    });
1035
1036    // NOW escape the remaining text (tokens are protected with null bytes)
1037    rendered = escapeHtml(rendered);
1038
1039    // Convert newlines to <br>
1040    rendered = rendered.replace(/\n/g, '<br>');
1041
1042    // DokuWiki text formatting (on escaped text)
1043    // Bold: **text** or __text__
1044    rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1045    rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>');
1046
1047    // Italic: //text//
1048    rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>');
1049
1050    // Strikethrough: <del>text</del>
1051    rendered = rendered.replace(/&lt;del&gt;(.+?)&lt;\/del&gt;/g, '<del>$1</del>');
1052
1053    // Monospace: ''text''
1054    rendered = rendered.replace(/&#39;&#39;(.+?)&#39;&#39;/g, '<code>$1</code>');
1055
1056    // Subscript: <sub>text</sub>
1057    rendered = rendered.replace(/&lt;sub&gt;(.+?)&lt;\/sub&gt;/g, '<sub>$1</sub>');
1058
1059    // Superscript: <sup>text</sup>
1060    rendered = rendered.replace(/&lt;sup&gt;(.+?)&lt;\/sup&gt;/g, '<sup>$1</sup>');
1061
1062    // Restore tokens (replace with actual HTML)
1063    for (let i = 0; i < tokens.length; i++) {
1064        const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g');
1065        rendered = rendered.replace(tokenPattern, tokens[i]);
1066    }
1067
1068    return rendered;
1069}
1070
1071// Open add event dialog
1072window.openAddEvent = function(calId, namespace, date) {
1073    const dialog = document.getElementById('dialog-' + calId);
1074    const form = document.getElementById('eventform-' + calId);
1075    const title = document.getElementById('dialog-title-' + calId);
1076    const dateField = document.getElementById('event-date-' + calId);
1077
1078    if (!dateField) {
1079        console.error('Date field not found! ID: event-date-' + calId);
1080        return;
1081    }
1082
1083    // Check if there's a filtered namespace active (only for regular calendars)
1084    const calendar = document.getElementById(calId);
1085    const filteredNamespace = calendar ? calendar.dataset.filteredNamespace : null;
1086
1087    // Use filtered namespace if available, otherwise use the passed namespace
1088    const effectiveNamespace = filteredNamespace || namespace;
1089
1090
1091    // Reset form
1092    form.reset();
1093    document.getElementById('event-id-' + calId).value = '';
1094
1095    // Store the effective namespace in a hidden field or data attribute
1096    form.dataset.effectiveNamespace = effectiveNamespace;
1097
1098    // Set namespace dropdown to effective namespace
1099    const namespaceSelect = document.getElementById('event-namespace-' + calId);
1100    if (namespaceSelect) {
1101        if (effectiveNamespace && effectiveNamespace !== '*' && effectiveNamespace.indexOf(';') === -1) {
1102            // Set to specific namespace if not wildcard or multi-namespace
1103            namespaceSelect.value = effectiveNamespace;
1104        } else {
1105            // Default to empty (default namespace) for wildcard/multi views
1106            namespaceSelect.value = '';
1107        }
1108    }
1109
1110    // Clear event namespace from previous edits
1111    delete form.dataset.eventNamespace;
1112
1113    // Set date - use local date, not UTC
1114    let defaultDate = date;
1115    if (!defaultDate) {
1116        // Get the currently displayed month from the calendar container
1117        const container = document.getElementById(calId);
1118        const displayedYear = parseInt(container.getAttribute('data-year'));
1119        const displayedMonth = parseInt(container.getAttribute('data-month'));
1120
1121
1122        if (displayedYear && displayedMonth) {
1123            // Use first day of the displayed month
1124            const year = displayedYear;
1125            const month = String(displayedMonth).padStart(2, '0');
1126            defaultDate = `${year}-${month}-01`;
1127        } else {
1128            // Fallback to today if attributes not found
1129            const today = new Date();
1130            const year = today.getFullYear();
1131            const month = String(today.getMonth() + 1).padStart(2, '0');
1132            const day = String(today.getDate()).padStart(2, '0');
1133            defaultDate = `${year}-${month}-${day}`;
1134        }
1135    }
1136    dateField.value = defaultDate;
1137    dateField.removeAttribute('data-original-date');
1138
1139    // Also set the end date field to the same default (user can change it)
1140    const endDateField = document.getElementById('event-end-date-' + calId);
1141    if (endDateField) {
1142        endDateField.value = ''; // Empty by default (single-day event)
1143        // Set min attribute to help the date picker open on the right month
1144        endDateField.setAttribute('min', defaultDate);
1145    }
1146
1147    // Set default color
1148    document.getElementById('event-color-' + calId).value = '#3498db';
1149
1150    // Initialize end time dropdown (disabled by default since no start time set)
1151    const endTimeField = document.getElementById('event-end-time-' + calId);
1152    if (endTimeField) {
1153        endTimeField.disabled = true;
1154        endTimeField.value = '';
1155    }
1156
1157    // Initialize namespace search
1158    initNamespaceSearch(calId);
1159
1160    // Set title
1161    title.textContent = 'Add Event';
1162
1163    // Show dialog
1164    dialog.style.display = 'flex';
1165
1166    // Focus title field
1167    setTimeout(() => {
1168        const titleField = document.getElementById('event-title-' + calId);
1169        if (titleField) titleField.focus();
1170    }, 100);
1171};
1172
1173// Edit event
1174window.editEvent = function(calId, eventId, date, namespace) {
1175    const params = new URLSearchParams({
1176        call: 'plugin_calendar',
1177        action: 'get_event',
1178        namespace: namespace,
1179        date: date,
1180        eventId: eventId
1181    });
1182
1183    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1184        method: 'POST',
1185        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1186        body: params.toString()
1187    })
1188    .then(r => r.json())
1189    .then(data => {
1190        if (data.success && data.event) {
1191            const event = data.event;
1192            const dialog = document.getElementById('dialog-' + calId);
1193            const title = document.getElementById('dialog-title-' + calId);
1194            const dateField = document.getElementById('event-date-' + calId);
1195            const form = document.getElementById('eventform-' + calId);
1196
1197            if (!dateField) {
1198                console.error('Date field not found when editing!');
1199                return;
1200            }
1201
1202            // Store the event's actual namespace for saving (important for namespace=* views)
1203            if (event.namespace !== undefined) {
1204                form.dataset.eventNamespace = event.namespace;
1205            }
1206
1207            // Populate form
1208            document.getElementById('event-id-' + calId).value = event.id;
1209            dateField.value = date;
1210            dateField.setAttribute('data-original-date', date);
1211
1212            const endDateField = document.getElementById('event-end-date-' + calId);
1213            endDateField.value = event.endDate || '';
1214            // Set min attribute to help date picker open on the start date's month
1215            endDateField.setAttribute('min', date);
1216
1217            document.getElementById('event-title-' + calId).value = event.title;
1218            document.getElementById('event-time-' + calId).value = event.time || '';
1219            document.getElementById('event-end-time-' + calId).value = event.endTime || '';
1220            document.getElementById('event-color-' + calId).value = event.color || '#3498db';
1221            document.getElementById('event-desc-' + calId).value = event.description || '';
1222            document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
1223
1224            // Update end time options based on start time
1225            if (event.time) {
1226                updateEndTimeOptions(calId);
1227            }
1228
1229            // Initialize namespace search
1230            initNamespaceSearch(calId);
1231
1232            // Set namespace fields if available
1233            const namespaceHidden = document.getElementById('event-namespace-' + calId);
1234            const namespaceSearch = document.getElementById('event-namespace-search-' + calId);
1235            if (namespaceHidden && event.namespace !== undefined) {
1236                namespaceHidden.value = event.namespace;
1237                if (namespaceSearch) {
1238                    namespaceSearch.value = event.namespace || '(default)';
1239                }
1240            }
1241
1242            title.textContent = 'Edit Event';
1243            dialog.style.display = 'flex';
1244        }
1245    })
1246    .catch(err => console.error('Error editing event:', err));
1247};
1248
1249// Delete event
1250window.deleteEvent = function(calId, eventId, date, namespace) {
1251    if (!confirm('Delete this event?')) return;
1252
1253    const params = new URLSearchParams({
1254        call: 'plugin_calendar',
1255        action: 'delete_event',
1256        namespace: namespace,
1257        date: date,
1258        eventId: eventId
1259    });
1260
1261    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1262        method: 'POST',
1263        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1264        body: params.toString()
1265    })
1266    .then(r => r.json())
1267    .then(data => {
1268        if (data.success) {
1269            // Extract year and month from date
1270            const [year, month] = date.split('-').map(Number);
1271
1272            // Reload calendar data via AJAX
1273            reloadCalendarData(calId, year, month, namespace);
1274        }
1275    })
1276    .catch(err => console.error('Error:', err));
1277};
1278
1279// Save event (add or edit)
1280window.saveEventCompact = function(calId, namespace) {
1281    const form = document.getElementById('eventform-' + calId);
1282
1283    // Get namespace from dropdown - this is what the user selected
1284    const namespaceSelect = document.getElementById('event-namespace-' + calId);
1285    const selectedNamespace = namespaceSelect ? namespaceSelect.value : '';
1286
1287    // ALWAYS use what the user selected in the dropdown
1288    // This allows changing namespace when editing
1289    const finalNamespace = selectedNamespace;
1290
1291    const eventId = document.getElementById('event-id-' + calId).value;
1292
1293    // eventNamespace is the ORIGINAL namespace (only used for finding/deleting old event)
1294    const originalNamespace = form.dataset.eventNamespace;
1295
1296
1297    const dateInput = document.getElementById('event-date-' + calId);
1298    const date = dateInput.value;
1299    const oldDate = dateInput.getAttribute('data-original-date') || date;
1300    const endDate = document.getElementById('event-end-date-' + calId).value;
1301    const title = document.getElementById('event-title-' + calId).value;
1302    const time = document.getElementById('event-time-' + calId).value;
1303    const endTime = document.getElementById('event-end-time-' + calId).value;
1304    const colorSelect = document.getElementById('event-color-' + calId);
1305    let color = colorSelect.value;
1306
1307    // Handle custom color
1308    if (color === 'custom') {
1309        color = colorSelect.dataset.customColor || document.getElementById('event-color-custom-' + calId).value;
1310    }
1311
1312    const description = document.getElementById('event-desc-' + calId).value;
1313    const isTask = document.getElementById('event-is-task-' + calId).checked;
1314    const completed = false; // New tasks are not completed
1315    const isRecurring = document.getElementById('event-recurring-' + calId).checked;
1316    const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value;
1317    const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value;
1318
1319    if (!title) {
1320        alert('Please enter a title');
1321        return;
1322    }
1323
1324    if (!date) {
1325        alert('Please select a date');
1326        return;
1327    }
1328
1329    const params = new URLSearchParams({
1330        call: 'plugin_calendar',
1331        action: 'save_event',
1332        namespace: finalNamespace,
1333        eventId: eventId,
1334        date: date,
1335        oldDate: oldDate,
1336        endDate: endDate,
1337        title: title,
1338        time: time,
1339        endTime: endTime,
1340        color: color,
1341        description: description,
1342        isTask: isTask ? '1' : '0',
1343        completed: completed ? '1' : '0',
1344        isRecurring: isRecurring ? '1' : '0',
1345        recurrenceType: recurrenceType,
1346        recurrenceEnd: recurrenceEnd
1347    });
1348
1349    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1350        method: 'POST',
1351        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1352        body: params.toString()
1353    })
1354    .then(r => r.json())
1355    .then(data => {
1356        if (data.success) {
1357            closeEventDialog(calId);
1358
1359            // For recurring events, do a full page reload to show all occurrences
1360            if (isRecurring) {
1361                location.reload();
1362                return;
1363            }
1364
1365            // Extract year and month from the NEW date (in case date was changed)
1366            const [year, month] = date.split('-').map(Number);
1367
1368            // Reload calendar data via AJAX to the month of the event
1369            reloadCalendarData(calId, year, month, namespace);
1370        } else {
1371            alert('Error: ' + (data.error || 'Unknown error'));
1372        }
1373    })
1374    .catch(err => {
1375        console.error('Error:', err);
1376        alert('Error saving event');
1377    });
1378};
1379
1380// Reload calendar data without page refresh
1381window.reloadCalendarData = function(calId, year, month, namespace) {
1382    const params = new URLSearchParams({
1383        call: 'plugin_calendar',
1384        action: 'load_month',
1385        year: year,
1386        month: month,
1387        namespace: namespace,
1388        _: new Date().getTime() // Cache buster
1389    });
1390
1391    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1392        method: 'POST',
1393        headers: {
1394            'Content-Type': 'application/x-www-form-urlencoded',
1395            'Cache-Control': 'no-cache, no-store, must-revalidate',
1396            'Pragma': 'no-cache'
1397        },
1398        body: params.toString()
1399    })
1400    .then(r => r.json())
1401    .then(data => {
1402        if (data.success) {
1403            const container = document.getElementById(calId);
1404
1405            // Check if this is a full calendar or just event panel
1406            if (container.classList.contains('calendar-compact-container')) {
1407                rebuildCalendar(calId, data.year, data.month, data.events, namespace);
1408            } else if (container.classList.contains('event-panel-standalone')) {
1409                rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
1410            }
1411        }
1412    })
1413    .catch(err => console.error('Error:', err));
1414};
1415
1416// Close event dialog
1417window.closeEventDialog = function(calId) {
1418    const dialog = document.getElementById('dialog-' + calId);
1419    dialog.style.display = 'none';
1420};
1421
1422// Escape HTML
1423window.escapeHtml = function(text) {
1424    const div = document.createElement('div');
1425    div.textContent = text;
1426    return div.innerHTML;
1427};
1428
1429// Highlight event when clicking on bar in calendar
1430window.highlightEvent = function(calId, eventId, date) {
1431    // Find the event item in the event list
1432    const eventList = document.querySelector('#' + calId + ' .event-list-compact');
1433    if (!eventList) return;
1434
1435    const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
1436    if (!eventItem) return;
1437
1438    // Remove previous highlights
1439    const previousHighlights = eventList.querySelectorAll('.event-highlighted');
1440    previousHighlights.forEach(el => el.classList.remove('event-highlighted'));
1441
1442    // Add highlight
1443    eventItem.classList.add('event-highlighted');
1444
1445    // Scroll to event
1446    eventItem.scrollIntoView({
1447        behavior: 'smooth',
1448        block: 'nearest',
1449        inline: 'nearest'
1450    });
1451
1452    // Remove highlight after 3 seconds
1453    setTimeout(() => {
1454        eventItem.classList.remove('event-highlighted');
1455    }, 3000);
1456};
1457
1458// Toggle recurring event options
1459window.toggleRecurringOptions = function(calId) {
1460    const checkbox = document.getElementById('event-recurring-' + calId);
1461    const options = document.getElementById('recurring-options-' + calId);
1462
1463    if (checkbox && options) {
1464        options.style.display = checkbox.checked ? 'block' : 'none';
1465    }
1466};
1467
1468// Close dialog on escape key
1469document.addEventListener('keydown', function(e) {
1470    if (e.key === 'Escape') {
1471        const dialogs = document.querySelectorAll('.event-dialog-compact');
1472        dialogs.forEach(dialog => {
1473            if (dialog.style.display === 'flex') {
1474                dialog.style.display = 'none';
1475            }
1476        });
1477    }
1478});
1479
1480// Event panel navigation
1481window.navEventPanel = function(calId, year, month, namespace) {
1482    const params = new URLSearchParams({
1483        call: 'plugin_calendar',
1484        action: 'load_month',
1485        year: year,
1486        month: month,
1487        namespace: namespace,
1488        _: new Date().getTime() // Cache buster
1489    });
1490
1491    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1492        method: 'POST',
1493        headers: {
1494            'Content-Type': 'application/x-www-form-urlencoded',
1495            'Cache-Control': 'no-cache, no-store, must-revalidate',
1496            'Pragma': 'no-cache'
1497        },
1498        body: params.toString()
1499    })
1500    .then(r => r.json())
1501    .then(data => {
1502        if (data.success) {
1503            rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
1504        }
1505    })
1506    .catch(err => console.error('Error:', err));
1507};
1508
1509// Rebuild event panel only
1510window.rebuildEventPanel = function(calId, year, month, events, namespace) {
1511    const container = document.getElementById(calId);
1512    const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
1513                       'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1514
1515    // Update month title in new compact header
1516    const monthTitle = container.querySelector('.panel-month-title');
1517    if (monthTitle) {
1518        monthTitle.textContent = monthNames[month - 1] + ' ' + year;
1519        monthTitle.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
1520        monthTitle.setAttribute('title', 'Click to jump to month');
1521    }
1522
1523    // Fallback: Update old header format if exists
1524    const oldHeader = container.querySelector('.panel-standalone-header h3, .calendar-month-picker');
1525    if (oldHeader && !monthTitle) {
1526        oldHeader.textContent = monthNames[month - 1] + ' ' + year + ' Events';
1527        oldHeader.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
1528    }
1529
1530    // Update nav buttons
1531    let prevMonth = month - 1;
1532    let prevYear = year;
1533    if (prevMonth < 1) {
1534        prevMonth = 12;
1535        prevYear--;
1536    }
1537
1538    let nextMonth = month + 1;
1539    let nextYear = year;
1540    if (nextMonth > 12) {
1541        nextMonth = 1;
1542        nextYear++;
1543    }
1544
1545    // Update new compact nav buttons
1546    const navBtns = container.querySelectorAll('.panel-nav-btn');
1547    if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
1548    if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
1549
1550    // Fallback for old nav buttons
1551    const oldNavBtns = container.querySelectorAll('.cal-nav-btn');
1552    if (oldNavBtns.length > 0 && navBtns.length === 0) {
1553        if (oldNavBtns[0]) oldNavBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
1554        if (oldNavBtns[1]) oldNavBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
1555    }
1556
1557    // Update Today button (works for both old and new)
1558    const todayBtn = container.querySelector('.panel-today-btn, .cal-today-btn, .cal-today-btn-compact');
1559    if (todayBtn) {
1560        todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`);
1561    }
1562
1563    // Rebuild event list
1564    const eventList = container.querySelector('.event-list-compact');
1565    if (eventList) {
1566        eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
1567    }
1568};
1569
1570// Open add event for panel
1571window.openAddEventPanel = function(calId, namespace) {
1572    const today = new Date();
1573    const year = today.getFullYear();
1574    const month = String(today.getMonth() + 1).padStart(2, '0');
1575    const day = String(today.getDate()).padStart(2, '0');
1576    const localDate = `${year}-${month}-${day}`;
1577    openAddEvent(calId, namespace, localDate);
1578};
1579
1580// Toggle task completion
1581window.toggleTaskComplete = function(calId, eventId, date, namespace, completed) {
1582    const params = new URLSearchParams({
1583        call: 'plugin_calendar',
1584        action: 'toggle_task',
1585        namespace: namespace,
1586        date: date,
1587        eventId: eventId,
1588        completed: completed ? '1' : '0'
1589    });
1590
1591    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1592        method: 'POST',
1593        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1594        body: params.toString()
1595    })
1596    .then(r => r.json())
1597    .then(data => {
1598        if (data.success) {
1599            const [year, month] = date.split('-').map(Number);
1600            reloadCalendarData(calId, year, month, namespace);
1601        }
1602    })
1603    .catch(err => console.error('Error toggling task:', err));
1604};
1605
1606// Make dialog draggable
1607window.makeDialogDraggable = function(calId) {
1608    const dialog = document.getElementById('dialog-content-' + calId);
1609    const handle = document.getElementById('drag-handle-' + calId);
1610
1611    if (!dialog || !handle) return;
1612
1613    let isDragging = false;
1614    let currentX;
1615    let currentY;
1616    let initialX;
1617    let initialY;
1618    let xOffset = 0;
1619    let yOffset = 0;
1620
1621    handle.addEventListener('mousedown', dragStart);
1622    document.addEventListener('mousemove', drag);
1623    document.addEventListener('mouseup', dragEnd);
1624
1625    function dragStart(e) {
1626        initialX = e.clientX - xOffset;
1627        initialY = e.clientY - yOffset;
1628        isDragging = true;
1629    }
1630
1631    function drag(e) {
1632        if (isDragging) {
1633            e.preventDefault();
1634            currentX = e.clientX - initialX;
1635            currentY = e.clientY - initialY;
1636            xOffset = currentX;
1637            yOffset = currentY;
1638            setTranslate(currentX, currentY, dialog);
1639        }
1640    }
1641
1642    function dragEnd(e) {
1643        initialX = currentX;
1644        initialY = currentY;
1645        isDragging = false;
1646    }
1647
1648    function setTranslate(xPos, yPos, el) {
1649        el.style.transform = `translate(${xPos}px, ${yPos}px)`;
1650    }
1651};
1652
1653// Initialize dialog draggability when opened (avoid duplicate declaration)
1654if (!window.calendarDraggabilityPatched) {
1655    window.calendarDraggabilityPatched = true;
1656
1657    const originalOpenAddEvent = openAddEvent;
1658    openAddEvent = function(calId, namespace, date) {
1659        originalOpenAddEvent(calId, namespace, date);
1660        setTimeout(() => makeDialogDraggable(calId), 100);
1661    };
1662
1663    const originalEditEvent = editEvent;
1664    editEvent = function(calId, eventId, date, namespace) {
1665        originalEditEvent(calId, eventId, date, namespace);
1666        setTimeout(() => makeDialogDraggable(calId), 100);
1667    };
1668}
1669
1670// Toggle expand/collapse for past events
1671window.togglePastEventExpand = function(element) {
1672    // Stop propagation to prevent any parent click handlers
1673    event.stopPropagation();
1674
1675    const meta = element.querySelector(".event-meta-compact");
1676    const desc = element.querySelector(".event-desc-compact");
1677
1678    // Toggle visibility
1679    if (meta.style.display === "none") {
1680        // Expand
1681        meta.style.display = "block";
1682        if (desc) desc.style.display = "block";
1683        element.classList.add("event-past-expanded");
1684    } else {
1685        // Collapse
1686        meta.style.display = "none";
1687        if (desc) desc.style.display = "none";
1688        element.classList.remove("event-past-expanded");
1689    }
1690};
1691
1692// Filter calendar by namespace when clicking namespace badge
1693document.addEventListener('click', function(e) {
1694    if (e.target.classList.contains('event-namespace-badge')) {
1695        const namespace = e.target.textContent;
1696        const eventItem = e.target.closest('.event-compact-item');
1697        const eventList = e.target.closest('.event-list-compact');
1698        const calendar = e.target.closest('.calendar-compact-container');
1699
1700        if (!eventList || !calendar) return;
1701
1702        const calId = calendar.id;
1703
1704        // Check if already filtered
1705        const isFiltered = eventList.classList.contains('namespace-filtered');
1706
1707        if (isFiltered && eventList.dataset.filterNamespace === namespace) {
1708            // Unfilter - show all
1709            eventList.classList.remove('namespace-filtered');
1710            delete eventList.dataset.filterNamespace;
1711            delete calendar.dataset.filteredNamespace;
1712            eventList.querySelectorAll('.event-compact-item').forEach(item => {
1713                item.style.display = '';
1714            });
1715
1716            // Update header to show "all namespaces"
1717            updateFilteredNamespaceDisplay(calId, null);
1718        } else {
1719            // Filter by this namespace
1720            eventList.classList.add('namespace-filtered');
1721            eventList.dataset.filterNamespace = namespace;
1722            calendar.dataset.filteredNamespace = namespace;
1723            eventList.querySelectorAll('.event-compact-item').forEach(item => {
1724                const itemBadge = item.querySelector('.event-namespace-badge');
1725                if (itemBadge && itemBadge.textContent === namespace) {
1726                    item.style.display = '';
1727                } else {
1728                    item.style.display = 'none';
1729                }
1730            });
1731
1732            // Update header to show filtered namespace
1733            updateFilteredNamespaceDisplay(calId, namespace);
1734        }
1735    }
1736});
1737
1738// Update the displayed filtered namespace in event list header
1739window.updateFilteredNamespaceDisplay = function(calId, namespace) {
1740    const calendar = document.getElementById(calId);
1741    if (!calendar) return;
1742
1743    const headerContent = calendar.querySelector('.event-list-header-content');
1744    if (!headerContent) return;
1745
1746    // Remove existing filter badge
1747    let filterBadge = headerContent.querySelector('.namespace-filter-badge');
1748    if (filterBadge) {
1749        filterBadge.remove();
1750    }
1751
1752    // Add new filter badge if filtering
1753    if (namespace) {
1754        filterBadge = document.createElement('span');
1755        filterBadge.className = 'namespace-badge namespace-filter-badge';
1756        filterBadge.innerHTML = escapeHtml(namespace) + ' <button class="filter-clear-inline" onclick="clearNamespaceFilter(\'' + calId + '\'); event.stopPropagation();">✕</button>';
1757        headerContent.appendChild(filterBadge);
1758    }
1759};
1760
1761// Clear namespace filter
1762window.clearNamespaceFilter = function(calId) {
1763
1764    const container = document.getElementById(calId);
1765    if (!container) {
1766        console.error('Calendar container not found:', calId);
1767        return;
1768    }
1769
1770    // Get current year and month
1771    const year = parseInt(container.dataset.year) || new Date().getFullYear();
1772    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
1773
1774    // Get original namespace (what the calendar was initialized with)
1775    const originalNamespace = container.dataset.originalNamespace || '';
1776
1777
1778    // Reload calendar with original namespace
1779    navCalendar(calId, year, month, originalNamespace);
1780};
1781
1782window.clearNamespaceFilterPanel = function(calId) {
1783
1784    const container = document.getElementById(calId);
1785    if (!container) {
1786        console.error('Event panel container not found:', calId);
1787        return;
1788    }
1789
1790    // Get current year and month from URL params or container
1791    const year = parseInt(container.dataset.year) || new Date().getFullYear();
1792    const month = parseInt(container.dataset.month) || (new Date().getMonth() + 1);
1793
1794    // Get original namespace (what the panel was initialized with)
1795    const originalNamespace = container.dataset.originalNamespace || '';
1796
1797
1798    // Reload event panel with original namespace
1799    navEventPanel(calId, year, month, originalNamespace);
1800};
1801
1802// Color picker functions
1803window.updateCustomColorPicker = function(calId) {
1804    const select = document.getElementById('event-color-' + calId);
1805    const picker = document.getElementById('event-color-custom-' + calId);
1806
1807    if (select.value === 'custom') {
1808        // Show color picker
1809        picker.style.display = 'inline-block';
1810        picker.click(); // Open color picker
1811    } else {
1812        // Hide color picker and sync value
1813        picker.style.display = 'none';
1814        picker.value = select.value;
1815    }
1816};
1817
1818function updateColorFromPicker(calId) {
1819    const select = document.getElementById('event-color-' + calId);
1820    const picker = document.getElementById('event-color-custom-' + calId);
1821
1822    // Set select to custom and update its underlying value
1823    select.value = 'custom';
1824    // Store the actual color value in a data attribute
1825    select.dataset.customColor = picker.value;
1826}
1827
1828// Toggle past events visibility
1829window.togglePastEvents = function(calId) {
1830    const content = document.getElementById('past-events-' + calId);
1831    const arrow = document.getElementById('past-arrow-' + calId);
1832
1833    if (!content || !arrow) {
1834        console.error('Past events elements not found for:', calId);
1835        return;
1836    }
1837
1838    // Check computed style instead of inline style
1839    const isHidden = window.getComputedStyle(content).display === 'none';
1840
1841    if (isHidden) {
1842        content.style.display = 'block';
1843        arrow.textContent = '▼';
1844    } else {
1845        content.style.display = 'none';
1846        arrow.textContent = '▶';
1847    }
1848};
1849
1850// Fuzzy match scoring function
1851window.fuzzyMatch = function(pattern, str) {
1852    pattern = pattern.toLowerCase();
1853    str = str.toLowerCase();
1854
1855    let patternIdx = 0;
1856    let score = 0;
1857    let consecutiveMatches = 0;
1858
1859    for (let i = 0; i < str.length; i++) {
1860        if (patternIdx < pattern.length && str[i] === pattern[patternIdx]) {
1861            score += 1 + consecutiveMatches;
1862            consecutiveMatches++;
1863            patternIdx++;
1864        } else {
1865            consecutiveMatches = 0;
1866        }
1867    }
1868
1869    // Return null if not all characters matched
1870    if (patternIdx !== pattern.length) {
1871        return null;
1872    }
1873
1874    // Bonus for exact match
1875    if (str === pattern) {
1876        score += 100;
1877    }
1878
1879    // Bonus for starts with
1880    if (str.startsWith(pattern)) {
1881        score += 50;
1882    }
1883
1884    return score;
1885};
1886
1887// Initialize namespace search for a calendar
1888window.initNamespaceSearch = function(calId) {
1889    const searchInput = document.getElementById('event-namespace-search-' + calId);
1890    const hiddenInput = document.getElementById('event-namespace-' + calId);
1891    const dropdown = document.getElementById('event-namespace-dropdown-' + calId);
1892    const dataElement = document.getElementById('namespaces-data-' + calId);
1893
1894    if (!searchInput || !hiddenInput || !dropdown || !dataElement) {
1895        return; // Elements not found
1896    }
1897
1898    let namespaces = [];
1899    try {
1900        namespaces = JSON.parse(dataElement.textContent);
1901    } catch (e) {
1902        console.error('Failed to parse namespaces data:', e);
1903        return;
1904    }
1905
1906    let selectedIndex = -1;
1907
1908    // Filter and show dropdown
1909    function filterNamespaces(query) {
1910        if (!query || query.trim() === '') {
1911            // Show all namespaces when empty
1912            hiddenInput.value = '';
1913            const results = namespaces.slice(0, 20); // Limit to 20
1914            showDropdown(results);
1915            return;
1916        }
1917
1918        // Fuzzy match and score
1919        const matches = [];
1920        for (let i = 0; i < namespaces.length; i++) {
1921            const score = fuzzyMatch(query, namespaces[i]);
1922            if (score !== null) {
1923                matches.push({ namespace: namespaces[i], score: score });
1924            }
1925        }
1926
1927        // Sort by score (descending)
1928        matches.sort((a, b) => b.score - a.score);
1929
1930        // Take top 20 results
1931        const results = matches.slice(0, 20).map(m => m.namespace);
1932        showDropdown(results);
1933    }
1934
1935    function showDropdown(results) {
1936        dropdown.innerHTML = '';
1937        selectedIndex = -1;
1938
1939        if (results.length === 0) {
1940            dropdown.style.display = 'none';
1941            return;
1942        }
1943
1944        // Add (default) option
1945        const defaultOption = document.createElement('div');
1946        defaultOption.className = 'namespace-option';
1947        defaultOption.textContent = '(default)';
1948        defaultOption.dataset.value = '';
1949        dropdown.appendChild(defaultOption);
1950
1951        results.forEach(ns => {
1952            const option = document.createElement('div');
1953            option.className = 'namespace-option';
1954            option.textContent = ns;
1955            option.dataset.value = ns;
1956            dropdown.appendChild(option);
1957        });
1958
1959        dropdown.style.display = 'block';
1960    }
1961
1962    function hideDropdown() {
1963        dropdown.style.display = 'none';
1964        selectedIndex = -1;
1965    }
1966
1967    function selectOption(namespace) {
1968        hiddenInput.value = namespace;
1969        searchInput.value = namespace || '(default)';
1970        hideDropdown();
1971    }
1972
1973    // Event listeners
1974    searchInput.addEventListener('input', function(e) {
1975        filterNamespaces(e.target.value);
1976    });
1977
1978    searchInput.addEventListener('focus', function(e) {
1979        filterNamespaces(e.target.value);
1980    });
1981
1982    searchInput.addEventListener('blur', function(e) {
1983        // Delay to allow click on dropdown
1984        setTimeout(hideDropdown, 200);
1985    });
1986
1987    searchInput.addEventListener('keydown', function(e) {
1988        const options = dropdown.querySelectorAll('.namespace-option');
1989
1990        if (e.key === 'ArrowDown') {
1991            e.preventDefault();
1992            selectedIndex = Math.min(selectedIndex + 1, options.length - 1);
1993            updateSelection(options);
1994        } else if (e.key === 'ArrowUp') {
1995            e.preventDefault();
1996            selectedIndex = Math.max(selectedIndex - 1, -1);
1997            updateSelection(options);
1998        } else if (e.key === 'Enter') {
1999            e.preventDefault();
2000            if (selectedIndex >= 0 && options[selectedIndex]) {
2001                selectOption(options[selectedIndex].dataset.value);
2002            }
2003        } else if (e.key === 'Escape') {
2004            hideDropdown();
2005        }
2006    });
2007
2008    function updateSelection(options) {
2009        options.forEach((opt, idx) => {
2010            if (idx === selectedIndex) {
2011                opt.classList.add('selected');
2012                opt.scrollIntoView({ block: 'nearest' });
2013            } else {
2014                opt.classList.remove('selected');
2015            }
2016        });
2017    }
2018
2019    // Click on dropdown option
2020    dropdown.addEventListener('mousedown', function(e) {
2021        if (e.target.classList.contains('namespace-option')) {
2022            selectOption(e.target.dataset.value);
2023        }
2024    });
2025};
2026
2027// Update end time options based on start time selection
2028window.updateEndTimeOptions = function(calId) {
2029    const startTimeSelect = document.getElementById('event-time-' + calId);
2030    const endTimeSelect = document.getElementById('event-end-time-' + calId);
2031
2032    if (!startTimeSelect || !endTimeSelect) return;
2033
2034    const startTime = startTimeSelect.value;
2035
2036    // If start time is empty (all day), disable end time
2037    if (!startTime) {
2038        endTimeSelect.disabled = true;
2039        endTimeSelect.value = '';
2040        return;
2041    }
2042
2043    // Enable end time select
2044    endTimeSelect.disabled = false;
2045
2046    // Convert start time to minutes
2047    const startMinutes = timeToMinutes(startTime);
2048
2049    // Get current end time value (to preserve if valid)
2050    const currentEndTime = endTimeSelect.value;
2051    const currentEndMinutes = currentEndTime ? timeToMinutes(currentEndTime) : 0;
2052
2053    // Filter options - show only times after start time
2054    const options = endTimeSelect.options;
2055    let firstValidOption = null;
2056    let currentStillValid = false;
2057
2058    for (let i = 0; i < options.length; i++) {
2059        const option = options[i];
2060        const optionValue = option.value;
2061
2062        if (optionValue === '') {
2063            // Keep "Same as start" option visible
2064            option.style.display = '';
2065            continue;
2066        }
2067
2068        const optionMinutes = timeToMinutes(optionValue);
2069
2070        if (optionMinutes > startMinutes) {
2071            // Show options after start time
2072            option.style.display = '';
2073            if (!firstValidOption) {
2074                firstValidOption = optionValue;
2075            }
2076            if (optionValue === currentEndTime) {
2077                currentStillValid = true;
2078            }
2079        } else {
2080            // Hide options before or equal to start time
2081            option.style.display = 'none';
2082        }
2083    }
2084
2085    // If current end time is now invalid, set a new one
2086    if (!currentStillValid || currentEndMinutes <= startMinutes) {
2087        // Try to set to 1 hour after start
2088        const [startHour, startMinute] = startTime.split(':').map(Number);
2089        let endHour = startHour + 1;
2090        let endMinute = startMinute;
2091
2092        if (endHour >= 24) {
2093            endHour = 23;
2094            endMinute = 45;
2095        }
2096
2097        const suggestedEndTime = String(endHour).padStart(2, '0') + ':' + String(endMinute).padStart(2, '0');
2098
2099        // Check if suggested time is in the list
2100        const suggestedExists = Array.from(options).some(opt => opt.value === suggestedEndTime);
2101
2102        if (suggestedExists) {
2103            endTimeSelect.value = suggestedEndTime;
2104        } else if (firstValidOption) {
2105            // Use first valid option
2106            endTimeSelect.value = firstValidOption;
2107        } else {
2108            // No valid options (shouldn't happen, but just in case)
2109            endTimeSelect.value = '';
2110        }
2111    }
2112};
2113
2114// Check for time conflicts between events on the same date
2115window.checkTimeConflicts = function(events, currentEventId) {
2116    const conflicts = [];
2117
2118    // Group events by date
2119    const eventsByDate = {};
2120    for (const [date, dateEvents] of Object.entries(events)) {
2121        if (!Array.isArray(dateEvents)) continue;
2122
2123        dateEvents.forEach(evt => {
2124            if (!evt.time || evt.id === currentEventId) return; // Skip all-day events and current event
2125
2126            if (!eventsByDate[date]) eventsByDate[date] = [];
2127            eventsByDate[date].push(evt);
2128        });
2129    }
2130
2131    // Check for overlaps on each date
2132    for (const [date, dateEvents] of Object.entries(eventsByDate)) {
2133        for (let i = 0; i < dateEvents.length; i++) {
2134            for (let j = i + 1; j < dateEvents.length; j++) {
2135                const evt1 = dateEvents[i];
2136                const evt2 = dateEvents[j];
2137
2138                if (eventsOverlap(evt1, evt2)) {
2139                    // Mark both events as conflicting
2140                    if (!evt1.hasConflict) evt1.hasConflict = true;
2141                    if (!evt2.hasConflict) evt2.hasConflict = true;
2142
2143                    // Store conflict info
2144                    if (!evt1.conflictsWith) evt1.conflictsWith = [];
2145                    if (!evt2.conflictsWith) evt2.conflictsWith = [];
2146
2147                    evt1.conflictsWith.push({id: evt2.id, title: evt2.title, time: evt2.time, endTime: evt2.endTime});
2148                    evt2.conflictsWith.push({id: evt1.id, title: evt1.title, time: evt1.time, endTime: evt1.endTime});
2149                }
2150            }
2151        }
2152    }
2153
2154    return events;
2155};
2156
2157// Check if two events overlap in time
2158function eventsOverlap(evt1, evt2) {
2159    if (!evt1.time || !evt2.time) return false; // All-day events don't conflict
2160
2161    const start1 = evt1.time;
2162    const end1 = evt1.endTime || evt1.time; // If no end time, treat as same as start
2163
2164    const start2 = evt2.time;
2165    const end2 = evt2.endTime || evt2.time;
2166
2167    // Convert to minutes for easier comparison
2168    const start1Mins = timeToMinutes(start1);
2169    const end1Mins = timeToMinutes(end1);
2170    const start2Mins = timeToMinutes(start2);
2171    const end2Mins = timeToMinutes(end2);
2172
2173    // Check for overlap
2174    // Events overlap if: start1 < end2 AND start2 < end1
2175    return start1Mins < end2Mins && start2Mins < end1Mins;
2176}
2177
2178// Convert HH:MM time to minutes since midnight
2179function timeToMinutes(timeStr) {
2180    const [hours, minutes] = timeStr.split(':').map(Number);
2181    return hours * 60 + minutes;
2182}
2183
2184// Format time range for display
2185window.formatTimeRange = function(startTime, endTime) {
2186    if (!startTime) return '';
2187
2188    const formatTime = (timeStr) => {
2189        const [hour24, minute] = timeStr.split(':').map(Number);
2190        const hour12 = hour24 === 0 ? 12 : (hour24 > 12 ? hour24 - 12 : hour24);
2191        const ampm = hour24 < 12 ? 'AM' : 'PM';
2192        return hour12 + ':' + String(minute).padStart(2, '0') + ' ' + ampm;
2193    };
2194
2195    if (!endTime || endTime === startTime) {
2196        return formatTime(startTime);
2197    }
2198
2199    return formatTime(startTime) + ' - ' + formatTime(endTime);
2200};
2201
2202// Show custom conflict tooltip
2203window.showConflictTooltip = function(badgeElement) {
2204    // Remove any existing tooltip
2205    hideConflictTooltip();
2206
2207    // Get conflict data
2208    const conflictsJson = badgeElement.getAttribute('data-conflicts');
2209    if (!conflictsJson) return;
2210
2211    let conflicts;
2212    try {
2213        conflicts = JSON.parse(conflictsJson);
2214    } catch (e) {
2215        console.error('Failed to parse conflicts:', e);
2216        return;
2217    }
2218
2219    // Create tooltip
2220    const tooltip = document.createElement('div');
2221    tooltip.id = 'conflict-tooltip';
2222    tooltip.className = 'conflict-tooltip';
2223
2224    // Build content
2225    let html = '<div class="conflict-tooltip-header">⚠️ Time Conflicts</div>';
2226    html += '<div class="conflict-tooltip-body">';
2227    conflicts.forEach(conflict => {
2228        html += '<div class="conflict-item">• ' + escapeHtml(conflict) + '</div>';
2229    });
2230    html += '</div>';
2231
2232    tooltip.innerHTML = html;
2233    document.body.appendChild(tooltip);
2234
2235    // Position tooltip
2236    const rect = badgeElement.getBoundingClientRect();
2237    const tooltipRect = tooltip.getBoundingClientRect();
2238
2239    // Position above the badge, centered
2240    let left = rect.left + (rect.width / 2) - (tooltipRect.width / 2);
2241    let top = rect.top - tooltipRect.height - 8;
2242
2243    // Keep tooltip within viewport
2244    if (left < 10) left = 10;
2245    if (left + tooltipRect.width > window.innerWidth - 10) {
2246        left = window.innerWidth - tooltipRect.width - 10;
2247    }
2248    if (top < 10) {
2249        // If not enough room above, show below
2250        top = rect.bottom + 8;
2251    }
2252
2253    tooltip.style.left = left + 'px';
2254    tooltip.style.top = top + 'px';
2255    tooltip.style.opacity = '1';
2256};
2257
2258// Hide conflict tooltip
2259window.hideConflictTooltip = function() {
2260    const tooltip = document.getElementById('conflict-tooltip');
2261    if (tooltip) {
2262        tooltip.remove();
2263    }
2264};
2265
2266// Filter events by search term
2267window.filterEvents = function(calId, searchTerm) {
2268    const eventList = document.getElementById('eventlist-' + calId);
2269    const searchClear = document.getElementById('search-clear-' + calId);
2270
2271    if (!eventList) return;
2272
2273    // Show/hide clear button
2274    if (searchClear) {
2275        searchClear.style.display = searchTerm ? 'block' : 'none';
2276    }
2277
2278    searchTerm = searchTerm.toLowerCase().trim();
2279
2280    // Get all event items
2281    const eventItems = eventList.querySelectorAll('.event-compact-item');
2282    let visibleCount = 0;
2283    let hiddenPastCount = 0;
2284
2285    eventItems.forEach(item => {
2286        const title = item.querySelector('.event-title-compact');
2287        const description = item.querySelector('.event-desc-compact');
2288        const dateTime = item.querySelector('.event-date-time');
2289
2290        // Build searchable text
2291        let searchableText = '';
2292        if (title) searchableText += title.textContent.toLowerCase() + ' ';
2293        if (description) searchableText += description.textContent.toLowerCase() + ' ';
2294        if (dateTime) searchableText += dateTime.textContent.toLowerCase() + ' ';
2295
2296        // Check if matches search
2297        const matches = !searchTerm || searchableText.includes(searchTerm);
2298
2299        if (matches) {
2300            item.style.display = '';
2301            visibleCount++;
2302        } else {
2303            item.style.display = 'none';
2304            // Check if this is a past event
2305            if (item.classList.contains('event-past') || item.classList.contains('event-completed')) {
2306                hiddenPastCount++;
2307            }
2308        }
2309    });
2310
2311    // Update past events toggle if it exists
2312    const pastToggle = eventList.querySelector('.past-events-toggle');
2313    const pastLabel = eventList.querySelector('.past-events-label');
2314    const pastContent = document.getElementById('past-events-' + calId);
2315
2316    if (pastToggle && pastLabel && pastContent) {
2317        const visiblePastEvents = pastContent.querySelectorAll('.event-compact-item:not([style*="display: none"])');
2318        const totalPastVisible = visiblePastEvents.length;
2319
2320        if (totalPastVisible > 0) {
2321            pastLabel.textContent = `Past Events (${totalPastVisible})`;
2322            pastToggle.style.display = '';
2323        } else {
2324            pastToggle.style.display = 'none';
2325        }
2326    }
2327
2328    // Show "no results" message if nothing visible
2329    let noResultsMsg = eventList.querySelector('.no-search-results');
2330    if (visibleCount === 0 && searchTerm) {
2331        if (!noResultsMsg) {
2332            noResultsMsg = document.createElement('p');
2333            noResultsMsg.className = 'no-search-results no-events-msg';
2334            noResultsMsg.textContent = 'No events match your search';
2335            eventList.appendChild(noResultsMsg);
2336        }
2337        noResultsMsg.style.display = 'block';
2338    } else if (noResultsMsg) {
2339        noResultsMsg.style.display = 'none';
2340    }
2341};
2342
2343// Clear event search
2344window.clearEventSearch = function(calId) {
2345    const searchInput = document.getElementById('event-search-' + calId);
2346    if (searchInput) {
2347        searchInput.value = '';
2348        filterEvents(calId, '');
2349        searchInput.focus();
2350    }
2351};
2352
2353// End of calendar plugin JavaScript
2354