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