xref: /plugin/calendar/script.js (revision 19378907f6c3c154fcddd2ddfe78fa2d88d43359)
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    });
14
15    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
16        method: 'POST',
17        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
18        body: params.toString()
19    })
20    .then(r => r.json())
21    .then(data => {
22        if (data.success) {
23            rebuildCalendar(calId, data.year, data.month, data.events, namespace);
24        }
25    })
26    .catch(err => console.error('Error:', err));
27}
28
29// Rebuild calendar grid after navigation
30function rebuildCalendar(calId, year, month, events, namespace) {
31    const container = document.getElementById(calId);
32    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
33                       'July', 'August', 'September', 'October', 'November', 'December'];
34
35    // Update embedded events data
36    let eventsDataEl = document.getElementById('events-data-' + calId);
37    if (eventsDataEl) {
38        eventsDataEl.textContent = JSON.stringify(events);
39    } else {
40        eventsDataEl = document.createElement('script');
41        eventsDataEl.type = 'application/json';
42        eventsDataEl.id = 'events-data-' + calId;
43        eventsDataEl.textContent = JSON.stringify(events);
44        container.appendChild(eventsDataEl);
45    }
46
47    // Update header
48    const header = container.querySelector('.calendar-compact-header h3');
49    header.textContent = monthNames[month - 1] + ' ' + year;
50
51    // Update nav buttons
52    let prevMonth = month - 1;
53    let prevYear = year;
54    if (prevMonth < 1) {
55        prevMonth = 12;
56        prevYear--;
57    }
58
59    let nextMonth = month + 1;
60    let nextYear = year;
61    if (nextMonth > 12) {
62        nextMonth = 1;
63        nextYear++;
64    }
65
66    const navBtns = container.querySelectorAll('.cal-nav-btn');
67    navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
68    navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
69
70    // Rebuild calendar grid
71    const tbody = container.querySelector('.calendar-compact-grid tbody');
72    const firstDay = new Date(year, month - 1, 1);
73    const daysInMonth = new Date(year, month, 0).getDate();
74    const dayOfWeek = firstDay.getDay();
75
76    let html = '';
77    let currentDay = 1;
78    const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7);
79
80    for (let row = 0; row < rowCount; row++) {
81        html += '<tr>';
82        for (let col = 0; col < 7; col++) {
83            if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) {
84                html += '<td class="cal-empty"></td>';
85            } else {
86                const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`;
87                const today = new Date().toISOString().split('T')[0];
88                const isToday = dateKey === today;
89                const hasEvents = events[dateKey] && events[dateKey].length > 0;
90
91                let classes = 'cal-day';
92                if (isToday) classes += ' cal-today';
93                if (hasEvents) classes += ' cal-has-events';
94
95                html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`;
96                html += `<span class="day-num">${currentDay}</span>`;
97
98                if (hasEvents) {
99                    // Sort events by time (no time first, then by time)
100                    const sortedEvents = [...events[dateKey]].sort((a, b) => {
101                        const timeA = a.time || '';
102                        const timeB = b.time || '';
103
104                        // Events without time go first
105                        if (!timeA && timeB) return -1;
106                        if (timeA && !timeB) return 1;
107                        if (!timeA && !timeB) return 0;
108
109                        // Sort by time
110                        return timeA.localeCompare(timeB);
111                    });
112
113                    // Show colored stacked bars for each event
114                    html += '<div class="event-indicators">';
115                    for (const evt of sortedEvents) {
116                        const eventId = evt.id || '';
117                        const eventColor = evt.color || '#3498db';
118                        const eventTime = evt.time || '';
119                        const eventTitle = evt.title || 'Event';
120                        const barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed';
121
122                        html += `<span class="event-bar ${barClass}" `;
123                        html += `style="background: ${eventColor};" `;
124                        html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `;
125                        html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${dateKey}');"></span>`;
126                    }
127                    html += '</div>';
128                }
129
130                html += '</td>';
131                currentDay++;
132            }
133        }
134        html += '</tr>';
135    }
136
137    tbody.innerHTML = html;
138
139    // Rebuild event list
140    const eventList = container.querySelector('.event-list-compact');
141    eventList.innerHTML = renderEventListFromData(events, calId, namespace);
142
143    // Update title
144    const title = container.querySelector('#eventlist-title-' + calId);
145    title.textContent = 'Events';
146}
147
148// Render event list from data
149function renderEventListFromData(events, calId, namespace) {
150    if (!events || Object.keys(events).length === 0) {
151        return '<p class="no-events-msg">No events this month</p>';
152    }
153
154    let html = '';
155    const sortedDates = Object.keys(events).sort();
156
157    for (const dateKey of sortedDates) {
158        const dayEvents = events[dateKey];
159        for (const event of dayEvents) {
160            html += renderEventItem(event, dateKey, calId, namespace);
161        }
162    }
163
164    return html;
165}
166
167// Show day popup with events when clicking a date
168function showDayPopup(calId, date, namespace) {
169    // Get events for this calendar
170    const eventsDataEl = document.getElementById('events-data-' + calId);
171    let events = {};
172
173    if (eventsDataEl) {
174        try {
175            events = JSON.parse(eventsDataEl.textContent);
176        } catch (e) {
177            console.error('Failed to parse events data:', e);
178        }
179    }
180
181    const dayEvents = events[date] || [];
182    const dateObj = new Date(date + 'T00:00:00');
183    const displayDate = dateObj.toLocaleDateString('en-US', {
184        weekday: 'long',
185        month: 'long',
186        day: 'numeric',
187        year: 'numeric'
188    });
189
190    // Create popup
191    let popup = document.getElementById('day-popup-' + calId);
192    if (!popup) {
193        popup = document.createElement('div');
194        popup.id = 'day-popup-' + calId;
195        popup.className = 'day-popup';
196        document.body.appendChild(popup);
197    }
198
199    let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>';
200    html += '<div class="day-popup-content">';
201    html += '<div class="day-popup-header">';
202    html += '<h4>' + displayDate + '</h4>';
203    html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>';
204    html += '</div>';
205
206    html += '<div class="day-popup-body">';
207
208    if (dayEvents.length === 0) {
209        html += '<p class="no-events-msg">No events on this day</p>';
210    } else {
211        html += '<div class="popup-events-list">';
212        dayEvents.forEach(event => {
213            const color = event.color || '#3498db';
214            html += '<div class="popup-event-item">';
215            html += '<div class="event-color-bar" style="background: ' + color + ';"></div>';
216            html += '<div class="popup-event-content">';
217            html += '<div class="popup-event-title">' + escapeHtml(event.title) + '</div>';
218            if (event.time) {
219                html += '<div class="popup-event-time">�� ' + escapeHtml(event.time) + '</div>';
220            }
221            if (event.description) {
222                html += '<div class="popup-event-desc">' + escapeHtml(event.description).replace(/\n/g, '<br>') + '</div>';
223            }
224            html += '<div class="popup-event-actions">';
225            html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\'); closeDayPopup(\'' + calId + '\')">Edit</button>';
226            html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\'); closeDayPopup(\'' + calId + '\')">Delete</button>';
227            html += '</div>';
228            html += '</div></div>';
229        });
230        html += '</div>';
231    }
232
233    html += '</div>';
234
235    html += '<div class="day-popup-footer">';
236    html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>';
237    html += '</div>';
238
239    html += '</div>';
240
241    popup.innerHTML = html;
242    popup.style.display = 'flex';
243}
244
245// Close day popup
246function closeDayPopup(calId) {
247    const popup = document.getElementById('day-popup-' + calId);
248    if (popup) {
249        popup.style.display = 'none';
250    }
251}
252
253// Show events for a specific day (for event list panel)
254function showDayEvents(calId, date, namespace) {
255    const params = new URLSearchParams({
256        call: 'plugin_calendar',
257        action: 'load_month',
258        year: date.split('-')[0],
259        month: parseInt(date.split('-')[1]),
260        namespace: namespace
261    });
262
263    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
264        method: 'POST',
265        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
266        body: params.toString()
267    })
268    .then(r => r.json())
269    .then(data => {
270        if (data.success) {
271            const eventList = document.getElementById('eventlist-' + calId);
272            const events = data.events;
273            const title = document.getElementById('eventlist-title-' + calId);
274
275            const dateObj = new Date(date + 'T00:00:00');
276            const displayDate = dateObj.toLocaleDateString('en-US', {
277                weekday: 'short',
278                month: 'short',
279                day: 'numeric'
280            });
281
282            title.textContent = 'Events - ' + displayDate;
283
284            // Filter events for this day
285            const dayEvents = events[date] || [];
286
287            if (dayEvents.length === 0) {
288                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>';
289            } else {
290                let html = '';
291                dayEvents.forEach(event => {
292                    html += renderEventItem(event, date, calId, namespace);
293                });
294                eventList.innerHTML = html;
295            }
296        }
297    })
298    .catch(err => console.error('Error:', err));
299}
300
301// Render a single event item
302function renderEventItem(event, date, calId, namespace) {
303    // Format date display
304    const dateObj = new Date(date + 'T00:00:00');
305    const displayDate = dateObj.toLocaleDateString('en-US', {
306        month: 'short',
307        day: 'numeric'
308    });
309
310    // Convert to 12-hour format
311    let displayTime = '';
312    if (event.time) {
313        const timeParts = event.time.split(':');
314        if (timeParts.length === 2) {
315            let hour = parseInt(timeParts[0]);
316            const minute = timeParts[1];
317            const ampm = hour >= 12 ? 'PM' : 'AM';
318            hour = hour % 12 || 12;
319            displayTime = hour + ':' + minute + ' ' + ampm;
320        } else {
321            displayTime = event.time;
322        }
323    }
324
325    // Multi-day indicator
326    let multiDay = '';
327    if (event.endDate && event.endDate !== date) {
328        const endObj = new Date(event.endDate + 'T00:00:00');
329        multiDay = ' → ' + endObj.toLocaleDateString('en-US', {
330            month: 'short',
331            day: 'numeric'
332        });
333    }
334
335    const completedClass = event.completed ? ' event-completed' : '';
336    const color = event.color || '#3498db';
337    const isTask = event.isTask || false;
338    const completed = event.completed || false;
339
340    let html = '<div class="event-compact-item' + completedClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';">';
341
342    html += '<div class="event-info">';
343    html += '<div class="event-title-row">';
344    html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>';
345    html += '</div>';
346
347    html += '<div class="event-meta-compact">';
348    html += '<span class="event-date-time">' + displayDate + multiDay;
349    if (displayTime) {
350        html += ' • ' + displayTime;
351    }
352    html += '</span>';
353    html += '</div>';
354
355    if (event.description) {
356        html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>';
357    }
358
359    html += '</div>'; // event-info
360
361    html += '<div class="event-actions-compact">';
362    html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\')">��️</button>';
363    html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\')">✏️</button>';
364    html += '</div>';
365
366    // Checkbox for tasks - ON THE FAR RIGHT
367    if (isTask) {
368        const checked = completed ? 'checked' : '';
369        html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + namespace + '\', this.checked)">';
370    }
371
372    html += '</div>';
373
374    return html;
375}
376
377// Render description with rich content support
378function renderDescription(description) {
379    if (!description) return '';
380
381    let rendered = escapeHtml(description);
382
383    // Convert newlines to <br>
384    rendered = rendered.replace(/\n/g, '<br>');
385
386    // Convert DokuWiki image syntax {{image.jpg}} to HTML
387    rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) {
388        imagePath = imagePath.trim();
389        alt = alt ? alt.trim() : '';
390
391        // Handle external URLs
392        if (imagePath.match(/^https?:\/\//)) {
393            return '<img src="' + imagePath + '" alt="' + alt + '" class="event-image" />';
394        }
395
396        // Handle internal DokuWiki images
397        const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath);
398        return '<img src="' + imageUrl + '" alt="' + alt + '" class="event-image" />';
399    });
400
401    // Convert DokuWiki link syntax [[link|text]] to HTML
402    rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) {
403        link = link.trim();
404        text = text ? text.trim() : link;
405
406        // Handle external URLs
407        if (link.match(/^https?:\/\//)) {
408            return '<a href="' + link + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
409        }
410
411        // Handle internal DokuWiki links
412        const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(link);
413        return '<a href="' + wikiUrl + '">' + text + '</a>';
414    });
415
416    // Convert markdown-style links [text](url) to HTML
417    rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) {
418        text = text.trim();
419        url = url.trim();
420
421        if (url.match(/^https?:\/\//)) {
422            return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + text + '</a>';
423        }
424
425        return '<a href="' + url + '">' + text + '</a>';
426    });
427
428    // Convert plain URLs to clickable links
429    rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) {
430        return '<a href="' + url + '" target="_blank" rel="noopener noreferrer">' + url + '</a>';
431    });
432
433    return rendered;
434}
435
436// Open add event dialog
437function openAddEvent(calId, namespace, date) {
438    const dialog = document.getElementById('dialog-' + calId);
439    const form = document.getElementById('eventform-' + calId);
440    const title = document.getElementById('dialog-title-' + calId);
441    const dateField = document.getElementById('event-date-' + calId);
442
443    if (!dateField) {
444        console.error('Date field not found! ID: event-date-' + calId);
445        return;
446    }
447
448    // Reset form
449    form.reset();
450    document.getElementById('event-id-' + calId).value = '';
451
452    // Set date
453    const defaultDate = date || new Date().toISOString().split('T')[0];
454    dateField.value = defaultDate;
455    dateField.removeAttribute('data-original-date');
456
457    // Set default color
458    document.getElementById('event-color-' + calId).value = '#3498db';
459
460    // Set title
461    title.textContent = 'Add Event';
462
463    // Show dialog
464    dialog.style.display = 'flex';
465
466    // Focus title field
467    setTimeout(() => {
468        const titleField = document.getElementById('event-title-' + calId);
469        if (titleField) titleField.focus();
470    }, 100);
471}
472
473// Edit event
474function editEvent(calId, eventId, date, namespace) {
475    const params = new URLSearchParams({
476        call: 'plugin_calendar',
477        action: 'get_event',
478        namespace: namespace,
479        date: date,
480        eventId: eventId
481    });
482
483    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
484        method: 'POST',
485        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
486        body: params.toString()
487    })
488    .then(r => r.json())
489    .then(data => {
490        if (data.success && data.event) {
491            const event = data.event;
492            const dialog = document.getElementById('dialog-' + calId);
493            const title = document.getElementById('dialog-title-' + calId);
494            const dateField = document.getElementById('event-date-' + calId);
495
496            if (!dateField) {
497                console.error('Date field not found when editing!');
498                return;
499            }
500
501            // Populate form
502            document.getElementById('event-id-' + calId).value = event.id;
503            dateField.value = date;
504            dateField.setAttribute('data-original-date', date);
505            document.getElementById('event-end-date-' + calId).value = event.endDate || '';
506            document.getElementById('event-title-' + calId).value = event.title;
507            document.getElementById('event-time-' + calId).value = event.time || '';
508            document.getElementById('event-color-' + calId).value = event.color || '#3498db';
509            document.getElementById('event-desc-' + calId).value = event.description || '';
510            document.getElementById('event-is-task-' + calId).checked = event.isTask || false;
511
512            title.textContent = 'Edit Event';
513            dialog.style.display = 'flex';
514        }
515    })
516    .catch(err => console.error('Error editing event:', err));
517}
518
519// Delete event
520function deleteEvent(calId, eventId, date, namespace) {
521    if (!confirm('Delete this event?')) return;
522
523    const params = new URLSearchParams({
524        call: 'plugin_calendar',
525        action: 'delete_event',
526        namespace: namespace,
527        date: date,
528        eventId: eventId
529    });
530
531    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
532        method: 'POST',
533        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
534        body: params.toString()
535    })
536    .then(r => r.json())
537    .then(data => {
538        if (data.success) {
539            // Extract year and month from date
540            const [year, month] = date.split('-').map(Number);
541
542            // Reload calendar data via AJAX
543            reloadCalendarData(calId, year, month, namespace);
544        }
545    })
546    .catch(err => console.error('Error:', err));
547}
548
549// Save event (add or edit)
550function saveEventCompact(calId, namespace) {
551    const eventId = document.getElementById('event-id-' + calId).value;
552    const dateInput = document.getElementById('event-date-' + calId);
553    const date = dateInput.value;
554    const oldDate = dateInput.getAttribute('data-original-date') || date;
555    const endDate = document.getElementById('event-end-date-' + calId).value;
556    const title = document.getElementById('event-title-' + calId).value;
557    const time = document.getElementById('event-time-' + calId).value;
558    const color = document.getElementById('event-color-' + calId).value;
559    const description = document.getElementById('event-desc-' + calId).value;
560    const isTask = document.getElementById('event-is-task-' + calId).checked;
561    const completed = false; // New tasks are not completed
562
563    if (!title) {
564        alert('Please enter a title');
565        return;
566    }
567
568    if (!date) {
569        alert('Please select a date');
570        return;
571    }
572
573    const params = new URLSearchParams({
574        call: 'plugin_calendar',
575        action: 'save_event',
576        namespace: namespace,
577        eventId: eventId,
578        date: date,
579        oldDate: oldDate,
580        endDate: endDate,
581        title: title,
582        time: time,
583        color: color,
584        description: description,
585        isTask: isTask ? '1' : '0',
586        completed: completed ? '1' : '0'
587    });
588
589    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
590        method: 'POST',
591        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
592        body: params.toString()
593    })
594    .then(r => r.json())
595    .then(data => {
596        if (data.success) {
597            closeEventDialog(calId);
598
599            // Extract year and month from the NEW date (in case date was changed)
600            const [year, month] = date.split('-').map(Number);
601
602            // Reload calendar data via AJAX to the month of the event
603            reloadCalendarData(calId, year, month, namespace);
604        } else {
605            alert('Error: ' + (data.error || 'Unknown error'));
606        }
607    })
608    .catch(err => {
609        console.error('Error:', err);
610        alert('Error saving event');
611    });
612}
613
614// Reload calendar data without page refresh
615function reloadCalendarData(calId, year, month, namespace) {
616    const params = new URLSearchParams({
617        call: 'plugin_calendar',
618        action: 'load_month',
619        year: year,
620        month: month,
621        namespace: namespace
622    });
623
624    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
625        method: 'POST',
626        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
627        body: params.toString()
628    })
629    .then(r => r.json())
630    .then(data => {
631        if (data.success) {
632            const container = document.getElementById(calId);
633
634            // Check if this is a full calendar or just event panel
635            if (container.classList.contains('calendar-compact-container')) {
636                rebuildCalendar(calId, data.year, data.month, data.events, namespace);
637            } else if (container.classList.contains('event-panel-standalone')) {
638                rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
639            }
640        }
641    })
642    .catch(err => console.error('Error:', err));
643}
644
645// Close event dialog
646function closeEventDialog(calId) {
647    const dialog = document.getElementById('dialog-' + calId);
648    dialog.style.display = 'none';
649}
650
651// Escape HTML
652function escapeHtml(text) {
653    const div = document.createElement('div');
654    div.textContent = text;
655    return div.innerHTML;
656}
657
658// Highlight event when clicking on bar in calendar
659function highlightEvent(calId, eventId, date) {
660    // Find the event item in the event list
661    const eventList = document.querySelector('#' + calId + ' .event-list-compact');
662    if (!eventList) return;
663
664    const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]');
665    if (!eventItem) return;
666
667    // Remove previous highlights
668    const previousHighlights = eventList.querySelectorAll('.event-highlighted');
669    previousHighlights.forEach(el => el.classList.remove('event-highlighted'));
670
671    // Add highlight
672    eventItem.classList.add('event-highlighted');
673
674    // Scroll to event
675    eventItem.scrollIntoView({
676        behavior: 'smooth',
677        block: 'nearest',
678        inline: 'nearest'
679    });
680
681    // Remove highlight after 3 seconds
682    setTimeout(() => {
683        eventItem.classList.remove('event-highlighted');
684    }, 3000);
685}
686
687// Close dialog on escape key
688document.addEventListener('keydown', function(e) {
689    if (e.key === 'Escape') {
690        const dialogs = document.querySelectorAll('.event-dialog-compact');
691        dialogs.forEach(dialog => {
692            if (dialog.style.display === 'flex') {
693                dialog.style.display = 'none';
694            }
695        });
696    }
697});
698
699// Event panel navigation
700function navEventPanel(calId, year, month, namespace) {
701    const params = new URLSearchParams({
702        call: 'plugin_calendar',
703        action: 'load_month',
704        year: year,
705        month: month,
706        namespace: namespace
707    });
708
709    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
710        method: 'POST',
711        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
712        body: params.toString()
713    })
714    .then(r => r.json())
715    .then(data => {
716        if (data.success) {
717            rebuildEventPanel(calId, data.year, data.month, data.events, namespace);
718        }
719    })
720    .catch(err => console.error('Error:', err));
721}
722
723// Rebuild event panel only
724function rebuildEventPanel(calId, year, month, events, namespace) {
725    const container = document.getElementById(calId);
726    const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
727                       'July', 'August', 'September', 'October', 'November', 'December'];
728
729    // Update header
730    const header = container.querySelector('.panel-standalone-header h3');
731    header.textContent = monthNames[month - 1] + ' ' + year + ' Events';
732
733    // Update nav buttons
734    let prevMonth = month - 1;
735    let prevYear = year;
736    if (prevMonth < 1) {
737        prevMonth = 12;
738        prevYear--;
739    }
740
741    let nextMonth = month + 1;
742    let nextYear = year;
743    if (nextMonth > 12) {
744        nextMonth = 1;
745        nextYear++;
746    }
747
748    const navBtns = container.querySelectorAll('.cal-nav-btn');
749    navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`);
750    navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`);
751
752    // Rebuild event list
753    const eventList = container.querySelector('.event-list-compact');
754    eventList.innerHTML = renderEventListFromData(events, calId, namespace);
755}
756
757// Open add event for panel
758function openAddEventPanel(calId, namespace) {
759    openAddEvent(calId, namespace, new Date().toISOString().split('T')[0]);
760}
761
762// Toggle task completion
763function toggleTaskComplete(calId, eventId, date, namespace, completed) {
764    const params = new URLSearchParams({
765        call: 'plugin_calendar',
766        action: 'toggle_task',
767        namespace: namespace,
768        date: date,
769        eventId: eventId,
770        completed: completed ? '1' : '0'
771    });
772
773    fetch(DOKU_BASE + 'lib/exe/ajax.php', {
774        method: 'POST',
775        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
776        body: params.toString()
777    })
778    .then(r => r.json())
779    .then(data => {
780        if (data.success) {
781            const [year, month] = date.split('-').map(Number);
782            reloadCalendarData(calId, year, month, namespace);
783        }
784    })
785    .catch(err => console.error('Error toggling task:', err));
786}
787
788// Make dialog draggable
789function makeDialogDraggable(calId) {
790    const dialog = document.getElementById('dialog-content-' + calId);
791    const handle = document.getElementById('drag-handle-' + calId);
792
793    if (!dialog || !handle) return;
794
795    let isDragging = false;
796    let currentX;
797    let currentY;
798    let initialX;
799    let initialY;
800    let xOffset = 0;
801    let yOffset = 0;
802
803    handle.addEventListener('mousedown', dragStart);
804    document.addEventListener('mousemove', drag);
805    document.addEventListener('mouseup', dragEnd);
806
807    function dragStart(e) {
808        initialX = e.clientX - xOffset;
809        initialY = e.clientY - yOffset;
810        isDragging = true;
811    }
812
813    function drag(e) {
814        if (isDragging) {
815            e.preventDefault();
816            currentX = e.clientX - initialX;
817            currentY = e.clientY - initialY;
818            xOffset = currentX;
819            yOffset = currentY;
820            setTranslate(currentX, currentY, dialog);
821        }
822    }
823
824    function dragEnd(e) {
825        initialX = currentX;
826        initialY = currentY;
827        isDragging = false;
828    }
829
830    function setTranslate(xPos, yPos, el) {
831        el.style.transform = `translate(${xPos}px, ${yPos}px)`;
832    }
833}
834
835// Initialize dialog draggability when opened
836const originalOpenAddEvent = openAddEvent;
837openAddEvent = function(calId, namespace, date) {
838    originalOpenAddEvent(calId, namespace, date);
839    setTimeout(() => makeDialogDraggable(calId), 100);
840};
841
842const originalEditEvent = editEvent;
843editEvent = function(calId, eventId, date, namespace) {
844    originalEditEvent(calId, eventId, date, namespace);
845    setTimeout(() => makeDialogDraggable(calId), 100);
846};
847