Calendar Refinement
This commit is contained in:
12
README.md
12
README.md
@@ -289,21 +289,27 @@ Render a basic monthly calendar that links each day to canonical chronological p
|
|||||||
```
|
```
|
||||||
{{calendar>}}
|
{{calendar>}}
|
||||||
{{calendar>2024-10}}
|
{{calendar>2024-10}}
|
||||||
|
{{calendar>2026-03&size=small}}
|
||||||
|
{{calendar>2026-03&size=large&show_times=0}}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
- `{{calendar>}}` renders the current month.
|
- `{{calendar>}}` renders the current month.
|
||||||
- `{{calendar>YYYY-MM}}` renders a specific month.
|
- `{{calendar>YYYY-MM}}` renders a specific month.
|
||||||
|
- `size=large|small` controls the widget layout and defaults to `large`.
|
||||||
|
- `show_times=1|0` controls inline event times in `large` mode and defaults to `1`.
|
||||||
- Day links target `chronological:YYYY:MM:DD`.
|
- Day links target `chronological:YYYY:MM:DD`.
|
||||||
- Header month/year links target `chronological:YYYY:MM` and `chronological:YYYY`.
|
- Header month/year links target `chronological:YYYY:MM` and `chronological:YYYY`.
|
||||||
- Prev/next month buttons update the widget in-place without a full page reload.
|
- Prev/next month buttons update the widget in-place without a full page reload.
|
||||||
- Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node.
|
- Month switches fetch server-rendered widget HTML via AJAX and replace only the widget node.
|
||||||
- Calendar output is marked as non-cacheable to keep missing/existing link styling and
|
- Calendar output is marked as non-cacheable to keep missing/existing link styling and
|
||||||
current-day highlighting up to date.
|
current-day highlighting up to date.
|
||||||
- Each day cell can show colored corner triangles for slots that have events on that day.
|
- Small mode keeps the compact day-number-plus-indicator layout.
|
||||||
Indicator placement is configured per slot via the `Display` setting.
|
- Large mode renders inline day events in the month cells and suppresses the corner indicators.
|
||||||
Indicator colors are taken from the slot's configured color.
|
- Only slots whose `Display` setting is not `None` participate in widget visibility.
|
||||||
|
- Indicator placement in small mode is configured per slot via the `Display` setting.
|
||||||
|
- Slot colors are reused for both indicators and inline event accents.
|
||||||
|
|
||||||
### 0.4) Virtual chronological day pages
|
### 0.4) Virtual chronological day pages
|
||||||
|
|
||||||
|
|||||||
23
action.php
23
action.php
@@ -155,6 +155,8 @@ class action_plugin_luxtools extends ActionPlugin
|
|||||||
if ($baseNs === '') {
|
if ($baseNs === '') {
|
||||||
$baseNs = 'chronological';
|
$baseNs = 'chronological';
|
||||||
}
|
}
|
||||||
|
$size = ChronologicalCalendarWidget::normalizeSize((string)$INPUT->str('size'));
|
||||||
|
$showTimes = ChronologicalCalendarWidget::normalizeShowTimes($INPUT->str('show_times'));
|
||||||
|
|
||||||
if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) {
|
if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) {
|
||||||
http_status(400);
|
http_status(400);
|
||||||
@@ -164,12 +166,21 @@ class action_plugin_luxtools extends ActionPlugin
|
|||||||
|
|
||||||
$this->sendNoStoreHeaders();
|
$this->sendNoStoreHeaders();
|
||||||
|
|
||||||
// Load slot indicators and colors for the calendar widget
|
|
||||||
$slots = CalendarSlot::loadEnabled($this);
|
$slots = CalendarSlot::loadEnabled($this);
|
||||||
$indicators = CalendarService::monthIndicators($slots, $year, $month);
|
$widgetSlots = CalendarSlot::filterWidgetVisible($slots);
|
||||||
|
$indicators = [];
|
||||||
|
$dayEvents = [];
|
||||||
|
if ($size === 'large') {
|
||||||
|
$widgetData = CalendarService::monthWidgetData($widgetSlots, $year, $month);
|
||||||
|
$indicators = $widgetData['indicators'];
|
||||||
|
$dayEvents = $widgetData['events'];
|
||||||
|
} else {
|
||||||
|
$indicators = CalendarService::monthIndicators($widgetSlots, $year, $month);
|
||||||
|
}
|
||||||
|
|
||||||
$slotColors = [];
|
$slotColors = [];
|
||||||
$slotDisplays = [];
|
$slotDisplays = [];
|
||||||
foreach ($slots as $slot) {
|
foreach ($widgetSlots as $slot) {
|
||||||
$color = $slot->getColor();
|
$color = $slot->getColor();
|
||||||
if ($color !== '') {
|
if ($color !== '') {
|
||||||
$slotColors[$slot->getKey()] = $color;
|
$slotColors[$slot->getKey()] = $color;
|
||||||
@@ -177,7 +188,11 @@ class action_plugin_luxtools extends ActionPlugin
|
|||||||
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays);
|
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays, [
|
||||||
|
'size' => $size,
|
||||||
|
'showTimes' => $showTimes,
|
||||||
|
'dayEvents' => $dayEvents,
|
||||||
|
]);
|
||||||
if ($html === '') {
|
if ($html === '') {
|
||||||
http_status(500);
|
http_status(500);
|
||||||
echo 'Calendar rendering failed';
|
echo 'Calendar rendering failed';
|
||||||
|
|||||||
@@ -54,7 +54,12 @@
|
|||||||
return 'luxtools.calendar.month.' + baseNs;
|
return 'luxtools.calendar.month.' + baseNs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldPersistCalendarMonth(calendar) {
|
||||||
|
return (calendar.getAttribute('data-luxtools-size') || 'large') === 'small';
|
||||||
|
}
|
||||||
|
|
||||||
function readSavedCalendarMonth(calendar) {
|
function readSavedCalendarMonth(calendar) {
|
||||||
|
if (!shouldPersistCalendarMonth(calendar)) return null;
|
||||||
if (!window.localStorage) return null;
|
if (!window.localStorage) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -73,6 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function saveCalendarMonth(calendar) {
|
function saveCalendarMonth(calendar) {
|
||||||
|
if (!shouldPersistCalendarMonth(calendar)) return;
|
||||||
if (!window.localStorage) return;
|
if (!window.localStorage) return;
|
||||||
|
|
||||||
var year = parseInt(calendar.getAttribute('data-current-year') || '', 10);
|
var year = parseInt(calendar.getAttribute('data-current-year') || '', 10);
|
||||||
@@ -90,6 +96,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearSavedCalendarMonth(calendar) {
|
function clearSavedCalendarMonth(calendar) {
|
||||||
|
if (!shouldPersistCalendarMonth(calendar)) return;
|
||||||
if (!window.localStorage) return;
|
if (!window.localStorage) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -104,11 +111,15 @@
|
|||||||
if (!ajaxUrl) return Promise.reject(new Error('Missing calendar ajax url'));
|
if (!ajaxUrl) return Promise.reject(new Error('Missing calendar ajax url'));
|
||||||
|
|
||||||
var baseNs = calendar.getAttribute('data-base-ns') || 'chronological';
|
var baseNs = calendar.getAttribute('data-base-ns') || 'chronological';
|
||||||
|
var size = calendar.getAttribute('data-luxtools-size') || 'large';
|
||||||
|
var showTimes = calendar.getAttribute('data-luxtools-show-times') || '1';
|
||||||
var params = new URLSearchParams({
|
var params = new URLSearchParams({
|
||||||
call: 'luxtools_calendar_month',
|
call: 'luxtools_calendar_month',
|
||||||
year: String(year),
|
year: String(year),
|
||||||
month: String(month),
|
month: String(month),
|
||||||
base: baseNs
|
base: baseNs,
|
||||||
|
size: size,
|
||||||
|
show_times: showTimes
|
||||||
});
|
});
|
||||||
|
|
||||||
var url = ajaxUrl + (ajaxUrl.indexOf('?') >= 0 ? '&' : '?') + params.toString();
|
var url = ajaxUrl + (ajaxUrl.indexOf('?') >= 0 ? '&' : '?') + params.toString();
|
||||||
@@ -212,6 +223,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function restoreCalendarMonth(calendar) {
|
function restoreCalendarMonth(calendar) {
|
||||||
|
if (!shouldPersistCalendarMonth(calendar)) return;
|
||||||
var saved = readSavedCalendarMonth(calendar);
|
var saved = readSavedCalendarMonth(calendar);
|
||||||
if (!saved) return;
|
if (!saved) return;
|
||||||
|
|
||||||
|
|||||||
@@ -199,13 +199,11 @@ class CalendarService
|
|||||||
for ($day = 1; $day <= $daysInMonth; $day++) {
|
for ($day = 1; $day <= $daysInMonth; $day++) {
|
||||||
$dateIso = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
$dateIso = sprintf('%04d-%02d-%02d', $year, $month, $day);
|
||||||
$events = self::collectFromCalendar($expanded, $slot->getKey(), $dateIso);
|
$events = self::collectFromCalendar($expanded, $slot->getKey(), $dateIso);
|
||||||
|
$cacheKey = $slot->getKey() . '|' . $dateIso;
|
||||||
|
self::$dayCache[$cacheKey] = $events;
|
||||||
|
|
||||||
if ($events !== []) {
|
if ($events !== []) {
|
||||||
$indicators[$dateIso][] = $slot->getKey();
|
$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) {
|
} catch (Throwable $e) {
|
||||||
@@ -216,6 +214,37 @@ class CalendarService
|
|||||||
return $indicators;
|
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.
|
* Read and parse an ICS file, caching the parsed VCalendar per file path.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -122,6 +122,16 @@ class CalendarSlot
|
|||||||
return $this->display !== 'none';
|
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.
|
* 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
|
protected static function normalizeIndicatorDisplay(string $display): string
|
||||||
{
|
{
|
||||||
$display = strtolower(trim($display));
|
$display = strtolower(trim($display));
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ namespace dokuwiki\plugin\luxtools;
|
|||||||
*/
|
*/
|
||||||
class ChronologicalCalendarWidget
|
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.
|
* 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[]> $indicators date => [slotKey, ...] from CalendarService::monthIndicators()
|
||||||
* @param array<string,string> $slotColors slotKey => CSS color
|
* @param array<string,string> $slotColors slotKey => CSS color
|
||||||
* @param array<string,string> $slotDisplays slotKey => configured indicator position
|
* @param array<string,string> $slotDisplays slotKey => configured indicator position
|
||||||
|
* @param array{size?:string,showTimes?:bool,dayEvents?:array<string,CalendarEvent[]>} $options
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public static function render(
|
public static function render(
|
||||||
@@ -24,10 +28,15 @@ class ChronologicalCalendarWidget
|
|||||||
string $baseNs = 'chronological',
|
string $baseNs = 'chronological',
|
||||||
array $indicators = [],
|
array $indicators = [],
|
||||||
array $slotColors = [],
|
array $slotColors = [],
|
||||||
array $slotDisplays = []
|
array $slotDisplays = [],
|
||||||
|
array $options = []
|
||||||
): string {
|
): string {
|
||||||
if (!self::isValidMonth($year, $month)) return '';
|
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);
|
$firstDayTs = mktime(0, 0, 0, $month, 1, $year);
|
||||||
$daysInMonth = (int)date('t', $firstDayTs);
|
$daysInMonth = (int)date('t', $firstDayTs);
|
||||||
$firstWeekday = (int)date('N', $firstDayTs); // 1..7 (Mon..Sun)
|
$firstWeekday = (int)date('N', $firstDayTs); // 1..7 (Mon..Sun)
|
||||||
@@ -59,10 +68,12 @@ class ChronologicalCalendarWidget
|
|||||||
$yearUrlTemplate = $dayUrlTemplate;
|
$yearUrlTemplate = $dayUrlTemplate;
|
||||||
$ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php';
|
$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-base-ns="' . hsc($baseNs) . '"'
|
||||||
. ' data-current-year="' . hsc((string)$year) . '"'
|
. ' data-current-year="' . hsc((string)$year) . '"'
|
||||||
. ' data-current-month="' . hsc(sprintf('%02d', $month)) . '"'
|
. ' 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-day-url-template="' . hsc($dayUrlTemplate) . '"'
|
||||||
. ' data-month-url-template="' . hsc($monthUrlTemplate) . '"'
|
. ' data-month-url-template="' . hsc($monthUrlTemplate) . '"'
|
||||||
. ' data-year-url-template="' . hsc($yearUrlTemplate) . '"'
|
. ' data-year-url-template="' . hsc($yearUrlTemplate) . '"'
|
||||||
@@ -114,12 +125,16 @@ class ChronologicalCalendarWidget
|
|||||||
} else {
|
} else {
|
||||||
$date = sprintf('%04d-%02d-%02d', $year, $month, $dayNumber);
|
$date = sprintf('%04d-%02d-%02d', $year, $month, $dayNumber);
|
||||||
$dayId = ChronoID::dateToDayId($date, $baseNs);
|
$dayId = ChronoID::dateToDayId($date, $baseNs);
|
||||||
|
$events = $dayEvents[$date] ?? [];
|
||||||
|
|
||||||
$classes = 'luxtools-calendar-day';
|
$classes = 'luxtools-calendar-day';
|
||||||
|
if ($events !== []) {
|
||||||
|
$classes .= ' luxtools-calendar-day-has-events';
|
||||||
|
}
|
||||||
|
|
||||||
$html .= '<td class="' . hsc($classes) . '" data-luxtools-date="' . hsc($date) . '">';
|
$html .= '<td class="' . hsc($classes) . '" data-luxtools-date="' . hsc($date) . '">';
|
||||||
|
|
||||||
// Render slot indicators if any
|
if ($size === 'small') {
|
||||||
$dayIndicators = $indicators[$date] ?? [];
|
$dayIndicators = $indicators[$date] ?? [];
|
||||||
if ($dayIndicators !== []) {
|
if ($dayIndicators !== []) {
|
||||||
$indicatorHtml = '';
|
$indicatorHtml = '';
|
||||||
@@ -137,10 +152,12 @@ class ChronologicalCalendarWidget
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($dayId !== null && function_exists('html_wikilink')) {
|
$html .= self::renderDayLink($dayId, (string)$dayNumber);
|
||||||
$html .= (string)html_wikilink($dayId, (string)$dayNumber);
|
|
||||||
} else {
|
} 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>';
|
$html .= '</td>';
|
||||||
}
|
}
|
||||||
@@ -155,6 +172,26 @@ class ChronologicalCalendarWidget
|
|||||||
return $html;
|
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 $year
|
||||||
* @param int $month
|
* @param int $month
|
||||||
@@ -166,4 +203,80 @@ class ChronologicalCalendarWidget
|
|||||||
if ($month < 1 || $month > 12) return false;
|
if ($month < 1 || $month > 12) return false;
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
131
style.css
131
style.css
@@ -599,7 +599,8 @@ div.luxtools-calendar td.luxtools-calendar-day-today {
|
|||||||
background-color: @ini_highlight;
|
background-color: @ini_highlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a {
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -612,27 +613,33 @@ div.luxtools-calendar td.luxtools-calendar-day a {
|
|||||||
padding: 0.1em 0;
|
padding: 0.1em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a.wikilink2:link,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a.wikilink2:link,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a.wikilink2:visited {
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a.wikilink2:visited,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a.wikilink2:link,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a.wikilink2:visited {
|
||||||
color: @ini_missing;
|
color: @ini_missing;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a:hover,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:hover,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a:focus,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:focus,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a:active,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:active,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day a:visited {
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:visited,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:hover,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:focus,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:active,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:visited {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.luxtools-calendar td.luxtools-calendar-day span.curid > a,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day span.curid > a:visited,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:visited,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day span.curid > a:hover,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:hover,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day span.curid > a:focus,
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:focus,
|
||||||
div.luxtools-calendar td.luxtools-calendar-day span.curid > a:active {
|
div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a:active {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
@@ -657,6 +664,106 @@ div.luxtools-calendar td.luxtools-calendar-day {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large table.luxtools-calendar-table td {
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large td.luxtools-calendar-day {
|
||||||
|
height: 8.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large td.luxtools-calendar-day-empty {
|
||||||
|
height: 8.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-frame {
|
||||||
|
min-height: 8.25em;
|
||||||
|
padding: 0.35em 0.4em 0.4em 0.4em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number {
|
||||||
|
text-align: right;
|
||||||
|
margin-bottom: 0.25em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number > a,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number > span.curid > a,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number span.curid > a {
|
||||||
|
display: inline;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number > a.wikilink2:link,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number > a.wikilink2:visited,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number span.curid > a.wikilink2:link,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number span.curid > a.wikilink2:visited {
|
||||||
|
color: @ini_missing;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-events {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large ul.luxtools-calendar-event-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 0.35em;
|
||||||
|
margin: 0 0 0.2em 0;
|
||||||
|
padding: 0.1em 0.2em 0.1em 0.35em;
|
||||||
|
border-left: 3px solid var(--luxtools-slot-color, @ini_border);
|
||||||
|
background-color: @ini_background_alt;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event:hover {
|
||||||
|
background-color: @ini_highlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-event-time {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-event-title {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event-more {
|
||||||
|
border-left-color: @ini_border;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large td.luxtools-calendar-day,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large td.luxtools-calendar-day-empty,
|
||||||
|
div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-frame {
|
||||||
|
height: 7em;
|
||||||
|
min-height: 7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.luxtools-calendar-indicators {
|
.luxtools-calendar-indicators {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin
|
|||||||
'year' => $resolved['year'],
|
'year' => $resolved['year'],
|
||||||
'month' => $resolved['month'],
|
'month' => $resolved['month'],
|
||||||
'base' => $baseNs,
|
'base' => $baseNs,
|
||||||
|
'size' => ChronologicalCalendarWidget::normalizeSize((string)($params['size'] ?? 'large')),
|
||||||
|
'show_times' => ChronologicalCalendarWidget::normalizeShowTimes($params['show_times'] ?? null),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +88,24 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin
|
|||||||
$year = (int)$data['year'];
|
$year = (int)$data['year'];
|
||||||
$month = (int)$data['month'];
|
$month = (int)$data['month'];
|
||||||
$baseNs = (string)$data['base'];
|
$baseNs = (string)$data['base'];
|
||||||
|
$size = ChronologicalCalendarWidget::normalizeSize((string)($data['size'] ?? 'large'));
|
||||||
|
$showTimes = (bool)($data['show_times'] ?? true);
|
||||||
|
|
||||||
// Load slot indicators and colors for the calendar widget
|
|
||||||
$slots = CalendarSlot::loadEnabled($this);
|
$slots = CalendarSlot::loadEnabled($this);
|
||||||
$indicators = CalendarService::monthIndicators($slots, $year, $month);
|
$widgetSlots = CalendarSlot::filterWidgetVisible($slots);
|
||||||
|
$indicators = [];
|
||||||
|
$dayEvents = [];
|
||||||
|
if ($size === 'large') {
|
||||||
|
$widgetData = CalendarService::monthWidgetData($widgetSlots, $year, $month);
|
||||||
|
$indicators = $widgetData['indicators'];
|
||||||
|
$dayEvents = $widgetData['events'];
|
||||||
|
} else {
|
||||||
|
$indicators = CalendarService::monthIndicators($widgetSlots, $year, $month);
|
||||||
|
}
|
||||||
|
|
||||||
$slotColors = [];
|
$slotColors = [];
|
||||||
$slotDisplays = [];
|
$slotDisplays = [];
|
||||||
foreach ($slots as $slot) {
|
foreach ($widgetSlots as $slot) {
|
||||||
$color = $slot->getColor();
|
$color = $slot->getColor();
|
||||||
if ($color !== '') {
|
if ($color !== '') {
|
||||||
$slotColors[$slot->getKey()] = $color;
|
$slotColors[$slot->getKey()] = $color;
|
||||||
@@ -100,7 +113,11 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin
|
|||||||
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
$renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays);
|
$renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays, [
|
||||||
|
'size' => $size,
|
||||||
|
'showTimes' => $showTimes,
|
||||||
|
'dayEvents' => $dayEvents,
|
||||||
|
]);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user