119378907SAtari911<?php 219378907SAtari911/** 319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component) 419378907SAtari911 * Compact design with integrated event list 519378907SAtari911 * 619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 719378907SAtari911 * @author DokuWiki Community 819378907SAtari911 */ 919378907SAtari911 1019378907SAtari911if (!defined('DOKU_INC')) die(); 1119378907SAtari911 1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin { 1319378907SAtari911 1419378907SAtari911 public function getType() { 1519378907SAtari911 return 'substition'; 1619378907SAtari911 } 1719378907SAtari911 1819378907SAtari911 public function getPType() { 1919378907SAtari911 return 'block'; 2019378907SAtari911 } 2119378907SAtari911 2219378907SAtari911 public function getSort() { 2319378907SAtari911 return 155; 2419378907SAtari911 } 2519378907SAtari911 2619378907SAtari911 public function connectTo($mode) { 2719378907SAtari911 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2819378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2919378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 3019378907SAtari911 } 3119378907SAtari911 3219378907SAtari911 public function handle($match, $state, $pos, Doku_Handler $handler) { 3319378907SAtari911 $isEventList = (strpos($match, '{{eventlist') === 0); 3419378907SAtari911 $isEventPanel = (strpos($match, '{{eventpanel') === 0); 3519378907SAtari911 3619378907SAtari911 if ($isEventList) { 3719378907SAtari911 $match = substr($match, 12, -2); 3819378907SAtari911 } elseif ($isEventPanel) { 3919378907SAtari911 $match = substr($match, 13, -2); 4019378907SAtari911 } else { 4119378907SAtari911 $match = substr($match, 10, -2); 4219378907SAtari911 } 4319378907SAtari911 4419378907SAtari911 $params = array( 4519378907SAtari911 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 4619378907SAtari911 'year' => date('Y'), 4719378907SAtari911 'month' => date('n'), 4819378907SAtari911 'namespace' => '', 4919378907SAtari911 'daterange' => '', 50*e3a9f44cSAtari911 'date' => '', 51*e3a9f44cSAtari911 'range' => '' 5219378907SAtari911 ); 5319378907SAtari911 5419378907SAtari911 if (trim($match)) { 5519378907SAtari911 $pairs = preg_split('/\s+/', trim($match)); 5619378907SAtari911 foreach ($pairs as $pair) { 5719378907SAtari911 if (strpos($pair, '=') !== false) { 5819378907SAtari911 list($key, $value) = explode('=', $pair, 2); 5919378907SAtari911 $params[trim($key)] = trim($value); 6087ac9bf3SAtari911 } else { 6187ac9bf3SAtari911 // Handle standalone flags like "today" 6287ac9bf3SAtari911 $params[trim($pair)] = true; 6319378907SAtari911 } 6419378907SAtari911 } 6519378907SAtari911 } 6619378907SAtari911 6719378907SAtari911 return $params; 6819378907SAtari911 } 6919378907SAtari911 7019378907SAtari911 public function render($mode, Doku_Renderer $renderer, $data) { 7119378907SAtari911 if ($mode !== 'xhtml') return false; 7219378907SAtari911 7319378907SAtari911 if ($data['type'] === 'eventlist') { 7419378907SAtari911 $html = $this->renderStandaloneEventList($data); 7519378907SAtari911 } elseif ($data['type'] === 'eventpanel') { 7619378907SAtari911 $html = $this->renderEventPanelOnly($data); 7719378907SAtari911 } else { 7819378907SAtari911 $html = $this->renderCompactCalendar($data); 7919378907SAtari911 } 8019378907SAtari911 8119378907SAtari911 $renderer->doc .= $html; 8219378907SAtari911 return true; 8319378907SAtari911 } 8419378907SAtari911 8519378907SAtari911 private function renderCompactCalendar($data) { 8619378907SAtari911 $year = (int)$data['year']; 8719378907SAtari911 $month = (int)$data['month']; 8819378907SAtari911 $namespace = $data['namespace']; 8919378907SAtari911 90*e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 91*e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 92*e3a9f44cSAtari911 93*e3a9f44cSAtari911 if ($isMultiNamespace) { 94*e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 95*e3a9f44cSAtari911 } else { 9619378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 97*e3a9f44cSAtari911 } 9819378907SAtari911 $calId = 'cal_' . md5(serialize($data) . microtime()); 9919378907SAtari911 10019378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 10119378907SAtari911 10219378907SAtari911 $prevMonth = $month - 1; 10319378907SAtari911 $prevYear = $year; 10419378907SAtari911 if ($prevMonth < 1) { 10519378907SAtari911 $prevMonth = 12; 10619378907SAtari911 $prevYear--; 10719378907SAtari911 } 10819378907SAtari911 10919378907SAtari911 $nextMonth = $month + 1; 11019378907SAtari911 $nextYear = $year; 11119378907SAtari911 if ($nextMonth > 12) { 11219378907SAtari911 $nextMonth = 1; 11319378907SAtari911 $nextYear++; 11419378907SAtari911 } 11519378907SAtari911 11619378907SAtari911 $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">'; 11719378907SAtari911 11819378907SAtari911 // Embed events data as JSON for JavaScript access 11919378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 12019378907SAtari911 12119378907SAtari911 // Left side: Calendar 12219378907SAtari911 $html .= '<div class="calendar-compact-left">'; 12319378907SAtari911 12419378907SAtari911 // Header with navigation 12519378907SAtari911 $html .= '<div class="calendar-compact-header">'; 12619378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 12787ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>'; 12819378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 12987ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 13019378907SAtari911 $html .= '</div>'; 13119378907SAtari911 13219378907SAtari911 // Calendar grid 13319378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 13419378907SAtari911 $html .= '<thead><tr>'; 13519378907SAtari911 $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>'; 13619378907SAtari911 $html .= '</tr></thead><tbody>'; 13719378907SAtari911 13819378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 13919378907SAtari911 $daysInMonth = date('t', $firstDay); 14019378907SAtari911 $dayOfWeek = date('w', $firstDay); 14119378907SAtari911 142*e3a9f44cSAtari911 // Build a map of all events with their date ranges for the calendar grid 14387ac9bf3SAtari911 $eventRanges = array(); 144*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 14587ac9bf3SAtari911 foreach ($dayEvents as $evt) { 14687ac9bf3SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 14787ac9bf3SAtari911 $startDate = $dateKey; 14887ac9bf3SAtari911 $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey; 14987ac9bf3SAtari911 15087ac9bf3SAtari911 // Only process events that touch this month 15187ac9bf3SAtari911 $eventStart = new DateTime($startDate); 15287ac9bf3SAtari911 $eventEnd = new DateTime($endDate); 15387ac9bf3SAtari911 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month)); 15487ac9bf3SAtari911 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth)); 15587ac9bf3SAtari911 15687ac9bf3SAtari911 // Skip if event doesn't overlap with current month 15787ac9bf3SAtari911 if ($eventEnd < $monthStart || $eventStart > $monthEnd) { 15887ac9bf3SAtari911 continue; 15987ac9bf3SAtari911 } 16087ac9bf3SAtari911 16187ac9bf3SAtari911 // Create entry for each day the event spans 16287ac9bf3SAtari911 $current = clone $eventStart; 16387ac9bf3SAtari911 while ($current <= $eventEnd) { 16487ac9bf3SAtari911 $currentKey = $current->format('Y-m-d'); 16587ac9bf3SAtari911 16687ac9bf3SAtari911 // Check if this date is in current month 16787ac9bf3SAtari911 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey); 16887ac9bf3SAtari911 if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) { 16987ac9bf3SAtari911 if (!isset($eventRanges[$currentKey])) { 17087ac9bf3SAtari911 $eventRanges[$currentKey] = array(); 17187ac9bf3SAtari911 } 17287ac9bf3SAtari911 17387ac9bf3SAtari911 // Add event with span information 17487ac9bf3SAtari911 $evt['_span_start'] = $startDate; 17587ac9bf3SAtari911 $evt['_span_end'] = $endDate; 17687ac9bf3SAtari911 $evt['_is_first_day'] = ($currentKey === $startDate); 17787ac9bf3SAtari911 $evt['_is_last_day'] = ($currentKey === $endDate); 17887ac9bf3SAtari911 $evt['_original_date'] = $dateKey; // Keep track of original date 17987ac9bf3SAtari911 18087ac9bf3SAtari911 // Check if event continues from previous month or to next month 18187ac9bf3SAtari911 $evt['_continues_from_prev'] = ($eventStart < $monthStart); 18287ac9bf3SAtari911 $evt['_continues_to_next'] = ($eventEnd > $monthEnd); 18387ac9bf3SAtari911 18487ac9bf3SAtari911 $eventRanges[$currentKey][] = $evt; 18587ac9bf3SAtari911 } 18687ac9bf3SAtari911 18787ac9bf3SAtari911 $current->modify('+1 day'); 18887ac9bf3SAtari911 } 18987ac9bf3SAtari911 } 19087ac9bf3SAtari911 } 19187ac9bf3SAtari911 19219378907SAtari911 $currentDay = 1; 19319378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 19419378907SAtari911 19519378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 19619378907SAtari911 $html .= '<tr>'; 19719378907SAtari911 for ($col = 0; $col < 7; $col++) { 19819378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 19919378907SAtari911 $html .= '<td class="cal-empty"></td>'; 20019378907SAtari911 } else { 20119378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 20219378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 20387ac9bf3SAtari911 $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]); 20419378907SAtari911 20519378907SAtari911 $classes = 'cal-day'; 20619378907SAtari911 if ($isToday) $classes .= ' cal-today'; 20719378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 20819378907SAtari911 20919378907SAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 21019378907SAtari911 $html .= '<span class="day-num">' . $currentDay . '</span>'; 21119378907SAtari911 21219378907SAtari911 if ($hasEvents) { 21319378907SAtari911 // Sort events by time (no time first, then by time) 21487ac9bf3SAtari911 $sortedEvents = $eventRanges[$dateKey]; 21519378907SAtari911 usort($sortedEvents, function($a, $b) { 21619378907SAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 21719378907SAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 21819378907SAtari911 21919378907SAtari911 // Events without time go first 22019378907SAtari911 if (empty($timeA) && !empty($timeB)) return -1; 22119378907SAtari911 if (!empty($timeA) && empty($timeB)) return 1; 22219378907SAtari911 if (empty($timeA) && empty($timeB)) return 0; 22319378907SAtari911 22419378907SAtari911 // Sort by time 22519378907SAtari911 return strcmp($timeA, $timeB); 22619378907SAtari911 }); 22719378907SAtari911 22819378907SAtari911 // Show colored stacked bars for each event 22919378907SAtari911 $html .= '<div class="event-indicators">'; 23019378907SAtari911 foreach ($sortedEvents as $evt) { 23119378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 23219378907SAtari911 $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; 23319378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 23419378907SAtari911 $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; 23587ac9bf3SAtari911 $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey; 23687ac9bf3SAtari911 $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true; 23787ac9bf3SAtari911 $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true; 23819378907SAtari911 23919378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 24019378907SAtari911 24187ac9bf3SAtari911 // Add classes for multi-day spanning 24287ac9bf3SAtari911 if (!$isFirstDay) $barClass .= ' event-bar-continues'; 24387ac9bf3SAtari911 if (!$isLastDay) $barClass .= ' event-bar-continuing'; 24487ac9bf3SAtari911 24519378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 24619378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 24719378907SAtari911 $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 24887ac9bf3SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">'; 24919378907SAtari911 $html .= '</span>'; 25019378907SAtari911 } 25119378907SAtari911 $html .= '</div>'; 25219378907SAtari911 } 25319378907SAtari911 25419378907SAtari911 $html .= '</td>'; 25519378907SAtari911 $currentDay++; 25619378907SAtari911 } 25719378907SAtari911 } 25819378907SAtari911 $html .= '</tr>'; 25919378907SAtari911 } 26019378907SAtari911 26119378907SAtari911 $html .= '</tbody></table>'; 26219378907SAtari911 $html .= '</div>'; // End calendar-left 26319378907SAtari911 26419378907SAtari911 // Right side: Event list 26519378907SAtari911 $html .= '<div class="calendar-compact-right">'; 26619378907SAtari911 $html .= '<div class="event-list-header">'; 26719378907SAtari911 $html .= '<div class="event-list-header-content">'; 26819378907SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 26919378907SAtari911 if ($namespace) { 27019378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 27119378907SAtari911 } 27219378907SAtari911 $html .= '</div>'; 27319378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 27419378907SAtari911 $html .= '</div>'; 27519378907SAtari911 27619378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 27719378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 27819378907SAtari911 $html .= '</div>'; 27919378907SAtari911 28019378907SAtari911 $html .= '</div>'; // End calendar-right 28119378907SAtari911 28219378907SAtari911 // Event dialog 28319378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 28419378907SAtari911 28587ac9bf3SAtari911 // Month/Year picker dialog (at container level for proper overlay) 28687ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 28787ac9bf3SAtari911 28819378907SAtari911 $html .= '</div>'; // End container 28919378907SAtari911 29019378907SAtari911 return $html; 29119378907SAtari911 } 29219378907SAtari911 29319378907SAtari911 private function renderEventListContent($events, $calId, $namespace) { 29419378907SAtari911 if (empty($events)) { 29519378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 29619378907SAtari911 } 29719378907SAtari911 298*e3a9f44cSAtari911 // Sort by date ascending (chronological order - oldest first) 29919378907SAtari911 ksort($events); 30019378907SAtari911 301*e3a9f44cSAtari911 // Sort events within each day by time 302*e3a9f44cSAtari911 foreach ($events as $dateKey => &$dayEvents) { 303*e3a9f44cSAtari911 usort($dayEvents, function($a, $b) { 304*e3a9f44cSAtari911 $timeA = isset($a['time']) ? $a['time'] : '00:00'; 305*e3a9f44cSAtari911 $timeB = isset($b['time']) ? $b['time'] : '00:00'; 306*e3a9f44cSAtari911 return strcmp($timeA, $timeB); 307*e3a9f44cSAtari911 }); 308*e3a9f44cSAtari911 } 309*e3a9f44cSAtari911 unset($dayEvents); // Break reference 310*e3a9f44cSAtari911 311*e3a9f44cSAtari911 // Get today's date for comparison 312*e3a9f44cSAtari911 $today = date('Y-m-d'); 313*e3a9f44cSAtari911 $firstFutureEventId = null; 314*e3a9f44cSAtari911 315*e3a9f44cSAtari911 // Build HTML for each event 316*e3a9f44cSAtari911 $html = ''; 317*e3a9f44cSAtari911 31819378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 319*e3a9f44cSAtari911 $isPast = $dateKey < $today; 320*e3a9f44cSAtari911 $isToday = $dateKey === $today; 321*e3a9f44cSAtari911 32219378907SAtari911 foreach ($dayEvents as $event) { 323*e3a9f44cSAtari911 // Track first future/today event for auto-scroll 324*e3a9f44cSAtari911 if (!$firstFutureEventId && $dateKey >= $today) { 325*e3a9f44cSAtari911 $firstFutureEventId = isset($event['id']) ? $event['id'] : ''; 326*e3a9f44cSAtari911 } 32719378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 32819378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 32919378907SAtari911 $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; 33019378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 33119378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 33219378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 33319378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 33419378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 33519378907SAtari911 33619378907SAtari911 // Process description for wiki syntax, HTML, images, and links 33719378907SAtari911 $renderedDescription = $this->renderDescription($description); 33819378907SAtari911 33919378907SAtari911 // Convert to 12-hour format 34019378907SAtari911 $displayTime = ''; 34119378907SAtari911 if ($time) { 34219378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 34319378907SAtari911 if ($timeObj) { 34419378907SAtari911 $displayTime = $timeObj->format('g:i A'); 34519378907SAtari911 } else { 34619378907SAtari911 $displayTime = $time; 34719378907SAtari911 } 34819378907SAtari911 } 34919378907SAtari911 35087ac9bf3SAtari911 // Format date display with day of week 351*e3a9f44cSAtari911 // Use originalStartDate if this is a multi-month event continuation 352*e3a9f44cSAtari911 $displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey; 353*e3a9f44cSAtari911 $dateObj = new DateTime($displayDateKey); 35487ac9bf3SAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24" 35519378907SAtari911 35619378907SAtari911 // Multi-day indicator 35719378907SAtari911 $multiDay = ''; 358*e3a9f44cSAtari911 if ($endDate && $endDate !== $displayDateKey) { 35919378907SAtari911 $endObj = new DateTime($endDate); 36087ac9bf3SAtari911 $multiDay = ' → ' . $endObj->format('D, M j'); 36119378907SAtari911 } 36219378907SAtari911 36319378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 364*e3a9f44cSAtari911 $pastClass = $isPast ? ' event-past' : ''; 365*e3a9f44cSAtari911 $firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : ''; 36619378907SAtari911 367*e3a9f44cSAtari911 $html .= '<div class="event-compact-item' . $completedClass . $pastClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';"' . $firstFutureAttr . '>'; 36819378907SAtari911 36919378907SAtari911 $html .= '<div class="event-info">'; 37019378907SAtari911 $html .= '<div class="event-title-row">'; 37119378907SAtari911 $html .= '<span class="event-title-compact">' . $title . '</span>'; 37219378907SAtari911 $html .= '</div>'; 37319378907SAtari911 374*e3a9f44cSAtari911 // For past events, hide meta and description (collapsed) 375*e3a9f44cSAtari911 if (!$isPast) { 37619378907SAtari911 $html .= '<div class="event-meta-compact">'; 37719378907SAtari911 $html .= '<span class="event-date-time">' . $displayDate . $multiDay; 37819378907SAtari911 if ($displayTime) { 37919378907SAtari911 $html .= ' • ' . $displayTime; 38019378907SAtari911 } 381*e3a9f44cSAtari911 // Add TODAY badge for today's events 382*e3a9f44cSAtari911 if ($isToday) { 383*e3a9f44cSAtari911 $html .= ' <span class="event-today-badge">TODAY</span>'; 384*e3a9f44cSAtari911 } 385*e3a9f44cSAtari911 // Add namespace badge (for multi-namespace or stored namespace) 386*e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 387*e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 388*e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility 389*e3a9f44cSAtari911 } 390*e3a9f44cSAtari911 if ($eventNamespace) { 391*e3a9f44cSAtari911 $html .= ' <span class="event-namespace-badge">' . htmlspecialchars($eventNamespace) . '</span>'; 392*e3a9f44cSAtari911 } 39319378907SAtari911 $html .= '</span>'; 39419378907SAtari911 $html .= '</div>'; 39519378907SAtari911 39619378907SAtari911 if ($description) { 39719378907SAtari911 $html .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 39819378907SAtari911 } 399*e3a9f44cSAtari911 } 40019378907SAtari911 40119378907SAtari911 $html .= '</div>'; // event-info 40219378907SAtari911 403*e3a9f44cSAtari911 // Use stored namespace from event, fallback to passed namespace 404*e3a9f44cSAtari911 $buttonNamespace = isset($event['namespace']) ? $event['namespace'] : $namespace; 405*e3a9f44cSAtari911 40619378907SAtari911 $html .= '<div class="event-actions-compact">'; 407*e3a9f44cSAtari911 $html .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">️</button>'; 408*e3a9f44cSAtari911 $html .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\')">✏️</button>'; 40919378907SAtari911 $html .= '</div>'; 41019378907SAtari911 41119378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 41219378907SAtari911 if ($isTask) { 41319378907SAtari911 $checked = $completed ? 'checked' : ''; 414*e3a9f44cSAtari911 $html .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $buttonNamespace . '\', this.checked)">'; 41519378907SAtari911 } 41619378907SAtari911 41719378907SAtari911 $html .= '</div>'; 418*e3a9f44cSAtari911 419*e3a9f44cSAtari911 // Add to HTML output 42019378907SAtari911 } 42119378907SAtari911 } 42219378907SAtari911 42319378907SAtari911 return $html; 42419378907SAtari911 } 42519378907SAtari911 42619378907SAtari911 private function renderEventPanelOnly($data) { 42719378907SAtari911 $year = (int)$data['year']; 42819378907SAtari911 $month = (int)$data['month']; 42919378907SAtari911 $namespace = $data['namespace']; 43087ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 43187ac9bf3SAtari911 43287ac9bf3SAtari911 // Validate height format (must be px, em, rem, vh, or %) 43387ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 43487ac9bf3SAtari911 $height = '400px'; // Default fallback 43587ac9bf3SAtari911 } 43619378907SAtari911 437*e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 438*e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 439*e3a9f44cSAtari911 440*e3a9f44cSAtari911 if ($isMultiNamespace) { 441*e3a9f44cSAtari911 $events = $this->loadEventsMultiNamespace($namespace, $year, $month); 442*e3a9f44cSAtari911 } else { 44319378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 444*e3a9f44cSAtari911 } 44519378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 44619378907SAtari911 44719378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 44819378907SAtari911 44919378907SAtari911 $prevMonth = $month - 1; 45019378907SAtari911 $prevYear = $year; 45119378907SAtari911 if ($prevMonth < 1) { 45219378907SAtari911 $prevMonth = 12; 45319378907SAtari911 $prevYear--; 45419378907SAtari911 } 45519378907SAtari911 45619378907SAtari911 $nextMonth = $month + 1; 45719378907SAtari911 $nextYear = $year; 45819378907SAtari911 if ($nextMonth > 12) { 45919378907SAtari911 $nextMonth = 1; 46019378907SAtari911 $nextYear++; 46119378907SAtari911 } 46219378907SAtari911 46387ac9bf3SAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '">'; 46419378907SAtari911 46519378907SAtari911 // Header with navigation 46619378907SAtari911 $html .= '<div class="panel-standalone-header">'; 46719378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 46887ac9bf3SAtari911 $html .= '<div class="panel-header-content">'; 46987ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . ' Events</h3>'; 47087ac9bf3SAtari911 if ($namespace) { 471*e3a9f44cSAtari911 // Show multiple namespace badges if multi-namespace 472*e3a9f44cSAtari911 if ($isMultiNamespace) { 473*e3a9f44cSAtari911 // Handle wildcard 474*e3a9f44cSAtari911 if (strpos($namespace, '*') !== false) { 475*e3a9f44cSAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span> '; 476*e3a9f44cSAtari911 } else { 477*e3a9f44cSAtari911 // Semicolon-separated list 478*e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespace)); 479*e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 480*e3a9f44cSAtari911 $ns = trim($ns); 481*e3a9f44cSAtari911 if (empty($ns)) continue; 482*e3a9f44cSAtari911 $namespaceUrl = DOKU_BASE . 'doku.php?id=' . str_replace(':', ':', $ns); 483*e3a9f44cSAtari911 $html .= '<a href="' . $namespaceUrl . '" class="namespace-badge" title="Go to namespace page">' . htmlspecialchars($ns) . '</a> '; 484*e3a9f44cSAtari911 } 485*e3a9f44cSAtari911 } 486*e3a9f44cSAtari911 } else { 48787ac9bf3SAtari911 $namespaceUrl = DOKU_BASE . 'doku.php?id=' . str_replace(':', ':', $namespace); 48887ac9bf3SAtari911 $html .= '<a href="' . $namespaceUrl . '" class="namespace-badge" title="Go to namespace page">' . htmlspecialchars($namespace) . '</a>'; 48987ac9bf3SAtari911 } 490*e3a9f44cSAtari911 } 49187ac9bf3SAtari911 $html .= '</div>'; 49219378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 49387ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 49419378907SAtari911 $html .= '</div>'; 49519378907SAtari911 49619378907SAtari911 $html .= '<div class="panel-standalone-actions">'; 49719378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add Event</button>'; 49819378907SAtari911 $html .= '</div>'; 49919378907SAtari911 50087ac9bf3SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">'; 50119378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 50219378907SAtari911 $html .= '</div>'; 50319378907SAtari911 50419378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 50519378907SAtari911 50687ac9bf3SAtari911 // Month/Year picker for event panel 50787ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 50887ac9bf3SAtari911 50919378907SAtari911 $html .= '</div>'; 51019378907SAtari911 51119378907SAtari911 return $html; 51219378907SAtari911 } 51319378907SAtari911 51419378907SAtari911 private function renderStandaloneEventList($data) { 51519378907SAtari911 $namespace = $data['namespace']; 51619378907SAtari911 $daterange = $data['daterange']; 51719378907SAtari911 $date = $data['date']; 518*e3a9f44cSAtari911 $range = isset($data['range']) ? strtolower($data['range']) : ''; 51987ac9bf3SAtari911 $today = isset($data['today']) ? true : false; 520*e3a9f44cSAtari911 $sidebar = isset($data['sidebar']) ? true : false; 52119378907SAtari911 522*e3a9f44cSAtari911 // Handle "range" parameter - day, week, or month 523*e3a9f44cSAtari911 if ($range === 'day') { 52487ac9bf3SAtari911 $startDate = date('Y-m-d'); 52587ac9bf3SAtari911 $endDate = date('Y-m-d'); 526*e3a9f44cSAtari911 $headerText = 'Today'; 527*e3a9f44cSAtari911 } elseif ($range === 'week') { 528*e3a9f44cSAtari911 $startDate = date('Y-m-d'); // Today 529*e3a9f44cSAtari911 $endDateTime = new DateTime($startDate); 530*e3a9f44cSAtari911 $endDateTime->modify('+7 days'); 531*e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); 532*e3a9f44cSAtari911 $headerText = 'This Week'; 533*e3a9f44cSAtari911 } elseif ($range === 'month') { 534*e3a9f44cSAtari911 $startDate = date('Y-m-01'); // First of current month 535*e3a9f44cSAtari911 $endDate = date('Y-m-t'); // Last of current month 536*e3a9f44cSAtari911 $dt = new DateTime($startDate); 537*e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 538*e3a9f44cSAtari911 } elseif ($sidebar) { 539*e3a9f44cSAtari911 // Handle "sidebar" parameter - shows today through one month from today 540*e3a9f44cSAtari911 $startDate = date('Y-m-d'); // Today 541*e3a9f44cSAtari911 $endDateTime = new DateTime($startDate); 542*e3a9f44cSAtari911 $endDateTime->modify('+1 month'); 543*e3a9f44cSAtari911 $endDate = $endDateTime->format('Y-m-d'); // One month from today 544*e3a9f44cSAtari911 $headerText = 'Upcoming'; 545*e3a9f44cSAtari911 } elseif ($today) { 546*e3a9f44cSAtari911 $startDate = date('Y-m-d'); 547*e3a9f44cSAtari911 $endDate = date('Y-m-d'); 548*e3a9f44cSAtari911 $headerText = 'Today'; 54987ac9bf3SAtari911 } elseif ($daterange) { 55019378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 551*e3a9f44cSAtari911 $start = new DateTime($startDate); 552*e3a9f44cSAtari911 $end = new DateTime($endDate); 553*e3a9f44cSAtari911 $headerText = $start->format('M j') . ' - ' . $end->format('M j, Y'); 55419378907SAtari911 } elseif ($date) { 55519378907SAtari911 $startDate = $date; 55619378907SAtari911 $endDate = $date; 557*e3a9f44cSAtari911 $dt = new DateTime($date); 558*e3a9f44cSAtari911 $headerText = $dt->format('l, F j, Y'); 55919378907SAtari911 } else { 56019378907SAtari911 $startDate = date('Y-m-01'); 56119378907SAtari911 $endDate = date('Y-m-t'); 562*e3a9f44cSAtari911 $dt = new DateTime($startDate); 563*e3a9f44cSAtari911 $headerText = $dt->format('F Y'); 56419378907SAtari911 } 56519378907SAtari911 566*e3a9f44cSAtari911 // Load all events in date range 56719378907SAtari911 $allEvents = array(); 56819378907SAtari911 $start = new DateTime($startDate); 56919378907SAtari911 $end = new DateTime($endDate); 57019378907SAtari911 $end->modify('+1 day'); 57119378907SAtari911 57219378907SAtari911 $interval = new DateInterval('P1D'); 57319378907SAtari911 $period = new DatePeriod($start, $interval, $end); 57419378907SAtari911 575*e3a9f44cSAtari911 // Check if multiple namespaces or wildcard specified 576*e3a9f44cSAtari911 $isMultiNamespace = !empty($namespace) && (strpos($namespace, ';') !== false || strpos($namespace, '*') !== false); 577*e3a9f44cSAtari911 57819378907SAtari911 static $loadedMonths = array(); 57919378907SAtari911 58019378907SAtari911 foreach ($period as $dt) { 58119378907SAtari911 $year = (int)$dt->format('Y'); 58219378907SAtari911 $month = (int)$dt->format('n'); 58319378907SAtari911 $dateKey = $dt->format('Y-m-d'); 58419378907SAtari911 585*e3a9f44cSAtari911 $monthKey = $year . '-' . $month . '-' . $namespace; 58619378907SAtari911 58719378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 588*e3a9f44cSAtari911 if ($isMultiNamespace) { 589*e3a9f44cSAtari911 $loadedMonths[$monthKey] = $this->loadEventsMultiNamespace($namespace, $year, $month); 590*e3a9f44cSAtari911 } else { 59119378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 59219378907SAtari911 } 593*e3a9f44cSAtari911 } 59419378907SAtari911 59519378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 59619378907SAtari911 59719378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 59819378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 59919378907SAtari911 } 60019378907SAtari911 } 60119378907SAtari911 602*e3a9f44cSAtari911 // Simple 2-line display widget 603*e3a9f44cSAtari911 $html = '<div class="eventlist-simple">'; 60419378907SAtari911 60519378907SAtari911 if (empty($allEvents)) { 606*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-empty">'; 607*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">' . htmlspecialchars($headerText); 608*e3a9f44cSAtari911 if ($namespace) { 609*e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($namespace) . '</span>'; 61087ac9bf3SAtari911 } 611*e3a9f44cSAtari911 $html .= '</div>'; 612*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">No events</div>'; 613*e3a9f44cSAtari911 $html .= '</div>'; 614*e3a9f44cSAtari911 } else { 615*e3a9f44cSAtari911 // Calculate today and tomorrow's dates for highlighting 616*e3a9f44cSAtari911 $today = date('Y-m-d'); 617*e3a9f44cSAtari911 $tomorrow = date('Y-m-d', strtotime('+1 day')); 618*e3a9f44cSAtari911 619*e3a9f44cSAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 620*e3a9f44cSAtari911 $dateObj = new DateTime($dateKey); 621*e3a9f44cSAtari911 $displayDate = $dateObj->format('D, M j'); 622*e3a9f44cSAtari911 623*e3a9f44cSAtari911 // Check if this date is today or tomorrow 624*e3a9f44cSAtari911 // Enable highlighting for sidebar mode AND range modes (day, week, month) 625*e3a9f44cSAtari911 $enableHighlighting = $sidebar || !empty($range); 626*e3a9f44cSAtari911 $isToday = $enableHighlighting && ($dateKey === $today); 627*e3a9f44cSAtari911 $isTomorrow = $enableHighlighting && ($dateKey === $tomorrow); 62819378907SAtari911 62919378907SAtari911 foreach ($dayEvents as $event) { 630*e3a9f44cSAtari911 // Skip completed tasks when in sidebar mode or day/week range 631*e3a9f44cSAtari911 $skipCompleted = $sidebar || ($range === 'day') || ($range === 'week'); 632*e3a9f44cSAtari911 if ($skipCompleted && !empty($event['isTask']) && !empty($event['completed'])) { 633*e3a9f44cSAtari911 continue; 634*e3a9f44cSAtari911 } 63519378907SAtari911 636*e3a9f44cSAtari911 // Line 1: Header (Title, Time, Date, Namespace) 637*e3a9f44cSAtari911 $todayClass = $isToday ? ' eventlist-simple-today' : ''; 638*e3a9f44cSAtari911 $tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : ''; 639*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-item' . $todayClass . $tomorrowClass . '">'; 640*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-header">'; 641*e3a9f44cSAtari911 642*e3a9f44cSAtari911 // Title 643*e3a9f44cSAtari911 $html .= '<span class="eventlist-simple-title">' . htmlspecialchars($event['title']) . '</span>'; 644*e3a9f44cSAtari911 645*e3a9f44cSAtari911 // Time (12-hour format) 646*e3a9f44cSAtari911 if (!empty($event['time'])) { 647*e3a9f44cSAtari911 $timeParts = explode(':', $event['time']); 64887ac9bf3SAtari911 if (count($timeParts) === 2) { 64987ac9bf3SAtari911 $hour = (int)$timeParts[0]; 65087ac9bf3SAtari911 $minute = $timeParts[1]; 65187ac9bf3SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 652*e3a9f44cSAtari911 $hour = $hour % 12 ?: 12; 65387ac9bf3SAtari911 $displayTime = $hour . ':' . $minute . ' ' . $ampm; 654*e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-time">' . $displayTime . '</span>'; 65519378907SAtari911 } 65687ac9bf3SAtari911 } 65787ac9bf3SAtari911 658*e3a9f44cSAtari911 // Date 659*e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-date">' . $displayDate . '</span>'; 660*e3a9f44cSAtari911 661*e3a9f44cSAtari911 // TODAY badge (show for today's events in sidebar) 662*e3a9f44cSAtari911 if ($isToday) { 663*e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-today-badge">TODAY</span>'; 66487ac9bf3SAtari911 } 665*e3a9f44cSAtari911 666*e3a9f44cSAtari911 // Namespace badge (show individual event's namespace) 667*e3a9f44cSAtari911 $eventNamespace = isset($event['namespace']) ? $event['namespace'] : ''; 668*e3a9f44cSAtari911 if (!$eventNamespace && isset($event['_namespace'])) { 669*e3a9f44cSAtari911 $eventNamespace = $event['_namespace']; // Fallback to _namespace for multi-namespace loading 67019378907SAtari911 } 671*e3a9f44cSAtari911 if ($eventNamespace) { 672*e3a9f44cSAtari911 $html .= ' <span class="eventlist-simple-namespace">' . htmlspecialchars($eventNamespace) . '</span>'; 673*e3a9f44cSAtari911 } 674*e3a9f44cSAtari911 675*e3a9f44cSAtari911 $html .= '</div>'; // header 676*e3a9f44cSAtari911 677*e3a9f44cSAtari911 // Line 2: Body (Description only) - only show if description exists 678*e3a9f44cSAtari911 if (!empty($event['description'])) { 679*e3a9f44cSAtari911 $html .= '<div class="eventlist-simple-body">' . $this->renderDescription($event['description']) . '</div>'; 680*e3a9f44cSAtari911 } 681*e3a9f44cSAtari911 682*e3a9f44cSAtari911 $html .= '</div>'; // item 68319378907SAtari911 } 68419378907SAtari911 } 68587ac9bf3SAtari911 } 68619378907SAtari911 687*e3a9f44cSAtari911 $html .= '</div>'; // eventlist-simple 68819378907SAtari911 68919378907SAtari911 return $html; 69019378907SAtari911 } 69119378907SAtari911 69219378907SAtari911 private function renderEventDialog($calId, $namespace) { 69319378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 69419378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 69519378907SAtari911 69619378907SAtari911 // Draggable dialog 69719378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 69819378907SAtari911 69919378907SAtari911 // Header with drag handle and close button 70019378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 70119378907SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 70219378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 70319378907SAtari911 $html .= '</div>'; 70419378907SAtari911 70519378907SAtari911 // Form content 70619378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 70719378907SAtari911 70819378907SAtari911 // Hidden ID field 70919378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 71019378907SAtari911 71119378907SAtari911 // Task checkbox 71219378907SAtari911 $html .= '<div class="form-field form-field-checkbox">'; 71319378907SAtari911 $html .= '<label class="checkbox-label">'; 71419378907SAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 71519378907SAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 71619378907SAtari911 $html .= '</label>'; 71719378907SAtari911 $html .= '</div>'; 71819378907SAtari911 71919378907SAtari911 // Date and Time in a row 72019378907SAtari911 $html .= '<div class="form-row-group">'; 72119378907SAtari911 72219378907SAtari911 // Start Date field 72319378907SAtari911 $html .= '<div class="form-field form-field-date">'; 72419378907SAtari911 $html .= '<label class="field-label"> Start Date</label>'; 72519378907SAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date">'; 72619378907SAtari911 $html .= '</div>'; 72719378907SAtari911 72819378907SAtari911 // End Date field (for multi-day events) 72919378907SAtari911 $html .= '<div class="form-field form-field-date">'; 73019378907SAtari911 $html .= '<label class="field-label"> End Date</label>'; 73119378907SAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date">'; 73219378907SAtari911 $html .= '</div>'; 73319378907SAtari911 73419378907SAtari911 $html .= '</div>'; 73519378907SAtari911 73687ac9bf3SAtari911 // Recurring event section 73787ac9bf3SAtari911 $html .= '<div class="form-field form-field-checkbox">'; 73887ac9bf3SAtari911 $html .= '<label class="checkbox-label">'; 73987ac9bf3SAtari911 $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">'; 74087ac9bf3SAtari911 $html .= '<span> Repeating Event</span>'; 74187ac9bf3SAtari911 $html .= '</label>'; 74287ac9bf3SAtari911 $html .= '</div>'; 74387ac9bf3SAtari911 74487ac9bf3SAtari911 // Recurring options (hidden by default) 74587ac9bf3SAtari911 $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">'; 74687ac9bf3SAtari911 74787ac9bf3SAtari911 // Recurrence pattern 74887ac9bf3SAtari911 $html .= '<div class="form-field">'; 74987ac9bf3SAtari911 $html .= '<label class="field-label">Repeat Every</label>'; 75087ac9bf3SAtari911 $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek">'; 75187ac9bf3SAtari911 $html .= '<option value="daily">Daily</option>'; 75287ac9bf3SAtari911 $html .= '<option value="weekly">Weekly</option>'; 75387ac9bf3SAtari911 $html .= '<option value="monthly">Monthly</option>'; 75487ac9bf3SAtari911 $html .= '<option value="yearly">Yearly</option>'; 75587ac9bf3SAtari911 $html .= '</select>'; 75687ac9bf3SAtari911 $html .= '</div>'; 75787ac9bf3SAtari911 75887ac9bf3SAtari911 // Recurrence end date 75987ac9bf3SAtari911 $html .= '<div class="form-field">'; 76087ac9bf3SAtari911 $html .= '<label class="field-label"> Repeat Until (optional)</label>'; 76187ac9bf3SAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date">'; 76287ac9bf3SAtari911 $html .= '</div>'; 76387ac9bf3SAtari911 76487ac9bf3SAtari911 $html .= '</div>'; 76587ac9bf3SAtari911 766*e3a9f44cSAtari911 // Time field - dropdown with 15-minute intervals 76719378907SAtari911 $html .= '<div class="form-field">'; 76819378907SAtari911 $html .= '<label class="field-label"> Time (optional)</label>'; 769*e3a9f44cSAtari911 $html .= '<select id="event-time-' . $calId . '" name="time" class="input-sleek">'; 770*e3a9f44cSAtari911 $html .= '<option value="">No specific time</option>'; 771*e3a9f44cSAtari911 772*e3a9f44cSAtari911 // Generate time options in 15-minute intervals 773*e3a9f44cSAtari911 for ($hour = 0; $hour < 24; $hour++) { 774*e3a9f44cSAtari911 for ($minute = 0; $minute < 60; $minute += 15) { 775*e3a9f44cSAtari911 $timeValue = sprintf('%02d:%02d', $hour, $minute); 776*e3a9f44cSAtari911 $displayHour = $hour == 0 ? 12 : ($hour > 12 ? $hour - 12 : $hour); 777*e3a9f44cSAtari911 $ampm = $hour < 12 ? 'AM' : 'PM'; 778*e3a9f44cSAtari911 $displayTime = sprintf('%d:%02d %s', $displayHour, $minute, $ampm); 779*e3a9f44cSAtari911 $html .= '<option value="' . $timeValue . '">' . $displayTime . '</option>'; 780*e3a9f44cSAtari911 } 781*e3a9f44cSAtari911 } 782*e3a9f44cSAtari911 783*e3a9f44cSAtari911 $html .= '</select>'; 78419378907SAtari911 $html .= '</div>'; 78519378907SAtari911 78619378907SAtari911 // Title field 78719378907SAtari911 $html .= '<div class="form-field">'; 78819378907SAtari911 $html .= '<label class="field-label"> Title</label>'; 78919378907SAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek" placeholder="Event or task title...">'; 79019378907SAtari911 $html .= '</div>'; 79119378907SAtari911 79219378907SAtari911 // Description field 79319378907SAtari911 $html .= '<div class="form-field">'; 79419378907SAtari911 $html .= '<label class="field-label"> Description</label>'; 79519378907SAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="3" class="input-sleek textarea-sleek" placeholder="Add details (optional)..."></textarea>'; 79619378907SAtari911 $html .= '</div>'; 79719378907SAtari911 79819378907SAtari911 // Color picker 79919378907SAtari911 $html .= '<div class="form-field">'; 80019378907SAtari911 $html .= '<label class="field-label"> Color</label>'; 80119378907SAtari911 $html .= '<div class="color-picker-container">'; 80219378907SAtari911 $html .= '<input type="color" id="event-color-' . $calId . '" name="color" value="#3498db" class="input-color-sleek">'; 80319378907SAtari911 $html .= '<span class="color-label">Choose event color</span>'; 80419378907SAtari911 $html .= '</div>'; 80519378907SAtari911 $html .= '</div>'; 80619378907SAtari911 80719378907SAtari911 // Action buttons 80819378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 80919378907SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 81019378907SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 81119378907SAtari911 $html .= '</div>'; 81219378907SAtari911 81319378907SAtari911 $html .= '</form>'; 81419378907SAtari911 $html .= '</div>'; 81519378907SAtari911 $html .= '</div>'; 81619378907SAtari911 81719378907SAtari911 return $html; 81819378907SAtari911 } 81919378907SAtari911 82087ac9bf3SAtari911 private function renderMonthPicker($calId, $year, $month, $namespace) { 82187ac9bf3SAtari911 $html = '<div class="month-picker-overlay" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">'; 82287ac9bf3SAtari911 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">'; 82387ac9bf3SAtari911 $html .= '<h4>Jump to Month</h4>'; 82487ac9bf3SAtari911 82587ac9bf3SAtari911 $html .= '<div class="month-picker-selects">'; 82687ac9bf3SAtari911 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">'; 82787ac9bf3SAtari911 $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 82887ac9bf3SAtari911 for ($m = 1; $m <= 12; $m++) { 82987ac9bf3SAtari911 $selected = ($m == $month) ? ' selected' : ''; 83087ac9bf3SAtari911 $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>'; 83187ac9bf3SAtari911 } 83287ac9bf3SAtari911 $html .= '</select>'; 83387ac9bf3SAtari911 83487ac9bf3SAtari911 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">'; 83587ac9bf3SAtari911 $currentYear = (int)date('Y'); 83687ac9bf3SAtari911 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) { 83787ac9bf3SAtari911 $selected = ($y == $year) ? ' selected' : ''; 83887ac9bf3SAtari911 $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>'; 83987ac9bf3SAtari911 } 84087ac9bf3SAtari911 $html .= '</select>'; 84187ac9bf3SAtari911 $html .= '</div>'; 84287ac9bf3SAtari911 84387ac9bf3SAtari911 $html .= '<div class="month-picker-actions">'; 84487ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>'; 84587ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>'; 84687ac9bf3SAtari911 $html .= '</div>'; 84787ac9bf3SAtari911 84887ac9bf3SAtari911 $html .= '</div>'; 84987ac9bf3SAtari911 $html .= '</div>'; 85087ac9bf3SAtari911 85187ac9bf3SAtari911 return $html; 85287ac9bf3SAtari911 } 85387ac9bf3SAtari911 85419378907SAtari911 private function renderDescription($description) { 85519378907SAtari911 if (empty($description)) { 85619378907SAtari911 return ''; 85719378907SAtari911 } 85819378907SAtari911 859*e3a9f44cSAtari911 // Token-based parsing to avoid escaping issues 860*e3a9f44cSAtari911 $rendered = $description; 861*e3a9f44cSAtari911 $tokens = array(); 862*e3a9f44cSAtari911 $tokenIndex = 0; 86319378907SAtari911 864*e3a9f44cSAtari911 // Convert DokuWiki image syntax {{image.jpg}} to tokens 865*e3a9f44cSAtari911 $pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/'; 866*e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 867*e3a9f44cSAtari911 foreach ($matches as $match) { 868*e3a9f44cSAtari911 $imagePath = trim($match[1]); 869*e3a9f44cSAtari911 $alt = isset($match[2]) ? trim($match[2]) : ''; 87019378907SAtari911 871*e3a9f44cSAtari911 // Handle external URLs 87219378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 873*e3a9f44cSAtari911 $imageHtml = '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 874*e3a9f44cSAtari911 } else { 87519378907SAtari911 // Handle internal DokuWiki images 87619378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 877*e3a9f44cSAtari911 $imageHtml = '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 878*e3a9f44cSAtari911 } 87919378907SAtari911 880*e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 881*e3a9f44cSAtari911 $tokens[$tokenIndex] = $imageHtml; 882*e3a9f44cSAtari911 $tokenIndex++; 883*e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 884*e3a9f44cSAtari911 } 885*e3a9f44cSAtari911 886*e3a9f44cSAtari911 // Convert DokuWiki link syntax [[link|text]] to tokens 887*e3a9f44cSAtari911 $pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/'; 888*e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 889*e3a9f44cSAtari911 foreach ($matches as $match) { 890*e3a9f44cSAtari911 $link = trim($match[1]); 891*e3a9f44cSAtari911 $text = isset($match[2]) ? trim($match[2]) : $link; 89219378907SAtari911 89319378907SAtari911 // Handle external URLs 89419378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 895*e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 896*e3a9f44cSAtari911 } else { 89787ac9bf3SAtari911 // Handle internal DokuWiki links with section anchors 89887ac9bf3SAtari911 $parts = explode('#', $link, 2); 89987ac9bf3SAtari911 $pagePart = $parts[0]; 90087ac9bf3SAtari911 $sectionPart = isset($parts[1]) ? '#' . $parts[1] : ''; 90187ac9bf3SAtari911 90287ac9bf3SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart; 903*e3a9f44cSAtari911 $linkHtml = '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>'; 90419378907SAtari911 } 90519378907SAtari911 906*e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 907*e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 908*e3a9f44cSAtari911 $tokenIndex++; 909*e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 910*e3a9f44cSAtari911 } 91119378907SAtari911 912*e3a9f44cSAtari911 // Convert markdown-style links [text](url) to tokens 913*e3a9f44cSAtari911 $pattern = '/\[([^\]]+)\]\(([^)]+)\)/'; 914*e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 915*e3a9f44cSAtari911 foreach ($matches as $match) { 916*e3a9f44cSAtari911 $text = trim($match[1]); 917*e3a9f44cSAtari911 $url = trim($match[2]); 91819378907SAtari911 919*e3a9f44cSAtari911 if (preg_match('/^https?:\/\//', $url)) { 920*e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 921*e3a9f44cSAtari911 } else { 922*e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>'; 923*e3a9f44cSAtari911 } 924*e3a9f44cSAtari911 925*e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 926*e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 927*e3a9f44cSAtari911 $tokenIndex++; 928*e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 929*e3a9f44cSAtari911 } 930*e3a9f44cSAtari911 931*e3a9f44cSAtari911 // Convert plain URLs to tokens 932*e3a9f44cSAtari911 $pattern = '/(https?:\/\/[^\s<]+)/'; 933*e3a9f44cSAtari911 preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER); 934*e3a9f44cSAtari911 foreach ($matches as $match) { 935*e3a9f44cSAtari911 $url = $match[1]; 936*e3a9f44cSAtari911 $linkHtml = '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>'; 937*e3a9f44cSAtari911 938*e3a9f44cSAtari911 $token = "\x00TOKEN" . $tokenIndex . "\x00"; 939*e3a9f44cSAtari911 $tokens[$tokenIndex] = $linkHtml; 940*e3a9f44cSAtari911 $tokenIndex++; 941*e3a9f44cSAtari911 $rendered = str_replace($match[0], $token, $rendered); 942*e3a9f44cSAtari911 } 943*e3a9f44cSAtari911 944*e3a9f44cSAtari911 // NOW escape HTML (tokens are protected) 945*e3a9f44cSAtari911 $rendered = htmlspecialchars($rendered); 946*e3a9f44cSAtari911 947*e3a9f44cSAtari911 // Convert newlines to <br> 948*e3a9f44cSAtari911 $rendered = nl2br($rendered); 949*e3a9f44cSAtari911 950*e3a9f44cSAtari911 // DokuWiki text formatting 951*e3a9f44cSAtari911 // Bold: **text** or __text__ 952*e3a9f44cSAtari911 $rendered = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $rendered); 953*e3a9f44cSAtari911 $rendered = preg_replace('/__(.+?)__/', '<strong>$1</strong>', $rendered); 954*e3a9f44cSAtari911 955*e3a9f44cSAtari911 // Italic: //text// 956*e3a9f44cSAtari911 $rendered = preg_replace('/\/\/(.+?)\/\//', '<em>$1</em>', $rendered); 957*e3a9f44cSAtari911 958*e3a9f44cSAtari911 // Strikethrough: <del>text</del> 959*e3a9f44cSAtari911 $rendered = preg_replace('/<del>(.+?)<\/del>/', '<del>$1</del>', $rendered); 960*e3a9f44cSAtari911 961*e3a9f44cSAtari911 // Monospace: ''text'' 962*e3a9f44cSAtari911 $rendered = preg_replace('/''(.+?)''/', '<code>$1</code>', $rendered); 963*e3a9f44cSAtari911 964*e3a9f44cSAtari911 // Subscript: <sub>text</sub> 965*e3a9f44cSAtari911 $rendered = preg_replace('/<sub>(.+?)<\/sub>/', '<sub>$1</sub>', $rendered); 966*e3a9f44cSAtari911 967*e3a9f44cSAtari911 // Superscript: <sup>text</sup> 968*e3a9f44cSAtari911 $rendered = preg_replace('/<sup>(.+?)<\/sup>/', '<sup>$1</sup>', $rendered); 969*e3a9f44cSAtari911 970*e3a9f44cSAtari911 // Restore tokens 971*e3a9f44cSAtari911 foreach ($tokens as $i => $html) { 972*e3a9f44cSAtari911 $rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered); 973*e3a9f44cSAtari911 } 97419378907SAtari911 97519378907SAtari911 return $rendered; 97619378907SAtari911 } 97719378907SAtari911 97819378907SAtari911 private function loadEvents($namespace, $year, $month) { 97919378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 98019378907SAtari911 if ($namespace) { 98119378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 98219378907SAtari911 } 98319378907SAtari911 $dataDir .= 'calendar/'; 98419378907SAtari911 98519378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 98619378907SAtari911 98719378907SAtari911 if (file_exists($eventFile)) { 98819378907SAtari911 $json = file_get_contents($eventFile); 98919378907SAtari911 return json_decode($json, true); 99019378907SAtari911 } 99119378907SAtari911 99219378907SAtari911 return array(); 99319378907SAtari911 } 994*e3a9f44cSAtari911 995*e3a9f44cSAtari911 private function loadEventsMultiNamespace($namespaces, $year, $month) { 996*e3a9f44cSAtari911 // Check for wildcard pattern (namespace:*) 997*e3a9f44cSAtari911 if (preg_match('/^(.+):\*$/', $namespaces, $matches)) { 998*e3a9f44cSAtari911 $baseNamespace = $matches[1]; 999*e3a9f44cSAtari911 return $this->loadEventsWildcard($baseNamespace, $year, $month); 1000*e3a9f44cSAtari911 } 1001*e3a9f44cSAtari911 1002*e3a9f44cSAtari911 // Check for root wildcard (just *) 1003*e3a9f44cSAtari911 if ($namespaces === '*') { 1004*e3a9f44cSAtari911 return $this->loadEventsWildcard('', $year, $month); 1005*e3a9f44cSAtari911 } 1006*e3a9f44cSAtari911 1007*e3a9f44cSAtari911 // Parse namespace list (semicolon separated) 1008*e3a9f44cSAtari911 // e.g., "team:projects;personal;work:tasks" = three namespaces 1009*e3a9f44cSAtari911 $namespaceList = array_map('trim', explode(';', $namespaces)); 1010*e3a9f44cSAtari911 1011*e3a9f44cSAtari911 // Load events from all namespaces 1012*e3a9f44cSAtari911 $allEvents = array(); 1013*e3a9f44cSAtari911 foreach ($namespaceList as $ns) { 1014*e3a9f44cSAtari911 $ns = trim($ns); 1015*e3a9f44cSAtari911 if (empty($ns)) continue; 1016*e3a9f44cSAtari911 1017*e3a9f44cSAtari911 $events = $this->loadEvents($ns, $year, $month); 1018*e3a9f44cSAtari911 1019*e3a9f44cSAtari911 // Add namespace tag to each event 1020*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1021*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1022*e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1023*e3a9f44cSAtari911 } 1024*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1025*e3a9f44cSAtari911 $event['_namespace'] = $ns; 1026*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1027*e3a9f44cSAtari911 } 1028*e3a9f44cSAtari911 } 1029*e3a9f44cSAtari911 } 1030*e3a9f44cSAtari911 1031*e3a9f44cSAtari911 return $allEvents; 1032*e3a9f44cSAtari911 } 1033*e3a9f44cSAtari911 1034*e3a9f44cSAtari911 private function loadEventsWildcard($baseNamespace, $year, $month) { 1035*e3a9f44cSAtari911 // Find all subdirectories under the base namespace 1036*e3a9f44cSAtari911 $dataDir = DOKU_INC . 'data/meta/'; 1037*e3a9f44cSAtari911 if ($baseNamespace) { 1038*e3a9f44cSAtari911 $dataDir .= str_replace(':', '/', $baseNamespace) . '/'; 1039*e3a9f44cSAtari911 } 1040*e3a9f44cSAtari911 1041*e3a9f44cSAtari911 $allEvents = array(); 1042*e3a9f44cSAtari911 1043*e3a9f44cSAtari911 // First, load events from the base namespace itself 1044*e3a9f44cSAtari911 if (empty($baseNamespace)) { 1045*e3a9f44cSAtari911 // Root wildcard - load from root calendar 1046*e3a9f44cSAtari911 $events = $this->loadEvents('', $year, $month); 1047*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1048*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1049*e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1050*e3a9f44cSAtari911 } 1051*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1052*e3a9f44cSAtari911 $event['_namespace'] = ''; 1053*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1054*e3a9f44cSAtari911 } 1055*e3a9f44cSAtari911 } 1056*e3a9f44cSAtari911 } else { 1057*e3a9f44cSAtari911 $events = $this->loadEvents($baseNamespace, $year, $month); 1058*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1059*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1060*e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1061*e3a9f44cSAtari911 } 1062*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1063*e3a9f44cSAtari911 $event['_namespace'] = $baseNamespace; 1064*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1065*e3a9f44cSAtari911 } 1066*e3a9f44cSAtari911 } 1067*e3a9f44cSAtari911 } 1068*e3a9f44cSAtari911 1069*e3a9f44cSAtari911 // Recursively find all subdirectories 1070*e3a9f44cSAtari911 $this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents); 1071*e3a9f44cSAtari911 1072*e3a9f44cSAtari911 return $allEvents; 1073*e3a9f44cSAtari911 } 1074*e3a9f44cSAtari911 1075*e3a9f44cSAtari911 private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) { 1076*e3a9f44cSAtari911 if (!is_dir($dir)) return; 1077*e3a9f44cSAtari911 1078*e3a9f44cSAtari911 $items = scandir($dir); 1079*e3a9f44cSAtari911 foreach ($items as $item) { 1080*e3a9f44cSAtari911 if ($item === '.' || $item === '..') continue; 1081*e3a9f44cSAtari911 1082*e3a9f44cSAtari911 $path = $dir . $item; 1083*e3a9f44cSAtari911 if (is_dir($path) && $item !== 'calendar') { 1084*e3a9f44cSAtari911 // This is a namespace directory 1085*e3a9f44cSAtari911 $namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item; 1086*e3a9f44cSAtari911 1087*e3a9f44cSAtari911 // Load events from this namespace 1088*e3a9f44cSAtari911 $events = $this->loadEvents($namespace, $year, $month); 1089*e3a9f44cSAtari911 foreach ($events as $dateKey => $dayEvents) { 1090*e3a9f44cSAtari911 if (!isset($allEvents[$dateKey])) { 1091*e3a9f44cSAtari911 $allEvents[$dateKey] = array(); 1092*e3a9f44cSAtari911 } 1093*e3a9f44cSAtari911 foreach ($dayEvents as $event) { 1094*e3a9f44cSAtari911 $event['_namespace'] = $namespace; 1095*e3a9f44cSAtari911 $allEvents[$dateKey][] = $event; 1096*e3a9f44cSAtari911 } 1097*e3a9f44cSAtari911 } 1098*e3a9f44cSAtari911 1099*e3a9f44cSAtari911 // Recurse into subdirectories 1100*e3a9f44cSAtari911 $this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents); 1101*e3a9f44cSAtari911 } 1102*e3a9f44cSAtari911 } 1103*e3a9f44cSAtari911 } 110419378907SAtari911} 1105