1*19378907SAtari911<?php 2*19378907SAtari911/** 3*19378907SAtari911 * DokuWiki Plugin calendar (Syntax Component) 4*19378907SAtari911 * Compact design with integrated event list 5*19378907SAtari911 * 6*19378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7*19378907SAtari911 * @author DokuWiki Community 8*19378907SAtari911 */ 9*19378907SAtari911 10*19378907SAtari911if (!defined('DOKU_INC')) die(); 11*19378907SAtari911 12*19378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin { 13*19378907SAtari911 14*19378907SAtari911 public function getType() { 15*19378907SAtari911 return 'substition'; 16*19378907SAtari911 } 17*19378907SAtari911 18*19378907SAtari911 public function getPType() { 19*19378907SAtari911 return 'block'; 20*19378907SAtari911 } 21*19378907SAtari911 22*19378907SAtari911 public function getSort() { 23*19378907SAtari911 return 155; 24*19378907SAtari911 } 25*19378907SAtari911 26*19378907SAtari911 public function connectTo($mode) { 27*19378907SAtari911 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 28*19378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 29*19378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 30*19378907SAtari911 } 31*19378907SAtari911 32*19378907SAtari911 public function handle($match, $state, $pos, Doku_Handler $handler) { 33*19378907SAtari911 $isEventList = (strpos($match, '{{eventlist') === 0); 34*19378907SAtari911 $isEventPanel = (strpos($match, '{{eventpanel') === 0); 35*19378907SAtari911 36*19378907SAtari911 if ($isEventList) { 37*19378907SAtari911 $match = substr($match, 12, -2); 38*19378907SAtari911 } elseif ($isEventPanel) { 39*19378907SAtari911 $match = substr($match, 13, -2); 40*19378907SAtari911 } else { 41*19378907SAtari911 $match = substr($match, 10, -2); 42*19378907SAtari911 } 43*19378907SAtari911 44*19378907SAtari911 $params = array( 45*19378907SAtari911 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 46*19378907SAtari911 'year' => date('Y'), 47*19378907SAtari911 'month' => date('n'), 48*19378907SAtari911 'namespace' => '', 49*19378907SAtari911 'daterange' => '', 50*19378907SAtari911 'date' => '' 51*19378907SAtari911 ); 52*19378907SAtari911 53*19378907SAtari911 if (trim($match)) { 54*19378907SAtari911 $pairs = preg_split('/\s+/', trim($match)); 55*19378907SAtari911 foreach ($pairs as $pair) { 56*19378907SAtari911 if (strpos($pair, '=') !== false) { 57*19378907SAtari911 list($key, $value) = explode('=', $pair, 2); 58*19378907SAtari911 $params[trim($key)] = trim($value); 59*19378907SAtari911 } 60*19378907SAtari911 } 61*19378907SAtari911 } 62*19378907SAtari911 63*19378907SAtari911 return $params; 64*19378907SAtari911 } 65*19378907SAtari911 66*19378907SAtari911 public function render($mode, Doku_Renderer $renderer, $data) { 67*19378907SAtari911 if ($mode !== 'xhtml') return false; 68*19378907SAtari911 69*19378907SAtari911 if ($data['type'] === 'eventlist') { 70*19378907SAtari911 $html = $this->renderStandaloneEventList($data); 71*19378907SAtari911 } elseif ($data['type'] === 'eventpanel') { 72*19378907SAtari911 $html = $this->renderEventPanelOnly($data); 73*19378907SAtari911 } else { 74*19378907SAtari911 $html = $this->renderCompactCalendar($data); 75*19378907SAtari911 } 76*19378907SAtari911 77*19378907SAtari911 $renderer->doc .= $html; 78*19378907SAtari911 return true; 79*19378907SAtari911 } 80*19378907SAtari911 81*19378907SAtari911 private function renderCompactCalendar($data) { 82*19378907SAtari911 $year = (int)$data['year']; 83*19378907SAtari911 $month = (int)$data['month']; 84*19378907SAtari911 $namespace = $data['namespace']; 85*19378907SAtari911 86*19378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 87*19378907SAtari911 $calId = 'cal_' . md5(serialize($data) . microtime()); 88*19378907SAtari911 89*19378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 90*19378907SAtari911 91*19378907SAtari911 $prevMonth = $month - 1; 92*19378907SAtari911 $prevYear = $year; 93*19378907SAtari911 if ($prevMonth < 1) { 94*19378907SAtari911 $prevMonth = 12; 95*19378907SAtari911 $prevYear--; 96*19378907SAtari911 } 97*19378907SAtari911 98*19378907SAtari911 $nextMonth = $month + 1; 99*19378907SAtari911 $nextYear = $year; 100*19378907SAtari911 if ($nextMonth > 12) { 101*19378907SAtari911 $nextMonth = 1; 102*19378907SAtari911 $nextYear++; 103*19378907SAtari911 } 104*19378907SAtari911 105*19378907SAtari911 $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">'; 106*19378907SAtari911 107*19378907SAtari911 // Embed events data as JSON for JavaScript access 108*19378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 109*19378907SAtari911 110*19378907SAtari911 // Left side: Calendar 111*19378907SAtari911 $html .= '<div class="calendar-compact-left">'; 112*19378907SAtari911 113*19378907SAtari911 // Header with navigation 114*19378907SAtari911 $html .= '<div class="calendar-compact-header">'; 115*19378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 116*19378907SAtari911 $html .= '<h3>' . $monthName . '</h3>'; 117*19378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 118*19378907SAtari911 $html .= '</div>'; 119*19378907SAtari911 120*19378907SAtari911 // Calendar grid 121*19378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 122*19378907SAtari911 $html .= '<thead><tr>'; 123*19378907SAtari911 $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>'; 124*19378907SAtari911 $html .= '</tr></thead><tbody>'; 125*19378907SAtari911 126*19378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 127*19378907SAtari911 $daysInMonth = date('t', $firstDay); 128*19378907SAtari911 $dayOfWeek = date('w', $firstDay); 129*19378907SAtari911 130*19378907SAtari911 $currentDay = 1; 131*19378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 132*19378907SAtari911 133*19378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 134*19378907SAtari911 $html .= '<tr>'; 135*19378907SAtari911 for ($col = 0; $col < 7; $col++) { 136*19378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 137*19378907SAtari911 $html .= '<td class="cal-empty"></td>'; 138*19378907SAtari911 } else { 139*19378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 140*19378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 141*19378907SAtari911 $hasEvents = isset($events[$dateKey]) && !empty($events[$dateKey]); 142*19378907SAtari911 143*19378907SAtari911 $classes = 'cal-day'; 144*19378907SAtari911 if ($isToday) $classes .= ' cal-today'; 145*19378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 146*19378907SAtari911 147*19378907SAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 148*19378907SAtari911 $html .= '<span class="day-num">' . $currentDay . '</span>'; 149*19378907SAtari911 150*19378907SAtari911 if ($hasEvents) { 151*19378907SAtari911 // Sort events by time (no time first, then by time) 152*19378907SAtari911 $sortedEvents = $events[$dateKey]; 153*19378907SAtari911 usort($sortedEvents, function($a, $b) { 154*19378907SAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 155*19378907SAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 156*19378907SAtari911 157*19378907SAtari911 // Events without time go first 158*19378907SAtari911 if (empty($timeA) && !empty($timeB)) return -1; 159*19378907SAtari911 if (!empty($timeA) && empty($timeB)) return 1; 160*19378907SAtari911 if (empty($timeA) && empty($timeB)) return 0; 161*19378907SAtari911 162*19378907SAtari911 // Sort by time 163*19378907SAtari911 return strcmp($timeA, $timeB); 164*19378907SAtari911 }); 165*19378907SAtari911 166*19378907SAtari911 // Show colored stacked bars for each event 167*19378907SAtari911 $html .= '<div class="event-indicators">'; 168*19378907SAtari911 foreach ($sortedEvents as $evt) { 169*19378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 170*19378907SAtari911 $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; 171*19378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 172*19378907SAtari911 $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; 173*19378907SAtari911 174*19378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 175*19378907SAtari911 176*19378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 177*19378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 178*19378907SAtari911 $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 179*19378907SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\');">'; 180*19378907SAtari911 $html .= '</span>'; 181*19378907SAtari911 } 182*19378907SAtari911 $html .= '</div>'; 183*19378907SAtari911 } 184*19378907SAtari911 185*19378907SAtari911 $html .= '</td>'; 186*19378907SAtari911 $currentDay++; 187*19378907SAtari911 } 188*19378907SAtari911 } 189*19378907SAtari911 $html .= '</tr>'; 190*19378907SAtari911 } 191*19378907SAtari911 192*19378907SAtari911 $html .= '</tbody></table>'; 193*19378907SAtari911 $html .= '</div>'; // End calendar-left 194*19378907SAtari911 195*19378907SAtari911 // Right side: Event list 196*19378907SAtari911 $html .= '<div class="calendar-compact-right">'; 197*19378907SAtari911 $html .= '<div class="event-list-header">'; 198*19378907SAtari911 $html .= '<div class="event-list-header-content">'; 199*19378907SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 200*19378907SAtari911 if ($namespace) { 201*19378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 202*19378907SAtari911 } 203*19378907SAtari911 $html .= '</div>'; 204*19378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 205*19378907SAtari911 $html .= '</div>'; 206*19378907SAtari911 207*19378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 208*19378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 209*19378907SAtari911 $html .= '</div>'; 210*19378907SAtari911 211*19378907SAtari911 $html .= '</div>'; // End calendar-right 212*19378907SAtari911 213*19378907SAtari911 // Event dialog 214*19378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 215*19378907SAtari911 216*19378907SAtari911 $html .= '</div>'; // End container 217*19378907SAtari911 218*19378907SAtari911 return $html; 219*19378907SAtari911 } 220*19378907SAtari911 221*19378907SAtari911 private function renderEventListContent($events, $calId, $namespace) { 222*19378907SAtari911 if (empty($events)) { 223*19378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 224*19378907SAtari911 } 225*19378907SAtari911 226*19378907SAtari911 $html = ''; 227*19378907SAtari911 ksort($events); 228*19378907SAtari911 229*19378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 230*19378907SAtari911 foreach ($dayEvents as $event) { 231*19378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 232*19378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 233*19378907SAtari911 $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; 234*19378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 235*19378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 236*19378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 237*19378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 238*19378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 239*19378907SAtari911 240*19378907SAtari911 // Process description for wiki syntax, HTML, images, and links 241*19378907SAtari911 $renderedDescription = $this->renderDescription($description); 242*19378907SAtari911 243*19378907SAtari911 // Convert to 12-hour format 244*19378907SAtari911 $displayTime = ''; 245*19378907SAtari911 if ($time) { 246*19378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 247*19378907SAtari911 if ($timeObj) { 248*19378907SAtari911 $displayTime = $timeObj->format('g:i A'); 249*19378907SAtari911 } else { 250*19378907SAtari911 $displayTime = $time; 251*19378907SAtari911 } 252*19378907SAtari911 } 253*19378907SAtari911 254*19378907SAtari911 // Format date display 255*19378907SAtari911 $dateObj = new DateTime($dateKey); 256*19378907SAtari911 $displayDate = $dateObj->format('M j'); 257*19378907SAtari911 258*19378907SAtari911 // Multi-day indicator 259*19378907SAtari911 $multiDay = ''; 260*19378907SAtari911 if ($endDate && $endDate !== $dateKey) { 261*19378907SAtari911 $endObj = new DateTime($endDate); 262*19378907SAtari911 $multiDay = ' → ' . $endObj->format('M j'); 263*19378907SAtari911 } 264*19378907SAtari911 265*19378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 266*19378907SAtari911 267*19378907SAtari911 $html .= '<div class="event-compact-item' . $completedClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';">'; 268*19378907SAtari911 269*19378907SAtari911 $html .= '<div class="event-info">'; 270*19378907SAtari911 $html .= '<div class="event-title-row">'; 271*19378907SAtari911 $html .= '<span class="event-title-compact">' . $title . '</span>'; 272*19378907SAtari911 $html .= '</div>'; 273*19378907SAtari911 274*19378907SAtari911 $html .= '<div class="event-meta-compact">'; 275*19378907SAtari911 $html .= '<span class="event-date-time">' . $displayDate . $multiDay; 276*19378907SAtari911 if ($displayTime) { 277*19378907SAtari911 $html .= ' • ' . $displayTime; 278*19378907SAtari911 } 279*19378907SAtari911 $html .= '</span>'; 280*19378907SAtari911 $html .= '</div>'; 281*19378907SAtari911 282*19378907SAtari911 if ($description) { 283*19378907SAtari911 $html .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 284*19378907SAtari911 } 285*19378907SAtari911 286*19378907SAtari911 $html .= '</div>'; // event-info 287*19378907SAtari911 288*19378907SAtari911 $html .= '<div class="event-actions-compact">'; 289*19378907SAtari911 $html .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">️</button>'; 290*19378907SAtari911 $html .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">✏️</button>'; 291*19378907SAtari911 $html .= '</div>'; 292*19378907SAtari911 293*19378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 294*19378907SAtari911 if ($isTask) { 295*19378907SAtari911 $checked = $completed ? 'checked' : ''; 296*19378907SAtari911 $html .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\', this.checked)">'; 297*19378907SAtari911 } 298*19378907SAtari911 299*19378907SAtari911 $html .= '</div>'; 300*19378907SAtari911 } 301*19378907SAtari911 } 302*19378907SAtari911 303*19378907SAtari911 return $html; 304*19378907SAtari911 } 305*19378907SAtari911 306*19378907SAtari911 private function renderEventPanelOnly($data) { 307*19378907SAtari911 $year = (int)$data['year']; 308*19378907SAtari911 $month = (int)$data['month']; 309*19378907SAtari911 $namespace = $data['namespace']; 310*19378907SAtari911 311*19378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 312*19378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 313*19378907SAtari911 314*19378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 315*19378907SAtari911 316*19378907SAtari911 $prevMonth = $month - 1; 317*19378907SAtari911 $prevYear = $year; 318*19378907SAtari911 if ($prevMonth < 1) { 319*19378907SAtari911 $prevMonth = 12; 320*19378907SAtari911 $prevYear--; 321*19378907SAtari911 } 322*19378907SAtari911 323*19378907SAtari911 $nextMonth = $month + 1; 324*19378907SAtari911 $nextYear = $year; 325*19378907SAtari911 if ($nextMonth > 12) { 326*19378907SAtari911 $nextMonth = 1; 327*19378907SAtari911 $nextYear++; 328*19378907SAtari911 } 329*19378907SAtari911 330*19378907SAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '">'; 331*19378907SAtari911 332*19378907SAtari911 // Header with navigation 333*19378907SAtari911 $html .= '<div class="panel-standalone-header">'; 334*19378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 335*19378907SAtari911 $html .= '<h3>' . $monthName . ' Events</h3>'; 336*19378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 337*19378907SAtari911 $html .= '</div>'; 338*19378907SAtari911 339*19378907SAtari911 $html .= '<div class="panel-standalone-actions">'; 340*19378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add Event</button>'; 341*19378907SAtari911 $html .= '</div>'; 342*19378907SAtari911 343*19378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 344*19378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 345*19378907SAtari911 $html .= '</div>'; 346*19378907SAtari911 347*19378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 348*19378907SAtari911 349*19378907SAtari911 $html .= '</div>'; 350*19378907SAtari911 351*19378907SAtari911 return $html; 352*19378907SAtari911 } 353*19378907SAtari911 354*19378907SAtari911 private function renderStandaloneEventList($data) { 355*19378907SAtari911 $namespace = $data['namespace']; 356*19378907SAtari911 $daterange = $data['daterange']; 357*19378907SAtari911 $date = $data['date']; 358*19378907SAtari911 359*19378907SAtari911 if ($daterange) { 360*19378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 361*19378907SAtari911 } elseif ($date) { 362*19378907SAtari911 $startDate = $date; 363*19378907SAtari911 $endDate = $date; 364*19378907SAtari911 } else { 365*19378907SAtari911 $startDate = date('Y-m-01'); 366*19378907SAtari911 $endDate = date('Y-m-t'); 367*19378907SAtari911 } 368*19378907SAtari911 369*19378907SAtari911 $allEvents = array(); 370*19378907SAtari911 $start = new DateTime($startDate); 371*19378907SAtari911 $end = new DateTime($endDate); 372*19378907SAtari911 $end->modify('+1 day'); 373*19378907SAtari911 374*19378907SAtari911 $interval = new DateInterval('P1D'); 375*19378907SAtari911 $period = new DatePeriod($start, $interval, $end); 376*19378907SAtari911 377*19378907SAtari911 static $loadedMonths = array(); 378*19378907SAtari911 379*19378907SAtari911 foreach ($period as $dt) { 380*19378907SAtari911 $year = (int)$dt->format('Y'); 381*19378907SAtari911 $month = (int)$dt->format('n'); 382*19378907SAtari911 $dateKey = $dt->format('Y-m-d'); 383*19378907SAtari911 384*19378907SAtari911 $monthKey = $year . '-' . $month; 385*19378907SAtari911 386*19378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 387*19378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 388*19378907SAtari911 } 389*19378907SAtari911 390*19378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 391*19378907SAtari911 392*19378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 393*19378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 394*19378907SAtari911 } 395*19378907SAtari911 } 396*19378907SAtari911 397*19378907SAtari911 $html = '<div class="eventlist-standalone">'; 398*19378907SAtari911 $html .= '<h3>Events: ' . date('M j', strtotime($startDate)) . ' - ' . date('M j, Y', strtotime($endDate)) . '</h3>'; 399*19378907SAtari911 400*19378907SAtari911 if (empty($allEvents)) { 401*19378907SAtari911 $html .= '<p class="no-events-msg">No events in this date range</p>'; 402*19378907SAtari911 } else { 403*19378907SAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 404*19378907SAtari911 $displayDate = date('l, F j, Y', strtotime($dateKey)); 405*19378907SAtari911 406*19378907SAtari911 $html .= '<div class="eventlist-day-group">'; 407*19378907SAtari911 $html .= '<h4 class="eventlist-date">' . $displayDate . '</h4>'; 408*19378907SAtari911 409*19378907SAtari911 foreach ($dayEvents as $event) { 410*19378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 411*19378907SAtari911 $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; 412*19378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 413*19378907SAtari911 $description = isset($event['description']) ? htmlspecialchars($event['description']) : ''; 414*19378907SAtari911 415*19378907SAtari911 $html .= '<div class="eventlist-item">'; 416*19378907SAtari911 $html .= '<div class="event-color-bar" style="background: ' . $color . ';"></div>'; 417*19378907SAtari911 $html .= '<div class="eventlist-content">'; 418*19378907SAtari911 if ($time) { 419*19378907SAtari911 $html .= '<span class="eventlist-time">' . $time . '</span>'; 420*19378907SAtari911 } 421*19378907SAtari911 $html .= '<span class="eventlist-title">' . $title . '</span>'; 422*19378907SAtari911 if ($description) { 423*19378907SAtari911 $html .= '<div class="eventlist-desc">' . nl2br($description) . '</div>'; 424*19378907SAtari911 } 425*19378907SAtari911 $html .= '</div></div>'; 426*19378907SAtari911 } 427*19378907SAtari911 428*19378907SAtari911 $html .= '</div>'; 429*19378907SAtari911 } 430*19378907SAtari911 } 431*19378907SAtari911 432*19378907SAtari911 $html .= '</div>'; 433*19378907SAtari911 434*19378907SAtari911 return $html; 435*19378907SAtari911 } 436*19378907SAtari911 437*19378907SAtari911 private function renderEventDialog($calId, $namespace) { 438*19378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 439*19378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 440*19378907SAtari911 441*19378907SAtari911 // Draggable dialog 442*19378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 443*19378907SAtari911 444*19378907SAtari911 // Header with drag handle and close button 445*19378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 446*19378907SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 447*19378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 448*19378907SAtari911 $html .= '</div>'; 449*19378907SAtari911 450*19378907SAtari911 // Form content 451*19378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 452*19378907SAtari911 453*19378907SAtari911 // Hidden ID field 454*19378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 455*19378907SAtari911 456*19378907SAtari911 // Task checkbox 457*19378907SAtari911 $html .= '<div class="form-field form-field-checkbox">'; 458*19378907SAtari911 $html .= '<label class="checkbox-label">'; 459*19378907SAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 460*19378907SAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 461*19378907SAtari911 $html .= '</label>'; 462*19378907SAtari911 $html .= '</div>'; 463*19378907SAtari911 464*19378907SAtari911 // Date and Time in a row 465*19378907SAtari911 $html .= '<div class="form-row-group">'; 466*19378907SAtari911 467*19378907SAtari911 // Start Date field 468*19378907SAtari911 $html .= '<div class="form-field form-field-date">'; 469*19378907SAtari911 $html .= '<label class="field-label"> Start Date</label>'; 470*19378907SAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date">'; 471*19378907SAtari911 $html .= '</div>'; 472*19378907SAtari911 473*19378907SAtari911 // End Date field (for multi-day events) 474*19378907SAtari911 $html .= '<div class="form-field form-field-date">'; 475*19378907SAtari911 $html .= '<label class="field-label"> End Date</label>'; 476*19378907SAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date">'; 477*19378907SAtari911 $html .= '</div>'; 478*19378907SAtari911 479*19378907SAtari911 $html .= '</div>'; 480*19378907SAtari911 481*19378907SAtari911 // Time field 482*19378907SAtari911 $html .= '<div class="form-field">'; 483*19378907SAtari911 $html .= '<label class="field-label"> Time (optional)</label>'; 484*19378907SAtari911 $html .= '<input type="time" id="event-time-' . $calId . '" name="time" class="input-sleek">'; 485*19378907SAtari911 $html .= '</div>'; 486*19378907SAtari911 487*19378907SAtari911 // Title field 488*19378907SAtari911 $html .= '<div class="form-field">'; 489*19378907SAtari911 $html .= '<label class="field-label"> Title</label>'; 490*19378907SAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek" placeholder="Event or task title...">'; 491*19378907SAtari911 $html .= '</div>'; 492*19378907SAtari911 493*19378907SAtari911 // Description field 494*19378907SAtari911 $html .= '<div class="form-field">'; 495*19378907SAtari911 $html .= '<label class="field-label"> Description</label>'; 496*19378907SAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="3" class="input-sleek textarea-sleek" placeholder="Add details (optional)..."></textarea>'; 497*19378907SAtari911 $html .= '</div>'; 498*19378907SAtari911 499*19378907SAtari911 // Color picker 500*19378907SAtari911 $html .= '<div class="form-field">'; 501*19378907SAtari911 $html .= '<label class="field-label"> Color</label>'; 502*19378907SAtari911 $html .= '<div class="color-picker-container">'; 503*19378907SAtari911 $html .= '<input type="color" id="event-color-' . $calId . '" name="color" value="#3498db" class="input-color-sleek">'; 504*19378907SAtari911 $html .= '<span class="color-label">Choose event color</span>'; 505*19378907SAtari911 $html .= '</div>'; 506*19378907SAtari911 $html .= '</div>'; 507*19378907SAtari911 508*19378907SAtari911 // Action buttons 509*19378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 510*19378907SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 511*19378907SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 512*19378907SAtari911 $html .= '</div>'; 513*19378907SAtari911 514*19378907SAtari911 $html .= '</form>'; 515*19378907SAtari911 $html .= '</div>'; 516*19378907SAtari911 $html .= '</div>'; 517*19378907SAtari911 518*19378907SAtari911 return $html; 519*19378907SAtari911 } 520*19378907SAtari911 521*19378907SAtari911 private function renderDescription($description) { 522*19378907SAtari911 if (empty($description)) { 523*19378907SAtari911 return ''; 524*19378907SAtari911 } 525*19378907SAtari911 526*19378907SAtari911 // Convert newlines to <br> for basic formatting 527*19378907SAtari911 $rendered = nl2br($description); 528*19378907SAtari911 529*19378907SAtari911 // Convert DokuWiki image syntax {{image.jpg}} to HTML 530*19378907SAtari911 $rendered = preg_replace_callback( 531*19378907SAtari911 '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/', 532*19378907SAtari911 function($matches) { 533*19378907SAtari911 $imagePath = trim($matches[1]); 534*19378907SAtari911 $alt = isset($matches[2]) ? trim($matches[2]) : ''; 535*19378907SAtari911 536*19378907SAtari911 // Handle external URLs (http:// or https://) 537*19378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 538*19378907SAtari911 return '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 539*19378907SAtari911 } 540*19378907SAtari911 541*19378907SAtari911 // Handle internal DokuWiki images 542*19378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 543*19378907SAtari911 return '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 544*19378907SAtari911 }, 545*19378907SAtari911 $rendered 546*19378907SAtari911 ); 547*19378907SAtari911 548*19378907SAtari911 // Convert DokuWiki link syntax [[link|text]] to HTML 549*19378907SAtari911 $rendered = preg_replace_callback( 550*19378907SAtari911 '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', 551*19378907SAtari911 function($matches) { 552*19378907SAtari911 $link = trim($matches[1]); 553*19378907SAtari911 $text = isset($matches[2]) ? trim($matches[2]) : $link; 554*19378907SAtari911 555*19378907SAtari911 // Handle external URLs 556*19378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 557*19378907SAtari911 return '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 558*19378907SAtari911 } 559*19378907SAtari911 560*19378907SAtari911 // Handle internal DokuWiki links 561*19378907SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($link); 562*19378907SAtari911 return '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>'; 563*19378907SAtari911 }, 564*19378907SAtari911 $rendered 565*19378907SAtari911 ); 566*19378907SAtari911 567*19378907SAtari911 // Convert markdown-style links [text](url) to HTML 568*19378907SAtari911 $rendered = preg_replace_callback( 569*19378907SAtari911 '/\[([^\]]+)\]\(([^)]+)\)/', 570*19378907SAtari911 function($matches) { 571*19378907SAtari911 $text = trim($matches[1]); 572*19378907SAtari911 $url = trim($matches[2]); 573*19378907SAtari911 574*19378907SAtari911 if (preg_match('/^https?:\/\//', $url)) { 575*19378907SAtari911 return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 576*19378907SAtari911 } 577*19378907SAtari911 578*19378907SAtari911 return '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>'; 579*19378907SAtari911 }, 580*19378907SAtari911 $rendered 581*19378907SAtari911 ); 582*19378907SAtari911 583*19378907SAtari911 // Convert plain URLs to clickable links 584*19378907SAtari911 $rendered = preg_replace_callback( 585*19378907SAtari911 '/(https?:\/\/[^\s<]+)/', 586*19378907SAtari911 function($matches) { 587*19378907SAtari911 $url = $matches[1]; 588*19378907SAtari911 return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>'; 589*19378907SAtari911 }, 590*19378907SAtari911 $rendered 591*19378907SAtari911 ); 592*19378907SAtari911 593*19378907SAtari911 // Allow basic HTML tags (bold, italic, strong, em, u, code) 594*19378907SAtari911 // Already in the description, just pass through 595*19378907SAtari911 596*19378907SAtari911 return $rendered; 597*19378907SAtari911 } 598*19378907SAtari911 599*19378907SAtari911 private function loadEvents($namespace, $year, $month) { 600*19378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 601*19378907SAtari911 if ($namespace) { 602*19378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 603*19378907SAtari911 } 604*19378907SAtari911 $dataDir .= 'calendar/'; 605*19378907SAtari911 606*19378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 607*19378907SAtari911 608*19378907SAtari911 if (file_exists($eventFile)) { 609*19378907SAtari911 $json = file_get_contents($eventFile); 610*19378907SAtari911 return json_decode($json, true); 611*19378907SAtari911 } 612*19378907SAtari911 613*19378907SAtari911 return array(); 614*19378907SAtari911 } 615*19378907SAtari911} 616