xref: /plugin/calendar/script.js (revision 87ac9bf3391b3f7059f4ccd6abc619e9db5fad8d)
1/**
2 * DokuWiki Compact Calendar Plugin JavaScript
3 */
4
5// Navigate to different month
6function navCalendar(calId, year, month, namespace) {
7    const params = new URLSearchParams({
8        call: 'plugin_calendar',
9        action: 'load_month',
10        year: year,
11        month: month,
12        namespace: namespace,
13        _: new Date().getTime() // Cache buster
14    });
15
16    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
17        method: 'POST',
18        headers: {
19            'Content-Type': 'application/x-www-form-urlencoded',
20            'Cache-Control': 'no-cache, no-store, must-revalidate',
21            'Pragma': 'no-cache'
22        },
23        body: params.toString()
24    })
25    .then(r => r.json())
26    .then(data => {
27        console.log('Month navigation data:', data);
28        if (data.success) {
29            console.log('Rebuilding calendar for', year, month, 'with', Object.keys(data.events || {}).length, 'date entries');
30            rebuildCalendar(calId, data.year, data.month, data.events, namespace);
31        } else {
32            console.error('Failed to load month:', data.error);
33        }
34    })
35    .catch(err => {
36        console.error('Error loading month:', err);
37    });
38}
39
40// Jump to current month
41function jumpToToday(calId, namespace) {
42    const today = new Date();
43    const year = today.getFullYear();
44    const month = today.getMonth() + 1; // JavaScript months are 0-indexed
45    navCalendar(calId, year, month, namespace);
46}
47
48// Jump to today for event panel
49function jumpTodayPanel(calId, namespace) {
50    const today = new Date();
51    const year = today.getFullYear();
52    const month = today.getMonth() + 1;
53    navEventPanel(calId, year, month, namespace);
54}
55
56// Open month picker dialog
57function openMonthPicker(calId, currentYear, currentMonth, namespace) {
58    console.log('openMonthPicker called:', calId, currentYear, currentMonth, namespace);
59
60    const overlay = document.getElementById('month-picker-overlay-' + calId);
61    console.log('Overlay element:', overlay);
62
63    const monthSelect = document.getElementById('month-picker-month-' + calId);
64    console.log('Month select:', monthSelect);
65
66    const yearSelect = document.getElementById('month-picker-year-' + calId);
67    console.log('Year select:', yearSelect);
68
69    if (!overlay) {
70        console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId);
71        return;
72    }
73
74    if (!monthSelect || !yearSelect) {
75        console.error('Select elements not found!');
76        return;
77    }
78
79    // Set current values
80    monthSelect.value = currentMonth;
81    yearSelect.value = currentYear;
82
83    // Show overlay
84    overlay.style.display = 'flex';
85    console.log('Overlay display set to flex');
86}
87
88// Open month picker dialog for event panel
89function openMonthPickerPanel(calId, currentYear, currentMonth, namespace) {
90    openMonthPicker(calId, currentYear, currentMonth, namespace);
91}
92
93// Close month picker dialog
94function closeMonthPicker(calId) {
95    const overlay = document.getElementById('month-picker-overlay-' + calId);
96    overlay.style.display = 'none';
97}
98
99// Jump to selected month
100function jumpToSelectedMonth(calId, namespace) {
101    const monthSelect = document.getElementById('month-picker-month-' + calId);
102    const yearSelect = document.getElementById('month-picker-year-' + calId);
103
104    const month = parseInt(monthSelect.value);
105    const year = parseInt(yearSelect.value);
106
107    closeMonthPicker(calId);
108
109    // Check if this is a calendar or event panel
110    const container = document.getElementById(calId);
111    if (container && container.classList.contains('event-panel-standalone')) {
112        navEventPanel(calId, year, month, namespace);
113    } else {
114        navCalendar(calId, year, month, namespace);
115    }
116}
117
118// Rebuild calendar grid after navigation
119function rebuildCalendar(calId, year, month, events, namespace) {
120    const container = document.getElementById(calId);
121    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
122                       'July', 'August', 'September', 'October', 'November', 'December'];
123
124    // Update embedded events data
125    let eventsDataEl = document.getElementById('events-data-' + calId);
126    if (eventsDataEl) {
127        eventsDataEl.textContent = JSON.stringify(events);
128    } else {
129        eventsDataEl = document.createElement('script');
130        eventsDataEl.type = 'application/json';
131        eventsDataEl.id = 'events-data-' + calId;
132        eventsDataEl.textContent = JSON.stringify(events);
133        container.appendChild(eventsDataEl);
134    }
135
136    // Update header
137    const header = container.querySelector('.calendar-compact-header h3');
138    header.textContent = monthNames[month - 1] + ' ' + year;
139
140    // Update nav buttons
141    let prevMonth = month - 1;
142    let prevYear = year;
143    if (prevMonth < 1) {
144        prevMonth = 12;
145        prevYear--;
146    }
147
148    let nextMonth = month + 1;
149    let nextYear = year;
150    if (nextMonth > 12) {
151        nextMonth = 1;
152        nextYear++;
153    }
154
155    const navBtns = container.querySelectorAll('.cal-nav-btn');
156    navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
157    navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
158
159    // Rebuild calendar grid
160    const tbody = container.querySelector('.calendar-compact-grid tbody');
161    const firstDay = new Date(year, month - 1, 1);
162    const daysInMonth = new Date(year, month, 0).getDate();
163    const dayOfWeek = firstDay.getDay();
164
165    // Calculate month boundaries
166    const monthStart = new Date(year, month - 1, 1);
167    const monthEnd = new Date(year, month - 1, daysInMonth);
168
169    // Build a map of all events with their date ranges
170    const eventRanges = {};
171    for (const [dateKey, dayEvents] of Object.entries(events)) {
172        // Only process events that could possibly overlap with this month/year
173        const dateYear = parseInt(dateKey.split('-')[0]);
174        const dateMonth = parseInt(dateKey.split('-')[1]);
175
176        // Skip events from completely different years (unless they're very long multi-day events)
177        if (Math.abs(dateYear - year) > 1) {
178            continue;
179        }
180
181        for (const evt of dayEvents) {
182            const startDate = dateKey;
183            const endDate = evt.endDate || dateKey;
184
185            // Check if event overlaps with current month
186            const eventStart = new Date(startDate + 'T00:00:00');
187            const eventEnd = new Date(endDate + 'T00:00:00');
188
189            // Skip if event doesn't overlap with current month
190            if (eventEnd < monthStart || eventStart > monthEnd) {
191                continue;
192            }
193
194            // Create entry for each day the event spans
195            const start = new Date(startDate + 'T00:00:00');
196            const end = new Date(endDate + 'T00:00:00');
197            const current = new Date(start);
198
199            while (current <= end) {
200                const currentKey = current.toISOString().split('T')[0];
201
202                // Check if this date is in current month
203                const currentDate = new Date(currentKey + 'T00:00:00');
204                if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) {
205                    if (!eventRanges[currentKey]) {
206                        eventRanges[currentKey] = [];
207                    }
208
209                    // Add event with span information
210                    const eventCopy = {...evt};
211                    eventCopy._span_start = startDate;
212                    eventCopy._span_end = endDate;
213                    eventCopy._is_first_day = (currentKey === startDate);
214                    eventCopy._is_last_day = (currentKey === endDate);
215                    eventCopy._original_date = dateKey;
216
217                    // Check if event continues from previous month or to next month
218                    eventCopy._continues_from_prev = (eventStart < monthStart);
219                    eventCopy._continues_to_next = (eventEnd > monthEnd);
220
221                    eventRanges[currentKey].push(eventCopy);
222                }
223
224                current.setDate(current.getDate() + 1);
225            }
226        }
227    }
228
229    let html = '';
230    let currentDay = 1;
231    const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
232
233    for (let row = 0; row < rowCount; row++) {
234        html += '<tr>';
235        for (let col = 0; col < 7; col++) {
236            if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
237                html += '<td class="cal-empty"></td>';
238            } else {
239                const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
240
241                // Get today's date in local timezone
242                const todayObj = new Date();
243                const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`;
244
245                const isToday = dateKey === today;
246                const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0;
247
248                let classes = 'cal-day';
249                if (isToday) classes += ' cal-today';
250                if (hasEvents) classes += ' cal-has-events';
251
252                html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`;
253                html += `<span class="day-num">${currentDay}</span>`;
254
255                if (hasEvents) {
256                    // Sort events by time (no time first, then by time)
257                    const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => {
258                        const timeA = a.time || '';
259                        const timeB = b.time || '';
260
261                        // Events without time go first
262                        if (!timeA && timeB) return -1;
263                        if (timeA && !timeB) return 1;
264                        if (!timeA && !timeB) return 0;
265
266                        // Sort by time
267                        return timeA.localeCompare(timeB);
268                    });
269
270                    // Show colored stacked bars for each event
271                    html += '<div class="event-indicators">';
272                    for (const evt of sortedEvents) {
273                        const eventId = evt.id || '';
274                        const eventColor = evt.color || '#3498db';
275                        const eventTime = evt.time || '';
276                        const eventTitle = evt.title || 'Event';
277                        const originalDate = evt._original_date || dateKey;
278                        const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true;
279                        const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true;
280
281                        let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
282
283                        // Add classes for multi-day spanning
284                        if (!isFirstDay) barClass += ' event-bar-continues';
285                        if (!isLastDay) barClass += ' event-bar-continuing';
286
287                        html += `<span class="event-bar ${barClass}" `;
288                        html += `style="background: ${eventColor};" `;
289                        html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `;
290                        html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');"></span>`;
291                    }
292                    html += '</div>';
293                }
294
295                html += '</td>';
296                currentDay++;
297            }
298        }
299        html += '</tr>';
300    }
301
302    tbody.innerHTML = html;
303
304    // Rebuild event list - show events that overlap with current month
305    const currentMonthEvents = {};
306
307    for (const [dateKey, dayEvents] of Object.entries(events)) {
308        for (const event of dayEvents) {
309            const startDate = dateKey;
310            const endDate = event.endDate || dateKey;
311
312            // Check if event overlaps with current month
313            // Event starts before month ends AND ends after month starts
314            const eventStartObj = new Date(startDate);
315            const eventEndObj = new Date(endDate);
316            const monthFirstDay = new Date(year, month - 1, 1);
317            const monthLastDay = new Date(year, month - 1, daysInMonth);
318
319            // Include if event overlaps this month at all
320            if (eventEndObj >= monthFirstDay && eventStartObj <= monthLastDay) {
321                if (!currentMonthEvents[dateKey]) {
322                    currentMonthEvents[dateKey] = [];
323                }
324                currentMonthEvents[dateKey].push(event);
325            }
326        }
327    }
328
329    const eventList = container.querySelector('.event-list-compact');
330    eventList.innerHTML = renderEventListFromData(currentMonthEvents, calId, namespace);
331
332    // Update title
333    const title = container.querySelector('#eventlist-title-' + calId);
334    title.textContent = 'Events';
335}
336
337// Render event list from data
338function renderEventListFromData(events, calId, namespace, year, month) {
339    if (!events || Object.keys(events).length === 0) {
340        return '<p class="no-events-msg">No events this month</p>';
341    }
342
343    let html = '';
344    const sortedDates = Object.keys(events).sort();
345
346    // Filter events to only current month if year/month provided
347    const monthStart = year && month ? new Date(year, month - 1, 1) : null;
348    const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null;
349
350    for (const dateKey of sortedDates) {
351        // Skip events not in current month if filtering
352        if (monthStart && monthEnd) {
353            const eventDate = new Date(dateKey + 'T00:00:00');
354            if (eventDate < monthStart || eventDate > monthEnd) {
355                continue;
356            }
357        }
358
359        const dayEvents = events[dateKey];
360        for (const event of dayEvents) {
361            html += renderEventItem(event, dateKey, calId, namespace);
362        }
363    }
364
365    if (!html) {
366        return '<p class="no-events-msg">No events this month</p>';
367    }
368
369    return html;
370}
371
372// Show day popup with events when clicking a date
373function showDayPopup(calId, date, namespace) {
374    // Get events for this calendar
375    const eventsDataEl = document.getElementById('events-data-' + calId);
376    let events = {};
377
378    if (eventsDataEl) {
379        try {
380            events = JSON.parse(eventsDataEl.textContent);
381        } catch (e) {
382            console.error('Failed to parse events data:', e);
383        }
384    }
385
386    const dayEvents = events[date] || [];
387    const dateObj = new Date(date + 'T00:00:00');
388    const displayDate = dateObj.toLocaleDateString('en-US', {
389        weekday: 'long',
390        month: 'long',
391        day: 'numeric',
392        year: 'numeric'
393    });
394
395    // Create popup
396    let popup = document.getElementById('day-popup-' + calId);
397    if (!popup) {
398        popup = document.createElement('div');
399        popup.id = 'day-popup-' + calId;
400        popup.className = 'day-popup';
401        document.body.appendChild(popup);
402    }
403
404    let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>';
405    html += '<div class="day-popup-content">';
406    html += '<div class="day-popup-header">';
407    html += '<h4>' + displayDate + '</h4>';
408    html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>';
409    html += '</div>';
410
411    html += '<div class="day-popup-body">';
412
413    if (dayEvents.length === 0) {
414        html += '<p class="no-events-msg">No events on this day</p>';
415    } else {
416        html += '<div class="popup-events-list">';
417        dayEvents.forEach(event => {
418            const color = event.color || '#3498db';
419            html += '<div class="popup-event-item">';
420            html += '<div class="event-color-bar" style="background: ' + color + ';"></div>';
421            html += '<div class="popup-event-content">';
422            html += '<div class="popup-event-title">' + escapeHtml(event.title) + '</div>';
423            if (event.time) {
424                html += '<div class="popup-event-time">�� ' + escapeHtml(event.time) + '</div>';
425            }
426            if (event.description) {
427                html += '<div class="popup-event-desc">' + escapeHtml(event.description).replace(/\n/g, '<br>') + '</div>';
428            }
429            html += '<div class="popup-event-actions">';
430            html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\'); closeDayPopup(\'' + calId + '\')">Edit</button>';
431            html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\'); closeDayPopup(\'' + calId + '\')">Delete</button>';
432            html += '</div>';
433            html += '</div></div>';
434        });
435        html += '</div>';
436    }
437
438    html += '</div>';
439
440    html += '<div class="day-popup-footer">';
441    html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>';
442    html += '</div>';
443
444    html += '</div>';
445
446    popup.innerHTML = html;
447    popup.style.display = 'flex';
448}
449
450// Close day popup
451function closeDayPopup(calId) {
452    const popup = document.getElementById('day-popup-' + calId);
453    if (popup) {
454        popup.style.display = 'none';
455    }
456}
457
458// Show events for a specific day (for event list panel)
459function showDayEvents(calId, date, namespace) {
460    const params = new URLSearchParams({
461        call: 'plugin_calendar',
462        action: 'load_month',
463        year: date.split('-')[0],
464        month: parseInt(date.split('-')[1]),
465        namespace: namespace
466    });
467
468    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
469        method: 'POST',
470        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
471        body: params.toString()
472    })
473    .then(r => r.json())
474    .then(data => {
475        if (data.success) {
476            const eventList = document.getElementById('eventlist-' + calId);
477            const events = data.events;
478            const title = document.getElementById('eventlist-title-' + calId);
479
480            const dateObj = new Date(date + 'T00:00:00');
481            const displayDate = dateObj.toLocaleDateString('en-US', {
482                weekday: 'short',
483                month: 'short',
484                day: 'numeric'
485            });
486
487            title.textContent = 'Events - ' + displayDate;
488
489            // Filter events for this day
490            const dayEvents = events[date] || [];
491
492            if (dayEvents.length === 0) {
493                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>';
494            } else {
495                let html = '';
496                dayEvents.forEach(event => {
497                    html += renderEventItem(event, date, calId, namespace);
498                });
499                eventList.innerHTML = html;
500            }
501        }
502    })
503    .catch(err => console.error('Error:', err));
504}
505
506// Render a single event item
507function renderEventItem(event, date, calId, namespace) {
508    // Format date display with day of week
509    const dateObj = new Date(date + 'T00:00:00');
510    const displayDate = dateObj.toLocaleDateString('en-US', {
511        weekday: 'short',
512        month: 'short',
513        day: 'numeric'
514    });
515
516    // Convert to 12-hour format
517    let displayTime = '';
518    if (event.time) {
519        const timeParts = event.time.split(':');
520        if (timeParts.length === 2) {
521            let hour = parseInt(timeParts[0]);
522            const minute = timeParts[1];
523            const ampm = hour >= 12 ? 'PM' : 'AM';
524            hour = hour % 12 || 12;
525            displayTime = hour + ':' + minute + ' ' + ampm;
526        } else {
527            displayTime = event.time;
528        }
529    }
530
531    // Multi-day indicator
532    let multiDay = '';
533    if (event.endDate && event.endDate !== date) {
534        const endObj = new Date(event.endDate + 'T00:00:00');
535        multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
536            weekday: 'short',
537            month: 'short',
538            day: 'numeric'
539        });
540    }
541
542    const completedClass = event.completed ? ' event-completed' : '';
543    const color = event.color || '#3498db';
544    const isTask = event.isTask || false;
545    const completed = event.completed || false;
546
547    let html = '<div class="event-compact-item' + completedClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';">';
548
549    html += '<div class="event-info">';
550    html += '<div class="event-title-row">';
551    html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>';
552    html += '</div>';
553
554    html += '<div class="event-meta-compact">';
555    html += '<span class="event-date-time">' + displayDate + multiDay;
556    if (displayTime) {
557        html += ' • ' + displayTime;
558    }
559    html += '</span>';
560    html += '</div>';
561
562    if (event.description) {
563        html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>';
564    }
565
566    html += '</div>'; // event-info
567
568    html += '<div class="event-actions-compact">';
569    html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\')">��️</button>';
570    html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\')">✏️</button>';
571    html += '</div>';
572
573    // Checkbox for tasks - ON THE FAR RIGHT
574    if (isTask) {
575        const checked = completed ? 'checked' : '';
576        html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\', this.checked)">';
577    }
578
579    html += '</div>';
580
581    return html;
582}
583
584// Render description with rich content support
585function renderDescription(description) {
586    if (!description) return '';
587
588    let rendered = escapeHtml(description);
589
590    // Convert newlines to <br>
591    rendered = rendered.replace(/\n/g, '<br>');
592
593    // Convert DokuWiki image syntax {{image.jpg}} to HTML
594    rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
595        imagePath = imagePath.trim();
596        alt = alt ? alt.trim() : '';
597
598        // Handle external URLs
599        if (imagePath.match(/^https?:\/\//)) {
600            return '<img src="' + imagePath + '" alt="' + alt + '" class="event-image" />';
601        }
602
603        // Handle internal DokuWiki images
604        const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
605        return '<img src="' + imageUrl + '" alt="' + alt + '" class="event-image" />';
606    });
607
608    // Convert DokuWiki link syntax [[link|text]] to HTML
609    rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
610        link = link.trim();
611        text = text ? text.trim() : link;
612
613        // Handle external URLs
614        if (link.match(/^https?:\/\//)) {
615            return '<a href="' + link + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
616        }
617
618        // Handle internal DokuWiki links with section anchors
619        // Split page and section (e.g., "page#section" or "namespace:page#section")
620        const hashIndex = link.indexOf('#');
621        let pagePart = link;
622        let sectionPart = '';
623
624        if (hashIndex !== -1) {
625            pagePart = link.substring(0, hashIndex);
626            sectionPart = link.substring(hashIndex); // Includes the #
627        }
628
629        // Build URL with properly encoded page and unencoded section anchor
630        const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart;
631        return '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>';
632    });
633
634    // Convert markdown-style links [text](url) to HTML
635    rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
636        text = text.trim();
637        url = url.trim();
638
639        if (url.match(/^https?:\/\//)) {
640            return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
641        }
642
643        return '<a href="' + url + '">' + text + '</a>';
644    });
645
646    // Convert plain URLs to clickable links
647    rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
648        return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
649    });
650
651    return rendered;
652}
653
654// Open add event dialog
655function openAddEvent(calId, namespace, date) {
656    const dialog = document.getElementById('dialog-' + calId);
657    const form = document.getElementById('eventform-' + calId);
658    const title = document.getElementById('dialog-title-' + calId);
659    const dateField = document.getElementById('event-date-' + calId);
660
661    if (!dateField) {
662        console.error('Date field not found! ID: event-date-' + calId);
663        return;
664    }
665
666    // Reset form
667    form.reset();
668    document.getElementById('event-id-' + calId).value = '';
669
670    // Set date - use local date, not UTC
671    let defaultDate = date;
672    if (!defaultDate) {
673        const today = new Date();
674        const year = today.getFullYear();
675        const month = String(today.getMonth() + 1).padStart(2, '0');
676        const day = String(today.getDate()).padStart(2, '0');
677        defaultDate = `${year}-${month}-${day}`;
678    }
679    dateField.value = defaultDate;
680    dateField.removeAttribute('data-original-date');
681
682    // Set default color
683    document.getElementById('event-color-' + calId).value = '#3498db';
684
685    // Set title
686    title.textContent = 'Add Event';
687
688    // Show dialog
689    dialog.style.display = 'flex';
690
691    // Focus title field
692    setTimeout(() => {
693        const titleField = document.getElementById('event-title-' + calId);
694        if (titleField) titleField.focus();
695    }, 100);
696}
697
698// Edit event
699function editEvent(calId, eventId, date, namespace) {
700    const params = new URLSearchParams({
701        call: 'plugin_calendar',
702        action: 'get_event',
703        namespace: namespace,
704        date: date,
705        eventId: eventId
706    });
707
708    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
709        method: 'POST',
710        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
711        body: params.toString()
712    })
713    .then(r => r.json())
714    .then(data => {
715        if (data.success && data.event) {
716            const event = data.event;
717            const dialog = document.getElementById('dialog-' + calId);
718            const title = document.getElementById('dialog-title-' + calId);
719            const dateField = document.getElementById('event-date-' + calId);
720
721            if (!dateField) {
722                console.error('Date field not found when editing!');
723                return;
724            }
725
726            // Populate form
727            document.getElementById('event-id-' + calId).value = event.id;
728            dateField.value = date;
729            dateField.setAttribute('data-original-date', date);
730            document.getElementById('event-end-date-' + calId).value = event.endDate || '';
731            document.getElementById('event-title-' + calId).value = event.title;
732            document.getElementById('event-time-' + calId).value = event.time || '';
733            document.getElementById('event-color-' + calId).value = event.color || '#3498db';
734            document.getElementById('event-desc-' + calId).value = event.description || '';
735            document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
736
737            title.textContent = 'Edit Event';
738            dialog.style.display = 'flex';
739        }
740    })
741    .catch(err => console.error('Error editing event:', err));
742}
743
744// Delete event
745function deleteEvent(calId, eventId, date, namespace) {
746    if (!confirm('Delete this event?')) return;
747
748    const params = new URLSearchParams({
749        call: 'plugin_calendar',
750        action: 'delete_event',
751        namespace: namespace,
752        date: date,
753        eventId: eventId
754    });
755
756    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
757        method: 'POST',
758        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
759        body: params.toString()
760    })
761    .then(r => r.json())
762    .then(data => {
763        if (data.success) {
764            // Extract year and month from date
765            const [year, month] = date.split('-').map(Number);
766
767            // Reload calendar data via AJAX
768            reloadCalendarData(calId, year, month, namespace);
769        }
770    })
771    .catch(err => console.error('Error:', err));
772}
773
774// Save event (add or edit)
775function saveEventCompact(calId, namespace) {
776    const eventId = document.getElementById('event-id-' + calId).value;
777    const dateInput = document.getElementById('event-date-' + calId);
778    const date = dateInput.value;
779    const oldDate = dateInput.getAttribute('data-original-date') || date;
780    const endDate = document.getElementById('event-end-date-' + calId).value;
781    const title = document.getElementById('event-title-' + calId).value;
782    const time = document.getElementById('event-time-' + calId).value;
783    const color = document.getElementById('event-color-' + calId).value;
784    const description = document.getElementById('event-desc-' + calId).value;
785    const isTask = document.getElementById('event-is-task-' + calId).checked;
786    const completed = false; // New tasks are not completed
787    const isRecurring = document.getElementById('event-recurring-' + calId).checked;
788    const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value;
789    const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value;
790
791    if (!title) {
792        alert('Please enter a title');
793        return;
794    }
795
796    if (!date) {
797        alert('Please select a date');
798        return;
799    }
800
801    const params = new URLSearchParams({
802        call: 'plugin_calendar',
803        action: 'save_event',
804        namespace: namespace,
805        eventId: eventId,
806        date: date,
807        oldDate: oldDate,
808        endDate: endDate,
809        title: title,
810        time: time,
811        color: color,
812        description: description,
813        isTask: isTask ? '1' : '0',
814        completed: completed ? '1' : '0',
815        isRecurring: isRecurring ? '1' : '0',
816        recurrenceType: recurrenceType,
817        recurrenceEnd: recurrenceEnd
818    });
819
820    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
821        method: 'POST',
822        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
823        body: params.toString()
824    })
825    .then(r => r.json())
826    .then(data => {
827        if (data.success) {
828            closeEventDialog(calId);
829
830            // For recurring events, do a full page reload to show all occurrences
831            if (isRecurring) {
832                location.reload();
833                return;
834            }
835
836            // Extract year and month from the NEW date (in case date was changed)
837            const [year, month] = date.split('-').map(Number);
838
839            // Reload calendar data via AJAX to the month of the event
840            reloadCalendarData(calId, year, month, namespace);
841        } else {
842            alert('Error: ' + (data.error || 'Unknown error'));
843        }
844    })
845    .catch(err => {
846        console.error('Error:', err);
847        alert('Error saving event');
848    });
849}
850
851// Reload calendar data without page refresh
852function reloadCalendarData(calId, year, month, namespace) {
853    const params = new URLSearchParams({
854        call: 'plugin_calendar',
855        action: 'load_month',
856        year: year,
857        month: month,
858        namespace: namespace,
859        _: new Date().getTime() // Cache buster
860    });
861
862    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
863        method: 'POST',
864        headers: {
865            'Content-Type': 'application/x-www-form-urlencoded',
866            'Cache-Control': 'no-cache, no-store, must-revalidate',
867            'Pragma': 'no-cache'
868        },
869        body: params.toString()
870    })
871    .then(r => r.json())
872    .then(data => {
873        if (data.success) {
874            const container = document.getElementById(calId);
875
876            // Check if this is a full calendar or just event panel
877            if (container.classList.contains('calendar-compact-container')) {
878                rebuildCalendar(calId, data.year, data.month, data.events, namespace);
879            } else if (container.classList.contains('event-panel-standalone')) {
880                rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
881            }
882        }
883    })
884    .catch(err => console.error('Error:', err));
885}
886
887// Close event dialog
888function closeEventDialog(calId) {
889    const dialog = document.getElementById('dialog-' + calId);
890    dialog.style.display = 'none';
891}
892
893// Escape HTML
894function escapeHtml(text) {
895    const div = document.createElement('div');
896    div.textContent = text;
897    return div.innerHTML;
898}
899
900// Highlight event when clicking on bar in calendar
901function highlightEvent(calId, eventId, date) {
902    // Find the event item in the event list
903    const eventList = document.querySelector('#' + calId + ' .event-list-compact');
904    if (!eventList) return;
905
906    const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
907    if (!eventItem) return;
908
909    // Remove previous highlights
910    const previousHighlights = eventList.querySelectorAll('.event-highlighted');
911    previousHighlights.forEach(el => el.classList.remove('event-highlighted'));
912
913    // Add highlight
914    eventItem.classList.add('event-highlighted');
915
916    // Scroll to event
917    eventItem.scrollIntoView({
918        behavior: 'smooth',
919        block: 'nearest',
920        inline: 'nearest'
921    });
922
923    // Remove highlight after 3 seconds
924    setTimeout(() => {
925        eventItem.classList.remove('event-highlighted');
926    }, 3000);
927}
928
929// Toggle recurring event options
930function toggleRecurringOptions(calId) {
931    const checkbox = document.getElementById('event-recurring-' + calId);
932    const options = document.getElementById('recurring-options-' + calId);
933
934    if (checkbox && options) {
935        options.style.display = checkbox.checked ? 'block' : 'none';
936    }
937}
938
939// Close dialog on escape key
940document.addEventListener('keydown', function(e) {
941    if (e.key === 'Escape') {
942        const dialogs = document.querySelectorAll('.event-dialog-compact');
943        dialogs.forEach(dialog => {
944            if (dialog.style.display === 'flex') {
945                dialog.style.display = 'none';
946            }
947        });
948    }
949});
950
951// Event panel navigation
952function navEventPanel(calId, year, month, namespace) {
953    const params = new URLSearchParams({
954        call: 'plugin_calendar',
955        action: 'load_month',
956        year: year,
957        month: month,
958        namespace: namespace
959    });
960
961    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
962        method: 'POST',
963        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
964        body: params.toString()
965    })
966    .then(r => r.json())
967    .then(data => {
968        if (data.success) {
969            rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
970        }
971    })
972    .catch(err => console.error('Error:', err));
973}
974
975// Rebuild event panel only
976function rebuildEventPanel(calId, year, month, events, namespace) {
977    const container = document.getElementById(calId);
978    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
979                       'July', 'August', 'September', 'October', 'November', 'December'];
980
981    // Update header - preserve the onclick and classes
982    const headerContent = container.querySelector('.panel-header-content');
983    const header = container.querySelector('.panel-standalone-header h3');
984    if (header) {
985        header.textContent = monthNames[month - 1] + ' ' + year + ' Events';
986        header.className = 'calendar-month-picker';
987        header.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`);
988        header.setAttribute('title', 'Click to jump to month');
989    }
990
991    // Update namespace badge if needed (preserve existing one)
992    // The namespace badge should already exist and doesn't need updating
993
994    // Update nav buttons
995    let prevMonth = month - 1;
996    let prevYear = year;
997    if (prevMonth < 1) {
998        prevMonth = 12;
999        prevYear--;
1000    }
1001
1002    let nextMonth = month + 1;
1003    let nextYear = year;
1004    if (nextMonth > 12) {
1005        nextMonth = 1;
1006        nextYear++;
1007    }
1008
1009    const navBtns = container.querySelectorAll('.cal-nav-btn');
1010    if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
1011    if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
1012
1013    // Update Today button
1014    const todayBtn = container.querySelector('.cal-today-btn');
1015    if (todayBtn) {
1016        todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`);
1017    }
1018
1019    // Rebuild event list
1020    const eventList = container.querySelector('.event-list-compact');
1021    if (eventList) {
1022        eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month);
1023    }
1024}
1025
1026// Open add event for panel
1027function openAddEventPanel(calId, namespace) {
1028    const today = new Date();
1029    const year = today.getFullYear();
1030    const month = String(today.getMonth() + 1).padStart(2, '0');
1031    const day = String(today.getDate()).padStart(2, '0');
1032    const localDate = `${year}-${month}-${day}`;
1033    openAddEvent(calId, namespace, localDate);
1034}
1035
1036// Toggle task completion
1037function toggleTaskComplete(calId, eventId, date, namespace, completed) {
1038    const params = new URLSearchParams({
1039        call: 'plugin_calendar',
1040        action: 'toggle_task',
1041        namespace: namespace,
1042        date: date,
1043        eventId: eventId,
1044        completed: completed ? '1' : '0'
1045    });
1046
1047    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
1048        method: 'POST',
1049        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
1050        body: params.toString()
1051    })
1052    .then(r => r.json())
1053    .then(data => {
1054        if (data.success) {
1055            const [year, month] = date.split('-').map(Number);
1056            reloadCalendarData(calId, year, month, namespace);
1057        }
1058    })
1059    .catch(err => console.error('Error toggling task:', err));
1060}
1061
1062// Make dialog draggable
1063function makeDialogDraggable(calId) {
1064    const dialog = document.getElementById('dialog-content-' + calId);
1065    const handle = document.getElementById('drag-handle-' + calId);
1066
1067    if (!dialog || !handle) return;
1068
1069    let isDragging = false;
1070    let currentX;
1071    let currentY;
1072    let initialX;
1073    let initialY;
1074    let xOffset = 0;
1075    let yOffset = 0;
1076
1077    handle.addEventListener('mousedown', dragStart);
1078    document.addEventListener('mousemove', drag);
1079    document.addEventListener('mouseup', dragEnd);
1080
1081    function dragStart(e) {
1082        initialX = e.clientX - xOffset;
1083        initialY = e.clientY - yOffset;
1084        isDragging = true;
1085    }
1086
1087    function drag(e) {
1088        if (isDragging) {
1089            e.preventDefault();
1090            currentX = e.clientX - initialX;
1091            currentY = e.clientY - initialY;
1092            xOffset = currentX;
1093            yOffset = currentY;
1094            setTranslate(currentX, currentY, dialog);
1095        }
1096    }
1097
1098    function dragEnd(e) {
1099        initialX = currentX;
1100        initialY = currentY;
1101        isDragging = false;
1102    }
1103
1104    function setTranslate(xPos, yPos, el) {
1105        el.style.transform = `translate(${xPos}px, ${yPos}px)`;
1106    }
1107}
1108
1109// Initialize dialog draggability when opened
1110const originalOpenAddEvent = openAddEvent;
1111openAddEvent = function(calId, namespace, date) {
1112    originalOpenAddEvent(calId, namespace, date);
1113    setTimeout(() => makeDialogDraggable(calId), 100);
1114};
1115
1116const originalEditEvent = editEvent;
1117editEvent = function(calId, eventId, date, namespace) {
1118    originalEditEvent(calId, eventId, date, namespace);
1119    setTimeout(() => makeDialogDraggable(calId), 100);
1120};
1121