diff --git a/library/Notifications/Widget/Calendar/BaseGrid.php b/library/Notifications/Widget/Calendar/BaseGrid.php index 9c3fc3e96..f5d4b6f6f 100644 --- a/library/Notifications/Widget/Calendar/BaseGrid.php +++ b/library/Notifications/Widget/Calendar/BaseGrid.php @@ -23,9 +23,7 @@ /** * @phpstan-type GridArea array{0: int, 1: int, 2: int, 3: int} - * @phpstan-type GridContinuationType self::FROM_PREV_GRID | self::TO_NEXT_GRID | self::ACROSS_GRID - * @phpstan-type EdgeContinuationType self::ACROSS_LEFT_EDGE | self::ACROSS_RIGHT_EDGE | self::ACROSS_BOTH_EDGES - * @phpstan-type ContinuationType GridContinuationType | EdgeContinuationType + * @phpstan-import-type ContinuationType from Entry */ abstract class BaseGrid extends BaseHtmlElement { @@ -37,24 +35,6 @@ abstract class BaseGrid extends BaseHtmlElement /** @var string The chronological order of entries is oriented vertically */ protected const VERTICAL_FLOW_OF_TIME = 'vertical-flow'; - /** @var string Continuation of an entry that started on the previous grid */ - protected const FROM_PREV_GRID = 'from-prev-grid'; - - /** @var string Continuation of an entry that continues on the next grid */ - protected const TO_NEXT_GRID = 'to-next-grid'; - - /** @var string Continuation of an entry that started on the previous grid and continues on the next */ - protected const ACROSS_GRID = 'across-grid'; - - /** @var string Continuation of an entry that started on a previous grid row */ - protected const ACROSS_LEFT_EDGE = 'across-left-edge'; - - /** @var string Continuation of an entry that continues on the next grid row */ - protected const ACROSS_RIGHT_EDGE = 'across-right-edge'; - - /** @var string Continuation of an entry that started on a previous grid row and continues on the next */ - protected const ACROSS_BOTH_EDGES = 'across-both-edges'; - /** @var int Return this in {@see getSectionsPerStep} to signal an infinite number of sections */ protected const INFINITE = 0; @@ -223,7 +203,7 @@ public function getExtraEntryCount(DateTime $date): int * * @param Traversable $entries * - * @return Generator + * @return Generator */ final protected function yieldFlowingEntries(Traversable $entries): Generator { @@ -370,21 +350,22 @@ final protected function yieldFlowingEntries(Traversable $entries): Generator $backward = $continuationType || $fromPrevGrid; $forward = ! $isLastRow || $toNextGrid; if ($backward && $forward) { - $continuationType = self::ACROSS_BOTH_EDGES; + $entry->setContinuationType(Entry::ACROSS_BOTH_EDGES); } elseif ($backward) { - $continuationType = self::ACROSS_LEFT_EDGE; + $entry->setContinuationType(Entry::ACROSS_LEFT_EDGE); } elseif ($forward) { - $continuationType = self::ACROSS_RIGHT_EDGE; + $entry->setContinuationType(Entry::ACROSS_RIGHT_EDGE); } elseif ($fromPrevGrid && $toNextGrid) { - $continuationType = self::ACROSS_GRID; + $entry->setContinuationType(Entry::ACROSS_GRID); } elseif ($fromPrevGrid) { - $continuationType = self::FROM_PREV_GRID; + $entry->setContinuationType(Entry::FROM_PREV_GRID); } elseif ($toNextGrid) { - $continuationType = self::TO_NEXT_GRID; + $entry->setContinuationType(Entry::TO_NEXT_GRID); } - yield [$gridArea, $continuationType] => $entry; + yield $gridArea => $entry; + $continuationType = $entry->getContinuationType(); $fromPrevGrid = false; $remainingRows -= 1; } @@ -412,7 +393,7 @@ final protected function yieldFlowingEntries(Traversable $entries): Generator * * @param Traversable $entries * - * @return Generator + * @return Generator */ final protected function yieldFixedEntries(Traversable $entries): Generator { @@ -478,16 +459,14 @@ final protected function yieldFixedEntries(Traversable $entries): Generator $fromPrevGrid = $gridStartsAt > $entry->getStart(); $toNextGrid = $gridEndsAt < $entry->getEnd(); if ($fromPrevGrid && $toNextGrid) { - $continuationType = self::ACROSS_GRID; + $entry->setContinuationType(Entry::ACROSS_GRID); } elseif ($fromPrevGrid) { - $continuationType = self::FROM_PREV_GRID; + $entry->setContinuationType(Entry::FROM_PREV_GRID); } elseif ($toNextGrid) { - $continuationType = self::TO_NEXT_GRID; - } else { - $continuationType = null; + $entry->setContinuationType(Entry::TO_NEXT_GRID); } - yield [$gridArea, $continuationType] => $entry; + yield $gridArea => $entry; } $this->style->addFor($this, [ @@ -510,139 +489,17 @@ protected function assembleGridOverlay(BaseHtmlElement $overlay): void $generator = $this->yieldFixedEntries($entries); } - foreach ($generator as $data => $entry) { - [$gridArea, $continuationType] = $data; - - $gradientClass = null; - if ($continuationType === self::ACROSS_GRID || $continuationType === self::ACROSS_BOTH_EDGES) { - $gradientClass = 'two-way-gradient'; - } elseif ($continuationType === self::FROM_PREV_GRID || $continuationType === self::ACROSS_LEFT_EDGE) { - $gradientClass = 'opening-gradient'; - } elseif ($continuationType === self::TO_NEXT_GRID || $continuationType === self::ACROSS_RIGHT_EDGE) { - $gradientClass = 'ending-gradient'; - } - - $entryHtml = new HtmlElement( - 'div', - Attributes::create([ - 'class' => ['entry', $gradientClass, 'area-' . implode('-', $gridArea)], - 'data-entry-id' => $entry->getId(), - 'data-row-start' => $gridArea[0], - 'data-col-start' => $gridArea[1], - 'data-row-end' => $gridArea[2], - 'data-col-end' => $gridArea[3] - ]) - ); - - $this->style->addFor($entryHtml, [ + foreach ($generator as $gridArea => $entry) { + $this->style->addFor($entry, [ '--entry-bg' => $this->getEntryColor($entry, 10), 'grid-area' => sprintf('~"%d / %d / %d / %d"', ...$gridArea), 'border-color' => $this->getEntryColor($entry, 50) ]); - $this->assembleEntry($entryHtml, $entry, $continuationType); - - $overlay->addHtml($entryHtml); + $overlay->addHtml($entry); } } - /** - * Assemble the entry in the grid - * - * @param BaseHtmlElement $html Container where to add the entry's HTML - * @param Entry $entry The entry to assemble - * @param ?ContinuationType $continuationType Continuation type of the entry's HTML - * - * @return void - */ - protected function assembleEntry(BaseHtmlElement $html, Entry $entry, ?string $continuationType): void - { - if (($url = $entry->getUrl()) !== null) { - $entryContainer = new Link(null, $url); - $entryContainer->openInModal(); - $html->addHtml($entryContainer); - } else { - $entryContainer = $html; - } - - $title = new HtmlElement('div', Attributes::create(['class' => 'title'])); - $content = new HtmlElement( - 'div', - Attributes::create(['class' => 'content']) - ); - - $description = $entry->getDescription(); - $titleAttr = $entry->getStart()->format('H:i') - . ' | ' . $entry->getAttendee()->getName() - . ($description ? ': ' . $description : ''); - - $startText = null; - $endText = null; - - if ($continuationType === self::ACROSS_GRID) { - $startText = sprintf($this->translate('starts %s'), $entry->getStart()->format('d/m/y')); - $endText = sprintf($this->translate('ends %s'), $entry->getEnd()->format('d/m/y H:i')); - } elseif ($continuationType === self::FROM_PREV_GRID) { - $startText = sprintf($this->translate('starts %s'), $entry->getStart()->format('d/m/y')); - } elseif ($continuationType === self::TO_NEXT_GRID) { - $endText = sprintf($this->translate('ends %s'), $entry->getEnd()->format('d/m/y H:i')); - } - - if ($startText) { - $titleAttr = $startText . ' ' . $titleAttr; - } - - if ($endText) { - $titleAttr = $titleAttr . ' | ' . $endText; - } - - $content->addAttributes(['title' => $titleAttr]); - - if ($continuationType !== null) { - $title->addHtml(new HtmlElement( - 'time', - Attributes::create([ - 'datetime' => $entry->getStart()->format(DateTimeInterface::ATOM) - ]), - Text::create($entry->getStart()->format($startText ? 'd/m/y H:i' : 'H:i')) - )); - } - - $title->addHtml( - new HtmlElement( - 'span', - Attributes::create(['class' => 'attendee']), - $entry->getAttendee()->getIcon(), - Text::create($entry->getAttendee()->getName()) - ) - ); - - $content->addHtml($title); - if ($description) { - $content->addHtml(new HtmlElement( - 'div', - Attributes::create(['class' => 'description']), - new HtmlElement( - 'p', - Attributes::create(['title' => $description]), - Text::create($description) - ) - )); - } - - if ($endText) { - $content->addHtml( - HtmlElement::create( - 'div', - ['class' => 'ends-at'], - $endText - ) - ); - } - - $entryContainer->addHtml($content); - } - /** * Get the given attendee's color with the given transparency suitable for CSS * diff --git a/library/Notifications/Widget/Calendar/Entry.php b/library/Notifications/Widget/Calendar/Entry.php index 807f472d0..2d87db615 100644 --- a/library/Notifications/Widget/Calendar/Entry.php +++ b/library/Notifications/Widget/Calendar/Entry.php @@ -5,10 +5,48 @@ namespace Icinga\Module\Notifications\Widget\Calendar; use DateTime; +use DateTimeInterface; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlElement; +use ipl\Html\Text; +use ipl\I18n\Translation; use ipl\Web\Url; - -class Entry +use ipl\Web\Widget\Link; + +/** + * An entry on a time grid + * + * @phpstan-type GridContinuationType self::FROM_PREV_GRID | self::TO_NEXT_GRID | self::ACROSS_GRID + * @phpstan-type EdgeContinuationType self::ACROSS_LEFT_EDGE | self::ACROSS_RIGHT_EDGE | self::ACROSS_BOTH_EDGES + * @phpstan-type ContinuationType GridContinuationType | EdgeContinuationType + */ +class Entry extends BaseHtmlElement { + use Translation; + + /** @var string Continuation of an entry that started on the previous grid */ + public const FROM_PREV_GRID = 'from-prev-grid'; + + /** @var string Continuation of an entry that continues on the next grid */ + public const TO_NEXT_GRID = 'to-next-grid'; + + /** @var string Continuation of an entry that started on the previous grid and continues on the next */ + public const ACROSS_GRID = 'across-grid'; + + /** @var string Continuation of an entry that started on a previous grid row */ + public const ACROSS_LEFT_EDGE = 'across-left-edge'; + + /** @var string Continuation of an entry that continues on the next grid row */ + public const ACROSS_RIGHT_EDGE = 'across-right-edge'; + + /** @var string Continuation of an entry that started on a previous grid row and continues on the next */ + public const ACROSS_BOTH_EDGES = 'across-both-edges'; + + protected $tag = 'div'; + + protected $defaultAttributes = ['class' => 'entry']; + protected $id; protected $description; @@ -20,6 +58,9 @@ class Entry /** @var ?int The 0-based position of the row where to place this entry on the grid */ protected $position; + /** @var ?ContinuationType */ + protected $continuationType; + protected $rrule; /** @var Url */ @@ -100,6 +141,30 @@ public function getPosition(): ?int return $this->position; } + /** + * Set the continuation type of this entry + * + * @param ?ContinuationType $continuationType + * + * @return $this + */ + public function setContinuationType(?string $continuationType): self + { + $this->continuationType = $continuationType; + + return $this; + } + + /** + * Get the continuation type of this entry + * + * @return ?ContinuationType + */ + public function getContinuationType(): ?string + { + return $this->continuationType; + } + public function setRecurrencyRule(?string $rrule): self { $this->rrule = $rrule; @@ -147,4 +212,105 @@ public function getAttendee(): Attendee { return $this->attendee; } + + protected function assemble() + { + $this->getAttributes() + ->add('data-entry-id', $this->getId()) + ->add('data-entry-position', $this->getPosition()); + + $continuationType = $this->getContinuationType(); + if ($continuationType === self::ACROSS_GRID || $continuationType === self::ACROSS_BOTH_EDGES) { + $this->getAttributes()->add('class', 'two-way-gradient'); + } elseif ($continuationType === self::FROM_PREV_GRID || $continuationType === self::ACROSS_LEFT_EDGE) { + $this->getAttributes()->add('class', 'opening-gradient'); + } elseif ($continuationType === self::TO_NEXT_GRID || $continuationType === self::ACROSS_RIGHT_EDGE) { + $this->getAttributes()->add('class', 'ending-gradient'); + } + + if (($url = $this->getUrl()) !== null) { + $entryContainer = new Link(null, $url); + $entryContainer->openInModal(); + $this->addHtml($entryContainer); + } else { + $entryContainer = $this; + } + + $title = new HtmlElement('div', Attributes::create(['class' => 'title'])); + $content = new HtmlElement( + 'div', + Attributes::create(['class' => 'content']) + ); + + $description = $this->getDescription(); + $titleAttr = $this->getStart()->format('H:i') + . ' | ' . $this->getAttendee()->getName() + . ($description ? ': ' . $description : ''); + + $startText = null; + $endText = null; + + if ($continuationType === self::ACROSS_GRID) { + $startText = sprintf($this->translate('starts %s'), $this->getStart()->format('d/m/y')); + $endText = sprintf($this->translate('ends %s'), $this->getEnd()->format('d/m/y H:i')); + } elseif ($continuationType === self::FROM_PREV_GRID) { + $startText = sprintf($this->translate('starts %s'), $this->getStart()->format('d/m/y')); + } elseif ($continuationType === self::TO_NEXT_GRID) { + $endText = sprintf($this->translate('ends %s'), $this->getEnd()->format('d/m/y H:i')); + } + + if ($startText) { + $titleAttr = $startText . ' ' . $titleAttr; + } + + if ($endText) { + $titleAttr = $titleAttr . ' | ' . $endText; + } + + $content->addAttributes(['title' => $titleAttr]); + + if ($continuationType !== null) { + $title->addHtml(new HtmlElement( + 'time', + Attributes::create([ + 'datetime' => $this->getStart()->format(DateTimeInterface::ATOM) + ]), + Text::create($this->getStart()->format($startText ? 'd/m/y H:i' : 'H:i')) + )); + } + + $title->addHtml( + new HtmlElement( + 'span', + Attributes::create(['class' => 'attendee']), + $this->getAttendee()->getIcon(), + Text::create($this->getAttendee()->getName()) + ) + ); + + $content->addHtml($title); + if ($description) { + $content->addHtml(new HtmlElement( + 'div', + Attributes::create(['class' => 'description']), + new HtmlElement( + 'p', + Attributes::create(['title' => $description]), + Text::create($description) + ) + )); + } + + if ($endText) { + $content->addHtml( + HtmlElement::create( + 'div', + ['class' => 'ends-at'], + $endText + ) + ); + } + + $entryContainer->addHtml($content); + } }