Calendar Refinement
This commit is contained in:
@@ -199,13 +199,11 @@ class CalendarService
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
$events = self::collectFromCalendar($expanded, $slot->getKey(), $dateIso);
|
||||
$cacheKey = $slot->getKey() . '|' . $dateIso;
|
||||
self::$dayCache[$cacheKey] = $events;
|
||||
|
||||
if ($events !== []) {
|
||||
$indicators[$dateIso][] = $slot->getKey();
|
||||
// Pre-populate the day cache
|
||||
$cacheKey = $slot->getKey() . '|' . $dateIso;
|
||||
if (!isset(self::$dayCache[$cacheKey])) {
|
||||
self::$dayCache[$cacheKey] = $events;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
@@ -216,6 +214,37 @@ class CalendarService
|
||||
return $indicators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare month data for the calendar widget in one pass.
|
||||
*
|
||||
* Uses monthIndicators() to warm the per-slot day cache, then reuses the
|
||||
* normalized events already cached for each day.
|
||||
*
|
||||
* @param CalendarSlot[] $slots
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
* @return array{indicators: array<string,string[]>, events: array<string,CalendarEvent[]>}
|
||||
*/
|
||||
public static function monthWidgetData(array $slots, int $year, int $month): array
|
||||
{
|
||||
$indicators = self::monthIndicators($slots, $year, $month);
|
||||
$eventsByDate = [];
|
||||
$daysInMonth = (int)date('t', mktime(0, 0, 0, $month, 1, $year));
|
||||
|
||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||
$dateIso = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||
$events = self::eventsForDate($slots, $dateIso);
|
||||
if ($events !== []) {
|
||||
$eventsByDate[$dateIso] = $events;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'indicators' => $indicators,
|
||||
'events' => $eventsByDate,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse an ICS file, caching the parsed VCalendar per file path.
|
||||
*
|
||||
|
||||
@@ -122,6 +122,16 @@ class CalendarSlot
|
||||
return $this->display !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this slot should participate in calendar widget visibility.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isVisibleInWidget(): bool
|
||||
{
|
||||
return $this->shouldDisplayIndicator();
|
||||
}
|
||||
|
||||
/**
|
||||
* A slot is enabled if it has a local file path or a CalDAV URL.
|
||||
*
|
||||
@@ -190,6 +200,19 @@ class CalendarSlot
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep only slots that should appear in the calendar widget.
|
||||
*
|
||||
* @param CalendarSlot[] $slots
|
||||
* @return CalendarSlot[]
|
||||
*/
|
||||
public static function filterWidgetVisible(array $slots): array
|
||||
{
|
||||
return array_filter($slots, static function (CalendarSlot $slot): bool {
|
||||
return $slot->isVisibleInWidget();
|
||||
});
|
||||
}
|
||||
|
||||
protected static function normalizeIndicatorDisplay(string $display): string
|
||||
{
|
||||
$display = strtolower(trim($display));
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace dokuwiki\plugin\luxtools;
|
||||
*/
|
||||
class ChronologicalCalendarWidget
|
||||
{
|
||||
/** @var int Maximum number of inline events shown per day cell in large mode */
|
||||
protected const MAX_INLINE_EVENTS = 3;
|
||||
|
||||
/**
|
||||
* Render full calendar widget HTML for one month.
|
||||
*
|
||||
@@ -16,6 +19,7 @@ class ChronologicalCalendarWidget
|
||||
* @param array<string,string[]> $indicators date => [slotKey, ...] from CalendarService::monthIndicators()
|
||||
* @param array<string,string> $slotColors slotKey => CSS color
|
||||
* @param array<string,string> $slotDisplays slotKey => configured indicator position
|
||||
* @param array{size?:string,showTimes?:bool,dayEvents?:array<string,CalendarEvent[]>} $options
|
||||
* @return string
|
||||
*/
|
||||
public static function render(
|
||||
@@ -24,10 +28,15 @@ class ChronologicalCalendarWidget
|
||||
string $baseNs = 'chronological',
|
||||
array $indicators = [],
|
||||
array $slotColors = [],
|
||||
array $slotDisplays = []
|
||||
array $slotDisplays = [],
|
||||
array $options = []
|
||||
): string {
|
||||
if (!self::isValidMonth($year, $month)) return '';
|
||||
|
||||
$size = self::normalizeSize((string)($options['size'] ?? 'large'));
|
||||
$showTimes = (bool)($options['showTimes'] ?? true);
|
||||
$dayEvents = is_array($options['dayEvents'] ?? null) ? $options['dayEvents'] : [];
|
||||
|
||||
$firstDayTs = mktime(0, 0, 0, $month, 1, $year);
|
||||
$daysInMonth = (int)date('t', $firstDayTs);
|
||||
$firstWeekday = (int)date('N', $firstDayTs); // 1..7 (Mon..Sun)
|
||||
@@ -59,10 +68,12 @@ class ChronologicalCalendarWidget
|
||||
$yearUrlTemplate = $dayUrlTemplate;
|
||||
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
|
||||
|
||||
$html = '<div class="luxtools-plugin luxtools-calendar" data-luxtools-calendar="1"'
|
||||
$html = '<div class="luxtools-plugin luxtools-calendar luxtools-calendar-size-' . hsc($size) . '" data-luxtools-calendar="1"'
|
||||
. ' data-base-ns="' . hsc($baseNs) . '"'
|
||||
. ' data-current-year="' . hsc((string)$year) . '"'
|
||||
. ' data-current-month="' . hsc(sprintf('%02d', $month)) . '"'
|
||||
. ' data-luxtools-size="' . hsc($size) . '"'
|
||||
. ' data-luxtools-show-times="' . ($showTimes ? '1' : '0') . '"'
|
||||
. ' data-day-url-template="' . hsc($dayUrlTemplate) . '"'
|
||||
. ' data-month-url-template="' . hsc($monthUrlTemplate) . '"'
|
||||
. ' data-year-url-template="' . hsc($yearUrlTemplate) . '"'
|
||||
@@ -114,33 +125,39 @@ class ChronologicalCalendarWidget
|
||||
} else {
|
||||
$date = sprintf('%04d-%02d-%02d', $year, $month, $dayNumber);
|
||||
$dayId = ChronoID::dateToDayId($date, $baseNs);
|
||||
$events = $dayEvents[$date] ?? [];
|
||||
|
||||
$classes = 'luxtools-calendar-day';
|
||||
if ($events !== []) {
|
||||
$classes .= ' luxtools-calendar-day-has-events';
|
||||
}
|
||||
|
||||
$html .= '<td class="' . hsc($classes) . '" data-luxtools-date="' . hsc($date) . '">';
|
||||
|
||||
// Render slot indicators if any
|
||||
$dayIndicators = $indicators[$date] ?? [];
|
||||
if ($dayIndicators !== []) {
|
||||
$indicatorHtml = '';
|
||||
foreach ($dayIndicators as $slotKey) {
|
||||
$display = $slotDisplays[$slotKey] ?? 'none';
|
||||
if ($display === 'none') continue;
|
||||
if ($size === 'small') {
|
||||
$dayIndicators = $indicators[$date] ?? [];
|
||||
if ($dayIndicators !== []) {
|
||||
$indicatorHtml = '';
|
||||
foreach ($dayIndicators as $slotKey) {
|
||||
$display = $slotDisplays[$slotKey] ?? 'none';
|
||||
if ($display === 'none') continue;
|
||||
|
||||
$color = $slotColors[$slotKey] ?? '';
|
||||
$style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : '';
|
||||
$indicatorHtml .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($display) . '"' . $style . '></span>';
|
||||
$color = $slotColors[$slotKey] ?? '';
|
||||
$style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : '';
|
||||
$indicatorHtml .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($display) . '"' . $style . '></span>';
|
||||
}
|
||||
|
||||
if ($indicatorHtml !== '') {
|
||||
$html .= '<div class="luxtools-calendar-indicators">' . $indicatorHtml . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($indicatorHtml !== '') {
|
||||
$html .= '<div class="luxtools-calendar-indicators">' . $indicatorHtml . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
if ($dayId !== null && function_exists('html_wikilink')) {
|
||||
$html .= (string)html_wikilink($dayId, (string)$dayNumber);
|
||||
$html .= self::renderDayLink($dayId, (string)$dayNumber);
|
||||
} else {
|
||||
$html .= hsc((string)$dayNumber);
|
||||
$html .= '<div class="luxtools-calendar-day-frame">';
|
||||
$html .= '<div class="luxtools-calendar-day-number">' . self::renderDayLink($dayId, (string)$dayNumber) . '</div>';
|
||||
$html .= self::renderInlineEvents($events, $slotColors, $showTimes);
|
||||
$html .= '</div>';
|
||||
}
|
||||
$html .= '</td>';
|
||||
}
|
||||
@@ -155,6 +172,26 @@ class ChronologicalCalendarWidget
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $size
|
||||
* @return string
|
||||
*/
|
||||
public static function normalizeSize(string $size): string
|
||||
{
|
||||
$size = strtolower(trim($size));
|
||||
return $size === 'small' ? 'small' : 'large';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $value
|
||||
* @return bool
|
||||
*/
|
||||
public static function normalizeShowTimes(?string $value): bool
|
||||
{
|
||||
if ($value === null) return true;
|
||||
return trim($value) !== '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $year
|
||||
* @param int $month
|
||||
@@ -166,4 +203,80 @@ class ChronologicalCalendarWidget
|
||||
if ($month < 1 || $month > 12) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $dayId
|
||||
* @param string $label
|
||||
* @return string
|
||||
*/
|
||||
protected static function renderDayLink(?string $dayId, string $label): string
|
||||
{
|
||||
if ($dayId !== null && function_exists('html_wikilink')) {
|
||||
return (string)html_wikilink($dayId, $label);
|
||||
}
|
||||
|
||||
return hsc($label);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalendarEvent[] $events
|
||||
* @param array<string,string> $slotColors
|
||||
* @param bool $showTimes
|
||||
* @return string
|
||||
*/
|
||||
protected static function renderInlineEvents(array $events, array $slotColors, bool $showTimes): string
|
||||
{
|
||||
if ($events === []) {
|
||||
return '<div class="luxtools-calendar-day-events"></div>';
|
||||
}
|
||||
|
||||
$html = '<div class="luxtools-calendar-day-events"><ul class="luxtools-calendar-event-list">';
|
||||
|
||||
$visibleCount = min(count($events), self::MAX_INLINE_EVENTS);
|
||||
for ($index = 0; $index < $visibleCount; $index++) {
|
||||
$event = $events[$index];
|
||||
$color = $slotColors[$event->slotKey] ?? '';
|
||||
$style = $color !== '' ? ' style="--luxtools-slot-color:' . hsc($color) . '"' : '';
|
||||
$dataAttrs = self::renderEventDataAttributes($event);
|
||||
|
||||
$html .= '<li class="luxtools-calendar-event"' . $style . $dataAttrs . '>';
|
||||
if ($showTimes && !$event->allDay && $event->time !== '') {
|
||||
$html .= '<span class="luxtools-calendar-event-time">' . hsc($event->time) . '</span>';
|
||||
}
|
||||
$html .= '<span class="luxtools-calendar-event-title">' . hsc($event->summary) . '</span>';
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$remaining = count($events) - $visibleCount;
|
||||
if ($remaining > 0) {
|
||||
$html .= '<li class="luxtools-calendar-event luxtools-calendar-event-more">+' . hsc((string)$remaining) . ' more</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CalendarEvent $event
|
||||
* @return string
|
||||
*/
|
||||
protected static function renderEventDataAttributes(CalendarEvent $event): string
|
||||
{
|
||||
$attrs = ' data-luxtools-event="1"';
|
||||
$attrs .= ' data-event-summary="' . hsc($event->summary) . '"';
|
||||
$attrs .= ' data-event-start="' . hsc($event->startIso) . '"';
|
||||
if ($event->endIso !== '') {
|
||||
$attrs .= ' data-event-end="' . hsc($event->endIso) . '"';
|
||||
}
|
||||
if ($event->location !== '') {
|
||||
$attrs .= ' data-event-location="' . hsc($event->location) . '"';
|
||||
}
|
||||
if ($event->description !== '') {
|
||||
$attrs .= ' data-event-description="' . hsc($event->description) . '"';
|
||||
}
|
||||
$attrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"';
|
||||
$attrs .= ' data-event-slot="' . hsc($event->slotKey) . '"';
|
||||
|
||||
return $attrs;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user