xref: /plugin/calendar/syntax.php (revision 19378907f6c3c154fcddd2ddfe78fa2d88d43359)
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