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