From a3f021e5e168e145119facbe618e5463bfae5ada Mon Sep 17 00:00:00 2001 From: luxick Date: Fri, 20 Mar 2026 07:56:41 +0100 Subject: [PATCH] Unify dialog infrastructure --- action.php | 1211 +++++++++++++++++++++++++++++--------------- dialog.css | 62 +++ js/dialog.js | 95 ++++ js/event-popup.js | 1236 +++++++++++++++++++++++++++------------------ js/main.js | 330 +++++++----- style.css | 263 +++++----- 6 files changed, 2047 insertions(+), 1150 deletions(-) create mode 100644 dialog.css create mode 100644 js/dialog.js diff --git a/action.php b/action.php index b864ac4..f6058fb 100644 --- a/action.php +++ b/action.php @@ -15,7 +15,7 @@ use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker; use dokuwiki\plugin\luxtools\ChronologicalDayTemplate; use dokuwiki\plugin\luxtools\IcsWriter; use dokuwiki\plugin\luxtools\MenuItem\InvalidateCache; -require_once(__DIR__ . '/autoload.php'); +require_once __DIR__ . "/autoload.php"; /** * luxtools action plugin: register JS assets. @@ -34,6 +34,12 @@ class action_plugin_luxtools extends ActionPlugin $this, "addScripts", ); + $controller->register_hook( + "CSS_STYLES_INCLUDED", + "BEFORE", + $this, + "addDialogCss", + ); $controller->register_hook( "DOKUWIKI_STARTED", "AFTER", @@ -140,6 +146,7 @@ class action_plugin_luxtools extends ActionPlugin "page-link.js", "linkfavicon.js", "calendar-widget.js", + "dialog.js", "event-popup.js", "movie-import.js", "main.js", @@ -153,6 +160,27 @@ class action_plugin_luxtools extends ActionPlugin } } + /** + * Register dialog.css with DokuWiki's CSS dispatcher so that + * @ini_* variables are resolved and the file is properly cached. + */ + public function addDialogCss(Event $event, $param) + { + if (($event->data["mediatype"] ?? "") !== "screen") { + return; + } + + $plugin = $this->getPluginName(); + $file = DOKU_PLUGIN . "$plugin/dialog.css"; + $location = DOKU_BASE . "lib/plugins/$plugin/"; + + if (!file_exists($file)) { + return; + } + + $event->data["files"][$file] = $location; + } + /** * Pass plugin data to client-side JavaScript via JSINFO. * @@ -167,7 +195,9 @@ class action_plugin_luxtools extends ActionPlugin { // Intentional: the key is exposed to the browser for direct OMDb lookups. global $JSINFO; - $JSINFO['luxtools_omdb_apikey'] = (string)$this->getConf('omdb_apikey'); + $JSINFO["luxtools_omdb_apikey"] = (string) $this->getConf( + "omdb_apikey", + ); } /** @@ -179,25 +209,31 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleCalendarWidgetAjax(Event $event, $param) { - if ($event->data !== 'luxtools_calendar_month') return; + if ($event->data !== "luxtools_calendar_month") { + return; + } $event->preventDefault(); $event->stopPropagation(); global $INPUT; - $year = (int)$INPUT->int('year'); - $month = (int)$INPUT->int('month'); - $baseNs = trim((string)$INPUT->str('base')); - if ($baseNs === '') { - $baseNs = 'chronological'; + $year = (int) $INPUT->int("year"); + $month = (int) $INPUT->int("month"); + $baseNs = trim((string) $INPUT->str("base")); + if ($baseNs === "") { + $baseNs = "chronological"; } - $size = ChronologicalCalendarWidget::normalizeSize((string)$INPUT->str('size')); - $showTimes = ChronologicalCalendarWidget::normalizeShowTimes($INPUT->str('show_times')); + $size = ChronologicalCalendarWidget::normalizeSize( + (string) $INPUT->str("size"), + ); + $showTimes = ChronologicalCalendarWidget::normalizeShowTimes( + $INPUT->str("show_times"), + ); if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) { http_status(400); - echo 'Invalid month'; + echo "Invalid month"; return; } @@ -207,36 +243,52 @@ class action_plugin_luxtools extends ActionPlugin $widgetSlots = CalendarSlot::filterWidgetVisible($slots); $indicators = []; $dayEvents = []; - if ($size === 'large') { - $widgetData = CalendarService::monthWidgetData($widgetSlots, $year, $month); - $indicators = $widgetData['indicators']; - $dayEvents = $widgetData['events']; + if ($size === "large") { + $widgetData = CalendarService::monthWidgetData( + $widgetSlots, + $year, + $month, + ); + $indicators = $widgetData["indicators"]; + $dayEvents = $widgetData["events"]; } else { - $indicators = CalendarService::monthIndicators($widgetSlots, $year, $month); + $indicators = CalendarService::monthIndicators( + $widgetSlots, + $year, + $month, + ); } $slotColors = []; $slotDisplays = []; foreach ($widgetSlots as $slot) { $color = $slot->getColor(); - if ($color !== '') { + if ($color !== "") { $slotColors[$slot->getKey()] = $color; } $slotDisplays[$slot->getKey()] = $slot->getDisplay(); } - $html = ChronologicalCalendarWidget::render($year, $month, $baseNs, $indicators, $slotColors, $slotDisplays, [ - 'size' => $size, - 'showTimes' => $showTimes, - 'dayEvents' => $dayEvents, - ]); - if ($html === '') { + $html = ChronologicalCalendarWidget::render( + $year, + $month, + $baseNs, + $indicators, + $slotColors, + $slotDisplays, + [ + "size" => $size, + "showTimes" => $showTimes, + "dayEvents" => $dayEvents, + ], + ); + if ($html === "") { http_status(500); - echo 'Calendar rendering failed'; + echo "Calendar rendering failed"; return; } - header('Content-Type: text/html; charset=utf-8'); + header("Content-Type: text/html; charset=utf-8"); echo $html; } @@ -251,14 +303,22 @@ class action_plugin_luxtools extends ActionPlugin */ public function autoLinkChronologicalDates(Event $event, $param) { - if (!is_array($event->data)) return; + if (!is_array($event->data)) { + return; + } - $mode = (string)($event->data[0] ?? ''); - if ($mode !== 'xhtml') return; + $mode = (string) ($event->data[0] ?? ""); + if ($mode !== "xhtml") { + return; + } $doc = $event->data[1] ?? null; - if (!is_string($doc) || $doc === '') return; - if (!preg_match('/\d{4}-\d{2}-\d{2}/', $doc)) return; + if (!is_string($doc) || $doc === "") { + return; + } + if (!preg_match("/\d{4}-\d{2}-\d{2}/", $doc)) { + return; + } $event->data[1] = ChronologicalDateAutoLinker::linkHtml($doc); } @@ -272,23 +332,33 @@ class action_plugin_luxtools extends ActionPlugin */ public function prefillChronologicalDayTemplate(Event $event, $param) { - if (!is_array($event->data)) return; - - $id = (string)($event->data['id'] ?? ''); - if ($id === '') return; - - if (function_exists('cleanID')) { - $id = (string)cleanID($id); + if (!is_array($event->data)) { + return; + } + + $id = (string) ($event->data["id"] ?? ""); + if ($id === "") { + return; + } + + if (function_exists("cleanID")) { + $id = (string) cleanID($id); + } + if ($id === "") { + return; + } + if (!ChronoID::isDayId($id)) { + return; } - if ($id === '') return; - if (!ChronoID::isDayId($id)) return; $template = ChronologicalDayTemplate::buildForDayId($id); - if ($template === null || $template === '') return; + if ($template === null || $template === "") { + return; + } - $event->data['tpl'] = $template; - $event->data['tplfile'] = ''; - $event->data['doreplace'] = false; + $event->data["tpl"] = $template; + $event->data["tplfile"] = ""; + $event->data["doreplace"] = false; } /** @@ -300,40 +370,71 @@ class action_plugin_luxtools extends ActionPlugin */ public function appendChronologicalDayPhotos(Event $event, $param) { - if (self::$internalRenderInProgress) return; - if (!is_array($event->data)) return; + if (self::$internalRenderInProgress) { + return; + } + if (!is_array($event->data)) { + return; + } - $mode = (string)($event->data[0] ?? ''); - if ($mode !== 'xhtml') return; + $mode = (string) ($event->data[0] ?? ""); + if ($mode !== "xhtml") { + return; + } global $ACT; - if (!is_string($ACT) || $ACT !== 'show') return; + if (!is_string($ACT) || $ACT !== "show") { + return; + } $doc = $event->data[1] ?? null; - if (!is_string($doc)) return; - if (str_contains($doc, 'luxtools-chronological-photos')) return; + if (!is_string($doc)) { + return; + } + if (str_contains($doc, "luxtools-chronological-photos")) { + return; + } global $ID; - $id = is_string($ID) ? $ID : ''; - if ($id === '') return; - if (function_exists('cleanID')) { - $id = (string)cleanID($id); + $id = is_string($ID) ? $ID : ""; + if ($id === "") { + return; + } + if (function_exists("cleanID")) { + $id = (string) cleanID($id); + } + if ($id === "") { + return; } - if ($id === '') return; $parts = ChronoID::parseDayId($id); - if ($parts === null) return; + if ($parts === null) { + return; + } - if (!function_exists('page_exists') || !page_exists($id)) return; + if (!function_exists("page_exists") || !page_exists($id)) { + return; + } - $basePath = trim((string)$this->getConf('image_base_path')); - if ($basePath === '') return; + $basePath = trim((string) $this->getConf("image_base_path")); + if ($basePath === "") { + return; + } - $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); - if (!$this->hasAnyChronologicalPhotos($dateIso)) return; + $dateIso = sprintf( + "%04d-%02d-%02d", + $parts["year"], + $parts["month"], + $parts["day"], + ); + if (!$this->hasAnyChronologicalPhotos($dateIso)) { + return; + } $photosHtml = $this->renderChronologicalPhotosMacro($dateIso); - if ($photosHtml === '') return; + if ($photosHtml === "") { + return; + } $event->data[1] = $doc . $photosHtml; } @@ -348,41 +449,70 @@ class action_plugin_luxtools extends ActionPlugin public function appendChronologicalDayEvents(Event $event, $param) { static $appendInProgress = false; - if ($appendInProgress) return; - if (self::$internalRenderInProgress) return; + if ($appendInProgress) { + return; + } + if (self::$internalRenderInProgress) { + return; + } - if (!is_array($event->data)) return; + if (!is_array($event->data)) { + return; + } - $mode = (string)($event->data[0] ?? ''); - if ($mode !== 'xhtml') return; + $mode = (string) ($event->data[0] ?? ""); + if ($mode !== "xhtml") { + return; + } global $ACT; - if (!is_string($ACT) || $ACT !== 'show') return; + if (!is_string($ACT) || $ACT !== "show") { + return; + } $doc = $event->data[1] ?? null; - if (!is_string($doc)) return; - if (str_contains($doc, 'luxtools-chronological-events')) return; + if (!is_string($doc)) { + return; + } + if (str_contains($doc, "luxtools-chronological-events")) { + return; + } global $ID; - $id = is_string($ID) ? $ID : ''; - if ($id === '') return; - if (function_exists('cleanID')) { - $id = (string)cleanID($id); + $id = is_string($ID) ? $ID : ""; + if ($id === "") { + return; + } + if (function_exists("cleanID")) { + $id = (string) cleanID($id); + } + if ($id === "") { + return; } - if ($id === '') return; $parts = ChronoID::parseDayId($id); - if ($parts === null) return; - if (!function_exists('page_exists') || !page_exists($id)) return; + if ($parts === null) { + return; + } + if (!function_exists("page_exists") || !page_exists($id)) { + return; + } - $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); + $dateIso = sprintf( + "%04d-%02d-%02d", + $parts["year"], + $parts["month"], + $parts["day"], + ); $appendInProgress = true; try { $eventsHtml = $this->renderChronologicalEventsHtml($dateIso); } finally { $appendInProgress = false; } - if ($eventsHtml === '') return; + if ($eventsHtml === "") { + return; + } $event->data[1] = $doc . $eventsHtml; } @@ -396,28 +526,38 @@ class action_plugin_luxtools extends ActionPlugin protected function renderChronologicalPhotosMacro(string $dateIso): string { $syntax = $this->buildChronologicalImagesSyntax($dateIso); - if ($syntax === '') return ''; + if ($syntax === "") { + return ""; + } - if (self::$internalRenderInProgress) return ''; + if (self::$internalRenderInProgress) { + return ""; + } self::$internalRenderInProgress = true; try { - $info = ['cache' => false]; + $info = ["cache" => false]; $instructions = p_get_instructions($syntax); - $galleryHtml = (string)p_render('xhtml', $instructions, $info); + $galleryHtml = (string) p_render("xhtml", $instructions, $info); } finally { self::$internalRenderInProgress = false; } - if ($galleryHtml === '') return ''; + if ($galleryHtml === "") { + return ""; + } - $title = (string)$this->getLang('chronological_photos_title'); - if ($title === '') $title = 'Photos'; + $title = (string) $this->getLang("chronological_photos_title"); + if ($title === "") { + $title = "Photos"; + } - return '
' - . '

' . hsc($title) . '

' - . $galleryHtml - . '
'; + return '
' . + "

" . + hsc($title) . + "

" . + $galleryHtml . + "
"; } /** @@ -428,16 +568,21 @@ class action_plugin_luxtools extends ActionPlugin */ protected function buildChronologicalImagesSyntax(string $dateIso): string { - $basePath = trim((string)$this->getConf('image_base_path')); - if ($basePath === '') return ''; + $basePath = trim((string) $this->getConf("image_base_path")); + if ($basePath === "") { + return ""; + } $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath); - if (!is_dir($base) || !is_readable($base)) return ''; + if (!is_dir($base) || !is_readable($base)) { + return ""; + } - $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/'; - $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base; + $yearDir = rtrim($base, "/") . "/" . substr($dateIso, 0, 4) . "/"; + $targetDir = + is_dir($yearDir) && is_readable($yearDir) ? $yearDir : $base; - return '{{images>' . $targetDir . $dateIso . '*&recursive=0}}'; + return "{{images>" . $targetDir . $dateIso . "*&recursive=0}}"; } /** @@ -451,54 +596,80 @@ class action_plugin_luxtools extends ActionPlugin */ public function renderVirtualChronologicalDayPage(Event $event, $param) { - if (!is_string($event->data) || $event->data !== 'show') return; + if (!is_string($event->data) || $event->data !== "show") { + return; + } global $ID; - $id = is_string($ID) ? $ID : ''; - if ($id === '') return; - if (function_exists('cleanID')) { - $id = (string)cleanID($id); + $id = is_string($ID) ? $ID : ""; + if ($id === "") { + return; + } + if (function_exists("cleanID")) { + $id = (string) cleanID($id); + } + if ($id === "") { + return; } - if ($id === '') return; - if (!ChronoID::isDayId($id)) return; + if (!ChronoID::isDayId($id)) { + return; + } $this->sendNoStoreHeaders(); - if (function_exists('page_exists') && page_exists($id)) return; + if (function_exists("page_exists") && page_exists($id)) { + return; + } - $wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? ''; - if ($wikiText === '') return; + $wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? ""; + if ($wikiText === "") { + return; + } $parts = ChronoID::parseDayId($id); - $extraHtml = ''; + $extraHtml = ""; if ($parts !== null) { - $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); + $dateIso = sprintf( + "%04d-%02d-%02d", + $parts["year"], + $parts["month"], + $parts["day"], + ); $eventsHtml = $this->renderChronologicalEventsHtml($dateIso); - if ($eventsHtml !== '') { + if ($eventsHtml !== "") { $extraHtml .= $eventsHtml; } if ($this->hasAnyChronologicalPhotos($dateIso)) { $photosHtml = $this->renderChronologicalPhotosMacro($dateIso); - if ($photosHtml !== '') { + if ($photosHtml !== "") { $extraHtml .= $photosHtml; } } } - $editUrl = function_exists('wl') ? (string)wl($id, ['do' => 'edit']) : ''; - $createLinkHtml = ''; - if ($editUrl !== '') { - $label = (string)$this->getLang('btn_create'); - if ($label === '') $label = 'Create this page'; - $createLinkHtml = '

✎ ' . hsc($label) . '

'; + $editUrl = function_exists("wl") + ? (string) wl($id, ["do" => "edit"]) + : ""; + $createLinkHtml = ""; + if ($editUrl !== "") { + $label = (string) $this->getLang("btn_create"); + if ($label === "") { + $label = "Create this page"; + } + $createLinkHtml = + '

✎ ' . + hsc($label) . + "

"; } - $info = ['cache' => false]; + $info = ["cache" => false]; $instructions = p_get_instructions($wikiText); - $html = (string)p_render('xhtml', $instructions, $info); + $html = (string) p_render("xhtml", $instructions, $info); echo $html . $createLinkHtml . $extraHtml; $event->preventDefault(); @@ -513,23 +684,32 @@ class action_plugin_luxtools extends ActionPlugin */ protected function hasAnyChronologicalPhotos(string $dateIso): bool { - if (!ChronoID::isIsoDate($dateIso)) return false; + if (!ChronoID::isIsoDate($dateIso)) { + return false; + } - $basePath = trim((string)$this->getConf('image_base_path')); - if ($basePath === '') return false; + $basePath = trim((string) $this->getConf("image_base_path")); + if ($basePath === "") { + return false; + } $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath); - if (!is_dir($base) || !is_readable($base)) return false; + if (!is_dir($base) || !is_readable($base)) { + return false; + } - $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/'; - $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base; + $yearDir = rtrim($base, "/") . "/" . substr($dateIso, 0, 4) . "/"; + $targetDir = + is_dir($yearDir) && is_readable($yearDir) ? $yearDir : $base; - $pattern = rtrim($targetDir, '/') . '/' . $dateIso . '*'; + $pattern = rtrim($targetDir, "/") . "/" . $dateIso . "*"; $matches = glob($pattern) ?: []; foreach ($matches as $match) { - if (!is_file($match)) continue; + if (!is_file($match)) { + continue; + } $ext = strtolower(pathinfo($match, PATHINFO_EXTENSION)); - if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) { + if (in_array($ext, ["jpg", "jpeg", "png", "gif", "webp"], true)) { return true; } } @@ -548,32 +728,52 @@ class action_plugin_luxtools extends ActionPlugin protected function renderChronologicalEventsHtml(string $dateIso): string { $slots = CalendarSlot::loadEnabled($this); - if ($slots === []) return ''; + if ($slots === []) { + return ""; + } $grouped = CalendarService::eventsForDateGrouped($slots, $dateIso); - if ($grouped === []) return ''; + if ($grouped === []) { + return ""; + } - $html = ''; + $html = ""; // Render general events - if (isset($grouped['general'])) { - $title = (string)$this->getLang('chronological_events_title'); - if ($title === '') $title = 'Events'; - $html .= $this->renderEventSection($grouped['general'], $title, 'general'); + if (isset($grouped["general"])) { + $title = (string) $this->getLang("chronological_events_title"); + if ($title === "") { + $title = "Events"; + } + $html .= $this->renderEventSection( + $grouped["general"], + $title, + "general", + ); } // Render maintenance tasks - if (isset($grouped['maintenance'])) { - $title = (string)$this->getLang('chronological_maintenance_title'); - if ($title === '') $title = 'Tasks'; - $html .= $this->renderMaintenanceSection($grouped['maintenance'], $title, $dateIso); + if (isset($grouped["maintenance"])) { + $title = (string) $this->getLang("chronological_maintenance_title"); + if ($title === "") { + $title = "Tasks"; + } + $html .= $this->renderMaintenanceSection( + $grouped["maintenance"], + $title, + $dateIso, + ); } // Render slot3/slot4 if present - foreach (['slot3', 'slot4'] as $slotKey) { + foreach (["slot3", "slot4"] as $slotKey) { if (isset($grouped[$slotKey]) && isset($slots[$slotKey])) { $label = $slots[$slotKey]->getLabel(); - $html .= $this->renderEventSection($grouped[$slotKey], $label, $slotKey); + $html .= $this->renderEventSection( + $grouped[$slotKey], + $label, + $slotKey, + ); } } @@ -588,18 +788,29 @@ class action_plugin_luxtools extends ActionPlugin * @param string $slotKey * @return string */ - protected function renderEventSection(array $events, string $title, string $slotKey): string - { - $items = ''; + protected function renderEventSection( + array $events, + string $title, + string $slotKey, + ): string { + $items = ""; foreach ($events as $event) { $items .= $this->renderEventListItem($event); } - if ($items === '') return ''; + if ($items === "") { + return ""; + } - return '
' - . '

' . hsc($title) . '

' - . '' - . '
'; + return '
' . + "

" . + hsc($title) . + "

" . + "" . + "
"; } /** @@ -610,24 +821,41 @@ class action_plugin_luxtools extends ActionPlugin * @param string $dateIso * @return string */ - protected function renderMaintenanceSection(array $events, string $title, string $dateIso): string - { - $items = ''; - $ajaxUrl = defined('DOKU_BASE') ? (string)DOKU_BASE . 'lib/exe/ajax.php' : 'lib/exe/ajax.php'; + protected function renderMaintenanceSection( + array $events, + string $title, + string $dateIso, + ): string { + $items = ""; + $ajaxUrl = defined("DOKU_BASE") + ? (string) DOKU_BASE . "lib/exe/ajax.php" + : "lib/exe/ajax.php"; foreach ($events as $event) { $items .= $this->renderMaintenanceListItem($event, $ajaxUrl); } - if ($items === '') return ''; + if ($items === "") { + return ""; + } - $secToken = function_exists('getSecurityToken') ? getSecurityToken() : ''; + $secToken = function_exists("getSecurityToken") + ? getSecurityToken() + : ""; - return '
' - . '

' . hsc($title) . '

' - . '' - . '
'; + return '
' . + "

" . + hsc($title) . + "

" . + "" . + "
"; } /** @@ -644,34 +872,52 @@ class action_plugin_luxtools extends ActionPlugin $dataAttrs = ' data-luxtools-event="1"'; $dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"'; $dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"'; - if ($event->endIso !== '') { + if ($event->endIso !== "") { $dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"'; } - if ($event->location !== '') { - $dataAttrs .= ' data-event-location="' . hsc($event->location) . '"'; + if ($event->location !== "") { + $dataAttrs .= + ' data-event-location="' . hsc($event->location) . '"'; } - if ($event->description !== '') { - $dataAttrs .= ' data-event-description="' . hsc($event->description) . '"'; + if ($event->description !== "") { + $dataAttrs .= + ' data-event-description="' . hsc($event->description) . '"'; } - $dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"'; + $dataAttrs .= + ' data-event-allday="' . ($event->allDay ? "1" : "0") . '"'; $dataAttrs .= ' data-event-slot="' . hsc($event->slotKey) . '"'; - if ($event->uid !== '') { + if ($event->uid !== "") { $dataAttrs .= ' data-event-uid="' . hsc($event->uid) . '"'; } - if ($event->recurrenceId !== '') { - $dataAttrs .= ' data-event-recurrence="' . hsc($event->recurrenceId) . '"'; + if ($event->recurrenceId !== "") { + $dataAttrs .= + ' data-event-recurrence="' . hsc($event->recurrenceId) . '"'; } - if ($event->dateIso !== '') { + if ($event->dateIso !== "") { $dataAttrs .= ' data-event-date="' . hsc($event->dateIso) . '"'; } - if ($event->allDay || $event->time === '') { - return '' . $summaryHtml . ''; + if ($event->allDay || $event->time === "") { + return "' . + $summaryHtml . + ""; } - $timeHtml = '' - . hsc($event->time) . ''; - return '' . $timeHtml . ' - ' . $summaryHtml . ''; + $timeHtml = + '' . + hsc($event->time) . + ""; + return "" . + $timeHtml . + ' - ' . + $summaryHtml . + ""; } /** @@ -681,11 +927,15 @@ class action_plugin_luxtools extends ActionPlugin * @param string $ajaxUrl * @return string */ - protected function renderMaintenanceListItem(CalendarEvent $event, string $ajaxUrl): string - { + protected function renderMaintenanceListItem( + CalendarEvent $event, + string $ajaxUrl, + ): string { $isCompleted = $event->isCompleted(); - $classes = 'luxtools-maintenance-task'; - if ($isCompleted) $classes .= ' luxtools-task-completed'; + $classes = "luxtools-maintenance-task"; + if ($isCompleted) { + $classes .= " luxtools-task-completed"; + } $summaryHtml = hsc($event->summary); @@ -693,42 +943,62 @@ class action_plugin_luxtools extends ActionPlugin $dataAttrs = ' data-luxtools-event="1"'; $dataAttrs .= ' data-event-summary="' . hsc($event->summary) . '"'; $dataAttrs .= ' data-event-start="' . hsc($event->startIso) . '"'; - if ($event->endIso !== '') { + if ($event->endIso !== "") { $dataAttrs .= ' data-event-end="' . hsc($event->endIso) . '"'; } - if ($event->location !== '') { - $dataAttrs .= ' data-event-location="' . hsc($event->location) . '"'; + if ($event->location !== "") { + $dataAttrs .= + ' data-event-location="' . hsc($event->location) . '"'; } - if ($event->description !== '') { - $dataAttrs .= ' data-event-description="' . hsc($event->description) . '"'; + if ($event->description !== "") { + $dataAttrs .= + ' data-event-description="' . hsc($event->description) . '"'; } - $dataAttrs .= ' data-event-allday="' . ($event->allDay ? '1' : '0') . '"'; + $dataAttrs .= + ' data-event-allday="' . ($event->allDay ? "1" : "0") . '"'; $dataAttrs .= ' data-event-slot="maintenance"'; $dataAttrs .= ' data-task-uid="' . hsc($event->uid) . '"'; $dataAttrs .= ' data-task-date="' . hsc($event->dateIso) . '"'; - $dataAttrs .= ' data-task-recurrence="' . hsc($event->recurrenceId) . '"'; + $dataAttrs .= + ' data-task-recurrence="' . hsc($event->recurrenceId) . '"'; $dataAttrs .= ' data-task-status="' . hsc($event->status) . '"'; $buttonLabel = $isCompleted - ? (string)$this->getLang('maintenance_task_reopen') - : (string)$this->getLang('maintenance_task_complete'); - if ($buttonLabel === '') $buttonLabel = $isCompleted ? 'Reopen' : 'Complete'; - $buttonAction = $isCompleted ? 'reopen' : 'complete'; + ? (string) $this->getLang("maintenance_task_reopen") + : (string) $this->getLang("maintenance_task_complete"); + if ($buttonLabel === "") { + $buttonLabel = $isCompleted ? "Reopen" : "Complete"; + } + $buttonAction = $isCompleted ? "reopen" : "complete"; - $buttonHtml = ''; + $buttonHtml = + '"; - $timeHtml = ''; - if (!$event->allDay && $event->time !== '') { - $timeHtml = '' - . hsc($event->time) . ' - '; + $timeHtml = ""; + if (!$event->allDay && $event->time !== "") { + $timeHtml = + '' . + hsc($event->time) . + " - "; } - return '
  • ' - . $timeHtml - . '' . $summaryHtml . ' ' - . $buttonHtml - . '
  • '; + return '
  • " . + $timeHtml . + '' . + $summaryHtml . + " " . + $buttonHtml . + "
  • "; } /** @@ -740,12 +1010,14 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleMaintenanceTaskAction(Event $event, $param) { - if ($event->data !== 'luxtools_maintenance_task') return; + if ($event->data !== "luxtools_maintenance_task") { + return; + } $event->preventDefault(); $event->stopPropagation(); - header('Content-Type: application/json; charset=utf-8'); + header("Content-Type: application/json; charset=utf-8"); $this->sendNoStoreHeaders(); global $INPUT; @@ -753,46 +1025,61 @@ class action_plugin_luxtools extends ActionPlugin // Verify security token if (!checkSecurityToken()) { http_status(403); - echo json_encode(['ok' => false, 'error' => 'Security token mismatch']); + echo json_encode([ + "ok" => false, + "error" => "Security token mismatch", + ]); return; } - $action = $INPUT->str('action'); // 'complete' or 'reopen' - $uid = $INPUT->str('uid'); - $dateIso = $INPUT->str('date'); - $recurrence = $INPUT->str('recurrence'); + $action = $INPUT->str("action"); // 'complete' or 'reopen' + $uid = $INPUT->str("uid"); + $dateIso = $INPUT->str("date"); + $recurrence = $INPUT->str("recurrence"); - if (!in_array($action, ['complete', 'reopen'], true)) { + if (!in_array($action, ["complete", "reopen"], true)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid action']); + echo json_encode(["ok" => false, "error" => "Invalid action"]); return; } - if ($uid === '' || !ChronoID::isIsoDate($dateIso)) { + if ($uid === "" || !ChronoID::isIsoDate($dateIso)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Missing uid or date']); + echo json_encode(["ok" => false, "error" => "Missing uid or date"]); return; } $slots = CalendarSlot::loadAll($this); - $maintenanceSlot = $slots['maintenance'] ?? null; + $maintenanceSlot = $slots["maintenance"] ?? null; if ($maintenanceSlot === null || !$maintenanceSlot->isEnabled()) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Maintenance calendar not configured']); + echo json_encode([ + "ok" => false, + "error" => "Maintenance calendar not configured", + ]); return; } - $newStatus = ($action === 'complete') ? 'COMPLETED' : 'TODO'; + $newStatus = $action === "complete" ? "COMPLETED" : "TODO"; // Update local ICS file $localOk = false; $file = $maintenanceSlot->getFile(); - if ($file !== '' && is_file($file)) { - $localOk = IcsWriter::updateEventStatus($file, $uid, $recurrence, $newStatus, $dateIso); + if ($file !== "" && is_file($file)) { + $localOk = IcsWriter::updateEventStatus( + $file, + $uid, + $recurrence, + $newStatus, + $dateIso, + ); } if (!$localOk) { http_status(500); - echo json_encode(['ok' => false, 'error' => $this->getLang('maintenance_complete_error')]); + echo json_encode([ + "ok" => false, + "error" => $this->getLang("maintenance_complete_error"), + ]); return; } @@ -801,7 +1088,7 @@ class action_plugin_luxtools extends ActionPlugin // Remote CalDAV write-back if configured $remoteOk = true; - $remoteError = ''; + $remoteError = ""; if ($maintenanceSlot->hasRemoteSource()) { try { $caldavResult = CalDavClient::updateEventStatus( @@ -811,27 +1098,34 @@ class action_plugin_luxtools extends ActionPlugin $uid, $recurrence, $newStatus, - $dateIso + $dateIso, ); - if ($caldavResult !== '') { + if ($caldavResult !== "") { $remoteOk = false; - $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $caldavResult; + $remoteError = + $this->getLang("maintenance_remote_write_failed") . + ": " . + $caldavResult; } } catch (Throwable $e) { $remoteOk = false; - $remoteError = $this->getLang('maintenance_remote_write_failed') . ': ' . $e->getMessage(); + $remoteError = + $this->getLang("maintenance_remote_write_failed") . + ": " . + $e->getMessage(); } } - $msg = ($action === 'complete') - ? $this->getLang('maintenance_complete_success') - : $this->getLang('maintenance_reopen_success'); + $msg = + $action === "complete" + ? $this->getLang("maintenance_complete_success") + : $this->getLang("maintenance_reopen_success"); echo json_encode([ - 'ok' => true, - 'message' => $msg, - 'remoteOk' => $remoteOk, - 'remoteError' => $remoteError, + "ok" => true, + "message" => $msg, + "remoteOk" => $remoteOk, + "remoteError" => $remoteError, ]); } @@ -844,39 +1138,47 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleCalendarSyncAction(Event $event, $param) { - if ($event->data !== 'luxtools_calendar_sync') return; + if ($event->data !== "luxtools_calendar_sync") { + return; + } $event->preventDefault(); $event->stopPropagation(); - header('Content-Type: application/json; charset=utf-8'); + header("Content-Type: application/json; charset=utf-8"); $this->sendNoStoreHeaders(); global $INPUT; if (!checkSecurityToken()) { http_status(403); - echo json_encode(['ok' => false, 'error' => 'Security token mismatch']); + echo json_encode([ + "ok" => false, + "error" => "Security token mismatch", + ]); return; } - if (empty($_SERVER['REMOTE_USER'])) { + if (empty($_SERVER["REMOTE_USER"])) { http_status(403); - echo json_encode(['ok' => false, 'error' => 'Authentication required']); + echo json_encode([ + "ok" => false, + "error" => "Authentication required", + ]); return; } $slots = CalendarSlot::loadEnabled($this); $result = CalendarSyncService::syncAll($slots); - $msg = $result['ok'] - ? $this->getLang('calendar_sync_success') - : $this->getLang('calendar_sync_partial'); + $msg = $result["ok"] + ? $this->getLang("calendar_sync_success") + : $this->getLang("calendar_sync_partial"); echo json_encode([ - 'ok' => $result['ok'], - 'message' => $msg, - 'results' => $result['results'], + "ok" => $result["ok"], + "message" => $msg, + "results" => $result["results"], ]); } @@ -889,24 +1191,26 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleCalendarSlotsAction(Event $event, $param) { - if ($event->data !== 'luxtools_calendar_slots') return; + if ($event->data !== "luxtools_calendar_slots") { + return; + } $event->preventDefault(); $event->stopPropagation(); - header('Content-Type: application/json; charset=utf-8'); + header("Content-Type: application/json; charset=utf-8"); $this->sendNoStoreHeaders(); $slots = CalendarSlot::loadEnabled($this); $result = []; foreach ($slots as $slot) { $result[] = [ - 'key' => $slot->getKey(), - 'label' => $slot->getLabel(), + "key" => $slot->getKey(), + "label" => $slot->getLabel(), ]; } - echo json_encode(['ok' => true, 'slots' => $result]); + echo json_encode(["ok" => true, "slots" => $result]); } /** @@ -918,12 +1222,14 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleCalendarEventAction(Event $event, $param) { - if ($event->data !== 'luxtools_calendar_event') return; + if ($event->data !== "luxtools_calendar_event") { + return; + } $event->preventDefault(); $event->stopPropagation(); - header('Content-Type: application/json; charset=utf-8'); + header("Content-Type: application/json; charset=utf-8"); $this->sendNoStoreHeaders(); global $INPUT; @@ -931,29 +1237,35 @@ class action_plugin_luxtools extends ActionPlugin // Require security token if (!checkSecurityToken()) { http_status(403); - echo json_encode(['ok' => false, 'error' => 'Security token mismatch']); + echo json_encode([ + "ok" => false, + "error" => "Security token mismatch", + ]); return; } // Require authenticated user - if (!isset($_SERVER['REMOTE_USER']) || $_SERVER['REMOTE_USER'] === '') { + if (!isset($_SERVER["REMOTE_USER"]) || $_SERVER["REMOTE_USER"] === "") { http_status(403); - echo json_encode(['ok' => false, 'error' => 'Authentication required']); + echo json_encode([ + "ok" => false, + "error" => "Authentication required", + ]); return; } - $action = $INPUT->str('action'); - if (!in_array($action, ['create', 'edit', 'delete'], true)) { + $action = $INPUT->str("action"); + if (!in_array($action, ["create", "edit", "delete"], true)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid action']); + echo json_encode(["ok" => false, "error" => "Invalid action"]); return; } - if ($action === 'create') { + if ($action === "create") { $this->handleEventCreate($INPUT); - } elseif ($action === 'edit') { + } elseif ($action === "edit") { $this->handleEventEdit($INPUT); - } elseif ($action === 'delete') { + } elseif ($action === "delete") { $this->handleEventDelete($INPUT); } } @@ -966,18 +1278,18 @@ class action_plugin_luxtools extends ActionPlugin */ protected function handleEventCreate($INPUT): void { - $slotKey = $INPUT->str('slot'); - $summary = trim($INPUT->str('summary')); - $dateIso = $INPUT->str('date'); + $slotKey = $INPUT->str("slot"); + $summary = trim($INPUT->str("summary")); + $dateIso = $INPUT->str("date"); - if ($summary === '') { + if ($summary === "") { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Summary is required']); + echo json_encode(["ok" => false, "error" => "Summary is required"]); return; } if (!ChronoID::isIsoDate($dateIso)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid date']); + echo json_encode(["ok" => false, "error" => "Invalid date"]); return; } @@ -985,31 +1297,40 @@ class action_plugin_luxtools extends ActionPlugin $slot = $slots[$slotKey] ?? null; if ($slot === null || !$slot->isEnabled()) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']); + echo json_encode([ + "ok" => false, + "error" => "Invalid calendar slot", + ]); return; } $file = $slot->getFile(); - if ($file === '') { + if ($file === "") { http_status(400); - echo json_encode(['ok' => false, 'error' => 'No local file configured for this slot']); + echo json_encode([ + "ok" => false, + "error" => "No local file configured for this slot", + ]); return; } $eventData = [ - 'summary' => $summary, - 'date' => $dateIso, - 'allDay' => $INPUT->bool('allday'), - 'startTime' => $INPUT->str('start_time'), - 'endTime' => $INPUT->str('end_time'), - 'location' => trim($INPUT->str('location')), - 'description' => trim($INPUT->str('description')), + "summary" => $summary, + "date" => $dateIso, + "allDay" => $INPUT->bool("allday"), + "startTime" => $INPUT->str("start_time"), + "endTime" => $INPUT->str("end_time"), + "location" => trim($INPUT->str("location")), + "description" => trim($INPUT->str("description")), ]; $uid = IcsWriter::createEvent($file, $eventData); - if ($uid === '') { + if ($uid === "") { http_status(500); - echo json_encode(['ok' => false, 'error' => 'Failed to create event']); + echo json_encode([ + "ok" => false, + "error" => "Failed to create event", + ]); return; } @@ -1017,20 +1338,20 @@ class action_plugin_luxtools extends ActionPlugin // CalDAV write-back if configured $remoteOk = true; - $remoteError = ''; + $remoteError = ""; if ($slot->hasRemoteSource()) { $remoteOk = $this->pushEventToCalDav($slot, $file, $uid); if (!$remoteOk) { - $remoteError = 'Local event created, but CalDAV upload failed.'; + $remoteError = "Local event created, but CalDAV upload failed."; } } echo json_encode([ - 'ok' => true, - 'message' => 'Event created.', - 'uid' => $uid, - 'remoteOk' => $remoteOk, - 'remoteError' => $remoteError, + "ok" => true, + "message" => "Event created.", + "uid" => $uid, + "remoteOk" => $remoteOk, + "remoteError" => $remoteError, ]); } @@ -1042,30 +1363,30 @@ class action_plugin_luxtools extends ActionPlugin */ protected function handleEventEdit($INPUT): void { - $uid = $INPUT->str('uid'); - $recurrence = $INPUT->str('recurrence'); - $slotKey = $INPUT->str('slot'); - $summary = trim($INPUT->str('summary')); - $dateIso = $INPUT->str('date'); - $scope = $INPUT->str('scope', 'all'); + $uid = $INPUT->str("uid"); + $recurrence = $INPUT->str("recurrence"); + $slotKey = $INPUT->str("slot"); + $summary = trim($INPUT->str("summary")); + $dateIso = $INPUT->str("date"); + $scope = $INPUT->str("scope", "all"); - if (!in_array($scope, ['all', 'this', 'future'], true)) { - $scope = 'all'; + if (!in_array($scope, ["all", "this", "future"], true)) { + $scope = "all"; } - if ($uid === '') { + if ($uid === "") { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Missing event UID']); + echo json_encode(["ok" => false, "error" => "Missing event UID"]); return; } - if ($summary === '') { + if ($summary === "") { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Summary is required']); + echo json_encode(["ok" => false, "error" => "Summary is required"]); return; } if (!ChronoID::isIsoDate($dateIso)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid date']); + echo json_encode(["ok" => false, "error" => "Invalid date"]); return; } @@ -1073,31 +1394,46 @@ class action_plugin_luxtools extends ActionPlugin $slot = $slots[$slotKey] ?? null; if ($slot === null || !$slot->isEnabled()) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']); + echo json_encode([ + "ok" => false, + "error" => "Invalid calendar slot", + ]); return; } $file = $slot->getFile(); - if ($file === '' || !is_file($file)) { + if ($file === "" || !is_file($file)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'No local file for this slot']); + echo json_encode([ + "ok" => false, + "error" => "No local file for this slot", + ]); return; } $eventData = [ - 'summary' => $summary, - 'date' => $dateIso, - 'allDay' => $INPUT->bool('allday'), - 'startTime' => $INPUT->str('start_time'), - 'endTime' => $INPUT->str('end_time'), - 'location' => trim($INPUT->str('location')), - 'description' => trim($INPUT->str('description')), + "summary" => $summary, + "date" => $dateIso, + "allDay" => $INPUT->bool("allday"), + "startTime" => $INPUT->str("start_time"), + "endTime" => $INPUT->str("end_time"), + "location" => trim($INPUT->str("location")), + "description" => trim($INPUT->str("description")), ]; - $ok = IcsWriter::editEvent($file, $uid, $recurrence, $eventData, $scope); + $ok = IcsWriter::editEvent( + $file, + $uid, + $recurrence, + $eventData, + $scope, + ); if (!$ok) { http_status(500); - echo json_encode(['ok' => false, 'error' => 'Failed to update event']); + echo json_encode([ + "ok" => false, + "error" => "Failed to update event", + ]); return; } @@ -1105,19 +1441,19 @@ class action_plugin_luxtools extends ActionPlugin // CalDAV write-back if configured $remoteOk = true; - $remoteError = ''; + $remoteError = ""; if ($slot->hasRemoteSource()) { $remoteOk = $this->pushEventToCalDav($slot, $file, $uid); if (!$remoteOk) { - $remoteError = 'Local event updated, but CalDAV upload failed.'; + $remoteError = "Local event updated, but CalDAV upload failed."; } } echo json_encode([ - 'ok' => true, - 'message' => 'Event updated.', - 'remoteOk' => $remoteOk, - 'remoteError' => $remoteError, + "ok" => true, + "message" => "Event updated.", + "remoteOk" => $remoteOk, + "remoteError" => $remoteError, ]); } @@ -1129,40 +1465,55 @@ class action_plugin_luxtools extends ActionPlugin */ protected function handleEventDelete($INPUT): void { - $uid = $INPUT->str('uid'); - $recurrence = $INPUT->str('recurrence'); - $slotKey = $INPUT->str('slot'); - $dateIso = $INPUT->str('date'); - $scope = $INPUT->str('scope'); + $uid = $INPUT->str("uid"); + $recurrence = $INPUT->str("recurrence"); + $slotKey = $INPUT->str("slot"); + $dateIso = $INPUT->str("date"); + $scope = $INPUT->str("scope"); - if ($uid === '') { + if ($uid === "") { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Missing event UID']); + echo json_encode(["ok" => false, "error" => "Missing event UID"]); return; } - if (!in_array($scope, ['all', 'this', 'future'], true)) { - $scope = 'all'; + if (!in_array($scope, ["all", "this", "future"], true)) { + $scope = "all"; } $slots = CalendarSlot::loadAll($this); $slot = $slots[$slotKey] ?? null; if ($slot === null || !$slot->isEnabled()) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'Invalid calendar slot']); + echo json_encode([ + "ok" => false, + "error" => "Invalid calendar slot", + ]); return; } $file = $slot->getFile(); - if ($file === '' || !is_file($file)) { + if ($file === "" || !is_file($file)) { http_status(400); - echo json_encode(['ok' => false, 'error' => 'No local file for this slot']); + echo json_encode([ + "ok" => false, + "error" => "No local file for this slot", + ]); return; } - $ok = IcsWriter::deleteEvent($file, $uid, $recurrence, $dateIso, $scope); + $ok = IcsWriter::deleteEvent( + $file, + $uid, + $recurrence, + $dateIso, + $scope, + ); if (!$ok) { http_status(500); - echo json_encode(['ok' => false, 'error' => 'Failed to delete event']); + echo json_encode([ + "ok" => false, + "error" => "Failed to delete event", + ]); return; } @@ -1170,23 +1521,23 @@ class action_plugin_luxtools extends ActionPlugin // CalDAV write-back: push updated file for this UID $remoteOk = true; - $remoteError = ''; + $remoteError = ""; if ($slot->hasRemoteSource()) { - if ($scope === 'all') { + if ($scope === "all") { $remoteOk = $this->deleteEventFromCalDav($slot, $uid); } else { $remoteOk = $this->pushEventToCalDav($slot, $file, $uid); } if (!$remoteOk) { - $remoteError = 'Local event deleted, but CalDAV update failed.'; + $remoteError = "Local event deleted, but CalDAV update failed."; } } echo json_encode([ - 'ok' => true, - 'message' => 'Event deleted.', - 'remoteOk' => $remoteOk, - 'remoteError' => $remoteError, + "ok" => true, + "message" => "Event deleted.", + "remoteOk" => $remoteOk, + "remoteError" => $remoteError, ]); } @@ -1199,33 +1550,45 @@ class action_plugin_luxtools extends ActionPlugin * @param string $uid * @return bool */ - protected function pushEventToCalDav(CalendarSlot $slot, string $file, string $uid): bool - { + protected function pushEventToCalDav( + CalendarSlot $slot, + string $file, + string $uid, + ): bool { try { $raw = @file_get_contents($file); - if (!is_string($raw) || trim($raw) === '') return false; + if (!is_string($raw) || trim($raw) === "") { + return false; + } - $calendar = \Sabre\VObject\Reader::read($raw, \Sabre\VObject\Reader::OPTION_FORGIVING); - if (!($calendar instanceof \Sabre\VObject\Component\VCalendar)) return false; + $calendar = \Sabre\VObject\Reader::read( + $raw, + \Sabre\VObject\Reader::OPTION_FORGIVING, + ); + if (!($calendar instanceof \Sabre\VObject\Component\VCalendar)) { + return false; + } // Extract just the components for this UID into a new calendar $eventCal = new \Sabre\VObject\Component\VCalendar(); - $eventCal->PRODID = '-//LuxTools DokuWiki Plugin//EN'; + $eventCal->PRODID = "-//LuxTools DokuWiki Plugin//EN"; $found = false; // Copy relevant VTIMEZONE - foreach ($calendar->select('VTIMEZONE') as $tz) { + foreach ($calendar->select("VTIMEZONE") as $tz) { $eventCal->add(clone $tz); } - foreach ($calendar->select('VEVENT') as $component) { - if (trim((string)($component->UID ?? '')) === $uid) { + foreach ($calendar->select("VEVENT") as $component) { + if (trim((string) ($component->UID ?? "")) === $uid) { $eventCal->add(clone $component); $found = true; } } - if (!$found) return false; + if (!$found) { + return false; + } $icsData = $eventCal->serialize(); @@ -1234,31 +1597,31 @@ class action_plugin_luxtools extends ActionPlugin $slot->getCaldavUrl(), $slot->getUsername(), $slot->getPassword(), - $uid + $uid, ); if ($objectInfo !== null) { // Update existing object $error = CalDavClient::putCalendarObjectPublic( - $objectInfo['href'], + $objectInfo["href"], $slot->getUsername(), $slot->getPassword(), $icsData, - $objectInfo['etag'] + $objectInfo["etag"], ); - return $error === ''; + return $error === ""; } // Create new object - $href = rtrim($slot->getCaldavUrl(), '/') . '/' . $uid . '.ics'; + $href = rtrim($slot->getCaldavUrl(), "/") . "/" . $uid . ".ics"; $error = CalDavClient::putCalendarObjectPublic( $href, $slot->getUsername(), $slot->getPassword(), $icsData, - '' + "", ); - return $error === ''; + return $error === ""; } catch (\Throwable $e) { return false; } @@ -1271,23 +1634,27 @@ class action_plugin_luxtools extends ActionPlugin * @param string $uid * @return bool */ - protected function deleteEventFromCalDav(CalendarSlot $slot, string $uid): bool - { + protected function deleteEventFromCalDav( + CalendarSlot $slot, + string $uid, + ): bool { try { $objectInfo = CalDavClient::findObjectByUidPublic( $slot->getCaldavUrl(), $slot->getUsername(), $slot->getPassword(), - $uid + $uid, ); - if ($objectInfo === null) return true; // Already gone + if ($objectInfo === null) { + return true; + } // Already gone return CalDavClient::deleteCalendarObject( - $objectInfo['href'], + $objectInfo["href"], $slot->getUsername(), $slot->getPassword(), - $objectInfo['etag'] + $objectInfo["etag"], ); } catch (\Throwable $e) { return false; @@ -1303,19 +1670,23 @@ class action_plugin_luxtools extends ActionPlugin protected function buildChronologicalEventsWiki(string $dateIso): string { $slots = CalendarSlot::loadEnabled($this); - if ($slots === []) return ''; + if ($slots === []) { + return ""; + } $events = CalendarService::eventsForDate($slots, $dateIso); - if ($events === []) return ''; + if ($events === []) { + return ""; + } $lines = []; foreach ($events as $event) { - $summary = str_replace(["\n", "\r"], ' ', $event->summary); + $summary = str_replace(["\n", "\r"], " ", $event->summary); - if ($event->allDay || $event->time === '') { - $lines[] = ' * ' . $summary; + if ($event->allDay || $event->time === "") { + $lines[] = " * " . $summary; } else { - $lines[] = ' * ' . $event->time . ' - ' . $summary; + $lines[] = " * " . $event->time . " - " . $summary; } } @@ -1377,18 +1748,30 @@ class action_plugin_luxtools extends ActionPlugin */ public function addInvalidateCacheMenuItem(Event $event, $param) { - if (!is_array($event->data)) return; - if (($event->data['view'] ?? '') !== 'page') return; - if (!function_exists('auth_isadmin') || !auth_isadmin()) return; - if (!isset($event->data['items']) || !is_array($event->data['items'])) return; + if (!is_array($event->data)) { + return; + } + if (($event->data["view"] ?? "") !== "page") { + return; + } + if (!function_exists("auth_isadmin") || !auth_isadmin()) { + return; + } + if (!isset($event->data["items"]) || !is_array($event->data["items"])) { + return; + } - $label = (string)$this->getLang('cache_invalidate_button'); - if ($label === '') $label = 'Invalidate Cache'; + $label = (string) $this->getLang("cache_invalidate_button"); + if ($label === "") { + $label = "Invalidate Cache"; + } - $title = (string)$this->getLang('cache_invalidate_button_title'); - if ($title === '') $title = 'Invalidate luxtools cache for this page'; + $title = (string) $this->getLang("cache_invalidate_button_title"); + if ($title === "") { + $title = "Invalidate luxtools cache for this page"; + } - $event->data['items'][] = new InvalidateCache($label, $title); + $event->data["items"][] = new InvalidateCache($label, $title); } /** @@ -1400,57 +1783,71 @@ class action_plugin_luxtools extends ActionPlugin */ public function handleInvalidateCacheAction(Event $event, $param) { - if (!is_string($event->data) || $event->data !== 'show') return; - - global $INPUT; - if (!$INPUT->bool('luxtools_invalidate_cache')) return; - - global $ID; - $id = is_string($ID) ? $ID : ''; - if (function_exists('cleanID')) { - $id = (string)cleanID($id); + if (!is_string($event->data) || $event->data !== "show") { + return; } - if (!function_exists('auth_isadmin') || !auth_isadmin()) { - $message = (string)$this->getLang('cache_invalidate_denied'); - if ($message === '') $message = 'Only admins can invalidate cache.'; + global $INPUT; + if (!$INPUT->bool("luxtools_invalidate_cache")) { + return; + } + + global $ID; + $id = is_string($ID) ? $ID : ""; + if (function_exists("cleanID")) { + $id = (string) cleanID($id); + } + + if (!function_exists("auth_isadmin") || !auth_isadmin()) { + $message = (string) $this->getLang("cache_invalidate_denied"); + if ($message === "") { + $message = "Only admins can invalidate cache."; + } msg($message, -1); $this->redirectToShow($id); return; } if (!checkSecurityToken()) { - $message = (string)$this->getLang('cache_invalidate_badtoken'); - if ($message === '') $message = 'Security token mismatch. Please retry.'; + $message = (string) $this->getLang("cache_invalidate_badtoken"); + if ($message === "") { + $message = "Security token mismatch. Please retry."; + } msg($message, -1); $this->redirectToShow($id); return; } $result = CacheInvalidation::purgeSelected( - $INPUT->bool('luxtools_purge_pagelinks'), - $INPUT->bool('luxtools_purge_thumbs') + $INPUT->bool("luxtools_purge_pagelinks"), + $INPUT->bool("luxtools_purge_thumbs"), ); $parts = []; - $dokuwikiMsg = (string)$this->getLang('cache_invalidate_success'); - if ($dokuwikiMsg === '') $dokuwikiMsg = 'DokuWiki cache invalidated.'; - $parts[] = $dokuwikiMsg . ' (' . $result['dokuwiki'] . ')'; + $dokuwikiMsg = (string) $this->getLang("cache_invalidate_success"); + if ($dokuwikiMsg === "") { + $dokuwikiMsg = "DokuWiki cache invalidated."; + } + $parts[] = $dokuwikiMsg . " (" . $result["dokuwiki"] . ")"; - if ($result['pagelinks'] !== null) { - $msg = (string)$this->getLang('cache_purge_pagelinks_success'); - if ($msg === '') $msg = 'Pagelinks cache purged.'; - $parts[] = $msg . ' (' . $result['pagelinks'] . ')'; + if ($result["pagelinks"] !== null) { + $msg = (string) $this->getLang("cache_purge_pagelinks_success"); + if ($msg === "") { + $msg = "Pagelinks cache purged."; + } + $parts[] = $msg . " (" . $result["pagelinks"] . ")"; } - if ($result['thumbs'] !== null) { - $msg = (string)$this->getLang('cache_purge_thumbs_success'); - if ($msg === '') $msg = 'Thumbnail cache purged.'; - $parts[] = $msg . ' (' . $result['thumbs'] . ')'; + if ($result["thumbs"] !== null) { + $msg = (string) $this->getLang("cache_purge_thumbs_success"); + if ($msg === "") { + $msg = "Thumbnail cache purged."; + } + $parts[] = $msg . " (" . $result["thumbs"] . ")"; } - msg(implode(' ', $parts), 1); + msg(implode(" ", $parts), 1); $this->redirectToShow($id); } @@ -1463,8 +1860,8 @@ class action_plugin_luxtools extends ActionPlugin */ protected function redirectToShow(string $id): void { - $params = ['do' => 'show']; - send_redirect(wl($id, $params, true, '&')); + $params = ["do" => "show"]; + send_redirect(wl($id, $params, true, "&")); } /** @@ -1474,10 +1871,12 @@ class action_plugin_luxtools extends ActionPlugin */ protected function sendNoStoreHeaders(): void { - if (headers_sent()) return; + if (headers_sent()) { + return; + } - header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); - header('Pragma: no-cache'); - header('Expires: 0'); + header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); + header("Pragma: no-cache"); + header("Expires: 0"); } } diff --git a/dialog.css b/dialog.css new file mode 100644 index 0000000..f53c9a7 --- /dev/null +++ b/dialog.css @@ -0,0 +1,62 @@ +/* ============================================================ + * Dialog Infrastructure (shared overlay & popup) + * ============================================================ */ +.luxtools-dialog-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.4); + z-index: 10000; + justify-content: center; + align-items: center; +} + +.luxtools-dialog { + background: @ini_background; + border: 1px solid @ini_border; + border-radius: 0.4em; + padding: 1.5em; + max-width: 500px; + width: 90%; + max-height: 80vh; + overflow-y: auto; + position: relative; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); +} + +.luxtools-dialog-close { + position: absolute; + top: 0.5em; + right: 0.75em; + background: none; + border: none; + font-size: 1.5em; + cursor: pointer; + color: @ini_text; + line-height: 1; +} + +.luxtools-dialog-close:hover { + opacity: 0.7; +} + +.luxtools-dialog-title { + margin: 0 0 0.75em 0; + padding-right: 1.5em; +} + +.luxtools-dialog-field { + margin: 0.5em 0; +} + +.luxtools-dialog-actions { + margin-top: 1em; + padding-top: 0.75em; + border-top: 1px solid @ini_border; + display: flex; + flex-wrap: wrap; + gap: 0.5em; +} diff --git a/js/dialog.js b/js/dialog.js new file mode 100644 index 0000000..d22eedd --- /dev/null +++ b/js/dialog.js @@ -0,0 +1,95 @@ +/* global window, document */ + +/** + * Unified Dialog Infrastructure + * + * Provides a shared modal overlay and dialog container that all other + * client-side modules (event popups, cache purge, etc.) can use. + * + * Usage: + * Luxtools.Dialog.show(htmlString) – render content and open + * Luxtools.Dialog.close() – close the dialog + * Luxtools.Dialog.getContainer() – return the dialog DOM element + */ +(function () { + 'use strict'; + + var Luxtools = window.Luxtools || (window.Luxtools = {}); + + var Dialog = (function () { + var overlay = null; + var container = null; + + /** + * Lazily create the overlay + dialog container and attach them to + * the document body. Wires up click-outside-to-close and Escape. + */ + function ensureElements() { + if (overlay) return; + + overlay = document.createElement('div'); + overlay.className = 'luxtools-dialog-overlay'; + overlay.style.display = 'none'; + + container = document.createElement('div'); + container.className = 'luxtools-dialog'; + container.setAttribute('role', 'dialog'); + container.setAttribute('aria-modal', 'true'); + + overlay.appendChild(container); + document.body.appendChild(overlay); + + // Close when clicking the backdrop (but not the dialog itself) + overlay.addEventListener('click', function (e) { + if (e.target === overlay) close(); + }); + + // Close on Escape + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape' && overlay && overlay.style.display !== 'none') { + close(); + } + }); + } + + /** + * Show the dialog with the given HTML content. + * + * The HTML should include a close button with class + * `luxtools-dialog-close` – it will be wired up automatically. + * + * @param {string} html - innerHTML for the dialog container + */ + function show(html) { + ensureElements(); + container.innerHTML = html; + overlay.style.display = 'flex'; + + // Auto-wire the close button inside the rendered content + var closeBtn = container.querySelector('.luxtools-dialog-close'); + if (closeBtn) closeBtn.addEventListener('click', close); + } + + /** + * Close (hide) the dialog. + */ + function close() { + if (overlay) overlay.style.display = 'none'; + } + + /** + * Return the dialog container element (creates it if necessary). + * Useful for querying form inputs after `show()`. + * + * @returns {HTMLElement} + */ + function getContainer() { + ensureElements(); + return container; + } + + return { show: show, close: close, getContainer: getContainer }; + })(); + + Luxtools.Dialog = Dialog; +})(); diff --git a/js/event-popup.js b/js/event-popup.js index f53f423..a7da062 100644 --- a/js/event-popup.js +++ b/js/event-popup.js @@ -11,7 +11,7 @@ * complete/reopen the task. */ (function () { - 'use strict'; + "use strict"; var Luxtools = window.Luxtools || (window.Luxtools = {}); @@ -22,40 +22,44 @@ // Shared helpers // ============================================================ function escapeHtml(text) { - var div = document.createElement('div'); + var div = document.createElement("div"); div.appendChild(document.createTextNode(text)); return div.innerHTML; } function pad2(value) { - return String(value).padStart(2, '0'); + return String(value).padStart(2, "0"); } function formatDate(isoStr) { - if (!isoStr) return ''; + if (!isoStr) return ""; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - return pad2(d.getDate()) + '.' + pad2(d.getMonth() + 1) + '.' + d.getFullYear(); + return ( + pad2(d.getDate()) + "." + pad2(d.getMonth() + 1) + "." + d.getFullYear() + ); } function formatDateTime(isoStr) { - if (!isoStr) return ''; + if (!isoStr) return ""; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - return formatDate(isoStr) + ' ' + pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + return ( + formatDate(isoStr) + " " + pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ); } function formatTimeOnly(isoStr) { - if (!isoStr) return ''; + if (!isoStr) return ""; var d = new Date(isoStr); if (isNaN(d.getTime())) return isoStr; - return pad2(d.getHours()) + ':' + pad2(d.getMinutes()); + return pad2(d.getHours()) + ":" + pad2(d.getMinutes()); } function formatEventListTime(startIso, fallbackTime) { var formatted = formatTimeOnly(startIso); if (!formatted || formatted === startIso) { - return fallbackTime || ''; + return fallbackTime || ""; } return formatted; } @@ -66,22 +70,25 @@ } function getAjaxUrl() { - return (window.DOKU_BASE || '/') + 'lib/exe/ajax.php'; + return (window.DOKU_BASE || "/") + "lib/exe/ajax.php"; } function getSecurityToken(el) { // Try element hierarchy if (el) { - var container = el.closest ? el.closest('[data-luxtools-sectok]') : null; + var container = el.closest ? el.closest("[data-luxtools-sectok]") : null; if (container) { - var tok = container.getAttribute('data-luxtools-sectok'); + var tok = container.getAttribute("data-luxtools-sectok"); if (tok) return tok; } } - if (window.JSINFO && window.JSINFO.sectok) return String(window.JSINFO.sectok); - var input = document.querySelector('input[name="sectok"], input[name="securitytoken"]'); + if (window.JSINFO && window.JSINFO.sectok) + return String(window.JSINFO.sectok); + var input = document.querySelector( + 'input[name="sectok"], input[name="securitytoken"]', + ); if (input && input.value) return String(input.value); - return ''; + return ""; } function isAuthenticated() { @@ -89,23 +96,25 @@ if (window.JSINFO && window.JSINFO.isadmin) return true; if (window.JSINFO && window.JSINFO.id !== undefined) { // Check if user info exists (logged in users have a userinfo) - var userinfo = document.querySelector('.user'); + var userinfo = document.querySelector(".user"); if (userinfo) return true; // Alternative: check for logout form - var logoutLink = document.querySelector('a[href*="do=logout"], .action.logout'); + var logoutLink = document.querySelector( + 'a[href*="do=logout"], .action.logout', + ); if (logoutLink) return true; } return false; } function showNotification(message, type) { - if (typeof window.msg === 'function') { - var level = (type === 'error') ? -1 : ((type === 'warning') ? 0 : 1); + if (typeof window.msg === "function") { + var level = type === "error" ? -1 : type === "warning" ? 0 : 1; window.msg(message, level); return; } - var notif = document.createElement('div'); - notif.className = 'luxtools-notification luxtools-notification-' + type; + var notif = document.createElement("div"); + notif.className = "luxtools-notification luxtools-notification-" + type; notif.textContent = message; document.body.appendChild(notif); setTimeout(function () { @@ -113,64 +122,16 @@ }, 5000); } - // ============================================================ - // Popup infrastructure (shared overlay) - // ============================================================ - var PopupUI = (function () { - var overlay = null; - var popup = null; - - function ensureElements() { - if (overlay) return; - - overlay = document.createElement('div'); - overlay.className = 'luxtools-event-popup-overlay'; - overlay.style.display = 'none'; - - popup = document.createElement('div'); - popup.className = 'luxtools-event-popup'; - popup.setAttribute('role', 'dialog'); - popup.setAttribute('aria-modal', 'true'); - - overlay.appendChild(popup); - document.body.appendChild(overlay); - - overlay.addEventListener('click', function (e) { - if (e.target === overlay) close(); - }); - - document.addEventListener('keydown', function (e) { - if (e.key === 'Escape' && overlay && overlay.style.display !== 'none') { - close(); - } - }); - } - - function show(html) { - ensureElements(); - popup.innerHTML = html; - overlay.style.display = 'flex'; - var closeBtn = popup.querySelector('.luxtools-event-popup-close'); - if (closeBtn) closeBtn.addEventListener('click', close); - } - - function close() { - if (overlay) overlay.style.display = 'none'; - } - - function getPopup() { - ensureElements(); - return popup; - } - - return { show: show, close: close, getPopup: getPopup }; - })(); + // Lazy reference to the shared dialog infrastructure (dialog.js). + // Accessed via function to handle any script-loading order variation. + function getDialog() { + return Luxtools.Dialog; + } // ============================================================ // Event Popup (single event detail) // ============================================================ var EventPopup = (function () { - /** * Open event detail popup. * @param {Element} el - Element with data-event-* attributes @@ -180,92 +141,131 @@ function open(el, opts) { opts = opts || {}; - var summary = el.getAttribute('data-event-summary') || ''; - var start = el.getAttribute('data-event-start') || ''; - var end = el.getAttribute('data-event-end') || ''; - var location = el.getAttribute('data-event-location') || ''; - var description = el.getAttribute('data-event-description') || ''; - var allDay = el.getAttribute('data-event-allday') === '1'; - var slot = el.getAttribute('data-event-slot') || ''; - var uid = el.getAttribute('data-event-uid') || ''; - var recurrence = el.getAttribute('data-event-recurrence') || ''; - var dateIso = el.getAttribute('data-event-date') || ''; + var summary = el.getAttribute("data-event-summary") || ""; + var start = el.getAttribute("data-event-start") || ""; + var end = el.getAttribute("data-event-end") || ""; + var location = el.getAttribute("data-event-location") || ""; + var description = el.getAttribute("data-event-description") || ""; + var allDay = el.getAttribute("data-event-allday") === "1"; + var slot = el.getAttribute("data-event-slot") || ""; + var uid = el.getAttribute("data-event-uid") || ""; + var recurrence = el.getAttribute("data-event-recurrence") || ""; + var dateIso = el.getAttribute("data-event-date") || ""; - var html = '
    '; - html += ''; - html += '

    ' + escapeHtml(summary) + '

    '; + var html = '
    '; + html += + ''; + html += + '

    ' + escapeHtml(summary) + "

    "; // Date/time - hide when opened from a day context if (!opts.hideDatetime) { - html += '
    '; + html += '
    '; if (allDay) { - html += 'Date: ' + formatDate(start); + html += "Date: " + formatDate(start); if (end && !isSameMoment(start, end)) { - html += ' – ' + formatDate(end); + html += " – " + formatDate(end); } } else { - html += 'Time: ' + formatDateTime(start); + html += "Time: " + formatDateTime(start); if (end && !isSameMoment(start, end)) { - html += ' – ' + formatDateTime(end); + html += " – " + formatDateTime(end); } } - html += '
    '; + html += "
    "; } else if (!allDay && start) { // In day context, show only time (not date) - html += '
    '; - html += 'Time: ' + formatTimeOnly(start); + html += '
    '; + html += "Time: " + formatTimeOnly(start); if (end && !isSameMoment(start, end)) { - html += ' – ' + formatTimeOnly(end); + html += " – " + formatTimeOnly(end); } - html += '
    '; + html += "
    "; } if (location) { - html += '
    Location: ' + escapeHtml(location) + '
    '; + html += + '
    Location: ' + + escapeHtml(location) + + "
    "; } if (description) { - html += '
    ' - + 'Description:
    ' - + escapeHtml(description).replace(/\n/g, '
    ') - + '
    '; + html += + '
    ' + + "Description:
    " + + escapeHtml(description).replace(/\n/g, "
    ") + + "
    "; } if (slot) { - html += '
    ' + escapeHtml(slot) + '
    '; + html += + '
    ' + + escapeHtml(slot) + + "
    "; } // Edit/Delete actions for authenticated users with a UID if (uid && isAuthenticated()) { - html += '
    '; - html += ' '; - html += ''; - html += '
    '; + html += '
    '; + html += + ' "; + html += + '"; + html += "
    "; } - html += '
    '; + html += "
    "; - PopupUI.show(html); + getDialog().show(html); } function close() { - PopupUI.close(); + getDialog().close(); } return { open: open, close: close }; @@ -275,65 +275,105 @@ // Day Popup (list events for a specific day) // ============================================================ var DayPopup = (function () { - function open(dayCell) { - var dateIso = dayCell.getAttribute('data-luxtools-date') || ''; + var dateIso = dayCell.getAttribute("data-luxtools-date") || ""; if (!dateIso) return; var events = []; - var rawJson = dayCell.getAttribute('data-luxtools-day-events'); + var rawJson = dayCell.getAttribute("data-luxtools-day-events"); if (rawJson) { - try { events = JSON.parse(rawJson); } catch (e) { events = []; } + try { + events = JSON.parse(rawJson); + } catch (e) { + events = []; + } } - var html = '
    '; - html += ''; - html += '

    ' + escapeHtml(formatDate(dateIso + 'T00:00:00')) + '

    '; + var html = '
    '; + html += + ''; + html += + '

    ' + + escapeHtml(formatDate(dateIso + "T00:00:00")) + + "

    "; if (events.length === 0) { - html += '
    No events
    '; + html += + '
    No events
    '; } else { html += '
      '; for (var i = 0; i < events.length; i++) { var ev = events[i]; - var attrs = ' data-luxtools-event="1"' - + ' data-event-summary="' + escapeHtml(ev.summary || '') + '"' - + ' data-event-start="' + escapeHtml(ev.start || '') + '"' - + ' data-event-end="' + escapeHtml(ev.end || '') + '"' - + ' data-event-location="' + escapeHtml(ev.location || '') + '"' - + ' data-event-description="' + escapeHtml(ev.description || '') + '"' - + ' data-event-allday="' + (ev.allDay ? '1' : '0') + '"' - + ' data-event-slot="' + escapeHtml(ev.slot || '') + '"' - + ' data-event-uid="' + escapeHtml(ev.uid || '') + '"' - + ' data-event-recurrence="' + escapeHtml(ev.recurrence || '') + '"' - + ' data-event-date="' + escapeHtml(ev.date || dateIso) + '"'; + var attrs = + ' data-luxtools-event="1"' + + ' data-event-summary="' + + escapeHtml(ev.summary || "") + + '"' + + ' data-event-start="' + + escapeHtml(ev.start || "") + + '"' + + ' data-event-end="' + + escapeHtml(ev.end || "") + + '"' + + ' data-event-location="' + + escapeHtml(ev.location || "") + + '"' + + ' data-event-description="' + + escapeHtml(ev.description || "") + + '"' + + ' data-event-allday="' + + (ev.allDay ? "1" : "0") + + '"' + + ' data-event-slot="' + + escapeHtml(ev.slot || "") + + '"' + + ' data-event-uid="' + + escapeHtml(ev.uid || "") + + '"' + + ' data-event-recurrence="' + + escapeHtml(ev.recurrence || "") + + '"' + + ' data-event-date="' + + escapeHtml(ev.date || dateIso) + + '"'; - var timeLabel = ''; - var timeStr = ''; + var timeLabel = ""; + var timeStr = ""; if (!ev.allDay) { - timeLabel = formatEventListTime(ev.start || '', ev.time || ''); + timeLabel = formatEventListTime(ev.start || "", ev.time || ""); } if (timeLabel) { - timeStr = '' + escapeHtml(timeLabel) + ' - '; + timeStr = + '' + + escapeHtml(timeLabel) + + " - "; } - html += '
    • ' - + timeStr - + '' + escapeHtml(ev.summary || '') + '' - + '
    • '; + html += + '
    • " + + timeStr + + '' + + escapeHtml(ev.summary || "") + + "" + + "
    • "; } - html += '
    '; + html += ""; } // Create Event action for authenticated users if (isAuthenticated()) { - html += '
    '; - html += ''; - html += '
    '; + html += '
    '; + html += + ''; + html += "
    "; } - html += '
    '; + html += "
    "; - PopupUI.show(html); + getDialog().show(html); } return { open: open }; @@ -346,11 +386,14 @@ var _calendarSlots = null; function loadSlots(callback) { - if (_calendarSlots) { callback(_calendarSlots); return; } + if (_calendarSlots) { + callback(_calendarSlots); + return; + } var xhr = new XMLHttpRequest(); - xhr.open('GET', getAjaxUrl() + '?call=luxtools_calendar_slots', true); - xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + xhr.open("GET", getAjaxUrl() + "?call=luxtools_calendar_slots", true); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); @@ -373,253 +416,338 @@ function openCreate(dateIso) { loadSlots(function (slots) { - renderForm({ - mode: 'create', - date: dateIso, - summary: '', - startTime: '', - endTime: '', - location: '', - description: '', - allDay: true, - slot: (slots.length > 0) ? slots[0].key : 'general', - }, slots); + renderForm( + { + mode: "create", + date: dateIso, + summary: "", + startTime: "", + endTime: "", + location: "", + description: "", + allDay: true, + slot: slots.length > 0 ? slots[0].key : "general", + }, + slots, + ); }); } function openEdit(data) { loadSlots(function (slots) { // Parse start/end times - var startTime = ''; - var endTime = ''; + var startTime = ""; + var endTime = ""; if (!data.allDay && data.start) { var sd = new Date(data.start); - if (!isNaN(sd.getTime())) startTime = pad2(sd.getHours()) + ':' + pad2(sd.getMinutes()); + if (!isNaN(sd.getTime())) + startTime = pad2(sd.getHours()) + ":" + pad2(sd.getMinutes()); } if (!data.allDay && data.end) { var ed = new Date(data.end); - if (!isNaN(ed.getTime())) endTime = pad2(ed.getHours()) + ':' + pad2(ed.getMinutes()); + if (!isNaN(ed.getTime())) + endTime = pad2(ed.getHours()) + ":" + pad2(ed.getMinutes()); } - renderForm({ - mode: 'edit', - uid: data.uid || '', - recurrence: data.recurrence || '', - date: data.date || '', - summary: data.summary || '', - startTime: startTime, - endTime: endTime, - location: data.location || '', - description: data.description || '', - allDay: data.allDay, - slot: data.slot || 'general', - }, slots); + renderForm( + { + mode: "edit", + uid: data.uid || "", + recurrence: data.recurrence || "", + date: data.date || "", + summary: data.summary || "", + startTime: startTime, + endTime: endTime, + location: data.location || "", + description: data.description || "", + allDay: data.allDay, + slot: data.slot || "general", + }, + slots, + ); }); } function renderForm(data, slots) { - var isEdit = data.mode === 'edit'; - var title = isEdit ? 'Edit Event' : 'Create Event'; + var isEdit = data.mode === "edit"; + var title = isEdit ? "Edit Event" : "Create Event"; - var html = '
    '; - html += ''; - html += '

    ' + escapeHtml(title) + '

    '; + var html = '
    '; + html += + ''; + html += + '

    ' + escapeHtml(title) + "

    "; html += '
    '; - html += ''; - html += '
    '; + html += + ''; + html += "
    "; html += '
    '; - html += ''; - html += '
    '; + html += + ''; + html += "
    "; html += '
    '; - html += ''; - html += '
    '; + html += + '"; + html += ""; - html += ''; + html += + ''; + html += ""; + html += ""; html += '
    '; - html += ''; - html += '
    '; + html += + ''; + html += ""; html += '
    '; - html += ''; - html += '
    '; + html += + '"; + html += ""; html += '
    '; html += ''; - html += '
    '; + html += ""; + html += ""; - html += '
    '; - html += ' '; - html += ''; - html += '
    '; + html += '
    '; + html += + ' "; + html += + ''; + html += "
    "; - html += ''; + html += ""; - PopupUI.show(html); + getDialog().show(html); // Wire up all-day checkbox toggle - var popup = PopupUI.getPopup(); - var allDayCheckbox = popup.querySelector('.luxtools-form-allday'); - var timeFields = popup.querySelector('.luxtools-event-form-time-fields'); + var popup = getDialog().getContainer(); + var allDayCheckbox = popup.querySelector(".luxtools-form-allday"); + var timeFields = popup.querySelector(".luxtools-event-form-time-fields"); if (allDayCheckbox && timeFields) { - allDayCheckbox.addEventListener('change', function () { - timeFields.style.display = allDayCheckbox.checked ? 'none' : ''; + allDayCheckbox.addEventListener("change", function () { + timeFields.style.display = allDayCheckbox.checked ? "none" : ""; }); } } function collectFormData() { - var popup = PopupUI.getPopup(); + var popup = getDialog().getContainer(); return { - summary: (popup.querySelector('.luxtools-form-summary') || {}).value || '', - date: (popup.querySelector('.luxtools-form-date') || {}).value || '', - allDay: !!(popup.querySelector('.luxtools-form-allday') || {}).checked, - startTime: (popup.querySelector('.luxtools-form-start-time') || {}).value || '', - endTime: (popup.querySelector('.luxtools-form-end-time') || {}).value || '', - location: (popup.querySelector('.luxtools-form-location') || {}).value || '', - description: (popup.querySelector('.luxtools-form-description') || {}).value || '', - slot: (popup.querySelector('.luxtools-form-slot') || {}).value || 'general', + summary: + (popup.querySelector(".luxtools-form-summary") || {}).value || "", + date: (popup.querySelector(".luxtools-form-date") || {}).value || "", + allDay: !!(popup.querySelector(".luxtools-form-allday") || {}).checked, + startTime: + (popup.querySelector(".luxtools-form-start-time") || {}).value || "", + endTime: + (popup.querySelector(".luxtools-form-end-time") || {}).value || "", + location: + (popup.querySelector(".luxtools-form-location") || {}).value || "", + description: + (popup.querySelector(".luxtools-form-description") || {}).value || "", + slot: + (popup.querySelector(".luxtools-form-slot") || {}).value || "general", }; } function save(saveBtn) { - var mode = saveBtn.getAttribute('data-mode'); + var mode = saveBtn.getAttribute("data-mode"); var formData = collectFormData(); if (!formData.summary.trim()) { - showNotification('Summary is required', 'error'); + showNotification("Summary is required", "error"); return; } if (!formData.date) { - showNotification('Date is required', 'error'); + showNotification("Date is required", "error"); return; } // For recurring event edits, ask about scope first - if (mode === 'edit') { - var recurrence = saveBtn.getAttribute('data-recurrence') || ''; + if (mode === "edit") { + var recurrence = saveBtn.getAttribute("data-recurrence") || ""; if (recurrence) { showRecurrenceEditScopeDialog(saveBtn, formData); return; } } - submitSave(saveBtn, formData, 'all'); + submitSave(saveBtn, formData, "all"); } function showRecurrenceEditScopeDialog(saveBtn, formData) { - var uid = saveBtn.getAttribute('data-uid') || ''; - var recurrence = saveBtn.getAttribute('data-recurrence') || ''; + var uid = saveBtn.getAttribute("data-uid") || ""; + var recurrence = saveBtn.getAttribute("data-recurrence") || ""; - var html = '
    '; - html += ''; - html += '

    Edit Recurring Event

    '; - html += '

    This is a recurring event. What would you like to edit?

    '; - html += '
    '; + var html = '
    '; + html += + ''; + html += '

    Edit Recurring Event

    '; + html += "

    This is a recurring event. What would you like to edit?

    "; + html += + '
    '; - var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"' - + ' data-recurrence="' + escapeHtml(recurrence) + '"' - + ' data-mode="edit"'; + var baseAttrs = + ' data-uid="' + + escapeHtml(uid) + + '"' + + ' data-recurrence="' + + escapeHtml(recurrence) + + '"' + + ' data-mode="edit"'; - html += ' '; - html += ' '; - html += ' '; - html += ''; - html += '
    '; + html += + ' '; + html += + ' '; + html += + ' '; + html += + ''; + html += "
    "; // Store formData on the global scope so the scope button handler can access it _pendingEditFormData = formData; - PopupUI.show(html); + getDialog().show(html); } function submitSave(saveBtn, formData, scope) { saveBtn.disabled = true; - saveBtn.textContent = 'Saving...'; + saveBtn.textContent = "Saving..."; - var mode = saveBtn.getAttribute('data-mode') || 'create'; + var mode = saveBtn.getAttribute("data-mode") || "create"; - var params = 'call=luxtools_calendar_event' - + '&action=' + encodeURIComponent(mode === 'edit' ? 'edit' : 'create') - + '&summary=' + encodeURIComponent(formData.summary) - + '&date=' + encodeURIComponent(formData.date) - + '&allday=' + (formData.allDay ? '1' : '0') - + '&start_time=' + encodeURIComponent(formData.startTime) - + '&end_time=' + encodeURIComponent(formData.endTime) - + '&location=' + encodeURIComponent(formData.location) - + '&description=' + encodeURIComponent(formData.description) - + '&slot=' + encodeURIComponent(formData.slot) - + '§ok=' + encodeURIComponent(getSecurityToken(saveBtn)); + var params = + "call=luxtools_calendar_event" + + "&action=" + + encodeURIComponent(mode === "edit" ? "edit" : "create") + + "&summary=" + + encodeURIComponent(formData.summary) + + "&date=" + + encodeURIComponent(formData.date) + + "&allday=" + + (formData.allDay ? "1" : "0") + + "&start_time=" + + encodeURIComponent(formData.startTime) + + "&end_time=" + + encodeURIComponent(formData.endTime) + + "&location=" + + encodeURIComponent(formData.location) + + "&description=" + + encodeURIComponent(formData.description) + + "&slot=" + + encodeURIComponent(formData.slot) + + "§ok=" + + encodeURIComponent(getSecurityToken(saveBtn)); - if (mode === 'edit') { - params += '&uid=' + encodeURIComponent(saveBtn.getAttribute('data-uid') || ''); - params += '&recurrence=' + encodeURIComponent(saveBtn.getAttribute('data-recurrence') || ''); - params += '&scope=' + encodeURIComponent(scope); + if (mode === "edit") { + params += + "&uid=" + encodeURIComponent(saveBtn.getAttribute("data-uid") || ""); + params += + "&recurrence=" + + encodeURIComponent(saveBtn.getAttribute("data-recurrence") || ""); + params += "&scope=" + encodeURIComponent(scope); } var xhr = new XMLHttpRequest(); - xhr.open('POST', getAjaxUrl(), true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.open("POST", getAjaxUrl(), true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); if (result.ok) { - PopupUI.close(); - showNotification(result.message || 'Saved', 'success'); + getDialog().close(); + showNotification(result.message || "Saved", "success"); window.location.reload(); } else { - showNotification(result.error || 'Save failed', 'error'); + showNotification(result.error || "Save failed", "error"); saveBtn.disabled = false; - saveBtn.textContent = 'Save'; + saveBtn.textContent = "Save"; } } catch (e) { - showNotification('Invalid response', 'error'); + showNotification("Invalid response", "error"); saveBtn.disabled = false; - saveBtn.textContent = 'Save'; + saveBtn.textContent = "Save"; } }; xhr.onerror = function () { - showNotification('Network error', 'error'); + showNotification("Network error", "error"); saveBtn.disabled = false; - saveBtn.textContent = 'Save'; + saveBtn.textContent = "Save"; }; xhr.send(params); } - return { openCreate: openCreate, openEdit: openEdit, save: save, submitSave: submitSave }; + return { + openCreate: openCreate, + openEdit: openEdit, + save: save, + submitSave: submitSave, + }; })(); // ============================================================ // Event Deletion // ============================================================ var EventDelete = (function () { - function confirmDelete(btn) { - var uid = btn.getAttribute('data-uid') || ''; - var slot = btn.getAttribute('data-slot') || ''; - var recurrence = btn.getAttribute('data-recurrence') || ''; - var dateIso = btn.getAttribute('data-date') || ''; + var uid = btn.getAttribute("data-uid") || ""; + var slot = btn.getAttribute("data-slot") || ""; + var recurrence = btn.getAttribute("data-recurrence") || ""; + var dateIso = btn.getAttribute("data-date") || ""; if (!uid) return; @@ -630,89 +758,129 @@ } // Simple confirmation - var html = '
    '; - html += ''; - html += '

    Delete Event

    '; - html += '

    Are you sure you want to delete this event?

    '; - html += '
    '; - html += ' '; - html += ''; - html += '
    '; + var html = '
    '; + html += + ''; + html += '

    Delete Event

    '; + html += "

    Are you sure you want to delete this event?

    "; + html += '
    '; + html += + ' "; + html += + ''; + html += "
    "; - PopupUI.show(html); + getDialog().show(html); } function showRecurrenceDeleteDialog(uid, slot, recurrence, dateIso) { - var html = '
    '; - html += ''; - html += '

    Delete Recurring Event

    '; - html += '

    This is a recurring event. What would you like to delete?

    '; - html += '
    '; + var html = '
    '; + html += + ''; + html += '

    Delete Recurring Event

    '; + html += + "

    This is a recurring event. What would you like to delete?

    "; + html += + '
    '; - var baseAttrs = ' data-uid="' + escapeHtml(uid) + '"' - + ' data-slot="' + escapeHtml(slot) + '"' - + ' data-recurrence="' + escapeHtml(recurrence) + '"' - + ' data-date="' + escapeHtml(dateIso) + '"'; + var baseAttrs = + ' data-uid="' + + escapeHtml(uid) + + '"' + + ' data-slot="' + + escapeHtml(slot) + + '"' + + ' data-recurrence="' + + escapeHtml(recurrence) + + '"' + + ' data-date="' + + escapeHtml(dateIso) + + '"'; - html += ' '; - html += ' '; - html += ' '; - html += ''; - html += '
    '; + html += + ' '; + html += + ' '; + html += + ' '; + html += + ''; + html += "
    "; - PopupUI.show(html); + getDialog().show(html); } function executeDelete(btn) { - var uid = btn.getAttribute('data-uid') || ''; - var slot = btn.getAttribute('data-slot') || ''; - var recurrence = btn.getAttribute('data-recurrence') || ''; - var dateIso = btn.getAttribute('data-date') || ''; - var scope = btn.getAttribute('data-scope') || 'all'; + var uid = btn.getAttribute("data-uid") || ""; + var slot = btn.getAttribute("data-slot") || ""; + var recurrence = btn.getAttribute("data-recurrence") || ""; + var dateIso = btn.getAttribute("data-date") || ""; + var scope = btn.getAttribute("data-scope") || "all"; btn.disabled = true; - btn.textContent = 'Deleting...'; + btn.textContent = "Deleting..."; - var params = 'call=luxtools_calendar_event' - + '&action=delete' - + '&uid=' + encodeURIComponent(uid) - + '&slot=' + encodeURIComponent(slot) - + '&recurrence=' + encodeURIComponent(recurrence) - + '&date=' + encodeURIComponent(dateIso) - + '&scope=' + encodeURIComponent(scope) - + '§ok=' + encodeURIComponent(getSecurityToken(btn)); + var params = + "call=luxtools_calendar_event" + + "&action=delete" + + "&uid=" + + encodeURIComponent(uid) + + "&slot=" + + encodeURIComponent(slot) + + "&recurrence=" + + encodeURIComponent(recurrence) + + "&date=" + + encodeURIComponent(dateIso) + + "&scope=" + + encodeURIComponent(scope) + + "§ok=" + + encodeURIComponent(getSecurityToken(btn)); var xhr = new XMLHttpRequest(); - xhr.open('POST', getAjaxUrl(), true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.open("POST", getAjaxUrl(), true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function () { try { var result = JSON.parse(xhr.responseText); if (result.ok) { - PopupUI.close(); - showNotification(result.message || 'Deleted', 'success'); + getDialog().close(); + showNotification(result.message || "Deleted", "success"); window.location.reload(); } else { - showNotification(result.error || 'Delete failed', 'error'); + showNotification(result.error || "Delete failed", "error"); btn.disabled = false; - btn.textContent = btn.getAttribute('data-scope') === 'all' ? 'Delete' : btn.textContent; + btn.textContent = + btn.getAttribute("data-scope") === "all" + ? "Delete" + : btn.textContent; } } catch (e) { - showNotification('Invalid response', 'error'); + showNotification("Invalid response", "error"); btn.disabled = false; } }; xhr.onerror = function () { - showNotification('Network error', 'error'); + showNotification("Network error", "error"); btn.disabled = false; }; xhr.send(params); @@ -725,18 +893,26 @@ // Maintenance Task Actions // ============================================================ var MaintenanceTasks = (function () { - function handleAction(button) { - var action = button.getAttribute('data-action'); + var action = button.getAttribute("data-action"); if (!action) return; - var item = button.closest('[data-task-uid]'); - if (!item) item = button.closest('[data-uid]'); + var item = button.closest("[data-task-uid]"); + if (!item) item = button.closest("[data-uid]"); if (!item) return; - var uid = item.getAttribute('data-task-uid') || item.getAttribute('data-uid') || ''; - var date = item.getAttribute('data-task-date') || item.getAttribute('data-date') || ''; - var recurrence = item.getAttribute('data-task-recurrence') || item.getAttribute('data-recurrence') || ''; + var uid = + item.getAttribute("data-task-uid") || + item.getAttribute("data-uid") || + ""; + var date = + item.getAttribute("data-task-date") || + item.getAttribute("data-date") || + ""; + var recurrence = + item.getAttribute("data-task-recurrence") || + item.getAttribute("data-recurrence") || + ""; if (!uid || !date) return; @@ -744,50 +920,59 @@ var sectok = getSecurityToken(item); button.disabled = true; - button.textContent = '...'; + button.textContent = "..."; - var params = 'call=luxtools_maintenance_task' - + '&action=' + encodeURIComponent(action) - + '&uid=' + encodeURIComponent(uid) - + '&date=' + encodeURIComponent(date) - + '&recurrence=' + encodeURIComponent(recurrence) - + '§ok=' + encodeURIComponent(sectok); + var params = + "call=luxtools_maintenance_task" + + "&action=" + + encodeURIComponent(action) + + "&uid=" + + encodeURIComponent(uid) + + "&date=" + + encodeURIComponent(date) + + "&recurrence=" + + encodeURIComponent(recurrence) + + "§ok=" + + encodeURIComponent(sectok); var xhr = new XMLHttpRequest(); - xhr.open('POST', ajaxUrl, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + xhr.open("POST", ajaxUrl, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function () { var result; - try { result = JSON.parse(xhr.responseText); } - catch (e) { result = { ok: false, error: 'Invalid response' }; } + try { + result = JSON.parse(xhr.responseText); + } catch (e) { + result = { ok: false, error: "Invalid response" }; + } if (result.ok) { - if (action === 'complete') { - item.classList.add('luxtools-task-completed'); - button.textContent = 'Reopen'; - button.setAttribute('data-action', 'reopen'); + if (action === "complete") { + item.classList.add("luxtools-task-completed"); + button.textContent = "Reopen"; + button.setAttribute("data-action", "reopen"); } else { - item.classList.remove('luxtools-task-completed'); - item.style.opacity = '1'; - button.textContent = 'Complete'; - button.setAttribute('data-action', 'complete'); + item.classList.remove("luxtools-task-completed"); + item.style.opacity = "1"; + button.textContent = "Complete"; + button.setAttribute("data-action", "complete"); } button.disabled = false; if (result.remoteOk === false && result.remoteError) { - showNotification(result.remoteError, 'warning'); + showNotification(result.remoteError, "warning"); } } else { - showNotification(result.error || 'Action failed', 'error'); - button.textContent = action === 'complete' ? 'Complete' : 'Reopen'; + showNotification(result.error || "Action failed", "error"); + button.textContent = action === "complete" ? "Complete" : "Reopen"; button.disabled = false; } }; xhr.onerror = function () { - showNotification('Network error', 'error'); - button.textContent = action === 'complete' ? 'Complete' : 'Reopen'; + showNotification("Network error", "error"); + button.textContent = action === "complete" ? "Complete" : "Reopen"; button.disabled = false; }; @@ -800,129 +985,184 @@ // ============================================================ // Event Delegation // ============================================================ - document.addEventListener('click', function (e) { - var target = e.target; + document.addEventListener( + "click", + function (e) { + var target = e.target; - // Maintenance task action buttons (day pages) - if (target.classList && target.classList.contains('luxtools-task-action')) { - e.preventDefault(); - MaintenanceTasks.handleAction(target); - return; - } - - // Maintenance task complete buttons (syntax plugin list) - if (target.classList && target.classList.contains('luxtools-task-complete-btn')) { - e.preventDefault(); - MaintenanceTasks.handleAction(target); - return; - } - - // Event form save - if (target.classList && target.classList.contains('luxtools-event-form-save')) { - e.preventDefault(); - EventForm.save(target); - return; - } - - // Event form cancel - if (target.classList && target.classList.contains('luxtools-event-form-cancel')) { - e.preventDefault(); - PopupUI.close(); - return; - } - - // Event create button (from day popup) - if (target.classList && target.classList.contains('luxtools-event-create-btn')) { - e.preventDefault(); - EventForm.openCreate(target.getAttribute('data-date') || ''); - return; - } - - // Event edit button - if (target.classList && target.classList.contains('luxtools-event-edit-btn')) { - e.preventDefault(); - EventForm.openEdit({ - uid: target.getAttribute('data-uid') || '', - slot: target.getAttribute('data-slot') || '', - recurrence: target.getAttribute('data-recurrence') || '', - date: target.getAttribute('data-date') || '', - summary: target.getAttribute('data-summary') || '', - start: target.getAttribute('data-start') || '', - end: target.getAttribute('data-end') || '', - location: target.getAttribute('data-location') || '', - description: target.getAttribute('data-description') || '', - allDay: target.getAttribute('data-allday') === '1', - }); - return; - } - - // Event delete button - if (target.classList && target.classList.contains('luxtools-event-delete-btn')) { - e.preventDefault(); - EventDelete.confirmDelete(target); - return; - } - - // Confirm delete button - if (target.classList && target.classList.contains('luxtools-event-confirm-delete')) { - e.preventDefault(); - EventDelete.executeDelete(target); - return; - } - - // Confirm edit scope button (recurring event edit) - if (target.classList && target.classList.contains('luxtools-event-confirm-edit-scope')) { - e.preventDefault(); - if (_pendingEditFormData) { - EventForm.submitSave(target, _pendingEditFormData, target.getAttribute('data-scope') || 'all'); - _pendingEditFormData = null; + // Maintenance task action buttons (day pages) + if ( + target.classList && + target.classList.contains("luxtools-task-action") + ) { + e.preventDefault(); + MaintenanceTasks.handleAction(target); + return; } - return; - } - // Event popup: clicking an event item within the day popup - // (items inside the popup that have data-luxtools-event) - var dayPopupEvent = target.closest ? target.closest('.luxtools-day-popup-event-item[data-luxtools-event]') : null; - if (dayPopupEvent) { - if (target.tagName === 'BUTTON' || (target.closest && target.closest('button'))) return; - e.preventDefault(); - EventPopup.open(dayPopupEvent, { hideDatetime: true }); - return; - } + // Maintenance task complete buttons (syntax plugin list) + if ( + target.classList && + target.classList.contains("luxtools-task-complete-btn") + ) { + e.preventDefault(); + MaintenanceTasks.handleAction(target); + return; + } - // Event popup: find closest element with data-luxtools-event - var eventEl = target.closest ? target.closest('[data-luxtools-event]') : null; - if (!eventEl) { - var el = target; - while (el && el !== document) { - if (el.getAttribute && el.getAttribute('data-luxtools-event') === '1') { - eventEl = el; - break; + // Event form save + if ( + target.classList && + target.classList.contains("luxtools-event-form-save") + ) { + e.preventDefault(); + EventForm.save(target); + return; + } + + // Event form cancel + if ( + target.classList && + target.classList.contains("luxtools-event-form-cancel") + ) { + e.preventDefault(); + getDialog().close(); + return; + } + + // Event create button (from day popup) + if ( + target.classList && + target.classList.contains("luxtools-event-create-btn") + ) { + e.preventDefault(); + EventForm.openCreate(target.getAttribute("data-date") || ""); + return; + } + + // Event edit button + if ( + target.classList && + target.classList.contains("luxtools-event-edit-btn") + ) { + e.preventDefault(); + EventForm.openEdit({ + uid: target.getAttribute("data-uid") || "", + slot: target.getAttribute("data-slot") || "", + recurrence: target.getAttribute("data-recurrence") || "", + date: target.getAttribute("data-date") || "", + summary: target.getAttribute("data-summary") || "", + start: target.getAttribute("data-start") || "", + end: target.getAttribute("data-end") || "", + location: target.getAttribute("data-location") || "", + description: target.getAttribute("data-description") || "", + allDay: target.getAttribute("data-allday") === "1", + }); + return; + } + + // Event delete button + if ( + target.classList && + target.classList.contains("luxtools-event-delete-btn") + ) { + e.preventDefault(); + EventDelete.confirmDelete(target); + return; + } + + // Confirm delete button + if ( + target.classList && + target.classList.contains("luxtools-event-confirm-delete") + ) { + e.preventDefault(); + EventDelete.executeDelete(target); + return; + } + + // Confirm edit scope button (recurring event edit) + if ( + target.classList && + target.classList.contains("luxtools-event-confirm-edit-scope") + ) { + e.preventDefault(); + if (_pendingEditFormData) { + EventForm.submitSave( + target, + _pendingEditFormData, + target.getAttribute("data-scope") || "all", + ); + _pendingEditFormData = null; } - el = el.parentNode; + return; } - } - if (eventEl) { - if (target.tagName === 'BUTTON' || (target.closest && target.closest('button'))) return; + // Event popup: clicking an event item within the day popup + // (items inside the popup that have data-luxtools-event) + var dayPopupEvent = target.closest + ? target.closest(".luxtools-day-popup-event-item[data-luxtools-event]") + : null; + if (dayPopupEvent) { + if ( + target.tagName === "BUTTON" || + (target.closest && target.closest("button")) + ) + return; + e.preventDefault(); + EventPopup.open(dayPopupEvent, { hideDatetime: true }); + return; + } - // Check if this event is inside a day cell (calendar context) - var dayCell = eventEl.closest ? eventEl.closest('td[data-luxtools-day="1"]') : null; - e.preventDefault(); - EventPopup.open(eventEl, { hideDatetime: !!dayCell }); - return; - } + // Event popup: find closest element with data-luxtools-event + var eventEl = target.closest + ? target.closest("[data-luxtools-event]") + : null; + if (!eventEl) { + var el = target; + while (el && el !== document) { + if ( + el.getAttribute && + el.getAttribute("data-luxtools-event") === "1" + ) { + eventEl = el; + break; + } + el = el.parentNode; + } + } - // Day cell click: open day popup when clicking empty space - var clickedDayCell = target.closest ? target.closest('td[data-luxtools-day="1"]') : null; - if (clickedDayCell) { - // Don't interfere with link clicks inside the cell - if (target.tagName === 'A' || (target.closest && target.closest('a'))) return; - e.preventDefault(); - DayPopup.open(clickedDayCell); - return; - } - }, false); + if (eventEl) { + if ( + target.tagName === "BUTTON" || + (target.closest && target.closest("button")) + ) + return; + + // Check if this event is inside a day cell (calendar context) + var dayCell = eventEl.closest + ? eventEl.closest('td[data-luxtools-day="1"]') + : null; + e.preventDefault(); + EventPopup.open(eventEl, { hideDatetime: !!dayCell }); + return; + } + + // Day cell click: open day popup when clicking empty space + var clickedDayCell = target.closest + ? target.closest('td[data-luxtools-day="1"]') + : null; + if (clickedDayCell) { + // Don't interfere with link clicks inside the cell + if (target.tagName === "A" || (target.closest && target.closest("a"))) + return; + e.preventDefault(); + DayPopup.open(clickedDayCell); + return; + } + }, + false, + ); Luxtools.EventPopup = EventPopup; Luxtools.DayPopup = DayPopup; diff --git a/js/main.js b/js/main.js index 61a629e..84cf290 100644 --- a/js/main.js +++ b/js/main.js @@ -1,7 +1,7 @@ /* global window, document */ (function () { - 'use strict'; + "use strict"; var Luxtools = window.Luxtools || (window.Luxtools = {}); var Lightbox = Luxtools.Lightbox; @@ -16,7 +16,7 @@ function findOpenElement(target) { var el = target; while (el && el !== document) { - if (el.classList && el.classList.contains('luxtools-open')) return el; + if (el.classList && el.classList.contains("luxtools-open")) return el; el = el.parentNode; } return null; @@ -25,7 +25,8 @@ function findGalleryItem(target) { var el = target; while (el && el !== document) { - if (el.classList && el.classList.contains('luxtools-gallery-item')) return el; + if (el.classList && el.classList.contains("luxtools-gallery-item")) + return el; el = el.parentNode; } return null; @@ -35,7 +36,9 @@ // Image gallery lightbox: intercept clicks so we don't navigate away. var galleryItem = findGalleryItem(event.target); if (galleryItem && Lightbox && Lightbox.open) { - var gallery = galleryItem.closest ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') : null; + var gallery = galleryItem.closest + ? galleryItem.closest('div.luxtools-gallery[data-luxtools-gallery="1"]') + : null; if (gallery) { event.preventDefault(); Lightbox.open(gallery, galleryItem); @@ -47,49 +50,56 @@ if (!el) return; // {{open>...}} renders as a link; avoid jumping to '#'. - if (el.tagName && el.tagName.toLowerCase() === 'a') { + if (el.tagName && el.tagName.toLowerCase() === "a") { event.preventDefault(); } - var raw = el.getAttribute('data-path') || ''; + var raw = el.getAttribute("data-path") || ""; if (!raw) return; if (!OpenService || !OpenService.openViaService) return; // Prefer local client service. - OpenService.openViaService(el, raw) - .catch(function (err) { - // If the browser blocks the request before it reaches localhost (mixed-content, - // extensions, stricter CORS handling), fall back to a no-CORS GET ping. - if (OpenService && OpenService.pingOpenViaImage) { - OpenService.pingOpenViaImage(el, raw); - } + OpenService.openViaService(el, raw).catch(function (err) { + // If the browser blocks the request before it reaches localhost (mixed-content, + // extensions, stricter CORS handling), fall back to a no-CORS GET ping. + if (OpenService && OpenService.pingOpenViaImage) { + OpenService.pingOpenViaImage(el, raw); + } - // Fallback to old behavior (often blocked in modern browsers). - var url = OpenService && OpenService.normalizeToFileUrl ? OpenService.normalizeToFileUrl(raw) : ''; - if (!url) return; - console.warn('Local client service failed, falling back to file:// navigation:', err); + // Fallback to old behavior (often blocked in modern browsers). + var url = + OpenService && OpenService.normalizeToFileUrl + ? OpenService.normalizeToFileUrl(raw) + : ""; + if (!url) return; + console.warn( + "Local client service failed, falling back to file:// navigation:", + err, + ); + try { + window.open(url, "_blank", "noopener"); + } catch (e) { try { - window.open(url, '_blank', 'noopener'); - } catch (e) { - try { - window.location.href = url; - } catch (e2) { - console.error('Failed to open file URL:', e2); - } + window.location.href = url; + } catch (e2) { + console.error("Failed to open file URL:", e2); } - }); + } + }); } function initChronologicalEventTimes() { - var nodes = document.querySelectorAll('.luxtools-event-time[data-luxtools-start]'); + var nodes = document.querySelectorAll( + ".luxtools-event-time[data-luxtools-start]", + ); if (!nodes || nodes.length === 0) return; var formatter; try { - formatter = new Intl.DateTimeFormat('de-DE', { - hour: '2-digit', - minute: '2-digit', - hour12: false + formatter = new Intl.DateTimeFormat("de-DE", { + hour: "2-digit", + minute: "2-digit", + hour12: false, }); } catch (e) { formatter = null; @@ -97,7 +107,7 @@ for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; - var raw = node.getAttribute('data-luxtools-start') || ''; + var raw = node.getAttribute("data-luxtools-start") || ""; if (!raw) continue; var date = new Date(raw); @@ -107,9 +117,9 @@ if (formatter) { label = formatter.format(date); } else { - var hh = String(date.getHours()).padStart(2, '0'); - var mm = String(date.getMinutes()).padStart(2, '0'); - label = hh + ':' + mm; + var hh = String(date.getHours()).padStart(2, "0"); + var mm = String(date.getMinutes()).padStart(2, "0"); + label = hh + ":" + mm; } node.textContent = label; @@ -120,123 +130,185 @@ // Purge Cache Dialog // ============================================================ function initPurgeCacheDialog() { - var $link = jQuery('a.luxtools-invalidate-cache'); - if ($link.length === 0) return; + document.addEventListener( + "click", + function (e) { + var link = e.target.closest + ? e.target.closest("a.luxtools-invalidate-cache") + : null; + if (!link) return; - $link.on('click.luxtools', function (e) { - e.preventDefault(); + e.preventDefault(); - var href = $link.attr('href') || ''; - var lang = (window.LANG && window.LANG.plugins && window.LANG.plugins.luxtools) - ? window.LANG.plugins.luxtools - : {}; + var href = link.getAttribute("href") || ""; + var lang = + window.LANG && window.LANG.plugins && window.LANG.plugins.luxtools + ? window.LANG.plugins.luxtools + : {}; - var $dialog = jQuery( - '
    ' + - '

    ' + (lang.cache_purge_dialog_intro || 'The DokuWiki cache will always be purged. Optionally also purge the luxtools-specific caches:') + '

    ' + - '

    ' + - '

    ' + - '
    ' - ); + var html = '
    '; + html += + ''; + html += + '

    ' + + (lang.cache_purge_dialog_title || "Purge Cache") + + "

    "; + html += + "

    " + + (lang.cache_purge_dialog_intro || + "The DokuWiki cache will always be purged. Optionally also purge the luxtools-specific caches:") + + "

    "; + html += + '

    "; + html += + '

    "; + html += '
    '; + html += + ' "; + html += + '"; + html += "
    "; + html += "
    "; - $dialog.dialog({ - title: lang.cache_purge_dialog_title || 'Purge Cache', - modal: true, - width: 420, - buttons: [ - { - text: lang.cache_purge_cancel || 'Cancel', - click: function () { $dialog.dialog('close'); } - }, - { - text: lang.cache_purge_confirm || 'Purge Cache', - click: function () { - var url = href; - if ($dialog.find('#luxtools-purge-pagelinks').prop('checked')) { - url += '&luxtools_purge_pagelinks=1'; - } - if ($dialog.find('#luxtools-purge-thumbs').prop('checked')) { - url += '&luxtools_purge_thumbs=1'; - } - $dialog.dialog('close'); - window.location.href = url; - } - } - ], - close: function () { - jQuery(this).dialog('destroy').remove(); + Luxtools.Dialog.show(html); + + var container = Luxtools.Dialog.getContainer(); + + var cancelBtn = container.querySelector(".luxtools-purge-cancel"); + if (cancelBtn) { + cancelBtn.addEventListener("click", function () { + Luxtools.Dialog.close(); + }); } - }); - }); + + var confirmBtn = container.querySelector(".luxtools-purge-confirm"); + if (confirmBtn) { + confirmBtn.addEventListener("click", function () { + var url = href; + var plCheck = container.querySelector("#luxtools-purge-pagelinks"); + var thCheck = container.querySelector("#luxtools-purge-thumbs"); + if (plCheck && plCheck.checked) { + url += "&luxtools_purge_pagelinks=1"; + } + if (thCheck && thCheck.checked) { + url += "&luxtools_purge_thumbs=1"; + } + Luxtools.Dialog.close(); + window.location.href = url; + }); + } + }, + false, + ); } // ============================================================ // Calendar Sync Button (syntax widget) // ============================================================ function initCalendarSyncButtons() { - document.addEventListener('click', function (e) { - var btn = e.target; - if (!btn || !btn.classList || !btn.classList.contains('luxtools-calendar-sync-btn')) return; + document.addEventListener( + "click", + function (e) { + var btn = e.target; + if ( + !btn || + !btn.classList || + !btn.classList.contains("luxtools-calendar-sync-btn") + ) + return; - e.preventDefault(); + e.preventDefault(); - var ajaxUrl = btn.getAttribute('data-luxtools-ajax-url') || ''; - var sectok = btn.getAttribute('data-luxtools-sectok') || ''; - if (!ajaxUrl) return; + var ajaxUrl = btn.getAttribute("data-luxtools-ajax-url") || ""; + var sectok = btn.getAttribute("data-luxtools-sectok") || ""; + if (!ajaxUrl) return; - var status = btn.parentNode ? btn.parentNode.querySelector('.luxtools-calendar-sync-status') : null; + var status = btn.parentNode + ? btn.parentNode.querySelector(".luxtools-calendar-sync-status") + : null; - btn.disabled = true; - if (status) { - status.textContent = 'Syncing...'; - status.style.color = ''; - } - - var xhr = new XMLHttpRequest(); - xhr.open('POST', ajaxUrl, true); - xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - xhr.onload = function () { - btn.disabled = false; - try { - var r = JSON.parse(xhr.responseText); - if (status) { - status.textContent = r.message || (r.ok ? 'Done' : 'Failed'); - status.style.color = r.ok ? 'green' : 'red'; - } - } catch (ex) { - if (status) { - status.textContent = 'Error'; - status.style.color = 'red'; - } - } - }; - xhr.onerror = function () { - btn.disabled = false; + btn.disabled = true; if (status) { - status.textContent = 'Network error'; - status.style.color = 'red'; + status.textContent = "Syncing..."; + status.style.color = ""; } - }; - xhr.send('call=luxtools_calendar_sync§ok=' + encodeURIComponent(sectok)); - }, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", ajaxUrl, true); + xhr.setRequestHeader( + "Content-Type", + "application/x-www-form-urlencoded", + ); + xhr.onload = function () { + btn.disabled = false; + try { + var r = JSON.parse(xhr.responseText); + if (status) { + status.textContent = r.message || (r.ok ? "Done" : "Failed"); + status.style.color = r.ok ? "green" : "red"; + } + } catch (ex) { + if (status) { + status.textContent = "Error"; + status.style.color = "red"; + } + } + }; + xhr.onerror = function () { + btn.disabled = false; + if (status) { + status.textContent = "Network error"; + status.style.color = "red"; + } + }; + xhr.send( + "call=luxtools_calendar_sync§ok=" + encodeURIComponent(sectok), + ); + }, + false, + ); } // ============================================================ // Initialize // ============================================================ - document.addEventListener('click', onClick, false); - document.addEventListener('DOMContentLoaded', function () { - if (GalleryThumbnails && GalleryThumbnails.init) GalleryThumbnails.init(); - initChronologicalEventTimes(); - if (CalendarWidget && CalendarWidget.init) CalendarWidget.init(); - initPurgeCacheDialog(); - initCalendarSyncButtons(); - }, false); - document.addEventListener('DOMContentLoaded', function () { - if (Scratchpads && Scratchpads.init) Scratchpads.init(); - }, false); + document.addEventListener("click", onClick, false); + document.addEventListener( + "DOMContentLoaded", + function () { + if (GalleryThumbnails && GalleryThumbnails.init) GalleryThumbnails.init(); + initChronologicalEventTimes(); + if (CalendarWidget && CalendarWidget.init) CalendarWidget.init(); + initPurgeCacheDialog(); + initCalendarSyncButtons(); + }, + false, + ); + document.addEventListener( + "DOMContentLoaded", + function () { + if (Scratchpads && Scratchpads.init) Scratchpads.init(); + }, + false, + ); Luxtools.initChronologicalEventTimes = initChronologicalEventTimes; })(); diff --git a/style.css b/style.css index d9336e5..d91986d 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,5 @@ +/* Dialog infrastructure styles are in dialog.css, loaded via CSS_STYLES_INCLUDED hook in action.php */ + /* luxtools plugin styles * Keep this minimal and scoped to the plugin container. */ @@ -7,7 +9,6 @@ div.luxtools-plugin table thead tr:hover > * { background-color: @ini_background_alt !important; } - /* "Open Location" row above the header should be visually smaller. */ div.luxtools-plugin table thead tr.luxtools-openlocation-row td { font-size: 80%; @@ -229,8 +230,12 @@ div.plugin_luxtools_admin form.plugin_luxtools_admin_form label.block > br { } div.plugin_luxtools_admin form.plugin_luxtools_admin_form textarea.edit, -div.plugin_luxtools_admin form.plugin_luxtools_admin_form input[type="text"].edit, -div.plugin_luxtools_admin form.plugin_luxtools_admin_form input[type="number"].edit, +div.plugin_luxtools_admin + form.plugin_luxtools_admin_form + input[type="text"].edit, +div.plugin_luxtools_admin + form.plugin_luxtools_admin_form + input[type="number"].edit, div.plugin_luxtools_admin form.plugin_luxtools_admin_form select { flex: 1 1 auto; margin-left: auto; @@ -249,7 +254,10 @@ div.plugin_luxtools_admin form.plugin_luxtools_admin_form select { } /* Checkbox controls: keep them in the control column, left-aligned. */ -div.plugin_luxtools_admin form.plugin_luxtools_admin_form label.block input[type="checkbox"] { +div.plugin_luxtools_admin + form.plugin_luxtools_admin_form + label.block + input[type="checkbox"] { margin-left: 0; align-self: center; } @@ -265,8 +273,12 @@ div.plugin_luxtools_admin form.plugin_luxtools_admin_form label.block input[type } div.plugin_luxtools_admin form.plugin_luxtools_admin_form textarea.edit, - div.plugin_luxtools_admin form.plugin_luxtools_admin_form input[type="text"].edit, - div.plugin_luxtools_admin form.plugin_luxtools_admin_form input[type="number"].edit, + div.plugin_luxtools_admin + form.plugin_luxtools_admin_form + input[type="text"].edit, + div.plugin_luxtools_admin + form.plugin_luxtools_admin_form + input[type="number"].edit, div.plugin_luxtools_admin form.plugin_luxtools_admin_form select { width: 100%; max-width: 100%; @@ -386,12 +398,12 @@ html.luxtools-noscroll body { } .luxtools-lightbox button.luxtools-lightbox-zone-prev::after { - content: '‹'; + content: "‹"; left: 0.35em; } .luxtools-lightbox button.luxtools-lightbox-zone-next::after { - content: '›'; + content: "›"; right: 0.35em; } @@ -416,7 +428,7 @@ html.luxtools-noscroll body { .luxtools-lightbox button.luxtools-lightbox-close:hover, .luxtools-lightbox button.luxtools-lightbox-close:focus-visible { - background: rgba(0, 0, 0, 0.60); + background: rgba(0, 0, 0, 0.6); border-radius: 999px; } @@ -432,7 +444,10 @@ html.luxtools-noscroll body { .luxtools-grouping { display: grid; - grid-template-columns: repeat(var(--luxtools-grouping-cols, 2), minmax(0, 1fr)); + grid-template-columns: repeat( + var(--luxtools-grouping-cols, 2), + minmax(0, 1fr) + ); gap: var(--luxtools-grouping-gap, 0); justify-content: var(--luxtools-grouping-justify, start); align-items: var(--luxtools-grouping-align, start); @@ -604,7 +619,10 @@ div.luxtools-calendar td.luxtools-calendar-day-today { } 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 { +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > span.curid + > a { display: flex; align-items: center; justify-content: center; @@ -617,33 +635,77 @@ div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > sp padding: 0.1em 0; } -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a.wikilink2:link, -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 { +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > a.wikilink2:link, +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; border-bottom: 0; - } +} - -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:hover, -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:focus, -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > a:active, -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 { +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > a:hover, +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > a:focus, +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > a:active, +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; border-bottom: 0; box-shadow: none; } -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > a, -div.luxtools-calendar.luxtools-calendar-size-small td.luxtools-calendar-day > span.curid > 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, +div.luxtools-calendar.luxtools-calendar-size-small + td.luxtools-calendar-day + > span.curid + > 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 { font-weight: bold; text-decoration: underline; border-bottom: 0; @@ -654,7 +716,8 @@ div.luxtools-calendar td.luxtools-calendar-day:hover { background-color: @ini_background_alt; } -div.luxtools-calendar td.luxtools-calendar-day.luxtools-calendar-day-today:hover { +div.luxtools-calendar + td.luxtools-calendar-day.luxtools-calendar-day-today:hover { background-color: @ini_highlight; } @@ -668,7 +731,9 @@ div.luxtools-calendar td.luxtools-calendar-day { position: relative; } -div.luxtools-calendar.luxtools-calendar-size-large table.luxtools-calendar-table td { +div.luxtools-calendar.luxtools-calendar-size-large + table.luxtools-calendar-table + td { text-align: left; vertical-align: top; } @@ -677,25 +742,36 @@ 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 { +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 { +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 { +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 { +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; @@ -706,18 +782,30 @@ div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-day-number 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 { +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 { +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 { +div.luxtools-calendar.luxtools-calendar-size-large + ul.luxtools-calendar-event-list { list-style: none; margin: 0; padding: 0; @@ -735,17 +823,20 @@ div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event { cursor: pointer; } -div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event:hover { +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 { +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 { +div.luxtools-calendar.luxtools-calendar-size-large + .luxtools-calendar-event-title { flex: 1 1 auto; min-width: 0; white-space: nowrap; @@ -753,7 +844,8 @@ div.luxtools-calendar.luxtools-calendar-size-large .luxtools-calendar-event-titl text-overflow: ellipsis; } -div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event-more { +div.luxtools-calendar.luxtools-calendar-size-large + li.luxtools-calendar-event-more { border-left-color: @ini_border; justify-content: flex-end; font-style: italic; @@ -761,8 +853,10 @@ div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event-mo @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 { + 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; } @@ -807,7 +901,6 @@ div.luxtools-calendar.luxtools-calendar-size-large li.luxtools-calendar-event-mo clip-path: polygon(0 0, 0 100%, 100% 100%); } - /* ============================================================ * Chronological Events on Day Pages * ============================================================ */ @@ -833,7 +926,6 @@ div.luxtools-chronological-events li[data-luxtools-event] .luxtools-event-time { margin-right: 0.25em; } - /* ============================================================ * Maintenance Tasks * ============================================================ */ @@ -868,7 +960,6 @@ button.luxtools-task-complete-btn:disabled { cursor: wait; } - /* ============================================================ * Maintenance Task List (syntax plugin) * ============================================================ */ @@ -907,61 +998,10 @@ li.luxtools-task-overdue .luxtools-task-date { text-decoration: line-through; } - /* ============================================================ - * Event Popup + * Event Popup (content-specific styles – structural dialog + * styles live in dialog.css) * ============================================================ */ -.luxtools-event-popup-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.4); - z-index: 10000; - justify-content: center; - align-items: center; -} - -.luxtools-event-popup { - background: @ini_background; - border: 1px solid @ini_border; - border-radius: 0.4em; - padding: 1.5em; - max-width: 500px; - width: 90%; - max-height: 80vh; - overflow-y: auto; - position: relative; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); -} - -.luxtools-event-popup-close { - position: absolute; - top: 0.5em; - right: 0.75em; - background: none; - border: none; - font-size: 1.5em; - cursor: pointer; - color: @ini_text; - line-height: 1; -} - -.luxtools-event-popup-close:hover { - opacity: 0.7; -} - -.luxtools-event-popup-title { - margin: 0 0 0.75em 0; - padding-right: 1.5em; -} - -.luxtools-event-popup-field { - margin: 0.5em 0; -} - .luxtools-event-popup-description { white-space: pre-wrap; word-break: break-word; @@ -973,16 +1013,6 @@ li.luxtools-task-overdue .luxtools-task-date { font-size: 0.9em; } -/* Event popup action buttons */ -.luxtools-event-popup-actions { - margin-top: 1em; - padding-top: 0.75em; - border-top: 1px solid @ini_border; - display: flex; - flex-wrap: wrap; - gap: 0.5em; -} - .luxtools-recurrence-actions { flex-direction: column; } @@ -1051,7 +1081,6 @@ td.luxtools-calendar-day[data-luxtools-day] { cursor: pointer; } - /* ============================================================ * Notifications (fallback) * ============================================================ */