Calendar V3
This commit is contained in:
@@ -174,6 +174,7 @@ Key settings:
|
||||
- **Username**: CalDAV authentication username
|
||||
- **Password**: CalDAV authentication password
|
||||
- **Color**: CSS color for calendar widget indicators
|
||||
- **Display**: Where to show that slot's calendar indicator (`None`, `Top Left`, `Top Right`, `Bottom Left`, `Bottom Right`)
|
||||
|
||||
A slot is enabled if it has a local file path or a CalDAV URL configured.
|
||||
The old `calendar_ics_files` setting has been replaced by the `general` slot's file path.
|
||||
@@ -300,9 +301,8 @@ Notes:
|
||||
- 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
|
||||
current-day highlighting up to date.
|
||||
- Each day cell shows colored corner indicators for calendar slots that have events on that day.
|
||||
Indicator positions (clockwise): general = top-left, maintenance = top-right,
|
||||
slot3 = bottom-right, slot4 = bottom-left.
|
||||
- Each day cell can show colored corner triangles for slots that have events on that day.
|
||||
Indicator placement is configured per slot via the `Display` setting.
|
||||
Indicator colors are taken from the slot's configured color.
|
||||
|
||||
### 0.4) Virtual chronological day pages
|
||||
|
||||
12
action.php
12
action.php
@@ -168,14 +168,16 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
$indicators = CalendarService::monthIndicators($slots, $year, $month);
|
||||
$slotColors = [];
|
||||
$slotDisplays = [];
|
||||
foreach ($slots as $slot) {
|
||||
$color = $slot->getColor();
|
||||
if ($color !== '') {
|
||||
$slotColors[$slot->getKey()] = $color;
|
||||
}
|
||||
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
||||
}
|
||||
|
||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors);
|
||||
$html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays);
|
||||
if ($html === '') {
|
||||
http_status(500);
|
||||
echo 'Calendar rendering failed';
|
||||
@@ -741,7 +743,7 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$remoteError = '';
|
||||
if ($maintenanceSlot->hasRemoteSource()) {
|
||||
try {
|
||||
$caldavOk = CalDavClient::updateEventStatus(
|
||||
$caldavResult = CalDavClient::updateEventStatus(
|
||||
$maintenanceSlot->getCaldavUrl(),
|
||||
$maintenanceSlot->getUsername(),
|
||||
$maintenanceSlot->getPassword(),
|
||||
@@ -750,13 +752,13 @@ class action_plugin_luxtools extends ActionPlugin
|
||||
$newStatus,
|
||||
$dateIso
|
||||
);
|
||||
if (!$caldavOk) {
|
||||
if ($caldavResult !== '') {
|
||||
$remoteOk = false;
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed');
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $caldavResult;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$remoteOk = false;
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed');
|
||||
$remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,21 +37,25 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
'calendar_general_username',
|
||||
'calendar_general_password',
|
||||
'calendar_general_color',
|
||||
'calendar_general_display',
|
||||
'calendar_maintenance_file',
|
||||
'calendar_maintenance_caldav_url',
|
||||
'calendar_maintenance_username',
|
||||
'calendar_maintenance_password',
|
||||
'calendar_maintenance_color',
|
||||
'calendar_maintenance_display',
|
||||
'calendar_slot3_file',
|
||||
'calendar_slot3_caldav_url',
|
||||
'calendar_slot3_username',
|
||||
'calendar_slot3_password',
|
||||
'calendar_slot3_color',
|
||||
'calendar_slot3_display',
|
||||
'calendar_slot4_file',
|
||||
'calendar_slot4_caldav_url',
|
||||
'calendar_slot4_username',
|
||||
'calendar_slot4_password',
|
||||
'calendar_slot4_color',
|
||||
'calendar_slot4_display',
|
||||
'pagelink_search_depth',
|
||||
];
|
||||
|
||||
@@ -119,6 +123,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
$newConf['calendar_' . $slot . '_username'] = trim($INPUT->str('calendar_' . $slot . '_username'));
|
||||
$newConf['calendar_' . $slot . '_password'] = trim($INPUT->str('calendar_' . $slot . '_password'));
|
||||
$newConf['calendar_' . $slot . '_color'] = trim($INPUT->str('calendar_' . $slot . '_color'));
|
||||
$newConf['calendar_' . $slot . '_display'] = trim($INPUT->str('calendar_' . $slot . '_display'));
|
||||
}
|
||||
|
||||
$depth = (int)$INPUT->int('pagelink_search_depth');
|
||||
@@ -274,6 +279,13 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
'slot3' => 'Slot 3',
|
||||
'slot4' => 'Slot 4',
|
||||
];
|
||||
$displayOptions = [
|
||||
'none' => (string)$this->getLang('calendar_slot_display_none'),
|
||||
'top-left' => (string)$this->getLang('calendar_slot_display_top_left'),
|
||||
'top-right' => (string)$this->getLang('calendar_slot_display_top_right'),
|
||||
'bottom-left' => (string)$this->getLang('calendar_slot_display_bottom_left'),
|
||||
'bottom-right' => (string)$this->getLang('calendar_slot_display_bottom_right'),
|
||||
];
|
||||
foreach ($this->calendarSlotKeys as $slot) {
|
||||
echo '<h2>' . hsc($this->getLang('calendar_slot_heading') . ': ' . $slotLabels[$slot]) . '</h2>';
|
||||
|
||||
@@ -298,6 +310,18 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_color')) . '</span> ';
|
||||
echo '<input type="color" name="' . hsc($prefix . 'color') . '" value="' . hsc((string)$this->getConf($prefix . 'color') ?: '#999999') . '" />';
|
||||
echo '</label><br />';
|
||||
|
||||
$currentDisplay = (string)$this->getConf($prefix . 'display');
|
||||
if ($currentDisplay === '') $currentDisplay = 'none';
|
||||
echo '<label class="block"><span>' . hsc($this->getLang('calendar_slot_display')) . '</span> ';
|
||||
echo '<select name="' . hsc($prefix . 'display') . '" class="edit">';
|
||||
foreach ($displayOptions as $value => $label) {
|
||||
if ($label === '') $label = $value;
|
||||
$selected = ($currentDisplay === $value) ? ' selected="selected"' : '';
|
||||
echo '<option value="' . hsc($value) . '"' . $selected . '>' . hsc($label) . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
echo '</label><br />';
|
||||
}
|
||||
|
||||
// CalDAV Sync button (outside the save form, separate action)
|
||||
|
||||
@@ -38,30 +38,34 @@ $conf['open_service_url'] = 'http://127.0.0.1:8765';
|
||||
$conf['image_base_path'] = '';
|
||||
|
||||
// Calendar slot configuration (4 slots: general, maintenance, slot3, slot4)
|
||||
// Each slot has: file, caldav_url, username, password, color
|
||||
// Each slot has: file, caldav_url, username, password, color, display
|
||||
$conf['calendar_general_file'] = '';
|
||||
$conf['calendar_general_caldav_url'] = '';
|
||||
$conf['calendar_general_username'] = '';
|
||||
$conf['calendar_general_password'] = '';
|
||||
$conf['calendar_general_color'] = '#4a90d9';
|
||||
$conf['calendar_general_display'] = 'none';
|
||||
|
||||
$conf['calendar_maintenance_file'] = '';
|
||||
$conf['calendar_maintenance_caldav_url'] = '';
|
||||
$conf['calendar_maintenance_username'] = '';
|
||||
$conf['calendar_maintenance_password'] = '';
|
||||
$conf['calendar_maintenance_color'] = '#e67e22';
|
||||
$conf['calendar_maintenance_display'] = 'none';
|
||||
|
||||
$conf['calendar_slot3_file'] = '';
|
||||
$conf['calendar_slot3_caldav_url'] = '';
|
||||
$conf['calendar_slot3_username'] = '';
|
||||
$conf['calendar_slot3_password'] = '';
|
||||
$conf['calendar_slot3_color'] = '#27ae60';
|
||||
$conf['calendar_slot3_display'] = 'none';
|
||||
|
||||
$conf['calendar_slot4_file'] = '';
|
||||
$conf['calendar_slot4_caldav_url'] = '';
|
||||
$conf['calendar_slot4_username'] = '';
|
||||
$conf['calendar_slot4_password'] = '';
|
||||
$conf['calendar_slot4_color'] = '#8e44ad';
|
||||
$conf['calendar_slot4_display'] = 'none';
|
||||
|
||||
// Maximum depth when searching for .pagelink files under allowed roots.
|
||||
$conf['pagelink_search_depth'] = 3;
|
||||
|
||||
@@ -66,12 +66,12 @@
|
||||
html += '<div class="luxtools-event-popup-field">';
|
||||
if (allDay) {
|
||||
html += '<strong>Date:</strong> ' + formatDate(start);
|
||||
if (end) {
|
||||
if (end && !isSameMoment(start, end)) {
|
||||
html += ' – ' + formatDate(end);
|
||||
}
|
||||
} else {
|
||||
html += '<strong>Time:</strong> ' + formatDateTime(start);
|
||||
if (end) {
|
||||
if (end && !isSameMoment(start, end)) {
|
||||
html += ' – ' + formatDateTime(end);
|
||||
}
|
||||
}
|
||||
@@ -114,14 +114,23 @@
|
||||
if (!isoStr) return '';
|
||||
var d = new Date(isoStr);
|
||||
if (isNaN(d.getTime())) return isoStr;
|
||||
return d.toLocaleDateString();
|
||||
return pad2(d.getDate()) + '.' + pad2(d.getMonth() + 1) + '.' + d.getFullYear();
|
||||
}
|
||||
|
||||
function formatDateTime(isoStr) {
|
||||
if (!isoStr) return '';
|
||||
var d = new Date(isoStr);
|
||||
if (isNaN(d.getTime())) return isoStr;
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
|
||||
return formatDate(isoStr) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes());
|
||||
}
|
||||
|
||||
function isSameMoment(left, right) {
|
||||
if (!left || !right) return false;
|
||||
return left === right;
|
||||
}
|
||||
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -198,11 +207,9 @@
|
||||
// Visual feedback: mark item as done or revert
|
||||
if (action === 'complete') {
|
||||
item.classList.add('luxtools-task-completed');
|
||||
// Fade out and remove after a short delay
|
||||
item.style.opacity = '0.5';
|
||||
setTimeout(function () {
|
||||
item.style.display = 'none';
|
||||
}, 1000);
|
||||
button.textContent = 'Reopen';
|
||||
button.setAttribute('data-action', 'reopen');
|
||||
button.disabled = false;
|
||||
} else {
|
||||
item.classList.remove('luxtools-task-completed');
|
||||
item.style.opacity = '1';
|
||||
|
||||
@@ -86,9 +86,10 @@
|
||||
|
||||
var formatter;
|
||||
try {
|
||||
formatter = new Intl.DateTimeFormat(undefined, {
|
||||
formatter = new Intl.DateTimeFormat('de-DE', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
} catch (e) {
|
||||
formatter = null;
|
||||
|
||||
@@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV-URL";
|
||||
$lang["calendar_slot_username"] = "Benutzername";
|
||||
$lang["calendar_slot_password"] = "Passwort";
|
||||
$lang["calendar_slot_color"] = "Anzeigefarbe";
|
||||
$lang["calendar_slot_display"] = "Anzeige";
|
||||
$lang["calendar_slot_display_none"] = "Keine";
|
||||
$lang["calendar_slot_display_top_left"] = "Oben links";
|
||||
$lang["calendar_slot_display_top_right"] = "Oben rechts";
|
||||
$lang["calendar_slot_display_bottom_left"] = "Unten links";
|
||||
$lang["calendar_slot_display_bottom_right"] = "Unten rechts";
|
||||
$lang["pagelink_search_depth"] =
|
||||
"Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root).";
|
||||
|
||||
|
||||
@@ -70,6 +70,12 @@ $lang["calendar_slot_caldav_url"] = "CalDAV URL";
|
||||
$lang["calendar_slot_username"] = "Username";
|
||||
$lang["calendar_slot_password"] = "Password";
|
||||
$lang["calendar_slot_color"] = "Display color";
|
||||
$lang["calendar_slot_display"] = "Display";
|
||||
$lang["calendar_slot_display_none"] = "None";
|
||||
$lang["calendar_slot_display_top_left"] = "Top Left";
|
||||
$lang["calendar_slot_display_top_right"] = "Top Right";
|
||||
$lang["calendar_slot_display_bottom_left"] = "Bottom Left";
|
||||
$lang["calendar_slot_display_bottom_right"] = "Bottom Right";
|
||||
$lang["pagelink_search_depth"] =
|
||||
"Maximum directory depth for .pagelink search (0 = only root).";
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ class CalDavClient
|
||||
/** @var int HTTP timeout in seconds */
|
||||
protected const TIMEOUT = 30;
|
||||
|
||||
/** @var string Last request error message for diagnostics */
|
||||
protected static string $lastRequestError = '';
|
||||
|
||||
/**
|
||||
* Update the STATUS of a specific event or task on the remote CalDAV server.
|
||||
*
|
||||
@@ -33,7 +36,7 @@ class CalDavClient
|
||||
* @param string $recurrenceId Recurrence ID (empty for non-recurring)
|
||||
* @param string $newStatus New status value (e.g. COMPLETED, TODO)
|
||||
* @param string $dateIso Occurrence date YYYY-MM-DD
|
||||
* @return bool True if the remote update succeeded
|
||||
* @return string Empty string on success, error message on failure
|
||||
*/
|
||||
public static function updateEventStatus(
|
||||
string $caldavUrl,
|
||||
@@ -43,13 +46,17 @@ class CalDavClient
|
||||
string $recurrenceId,
|
||||
string $newStatus,
|
||||
string $dateIso
|
||||
): bool {
|
||||
if ($caldavUrl === '' || $uid === '') return false;
|
||||
): string {
|
||||
if ($caldavUrl === '' || $uid === '') return 'Missing CalDAV URL or UID';
|
||||
|
||||
try {
|
||||
// Find the calendar object href for this UID via REPORT
|
||||
$objectInfo = self::findObjectByUid($caldavUrl, $username, $password, $uid);
|
||||
if ($objectInfo === null) return false;
|
||||
if ($objectInfo === null) {
|
||||
$msg = "CalDAV: Could not find object with UID '$uid' on server";
|
||||
dbglog($msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
$objectHref = $objectInfo['href'];
|
||||
$etag = $objectInfo['etag'];
|
||||
@@ -57,19 +64,35 @@ class CalDavClient
|
||||
|
||||
// Parse and update the status
|
||||
$calendar = Reader::read($calendarData, Reader::OPTION_FORGIVING);
|
||||
if (!($calendar instanceof VCalendar)) return false;
|
||||
if (!($calendar instanceof VCalendar)) {
|
||||
$msg = "CalDAV: Failed to parse calendar data for UID '$uid'";
|
||||
dbglog($msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
$updated = IcsWriter::applyStatusUpdateToCalendar(
|
||||
$calendar, $uid, $recurrenceId, $newStatus, $dateIso
|
||||
);
|
||||
if (!$updated) return false;
|
||||
if (!$updated) {
|
||||
$msg = "CalDAV: applyStatusUpdateToCalendar failed for UID '$uid'";
|
||||
dbglog($msg);
|
||||
return $msg;
|
||||
}
|
||||
|
||||
$newData = $calendar->serialize();
|
||||
|
||||
// PUT the updated object back with If-Match for conflict detection
|
||||
return self::putCalendarObject($objectHref, $username, $password, $newData, $etag);
|
||||
$putError = self::putCalendarObject($objectHref, $username, $password, $newData, $etag);
|
||||
if ($putError !== '') {
|
||||
dbglog($putError);
|
||||
return $putError;
|
||||
}
|
||||
|
||||
return '';
|
||||
} catch (Throwable $e) {
|
||||
return false;
|
||||
$msg = 'CalDAV: Exception during updateEventStatus: ' . $e->getMessage();
|
||||
dbglog($msg);
|
||||
return $msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +266,7 @@ class CalDavClient
|
||||
* @param string $password
|
||||
* @param string $data ICS data to write
|
||||
* @param string $etag ETag for If-Match header (empty to skip)
|
||||
* @return bool
|
||||
* @return string Empty string on success, error message on failure
|
||||
*/
|
||||
protected static function putCalendarObject(
|
||||
string $href,
|
||||
@@ -251,7 +274,7 @@ class CalDavClient
|
||||
string $password,
|
||||
string $data,
|
||||
string $etag
|
||||
): bool {
|
||||
): string {
|
||||
$headers = [
|
||||
'Content-Type: text/calendar; charset=utf-8',
|
||||
];
|
||||
@@ -260,9 +283,10 @@ class CalDavClient
|
||||
}
|
||||
|
||||
$response = self::request('PUT', $href, $username, $password, $data, $headers);
|
||||
// PUT returns null body but we check by HTTP status via the request method
|
||||
// A successful PUT returns 2xx
|
||||
return $response !== null;
|
||||
if ($response === null) {
|
||||
return self::$lastRequestError ?: 'CalDAV PUT failed (unknown error)';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,8 +317,6 @@ class CalDavClient
|
||||
|
||||
$etags = $resp->xpath('.//d:getetag');
|
||||
$etag = ($etags && count($etags) > 0) ? trim((string)$etags[0]) : '';
|
||||
// Strip surrounding quotes from etag if present
|
||||
$etag = trim($etag, '"');
|
||||
|
||||
$caldata = $resp->xpath('.//cal:calendar-data');
|
||||
$data = ($caldata && count($caldata) > 0) ? trim((string)$caldata[0]) : '';
|
||||
@@ -392,10 +414,18 @@ class CalDavClient
|
||||
string $body = '',
|
||||
array $headers = []
|
||||
): ?string {
|
||||
if (!function_exists('curl_init')) return null;
|
||||
self::$lastRequestError = '';
|
||||
|
||||
if (!function_exists('curl_init')) {
|
||||
self::$lastRequestError = 'CalDAV: curl extension not available';
|
||||
return null;
|
||||
}
|
||||
|
||||
$ch = curl_init();
|
||||
if ($ch === false) return null;
|
||||
if ($ch === false) {
|
||||
self::$lastRequestError = 'CalDAV: curl_init() failed';
|
||||
return null;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
@@ -423,9 +453,13 @@ class CalDavClient
|
||||
// Capture HTTP status code
|
||||
$responseBody = curl_exec($ch);
|
||||
$httpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if (!is_string($responseBody)) return null;
|
||||
if (!is_string($responseBody)) {
|
||||
self::$lastRequestError = "CalDAV $method failed: curl error: $curlError";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Accept 2xx and 207 (multistatus) responses
|
||||
if ($httpCode >= 200 && $httpCode < 300) {
|
||||
@@ -435,6 +469,7 @@ class CalDavClient
|
||||
return $responseBody;
|
||||
}
|
||||
|
||||
self::$lastRequestError = "CalDAV $method failed: HTTP $httpCode";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,17 @@ namespace dokuwiki\plugin\luxtools;
|
||||
* Represents one calendar slot configuration.
|
||||
*
|
||||
* Each slot has a stable key, a human-readable label, local/remote source
|
||||
* configuration, a display color, and a derived enabled state.
|
||||
* configuration, a display color, an optional widget indicator position,
|
||||
* and a derived enabled state.
|
||||
*/
|
||||
class CalendarSlot
|
||||
{
|
||||
/** @var string[] Ordered list of all supported slot keys */
|
||||
public const SLOT_KEYS = ['general', 'maintenance', 'slot3', 'slot4'];
|
||||
|
||||
/** @var string[] Allowed widget indicator display positions */
|
||||
public const INDICATOR_DISPLAYS = ['none', 'top-left', 'top-right', 'bottom-left', 'bottom-right'];
|
||||
|
||||
/** @var array<string,string> Human-readable labels for slot keys */
|
||||
public const SLOT_LABELS = [
|
||||
'general' => 'General',
|
||||
@@ -42,6 +46,9 @@ class CalendarSlot
|
||||
/** @var string CSS color for widget indicators */
|
||||
protected $color;
|
||||
|
||||
/** @var string Widget indicator display position */
|
||||
protected $display;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param string $file
|
||||
@@ -49,6 +56,7 @@ class CalendarSlot
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param string $color
|
||||
* @param string $display
|
||||
*/
|
||||
public function __construct(
|
||||
string $key,
|
||||
@@ -56,7 +64,8 @@ class CalendarSlot
|
||||
string $caldavUrl = '',
|
||||
string $username = '',
|
||||
string $password = '',
|
||||
string $color = ''
|
||||
string $color = '',
|
||||
string $display = 'none'
|
||||
) {
|
||||
$this->key = $key;
|
||||
$this->label = self::SLOT_LABELS[$key] ?? $key;
|
||||
@@ -65,6 +74,7 @@ class CalendarSlot
|
||||
$this->username = trim($username);
|
||||
$this->password = trim($password);
|
||||
$this->color = trim($color);
|
||||
$this->display = self::normalizeIndicatorDisplay($display);
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
@@ -102,6 +112,16 @@ class CalendarSlot
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function getDisplay(): string
|
||||
{
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
public function shouldDisplayIndicator(): bool
|
||||
{
|
||||
return $this->display !== 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* A slot is enabled if it has a local file path or a CalDAV URL.
|
||||
*
|
||||
@@ -149,7 +169,8 @@ class CalendarSlot
|
||||
(string)$plugin->getConf('calendar_' . $key . '_caldav_url'),
|
||||
(string)$plugin->getConf('calendar_' . $key . '_username'),
|
||||
(string)$plugin->getConf('calendar_' . $key . '_password'),
|
||||
(string)$plugin->getConf('calendar_' . $key . '_color')
|
||||
(string)$plugin->getConf('calendar_' . $key . '_color'),
|
||||
(string)$plugin->getConf('calendar_' . $key . '_display')
|
||||
);
|
||||
}
|
||||
return $slots;
|
||||
@@ -168,4 +189,14 @@ class CalendarSlot
|
||||
return $slot->isEnabled();
|
||||
});
|
||||
}
|
||||
|
||||
protected static function normalizeIndicatorDisplay(string $display): string
|
||||
{
|
||||
$display = strtolower(trim($display));
|
||||
$display = str_replace(['_', ' '], '-', $display);
|
||||
if (!in_array($display, self::INDICATOR_DISPLAYS, true)) {
|
||||
return 'none';
|
||||
}
|
||||
return $display;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class ChronologicalCalendarWidget
|
||||
* @param string $baseNs
|
||||
* @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
|
||||
* @return string
|
||||
*/
|
||||
public static function render(
|
||||
@@ -22,7 +23,8 @@ class ChronologicalCalendarWidget
|
||||
int $month,
|
||||
string $baseNs = 'chronological',
|
||||
array $indicators = [],
|
||||
array $slotColors = []
|
||||
array $slotColors = [],
|
||||
array $slotDisplays = []
|
||||
): string {
|
||||
if (!self::isValidMonth($year, $month)) return '';
|
||||
|
||||
@@ -120,13 +122,19 @@ class ChronologicalCalendarWidget
|
||||
// Render slot indicators if any
|
||||
$dayIndicators = $indicators[$date] ?? [];
|
||||
if ($dayIndicators !== []) {
|
||||
$html .= '<div class="luxtools-calendar-indicators">';
|
||||
$indicatorHtml = '';
|
||||
foreach ($dayIndicators as $slotKey) {
|
||||
$display = $slotDisplays[$slotKey] ?? 'none';
|
||||
if ($display === 'none') continue;
|
||||
|
||||
$color = $slotColors[$slotKey] ?? '';
|
||||
$style = ($color !== '') ? ' style="background-color:' . hsc($color) . '"' : '';
|
||||
$html .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($slotKey) . '"' . $style . '></span>';
|
||||
$indicatorHtml .= '<span class="luxtools-calendar-indicator luxtools-indicator-' . hsc($display) . '"' . $style . '></span>';
|
||||
}
|
||||
|
||||
if ($indicatorHtml !== '') {
|
||||
$html .= '<div class="luxtools-calendar-indicators">' . $indicatorHtml . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
if ($dayId !== null && function_exists('html_wikilink')) {
|
||||
|
||||
33
style.css
33
style.css
@@ -668,29 +668,32 @@ div.luxtools-calendar td.luxtools-calendar-day {
|
||||
|
||||
.luxtools-calendar-indicator {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.luxtools-indicator-general {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
.luxtools-indicator-top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip-path: polygon(0 0, 100% 0, 0 100%);
|
||||
}
|
||||
|
||||
.luxtools-indicator-maintenance {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
.luxtools-indicator-top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
clip-path: polygon(0 0, 100% 0, 100% 100%);
|
||||
}
|
||||
|
||||
.luxtools-indicator-slot3 {
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
.luxtools-indicator-bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
clip-path: polygon(100% 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.luxtools-indicator-slot4 {
|
||||
bottom: 2px;
|
||||
left: 2px;
|
||||
.luxtools-indicator-bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
clip-path: polygon(0 0, 0 100%, 100% 100%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -91,14 +91,16 @@ class syntax_plugin_luxtools_calendar extends SyntaxPlugin
|
||||
$slots = CalendarSlot::loadEnabled($this);
|
||||
$indicators = CalendarService::monthIndicators($slots, $year, $month);
|
||||
$slotColors = [];
|
||||
$slotDisplays = [];
|
||||
foreach ($slots as $slot) {
|
||||
$color = $slot->getColor();
|
||||
if ($color !== '') {
|
||||
$slotColors[$slot->getKey()] = $color;
|
||||
}
|
||||
$slotDisplays[$slot->getKey()] = $slot->getDisplay();
|
||||
}
|
||||
|
||||
$renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors);
|
||||
$renderer->doc .= ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user