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