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('=== navCalendar AJAX Response ==='); 28 console.log('Requested year:', year, 'month:', month); 29 console.log('Response:', data); 30 console.log('Response year:', data.year, 'month:', data.month); 31 console.log('Event date keys:', Object.keys(data.events || {})); 32 if (data.success) { 33 console.log('Rebuilding calendar for', year, month, 'with', Object.keys(data.events || {}).length, 'date entries'); 34 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 35 } else { 36 console.error('Failed to load month:', data.error); 37 } 38 }) 39 .catch(err => { 40 console.error('Error loading month:', err); 41 }); 42} 43 44// Jump to current month 45function jumpToToday(calId, namespace) { 46 const today = new Date(); 47 const year = today.getFullYear(); 48 const month = today.getMonth() + 1; // JavaScript months are 0-indexed 49 navCalendar(calId, year, month, namespace); 50} 51 52// Jump to today for event panel 53function jumpTodayPanel(calId, namespace) { 54 const today = new Date(); 55 const year = today.getFullYear(); 56 const month = today.getMonth() + 1; 57 navEventPanel(calId, year, month, namespace); 58} 59 60// Open month picker dialog 61function openMonthPicker(calId, currentYear, currentMonth, namespace) { 62 console.log('openMonthPicker called:', calId, currentYear, currentMonth, namespace); 63 64 const overlay = document.getElementById('month-picker-overlay-' + calId); 65 console.log('Overlay element:', overlay); 66 67 const monthSelect = document.getElementById('month-picker-month-' + calId); 68 console.log('Month select:', monthSelect); 69 70 const yearSelect = document.getElementById('month-picker-year-' + calId); 71 console.log('Year select:', yearSelect); 72 73 if (!overlay) { 74 console.error('Month picker overlay not found! ID:', 'month-picker-overlay-' + calId); 75 return; 76 } 77 78 if (!monthSelect || !yearSelect) { 79 console.error('Select elements not found!'); 80 return; 81 } 82 83 // Set current values 84 monthSelect.value = currentMonth; 85 yearSelect.value = currentYear; 86 87 // Show overlay 88 overlay.style.display = 'flex'; 89 console.log('Overlay display set to flex'); 90} 91 92// Open month picker dialog for event panel 93function openMonthPickerPanel(calId, currentYear, currentMonth, namespace) { 94 openMonthPicker(calId, currentYear, currentMonth, namespace); 95} 96 97// Close month picker dialog 98function closeMonthPicker(calId) { 99 const overlay = document.getElementById('month-picker-overlay-' + calId); 100 overlay.style.display = 'none'; 101} 102 103// Jump to selected month 104function jumpToSelectedMonth(calId, namespace) { 105 const monthSelect = document.getElementById('month-picker-month-' + calId); 106 const yearSelect = document.getElementById('month-picker-year-' + calId); 107 108 const month = parseInt(monthSelect.value); 109 const year = parseInt(yearSelect.value); 110 111 closeMonthPicker(calId); 112 113 // Check if this is a calendar or event panel 114 const container = document.getElementById(calId); 115 if (container && container.classList.contains('event-panel-standalone')) { 116 navEventPanel(calId, year, month, namespace); 117 } else { 118 navCalendar(calId, year, month, namespace); 119 } 120} 121 122// Rebuild calendar grid after navigation 123function rebuildCalendar(calId, year, month, events, namespace) { 124 console.log('=== rebuildCalendar DEBUG ==='); 125 console.log('Requested:', {year, month, namespace}); 126 console.log('Event date keys received:', Object.keys(events)); 127 console.log('Events object:', events); 128 129 const container = document.getElementById(calId); 130 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 131 'July', 'August', 'September', 'October', 'November', 'December']; 132 133 // Update container data attributes for current month/year 134 container.setAttribute('data-year', year); 135 container.setAttribute('data-month', month); 136 137 // Update embedded events data 138 let eventsDataEl = document.getElementById('events-data-' + calId); 139 if (eventsDataEl) { 140 eventsDataEl.textContent = JSON.stringify(events); 141 } else { 142 eventsDataEl = document.createElement('script'); 143 eventsDataEl.type = 'application/json'; 144 eventsDataEl.id = 'events-data-' + calId; 145 eventsDataEl.textContent = JSON.stringify(events); 146 container.appendChild(eventsDataEl); 147 } 148 149 // Update header 150 const header = container.querySelector('.calendar-compact-header h3'); 151 header.textContent = monthNames[month - 1] + ' ' + year; 152 153 // Update nav buttons 154 let prevMonth = month - 1; 155 let prevYear = year; 156 if (prevMonth < 1) { 157 prevMonth = 12; 158 prevYear--; 159 } 160 161 let nextMonth = month + 1; 162 let nextYear = year; 163 if (nextMonth > 12) { 164 nextMonth = 1; 165 nextYear++; 166 } 167 168 const navBtns = container.querySelectorAll('.cal-nav-btn'); 169 navBtns[0].setAttribute('onclick', `navCalendar('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 170 navBtns[1].setAttribute('onclick', `navCalendar('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 171 172 // Rebuild calendar grid 173 const tbody = container.querySelector('.calendar-compact-grid tbody'); 174 const firstDay = new Date(year, month - 1, 1); 175 const daysInMonth = new Date(year, month, 0).getDate(); 176 const dayOfWeek = firstDay.getDay(); 177 178 // Calculate month boundaries 179 const monthStart = new Date(year, month - 1, 1); 180 const monthEnd = new Date(year, month - 1, daysInMonth); 181 182 // Build a map of all events with their date ranges 183 const eventRanges = {}; 184 for (const [dateKey, dayEvents] of Object.entries(events)) { 185 // Defensive check: ensure dayEvents is an array 186 if (!Array.isArray(dayEvents)) { 187 console.error('dayEvents is not an array for dateKey:', dateKey, 'value:', dayEvents); 188 continue; 189 } 190 191 // Only process events that could possibly overlap with this month/year 192 const dateYear = parseInt(dateKey.split('-')[0]); 193 const dateMonth = parseInt(dateKey.split('-')[1]); 194 195 // Skip events from completely different years (unless they're very long multi-day events) 196 if (Math.abs(dateYear - year) > 1) { 197 continue; 198 } 199 200 for (const evt of dayEvents) { 201 const startDate = dateKey; 202 const endDate = evt.endDate || dateKey; 203 204 // Check if event overlaps with current month 205 const eventStart = new Date(startDate + 'T00:00:00'); 206 const eventEnd = new Date(endDate + 'T00:00:00'); 207 208 // Skip if event doesn't overlap with current month 209 if (eventEnd < monthStart || eventStart > monthEnd) { 210 continue; 211 } 212 213 // Create entry for each day the event spans 214 const start = new Date(startDate + 'T00:00:00'); 215 const end = new Date(endDate + 'T00:00:00'); 216 const current = new Date(start); 217 218 while (current <= end) { 219 const currentKey = current.toISOString().split('T')[0]; 220 221 // Check if this date is in current month 222 const currentDate = new Date(currentKey + 'T00:00:00'); 223 if (currentDate.getFullYear() === year && currentDate.getMonth() === month - 1) { 224 if (!eventRanges[currentKey]) { 225 eventRanges[currentKey] = []; 226 } 227 228 // Add event with span information 229 const eventCopy = {...evt}; 230 eventCopy._span_start = startDate; 231 eventCopy._span_end = endDate; 232 eventCopy._is_first_day = (currentKey === startDate); 233 eventCopy._is_last_day = (currentKey === endDate); 234 eventCopy._original_date = dateKey; 235 236 // Check if event continues from previous month or to next month 237 eventCopy._continues_from_prev = (eventStart < monthStart); 238 eventCopy._continues_to_next = (eventEnd > monthEnd); 239 240 eventRanges[currentKey].push(eventCopy); 241 } 242 243 current.setDate(current.getDate() + 1); 244 } 245 } 246 } 247 248 let html = ''; 249 let currentDay = 1; 250 const rowCount = Math.ceil((daysInMonth + dayOfWeek) / 7); 251 252 for (let row = 0; row < rowCount; row++) { 253 html += '<tr>'; 254 for (let col = 0; col < 7; col++) { 255 if ((row === 0 && col < dayOfWeek) || currentDay > daysInMonth) { 256 html += '<td class="cal-empty"></td>'; 257 } else { 258 const dateKey = `${year}-${String(month).padStart(2, '0')}-${String(currentDay).padStart(2, '0')}`; 259 260 // Get today's date in local timezone 261 const todayObj = new Date(); 262 const today = `${todayObj.getFullYear()}-${String(todayObj.getMonth() + 1).padStart(2, '0')}-${String(todayObj.getDate()).padStart(2, '0')}`; 263 264 const isToday = dateKey === today; 265 const hasEvents = eventRanges[dateKey] && eventRanges[dateKey].length > 0; 266 267 let classes = 'cal-day'; 268 if (isToday) classes += ' cal-today'; 269 if (hasEvents) classes += ' cal-has-events'; 270 271 html += `<td class="${classes}" data-date="${dateKey}" onclick="showDayPopup('${calId}', '${dateKey}', '${namespace}')">`; 272 html += `<span class="day-num">${currentDay}</span>`; 273 274 if (hasEvents) { 275 // Sort events by time (no time first, then by time) 276 const sortedEvents = [...eventRanges[dateKey]].sort((a, b) => { 277 const timeA = a.time || ''; 278 const timeB = b.time || ''; 279 280 // Events without time go first 281 if (!timeA && timeB) return -1; 282 if (timeA && !timeB) return 1; 283 if (!timeA && !timeB) return 0; 284 285 // Sort by time 286 return timeA.localeCompare(timeB); 287 }); 288 289 // Show colored stacked bars for each event 290 html += '<div class="event-indicators">'; 291 for (const evt of sortedEvents) { 292 const eventId = evt.id || ''; 293 const eventColor = evt.color || '#3498db'; 294 const eventTime = evt.time || ''; 295 const eventTitle = evt.title || 'Event'; 296 const originalDate = evt._original_date || dateKey; 297 const isFirstDay = evt._is_first_day !== undefined ? evt._is_first_day : true; 298 const isLastDay = evt._is_last_day !== undefined ? evt._is_last_day : true; 299 300 let barClass = !eventTime ? 'event-bar-no-time' : 'event-bar-timed'; 301 302 // Add classes for multi-day spanning 303 if (!isFirstDay) barClass += ' event-bar-continues'; 304 if (!isLastDay) barClass += ' event-bar-continuing'; 305 306 html += `<span class="event-bar ${barClass}" `; 307 html += `style="background: ${eventColor};" `; 308 html += `title="${escapeHtml(eventTitle)}${eventTime ? ' @ ' + eventTime : ''}" `; 309 html += `onclick="event.stopPropagation(); highlightEvent('${calId}', '${eventId}', '${originalDate}');"></span>`; 310 } 311 html += '</div>'; 312 } 313 314 html += '</td>'; 315 currentDay++; 316 } 317 } 318 html += '</tr>'; 319 } 320 321 tbody.innerHTML = html; 322 323 // Rebuild event list - server already filtered to current month 324 const eventList = container.querySelector('.event-list-compact'); 325 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 326 327 // Auto-scroll to first future event (past events will be above viewport) 328 setTimeout(() => { 329 const firstFuture = eventList.querySelector('[data-first-future="true"]'); 330 if (firstFuture) { 331 firstFuture.scrollIntoView({ behavior: 'smooth', block: 'start' }); 332 } 333 }, 100); 334 335 // Update title 336 const title = container.querySelector('#eventlist-title-' + calId); 337 title.textContent = 'Events'; 338} 339 340// Render event list from data 341function renderEventListFromData(events, calId, namespace, year, month) { 342 if (!events || Object.keys(events).length === 0) { 343 return '<p class="no-events-msg">No events this month</p>'; 344 } 345 346 let html = ''; 347 const sortedDates = Object.keys(events).sort(); 348 349 // Filter events to only current month if year/month provided 350 const monthStart = year && month ? new Date(year, month - 1, 1) : null; 351 const monthEnd = year && month ? new Date(year, month, 0, 23, 59, 59) : null; 352 353 for (const dateKey of sortedDates) { 354 // Skip events not in current month if filtering 355 if (monthStart && monthEnd) { 356 const eventDate = new Date(dateKey + 'T00:00:00'); 357 358 if (eventDate < monthStart || eventDate > monthEnd) { 359 continue; 360 } 361 } 362 363 // Sort events within this day by time 364 const dayEvents = events[dateKey]; 365 dayEvents.sort((a, b) => { 366 const timeA = a.time || '00:00'; 367 const timeB = b.time || '00:00'; 368 return timeA.localeCompare(timeB); 369 }); 370 371 for (const event of dayEvents) { 372 html += renderEventItem(event, dateKey, calId, namespace); 373 } 374 } 375 376 if (!html) { 377 return '<p class="no-events-msg">No events this month</p>'; 378 } 379 380 return html; 381} 382 383// Show day popup with events when clicking a date 384function showDayPopup(calId, date, namespace) { 385 // Get events for this calendar 386 const eventsDataEl = document.getElementById('events-data-' + calId); 387 let events = {}; 388 389 if (eventsDataEl) { 390 try { 391 events = JSON.parse(eventsDataEl.textContent); 392 } catch (e) { 393 console.error('Failed to parse events data:', e); 394 } 395 } 396 397 const dayEvents = events[date] || []; 398 const dateObj = new Date(date + 'T00:00:00'); 399 const displayDate = dateObj.toLocaleDateString('en-US', { 400 weekday: 'long', 401 month: 'long', 402 day: 'numeric', 403 year: 'numeric' 404 }); 405 406 // Create popup 407 let popup = document.getElementById('day-popup-' + calId); 408 if (!popup) { 409 popup = document.createElement('div'); 410 popup.id = 'day-popup-' + calId; 411 popup.className = 'day-popup'; 412 document.body.appendChild(popup); 413 } 414 415 let html = '<div class="day-popup-overlay" onclick="closeDayPopup(\'' + calId + '\')"></div>'; 416 html += '<div class="day-popup-content">'; 417 html += '<div class="day-popup-header">'; 418 html += '<h4>' + displayDate + '</h4>'; 419 html += '<button class="popup-close" onclick="closeDayPopup(\'' + calId + '\')">×</button>'; 420 html += '</div>'; 421 422 html += '<div class="day-popup-body">'; 423 424 if (dayEvents.length === 0) { 425 html += '<p class="no-events-msg">No events on this day</p>'; 426 } else { 427 html += '<div class="popup-events-list">'; 428 dayEvents.forEach(event => { 429 const color = event.color || '#3498db'; 430 431 // Use individual event namespace if available (for multi-namespace support) 432 const eventNamespace = event._namespace !== undefined ? event._namespace : namespace; 433 434 // Convert to 12-hour format 435 let displayTime = ''; 436 if (event.time) { 437 const timeParts = event.time.split(':'); 438 if (timeParts.length === 2) { 439 let hour = parseInt(timeParts[0]); 440 const minute = timeParts[1]; 441 const ampm = hour >= 12 ? 'PM' : 'AM'; 442 hour = hour % 12 || 12; 443 displayTime = hour + ':' + minute + ' ' + ampm; 444 } else { 445 displayTime = event.time; 446 } 447 } 448 449 // Multi-day indicator 450 let multiDay = ''; 451 if (event.endDate && event.endDate !== date) { 452 const endObj = new Date(event.endDate + 'T00:00:00'); 453 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 454 month: 'short', 455 day: 'numeric' 456 }); 457 } 458 459 html += '<div class="popup-event-item">'; 460 html += '<div class="event-color-bar" style="background: ' + color + ';"></div>'; 461 html += '<div class="popup-event-content">'; 462 463 // Single line with title, time, date range, namespace, and actions 464 html += '<div class="popup-event-main-row">'; 465 html += '<div class="popup-event-info-inline">'; 466 html += '<span class="popup-event-title">' + escapeHtml(event.title) + '</span>'; 467 if (displayTime) { 468 html += '<span class="popup-event-time"> ' + displayTime + '</span>'; 469 } 470 if (multiDay) { 471 html += '<span class="popup-event-multiday">' + multiDay + '</span>'; 472 } 473 if (eventNamespace) { 474 html += '<span class="popup-event-namespace">' + escapeHtml(eventNamespace) + '</span>'; 475 } 476 html += '</div>'; 477 html += '<div class="popup-event-actions">'; 478 html += '<button class="event-edit-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">✏️</button>'; 479 html += '<button class="event-delete-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + eventNamespace + '\'); closeDayPopup(\'' + calId + '\')">️</button>'; 480 html += '</div>'; 481 html += '</div>'; 482 483 // Description on separate line if present 484 if (event.description) { 485 html += '<div class="popup-event-desc">' + renderDescription(event.description) + '</div>'; 486 } 487 488 html += '</div></div>'; 489 }); 490 html += '</div>'; 491 } 492 493 html += '</div>'; 494 495 html += '<div class="day-popup-footer">'; 496 html += '<button class="btn-add-event" onclick="openAddEvent(\'' + calId + '\', \'' + namespace + '\', \'' + date + '\'); closeDayPopup(\'' + calId + '\')">+ Add Event</button>'; 497 html += '</div>'; 498 499 html += '</div>'; 500 501 popup.innerHTML = html; 502 popup.style.display = 'flex'; 503} 504 505// Close day popup 506function closeDayPopup(calId) { 507 const popup = document.getElementById('day-popup-' + calId); 508 if (popup) { 509 popup.style.display = 'none'; 510 } 511} 512 513// Show events for a specific day (for event list panel) 514function showDayEvents(calId, date, namespace) { 515 const params = new URLSearchParams({ 516 call: 'plugin_calendar', 517 action: 'load_month', 518 year: date.split('-')[0], 519 month: parseInt(date.split('-')[1]), 520 namespace: namespace, 521 _: new Date().getTime() // Cache buster 522 }); 523 524 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 525 method: 'POST', 526 headers: { 527 'Content-Type': 'application/x-www-form-urlencoded', 528 'Cache-Control': 'no-cache, no-store, must-revalidate', 529 'Pragma': 'no-cache' 530 }, 531 body: params.toString() 532 }) 533 .then(r => r.json()) 534 .then(data => { 535 if (data.success) { 536 const eventList = document.getElementById('eventlist-' + calId); 537 const events = data.events; 538 const title = document.getElementById('eventlist-title-' + calId); 539 540 const dateObj = new Date(date + 'T00:00:00'); 541 const displayDate = dateObj.toLocaleDateString('en-US', { 542 weekday: 'short', 543 month: 'short', 544 day: 'numeric' 545 }); 546 547 title.textContent = 'Events - ' + displayDate; 548 549 // Filter events for this day 550 const dayEvents = events[date] || []; 551 552 if (dayEvents.length === 0) { 553 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>'; 554 } else { 555 let html = ''; 556 dayEvents.forEach(event => { 557 html += renderEventItem(event, date, calId, namespace); 558 }); 559 eventList.innerHTML = html; 560 } 561 } 562 }) 563 .catch(err => console.error('Error:', err)); 564} 565 566// Render a single event item 567function renderEventItem(event, date, calId, namespace) { 568 // Check if this event is in the past or today 569 const today = new Date(); 570 today.setHours(0, 0, 0, 0); 571 const eventDate = new Date(date + 'T00:00:00'); 572 const isPast = eventDate < today; 573 const isToday = eventDate.getTime() === today.getTime(); 574 575 // Format date display with day of week 576 // Use originalStartDate if this is a multi-month event continuation 577 const displayDateKey = event.originalStartDate || date; 578 const dateObj = new Date(displayDateKey + 'T00:00:00'); 579 const displayDate = dateObj.toLocaleDateString('en-US', { 580 weekday: 'short', 581 month: 'short', 582 day: 'numeric' 583 }); 584 585 // Convert to 12-hour format 586 let displayTime = ''; 587 if (event.time) { 588 const timeParts = event.time.split(':'); 589 if (timeParts.length === 2) { 590 let hour = parseInt(timeParts[0]); 591 const minute = timeParts[1]; 592 const ampm = hour >= 12 ? 'PM' : 'AM'; 593 hour = hour % 12 || 12; 594 displayTime = hour + ':' + minute + ' ' + ampm; 595 } else { 596 displayTime = event.time; 597 } 598 } 599 600 // Multi-day indicator 601 let multiDay = ''; 602 if (event.endDate && event.endDate !== displayDateKey) { 603 const endObj = new Date(event.endDate + 'T00:00:00'); 604 multiDay = ' → ' + endObj.toLocaleDateString('en-US', { 605 weekday: 'short', 606 month: 'short', 607 day: 'numeric' 608 }); 609 } 610 611 const completedClass = event.completed ? ' event-completed' : ''; 612 const pastClass = isPast ? ' event-past' : ''; 613 const color = event.color || '#3498db'; 614 const isTask = event.isTask || false; 615 const completed = event.completed || false; 616 617 let html = '<div class="event-compact-item' + completedClass + pastClass + '" data-event-id="' + event.id + '" data-date="' + date + '" style="border-left-color: ' + color + ';" onclick="' + (isPast ? 'togglePastEventExpand(this)' : '') + '">'; 618 619 html += '<div class="event-info">'; 620 html += '<div class="event-title-row">'; 621 html += '<span class="event-title-compact">' + escapeHtml(event.title) + '</span>'; 622 html += '</div>'; 623 624 // Only show meta and description for non-past events (collapsed for past) 625 if (!isPast) { 626 html += '<div class="event-meta-compact">'; 627 html += '<span class="event-date-time">' + displayDate + multiDay; 628 if (displayTime) { 629 html += ' • ' + displayTime; 630 } 631 // Add TODAY badge for today's events 632 if (isToday) { 633 html += ' <span class="event-today-badge">TODAY</span>'; 634 } 635 // Add namespace badge (stored namespace or _namespace for multi-namespace) 636 let eventNamespace = event.namespace || ''; 637 if (!eventNamespace && event._namespace !== undefined) { 638 eventNamespace = event._namespace; // Fallback to _namespace for multi-namespace loading 639 } 640 if (eventNamespace) { 641 html += ' <span class="event-namespace-badge">' + escapeHtml(eventNamespace) + '</span>'; 642 } 643 html += '</span>'; 644 html += '</div>'; 645 646 if (event.description) { 647 html += '<div class="event-desc-compact">' + renderDescription(event.description) + '</div>'; 648 } 649 } else { 650 // For past events, store data in hidden divs for expand/collapse 651 html += '<div class="event-meta-compact" style="display: none;">'; 652 html += '<span class="event-date-time">' + displayDate + multiDay; 653 if (displayTime) { 654 html += ' • ' + displayTime; 655 } 656 html += '</span>'; 657 html += '</div>'; 658 659 if (event.description) { 660 html += '<div class="event-desc-compact" style="display: none;">' + renderDescription(event.description) + '</div>'; 661 } 662 } 663 664 html += '</div>'; // event-info 665 666 // Use stored namespace from event, fallback to _namespace, then passed namespace 667 let buttonNamespace = event.namespace || ''; 668 if (!buttonNamespace && event._namespace !== undefined) { 669 buttonNamespace = event._namespace; 670 } 671 if (!buttonNamespace) { 672 buttonNamespace = namespace; 673 } 674 675 html += '<div class="event-actions-compact">'; 676 html += '<button class="event-action-btn" onclick="deleteEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">️</button>'; 677 html += '<button class="event-action-btn" onclick="editEvent(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\')">✏️</button>'; 678 html += '</div>'; 679 680 // Checkbox for tasks - ON THE FAR RIGHT 681 if (isTask) { 682 const checked = completed ? 'checked' : ''; 683 html += '<input type="checkbox" class="task-checkbox" ' + checked + ' onclick="toggleTaskComplete(\'' + calId + '\', \'' + event.id + '\', \'' + date + '\', \'' + buttonNamespace + '\', this.checked)">'; 684 } 685 686 html += '</div>'; 687 688 return html; 689} 690 691// Render description with rich content support 692function renderDescription(description) { 693 if (!description) return ''; 694 695 // First, convert DokuWiki/Markdown syntax to placeholder tokens (before escaping) 696 // Use a format that won't be affected by HTML escaping: \x00TOKEN_N\x00 697 698 let rendered = description; 699 const tokens = []; 700 let tokenIndex = 0; 701 702 // Convert DokuWiki image syntax {{image.jpg}} to tokens 703 rendered = rendered.replace(/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/g, function(match, imagePath, alt) { 704 imagePath = imagePath.trim(); 705 alt = alt ? alt.trim() : ''; 706 707 let imageHtml; 708 // Handle external URLs 709 if (imagePath.match(/^https?:\/\//)) { 710 imageHtml = '<img src="' + imagePath + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 711 } else { 712 // Handle internal DokuWiki images 713 const imageUrl = DOKU_BASE + 'lib/exe/fetch.php?media=' + encodeURIComponent(imagePath); 714 imageHtml = '<img src="' + imageUrl + '" alt="' + escapeHtml(alt) + '" class="event-image" />'; 715 } 716 717 const token = '\x00TOKEN' + tokenIndex + '\x00'; 718 tokens[tokenIndex] = imageHtml; 719 tokenIndex++; 720 return token; 721 }); 722 723 // Convert DokuWiki link syntax [[link|text]] to tokens 724 rendered = rendered.replace(/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/g, function(match, link, text) { 725 link = link.trim(); 726 text = text ? text.trim() : link; 727 728 let linkHtml; 729 // Handle external URLs 730 if (link.match(/^https?:\/\//)) { 731 linkHtml = '<a href="' + escapeHtml(link) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 732 } else { 733 // Handle internal DokuWiki links with section anchors 734 const hashIndex = link.indexOf('#'); 735 let pagePart = link; 736 let sectionPart = ''; 737 738 if (hashIndex !== -1) { 739 pagePart = link.substring(0, hashIndex); 740 sectionPart = link.substring(hashIndex); // Includes the # 741 } 742 743 const wikiUrl = DOKU_BASE + 'doku.php?id=' + encodeURIComponent(pagePart) + sectionPart; 744 linkHtml = '<a href="' + wikiUrl + '">' + escapeHtml(text) + '</a>'; 745 } 746 747 const token = '\x00TOKEN' + tokenIndex + '\x00'; 748 tokens[tokenIndex] = linkHtml; 749 tokenIndex++; 750 return token; 751 }); 752 753 // Convert markdown-style links [text](url) to tokens 754 rendered = rendered.replace(/\[([^\]]+)\]\(([^)]+)\)/g, function(match, text, url) { 755 text = text.trim(); 756 url = url.trim(); 757 758 let linkHtml; 759 if (url.match(/^https?:\/\//)) { 760 linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(text) + '</a>'; 761 } else { 762 linkHtml = '<a href="' + escapeHtml(url) + '">' + escapeHtml(text) + '</a>'; 763 } 764 765 const token = '\x00TOKEN' + tokenIndex + '\x00'; 766 tokens[tokenIndex] = linkHtml; 767 tokenIndex++; 768 return token; 769 }); 770 771 // Convert plain URLs to tokens 772 rendered = rendered.replace(/(https?:\/\/[^\s<]+)/g, function(match, url) { 773 const linkHtml = '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(url) + '</a>'; 774 const token = '\x00TOKEN' + tokenIndex + '\x00'; 775 tokens[tokenIndex] = linkHtml; 776 tokenIndex++; 777 return token; 778 }); 779 780 // NOW escape the remaining text (tokens are protected with null bytes) 781 rendered = escapeHtml(rendered); 782 783 // Convert newlines to <br> 784 rendered = rendered.replace(/\n/g, '<br>'); 785 786 // DokuWiki text formatting (on escaped text) 787 // Bold: **text** or __text__ 788 rendered = rendered.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); 789 rendered = rendered.replace(/__(.+?)__/g, '<strong>$1</strong>'); 790 791 // Italic: //text// 792 rendered = rendered.replace(/\/\/(.+?)\/\//g, '<em>$1</em>'); 793 794 // Strikethrough: <del>text</del> 795 rendered = rendered.replace(/<del>(.+?)<\/del>/g, '<del>$1</del>'); 796 797 // Monospace: ''text'' 798 rendered = rendered.replace(/''(.+?)''/g, '<code>$1</code>'); 799 800 // Subscript: <sub>text</sub> 801 rendered = rendered.replace(/<sub>(.+?)<\/sub>/g, '<sub>$1</sub>'); 802 803 // Superscript: <sup>text</sup> 804 rendered = rendered.replace(/<sup>(.+?)<\/sup>/g, '<sup>$1</sup>'); 805 806 // Restore tokens (replace with actual HTML) 807 for (let i = 0; i < tokens.length; i++) { 808 const tokenPattern = new RegExp('\x00TOKEN' + i + '\x00', 'g'); 809 rendered = rendered.replace(tokenPattern, tokens[i]); 810 } 811 812 return rendered; 813} 814 815// Open add event dialog 816function openAddEvent(calId, namespace, date) { 817 const dialog = document.getElementById('dialog-' + calId); 818 const form = document.getElementById('eventform-' + calId); 819 const title = document.getElementById('dialog-title-' + calId); 820 const dateField = document.getElementById('event-date-' + calId); 821 822 if (!dateField) { 823 console.error('Date field not found! ID: event-date-' + calId); 824 return; 825 } 826 827 // Check if there's a filtered namespace active 828 const calendar = document.getElementById(calId); 829 const filteredNamespace = calendar.dataset.filteredNamespace; 830 831 // Use filtered namespace if available, otherwise use the passed namespace 832 const effectiveNamespace = filteredNamespace || namespace; 833 834 console.log('Opening add event: filtered=' + filteredNamespace + ', passed=' + namespace + ', using=' + effectiveNamespace); 835 836 // Reset form 837 form.reset(); 838 document.getElementById('event-id-' + calId).value = ''; 839 840 // Store the effective namespace in a hidden field or data attribute 841 form.dataset.effectiveNamespace = effectiveNamespace; 842 843 // Set date - use local date, not UTC 844 let defaultDate = date; 845 if (!defaultDate) { 846 // Get the currently displayed month from the calendar container 847 const container = document.getElementById(calId); 848 const displayedYear = parseInt(container.getAttribute('data-year')); 849 const displayedMonth = parseInt(container.getAttribute('data-month')); 850 851 console.log('Setting default date: year=' + displayedYear + ', month=' + displayedMonth); 852 853 if (displayedYear && displayedMonth) { 854 // Use first day of the displayed month 855 const year = displayedYear; 856 const month = String(displayedMonth).padStart(2, '0'); 857 defaultDate = `${year}-${month}-01`; 858 console.log('Using displayed month:', defaultDate); 859 } else { 860 // Fallback to today if attributes not found 861 const today = new Date(); 862 const year = today.getFullYear(); 863 const month = String(today.getMonth() + 1).padStart(2, '0'); 864 const day = String(today.getDate()).padStart(2, '0'); 865 defaultDate = `${year}-${month}-${day}`; 866 console.log('Fallback to today:', defaultDate); 867 } 868 } 869 dateField.value = defaultDate; 870 dateField.removeAttribute('data-original-date'); 871 872 // Also set the end date field to the same default (user can change it) 873 const endDateField = document.getElementById('event-end-date-' + calId); 874 if (endDateField) { 875 endDateField.value = ''; // Empty by default (single-day event) 876 // Set min attribute to help the date picker open on the right month 877 endDateField.setAttribute('min', defaultDate); 878 } 879 880 // Set default color 881 document.getElementById('event-color-' + calId).value = '#3498db'; 882 883 // Set title 884 title.textContent = 'Add Event'; 885 886 // Show dialog 887 dialog.style.display = 'flex'; 888 889 // Focus title field 890 setTimeout(() => { 891 const titleField = document.getElementById('event-title-' + calId); 892 if (titleField) titleField.focus(); 893 }, 100); 894} 895 896// Edit event 897function editEvent(calId, eventId, date, namespace) { 898 const params = new URLSearchParams({ 899 call: 'plugin_calendar', 900 action: 'get_event', 901 namespace: namespace, 902 date: date, 903 eventId: eventId 904 }); 905 906 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 907 method: 'POST', 908 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 909 body: params.toString() 910 }) 911 .then(r => r.json()) 912 .then(data => { 913 if (data.success && data.event) { 914 const event = data.event; 915 const dialog = document.getElementById('dialog-' + calId); 916 const title = document.getElementById('dialog-title-' + calId); 917 const dateField = document.getElementById('event-date-' + calId); 918 919 if (!dateField) { 920 console.error('Date field not found when editing!'); 921 return; 922 } 923 924 // Populate form 925 document.getElementById('event-id-' + calId).value = event.id; 926 dateField.value = date; 927 dateField.setAttribute('data-original-date', date); 928 929 const endDateField = document.getElementById('event-end-date-' + calId); 930 endDateField.value = event.endDate || ''; 931 // Set min attribute to help date picker open on the start date's month 932 endDateField.setAttribute('min', date); 933 934 document.getElementById('event-title-' + calId).value = event.title; 935 document.getElementById('event-time-' + calId).value = event.time || ''; 936 document.getElementById('event-color-' + calId).value = event.color || '#3498db'; 937 document.getElementById('event-desc-' + calId).value = event.description || ''; 938 document.getElementById('event-is-task-' + calId).checked = event.isTask || false; 939 940 title.textContent = 'Edit Event'; 941 dialog.style.display = 'flex'; 942 } 943 }) 944 .catch(err => console.error('Error editing event:', err)); 945} 946 947// Delete event 948function deleteEvent(calId, eventId, date, namespace) { 949 if (!confirm('Delete this event?')) return; 950 951 const params = new URLSearchParams({ 952 call: 'plugin_calendar', 953 action: 'delete_event', 954 namespace: namespace, 955 date: date, 956 eventId: eventId 957 }); 958 959 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 960 method: 'POST', 961 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 962 body: params.toString() 963 }) 964 .then(r => r.json()) 965 .then(data => { 966 if (data.success) { 967 // Extract year and month from date 968 const [year, month] = date.split('-').map(Number); 969 970 // Reload calendar data via AJAX 971 reloadCalendarData(calId, year, month, namespace); 972 } 973 }) 974 .catch(err => console.error('Error:', err)); 975} 976 977// Save event (add or edit) 978function saveEventCompact(calId, namespace) { 979 const form = document.getElementById('eventform-' + calId); 980 981 // Use the effective namespace (filtered namespace if active, otherwise passed namespace) 982 const effectiveNamespace = form.dataset.effectiveNamespace || namespace; 983 984 console.log('Saving event: passed namespace=' + namespace + ', effective=' + effectiveNamespace); 985 986 const eventId = document.getElementById('event-id-' + calId).value; 987 const dateInput = document.getElementById('event-date-' + calId); 988 const date = dateInput.value; 989 const oldDate = dateInput.getAttribute('data-original-date') || date; 990 const endDate = document.getElementById('event-end-date-' + calId).value; 991 const title = document.getElementById('event-title-' + calId).value; 992 const time = document.getElementById('event-time-' + calId).value; 993 const color = document.getElementById('event-color-' + calId).value; 994 const description = document.getElementById('event-desc-' + calId).value; 995 const isTask = document.getElementById('event-is-task-' + calId).checked; 996 const completed = false; // New tasks are not completed 997 const isRecurring = document.getElementById('event-recurring-' + calId).checked; 998 const recurrenceType = document.getElementById('event-recurrence-type-' + calId).value; 999 const recurrenceEnd = document.getElementById('event-recurrence-end-' + calId).value; 1000 1001 if (!title) { 1002 alert('Please enter a title'); 1003 return; 1004 } 1005 1006 if (!date) { 1007 alert('Please select a date'); 1008 return; 1009 } 1010 1011 const params = new URLSearchParams({ 1012 call: 'plugin_calendar', 1013 action: 'save_event', 1014 namespace: effectiveNamespace, 1015 eventId: eventId, 1016 date: date, 1017 oldDate: oldDate, 1018 endDate: endDate, 1019 title: title, 1020 time: time, 1021 color: color, 1022 description: description, 1023 isTask: isTask ? '1' : '0', 1024 completed: completed ? '1' : '0', 1025 isRecurring: isRecurring ? '1' : '0', 1026 recurrenceType: recurrenceType, 1027 recurrenceEnd: recurrenceEnd 1028 }); 1029 1030 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1031 method: 'POST', 1032 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 1033 body: params.toString() 1034 }) 1035 .then(r => r.json()) 1036 .then(data => { 1037 if (data.success) { 1038 closeEventDialog(calId); 1039 1040 // For recurring events, do a full page reload to show all occurrences 1041 if (isRecurring) { 1042 location.reload(); 1043 return; 1044 } 1045 1046 // Extract year and month from the NEW date (in case date was changed) 1047 const [year, month] = date.split('-').map(Number); 1048 1049 // Reload calendar data via AJAX to the month of the event 1050 reloadCalendarData(calId, year, month, namespace); 1051 } else { 1052 alert('Error: ' + (data.error || 'Unknown error')); 1053 } 1054 }) 1055 .catch(err => { 1056 console.error('Error:', err); 1057 alert('Error saving event'); 1058 }); 1059} 1060 1061// Reload calendar data without page refresh 1062function reloadCalendarData(calId, year, month, namespace) { 1063 const params = new URLSearchParams({ 1064 call: 'plugin_calendar', 1065 action: 'load_month', 1066 year: year, 1067 month: month, 1068 namespace: namespace, 1069 _: new Date().getTime() // Cache buster 1070 }); 1071 1072 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1073 method: 'POST', 1074 headers: { 1075 'Content-Type': 'application/x-www-form-urlencoded', 1076 'Cache-Control': 'no-cache, no-store, must-revalidate', 1077 'Pragma': 'no-cache' 1078 }, 1079 body: params.toString() 1080 }) 1081 .then(r => r.json()) 1082 .then(data => { 1083 if (data.success) { 1084 const container = document.getElementById(calId); 1085 1086 // Check if this is a full calendar or just event panel 1087 if (container.classList.contains('calendar-compact-container')) { 1088 rebuildCalendar(calId, data.year, data.month, data.events, namespace); 1089 } else if (container.classList.contains('event-panel-standalone')) { 1090 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 1091 } 1092 } 1093 }) 1094 .catch(err => console.error('Error:', err)); 1095} 1096 1097// Close event dialog 1098function closeEventDialog(calId) { 1099 const dialog = document.getElementById('dialog-' + calId); 1100 dialog.style.display = 'none'; 1101} 1102 1103// Escape HTML 1104function escapeHtml(text) { 1105 const div = document.createElement('div'); 1106 div.textContent = text; 1107 return div.innerHTML; 1108} 1109 1110// Highlight event when clicking on bar in calendar 1111function highlightEvent(calId, eventId, date) { 1112 // Find the event item in the event list 1113 const eventList = document.querySelector('#' + calId + ' .event-list-compact'); 1114 if (!eventList) return; 1115 1116 const eventItem = eventList.querySelector('[data-event-id="' + eventId + '"][data-date="' + date + '"]'); 1117 if (!eventItem) return; 1118 1119 // Remove previous highlights 1120 const previousHighlights = eventList.querySelectorAll('.event-highlighted'); 1121 previousHighlights.forEach(el => el.classList.remove('event-highlighted')); 1122 1123 // Add highlight 1124 eventItem.classList.add('event-highlighted'); 1125 1126 // Scroll to event 1127 eventItem.scrollIntoView({ 1128 behavior: 'smooth', 1129 block: 'nearest', 1130 inline: 'nearest' 1131 }); 1132 1133 // Remove highlight after 3 seconds 1134 setTimeout(() => { 1135 eventItem.classList.remove('event-highlighted'); 1136 }, 3000); 1137} 1138 1139// Toggle recurring event options 1140function toggleRecurringOptions(calId) { 1141 const checkbox = document.getElementById('event-recurring-' + calId); 1142 const options = document.getElementById('recurring-options-' + calId); 1143 1144 if (checkbox && options) { 1145 options.style.display = checkbox.checked ? 'block' : 'none'; 1146 } 1147} 1148 1149// Close dialog on escape key 1150document.addEventListener('keydown', function(e) { 1151 if (e.key === 'Escape') { 1152 const dialogs = document.querySelectorAll('.event-dialog-compact'); 1153 dialogs.forEach(dialog => { 1154 if (dialog.style.display === 'flex') { 1155 dialog.style.display = 'none'; 1156 } 1157 }); 1158 } 1159}); 1160 1161// Event panel navigation 1162function navEventPanel(calId, year, month, namespace) { 1163 const params = new URLSearchParams({ 1164 call: 'plugin_calendar', 1165 action: 'load_month', 1166 year: year, 1167 month: month, 1168 namespace: namespace, 1169 _: new Date().getTime() // Cache buster 1170 }); 1171 1172 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1173 method: 'POST', 1174 headers: { 1175 'Content-Type': 'application/x-www-form-urlencoded', 1176 'Cache-Control': 'no-cache, no-store, must-revalidate', 1177 'Pragma': 'no-cache' 1178 }, 1179 body: params.toString() 1180 }) 1181 .then(r => r.json()) 1182 .then(data => { 1183 if (data.success) { 1184 rebuildEventPanel(calId, data.year, data.month, data.events, namespace); 1185 } 1186 }) 1187 .catch(err => console.error('Error:', err)); 1188} 1189 1190// Rebuild event panel only 1191function rebuildEventPanel(calId, year, month, events, namespace) { 1192 const container = document.getElementById(calId); 1193 const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 1194 'July', 'August', 'September', 'October', 'November', 'December']; 1195 1196 // Update header - preserve the onclick and classes 1197 const headerContent = container.querySelector('.panel-header-content'); 1198 const header = container.querySelector('.panel-standalone-header h3'); 1199 if (header) { 1200 header.textContent = monthNames[month - 1] + ' ' + year + ' Events'; 1201 header.className = 'calendar-month-picker'; 1202 header.setAttribute('onclick', `openMonthPickerPanel('${calId}', ${year}, ${month}, '${namespace}')`); 1203 header.setAttribute('title', 'Click to jump to month'); 1204 } 1205 1206 // Update namespace badge if needed (preserve existing one) 1207 // The namespace badge should already exist and doesn't need updating 1208 1209 // Update nav buttons 1210 let prevMonth = month - 1; 1211 let prevYear = year; 1212 if (prevMonth < 1) { 1213 prevMonth = 12; 1214 prevYear--; 1215 } 1216 1217 let nextMonth = month + 1; 1218 let nextYear = year; 1219 if (nextMonth > 12) { 1220 nextMonth = 1; 1221 nextYear++; 1222 } 1223 1224 const navBtns = container.querySelectorAll('.cal-nav-btn'); 1225 if (navBtns[0]) navBtns[0].setAttribute('onclick', `navEventPanel('${calId}', ${prevYear}, ${prevMonth}, '${namespace}')`); 1226 if (navBtns[1]) navBtns[1].setAttribute('onclick', `navEventPanel('${calId}', ${nextYear}, ${nextMonth}, '${namespace}')`); 1227 1228 // Update Today button 1229 const todayBtn = container.querySelector('.cal-today-btn'); 1230 if (todayBtn) { 1231 todayBtn.setAttribute('onclick', `jumpTodayPanel('${calId}', '${namespace}')`); 1232 } 1233 1234 // Rebuild event list 1235 const eventList = container.querySelector('.event-list-compact'); 1236 if (eventList) { 1237 eventList.innerHTML = renderEventListFromData(events, calId, namespace, year, month); 1238 } 1239} 1240 1241// Open add event for panel 1242function openAddEventPanel(calId, namespace) { 1243 const today = new Date(); 1244 const year = today.getFullYear(); 1245 const month = String(today.getMonth() + 1).padStart(2, '0'); 1246 const day = String(today.getDate()).padStart(2, '0'); 1247 const localDate = `${year}-${month}-${day}`; 1248 openAddEvent(calId, namespace, localDate); 1249} 1250 1251// Toggle task completion 1252function toggleTaskComplete(calId, eventId, date, namespace, completed) { 1253 const params = new URLSearchParams({ 1254 call: 'plugin_calendar', 1255 action: 'toggle_task', 1256 namespace: namespace, 1257 date: date, 1258 eventId: eventId, 1259 completed: completed ? '1' : '0' 1260 }); 1261 1262 fetch(DOKU_BASE + 'lib/exe/ajax.php', { 1263 method: 'POST', 1264 headers: {'Content-Type': 'application/x-www-form-urlencoded'}, 1265 body: params.toString() 1266 }) 1267 .then(r => r.json()) 1268 .then(data => { 1269 if (data.success) { 1270 const [year, month] = date.split('-').map(Number); 1271 reloadCalendarData(calId, year, month, namespace); 1272 } 1273 }) 1274 .catch(err => console.error('Error toggling task:', err)); 1275} 1276 1277// Make dialog draggable 1278function makeDialogDraggable(calId) { 1279 const dialog = document.getElementById('dialog-content-' + calId); 1280 const handle = document.getElementById('drag-handle-' + calId); 1281 1282 if (!dialog || !handle) return; 1283 1284 let isDragging = false; 1285 let currentX; 1286 let currentY; 1287 let initialX; 1288 let initialY; 1289 let xOffset = 0; 1290 let yOffset = 0; 1291 1292 handle.addEventListener('mousedown', dragStart); 1293 document.addEventListener('mousemove', drag); 1294 document.addEventListener('mouseup', dragEnd); 1295 1296 function dragStart(e) { 1297 initialX = e.clientX - xOffset; 1298 initialY = e.clientY - yOffset; 1299 isDragging = true; 1300 } 1301 1302 function drag(e) { 1303 if (isDragging) { 1304 e.preventDefault(); 1305 currentX = e.clientX - initialX; 1306 currentY = e.clientY - initialY; 1307 xOffset = currentX; 1308 yOffset = currentY; 1309 setTranslate(currentX, currentY, dialog); 1310 } 1311 } 1312 1313 function dragEnd(e) { 1314 initialX = currentX; 1315 initialY = currentY; 1316 isDragging = false; 1317 } 1318 1319 function setTranslate(xPos, yPos, el) { 1320 el.style.transform = `translate(${xPos}px, ${yPos}px)`; 1321 } 1322} 1323 1324// Initialize dialog draggability when opened 1325const originalOpenAddEvent = openAddEvent; 1326openAddEvent = function(calId, namespace, date) { 1327 originalOpenAddEvent(calId, namespace, date); 1328 setTimeout(() => makeDialogDraggable(calId), 100); 1329}; 1330 1331const originalEditEvent = editEvent; 1332editEvent = function(calId, eventId, date, namespace) { 1333 originalEditEvent(calId, eventId, date, namespace); 1334 setTimeout(() => makeDialogDraggable(calId), 100); 1335}; 1336 1337// Toggle expand/collapse for past events 1338function togglePastEventExpand(element) { 1339 // Stop propagation to prevent any parent click handlers 1340 event.stopPropagation(); 1341 1342 const meta = element.querySelector(".event-meta-compact"); 1343 const desc = element.querySelector(".event-desc-compact"); 1344 1345 // Toggle visibility 1346 if (meta.style.display === "none") { 1347 // Expand 1348 meta.style.display = "block"; 1349 if (desc) desc.style.display = "block"; 1350 element.classList.add("event-past-expanded"); 1351 } else { 1352 // Collapse 1353 meta.style.display = "none"; 1354 if (desc) desc.style.display = "none"; 1355 element.classList.remove("event-past-expanded"); 1356 } 1357} 1358 1359// Filter calendar by namespace when clicking namespace badge 1360document.addEventListener('click', function(e) { 1361 if (e.target.classList.contains('event-namespace-badge')) { 1362 const namespace = e.target.textContent; 1363 const eventItem = e.target.closest('.event-compact-item'); 1364 const eventList = e.target.closest('.event-list-compact'); 1365 const calendar = e.target.closest('.calendar-compact-container'); 1366 1367 if (!eventList || !calendar) return; 1368 1369 const calId = calendar.id; 1370 1371 // Check if already filtered 1372 const isFiltered = eventList.classList.contains('namespace-filtered'); 1373 1374 if (isFiltered && eventList.dataset.filterNamespace === namespace) { 1375 // Unfilter - show all 1376 eventList.classList.remove('namespace-filtered'); 1377 delete eventList.dataset.filterNamespace; 1378 delete calendar.dataset.filteredNamespace; 1379 eventList.querySelectorAll('.event-compact-item').forEach(item => { 1380 item.style.display = ''; 1381 }); 1382 1383 // Update header to show "all namespaces" 1384 updateFilteredNamespaceDisplay(calId, null); 1385 } else { 1386 // Filter by this namespace 1387 eventList.classList.add('namespace-filtered'); 1388 eventList.dataset.filterNamespace = namespace; 1389 calendar.dataset.filteredNamespace = namespace; 1390 eventList.querySelectorAll('.event-compact-item').forEach(item => { 1391 const itemBadge = item.querySelector('.event-namespace-badge'); 1392 if (itemBadge && itemBadge.textContent === namespace) { 1393 item.style.display = ''; 1394 } else { 1395 item.style.display = 'none'; 1396 } 1397 }); 1398 1399 // Update header to show filtered namespace 1400 updateFilteredNamespaceDisplay(calId, namespace); 1401 } 1402 } 1403}); 1404 1405// Update the displayed filtered namespace in event list header 1406function updateFilteredNamespaceDisplay(calId, namespace) { 1407 const calendar = document.getElementById(calId); 1408 if (!calendar) return; 1409 1410 const headerContent = calendar.querySelector('.event-list-header-content'); 1411 if (!headerContent) return; 1412 1413 // Remove existing filter badge 1414 let filterBadge = headerContent.querySelector('.namespace-filter-badge'); 1415 if (filterBadge) { 1416 filterBadge.remove(); 1417 } 1418 1419 // Add new filter badge if filtering 1420 if (namespace) { 1421 filterBadge = document.createElement('span'); 1422 filterBadge.className = 'namespace-badge namespace-filter-badge'; 1423 filterBadge.innerHTML = escapeHtml(namespace) + ' <button class="filter-clear-inline" onclick="clearNamespaceFilter(\'' + calId + '\'); event.stopPropagation();">✕</button>'; 1424 headerContent.appendChild(filterBadge); 1425 } 1426} 1427 1428// Clear namespace filter 1429function clearNamespaceFilter(calId) { 1430 const calendar = document.getElementById(calId); 1431 if (!calendar) return; 1432 1433 const eventList = calendar.querySelector('.event-list-compact'); 1434 if (!eventList) return; 1435 1436 // Clear filter 1437 eventList.classList.remove('namespace-filtered'); 1438 delete eventList.dataset.filterNamespace; 1439 delete calendar.dataset.filteredNamespace; 1440 eventList.querySelectorAll('.event-compact-item').forEach(item => { 1441 item.style.display = ''; 1442 }); 1443 1444 // Update header 1445 updateFilteredNamespaceDisplay(calId, null); 1446} 1447