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