From 749cd067da983c4f99d97389981ab6bc6e24e1e5 Mon Sep 17 00:00:00 2001 From: Sli Date: Tue, 16 Dec 2025 17:03:23 +0100 Subject: [PATCH] Add different colors for recurring events on event calendar --- .../com/components/ics-calendar-index.ts | 29 ++++++++++++++++++- com/static/com/components/ics-calendar.scss | 29 ++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/com/static/bundled/com/components/ics-calendar-index.ts b/com/static/bundled/com/components/ics-calendar-index.ts index bc2ec9b4..f3af0a18 100644 --- a/com/static/bundled/com/components/ics-calendar-index.ts +++ b/com/static/bundled/com/components/ics-calendar-index.ts @@ -1,6 +1,6 @@ import { makeUrl } from "#core:utils/api"; import { inheritHtmlElement, registerComponent } from "#core:utils/web-components"; -import { Calendar, type EventClickArg } from "@fullcalendar/core"; +import { Calendar, type EventClickArg, type EventContentArg } from "@fullcalendar/core"; import type { EventImpl } from "@fullcalendar/core/internal"; import enLocale from "@fullcalendar/core/locales/en-gb"; import frLocale from "@fullcalendar/core/locales/fr"; @@ -25,6 +25,11 @@ export class IcsCalendar extends inheritHtmlElement("div") { private canDelete = false; private helpUrl = ""; + // Hack variable to detect recurring events + // The underlying ics library doesn't include any info about rrules + // That's why we have to detect those events ourselves + private recurrenceMap: Map = new Map(); + attributeChangedCallback(name: string, _oldValue?: string, newValue?: string) { if (name === "locale") { this.locale = newValue; @@ -95,6 +100,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { refreshEvents() { this.click(); // Remove focus from popup + this.recurrenceMap.clear(); // Avoid double detection of the same non recurring event this.calendar.refetchEvents(); } @@ -153,12 +159,24 @@ export class IcsCalendar extends inheritHtmlElement("div") { } async getEventSources() { + const tagRecurringEvents = (eventData: EventImpl) => { + // This functions tags events with a similar event url + // We rely on the fact that the event url is always the same + // for recurring events and always different for single events + const firstEvent = this.recurrenceMap.get(eventData.url); + if (firstEvent !== undefined) { + eventData.extendedProps.isRecurring = true; + firstEvent.extendedProps.isRecurring = true; // Don't forget the first event + } + this.recurrenceMap.set(eventData.url, eventData); + }; return [ { url: `${await makeUrl(calendarCalendarInternal)}`, format: "ics", className: "internal", cache: false, + eventDataTransform: tagRecurringEvents, }, { url: `${await makeUrl(calendarCalendarUnpublished)}`, @@ -166,6 +184,7 @@ export class IcsCalendar extends inheritHtmlElement("div") { color: "red", className: "unpublished", cache: false, + eventDataTransform: tagRecurringEvents, }, ]; } @@ -361,6 +380,14 @@ export class IcsCalendar extends inheritHtmlElement("div") { event.jsEvent.preventDefault(); this.createEventDetailPopup(event); }, + eventClassNames: (classNamesEvent: EventContentArg) => { + const classes: string[] = []; + if (classNamesEvent.event.extendedProps?.isRecurring) { + classes.push("recurring"); + } + + return classes; + }, }); this.calendar.render(); diff --git a/com/static/com/components/ics-calendar.scss b/com/static/com/components/ics-calendar.scss index 1c0a15bd..74a76397 100644 --- a/com/static/com/components/ics-calendar.scss +++ b/com/static/com/components/ics-calendar.scss @@ -18,6 +18,8 @@ --event-details-border-radius: 4px; --event-details-box-shadow: 0px 6px 20px 4px rgb(0 0 0 / 16%); --event-details-max-width: 600px; + --event-recurring-internal-color: #6f69cd; + --event-recurring-unpublished-color: orange; } ics-calendar { @@ -146,4 +148,29 @@ ics-calendar { .tooltip.calendar-copy-tooltip.text-copied { opacity: 0; transition: opacity 500ms ease-out; -} \ No newline at end of file +} + +// We have to override the color set by the lib in the html +// Hence the !important tag everywhere +.internal.recurring { + .fc-daygrid-event-dot { + border-color: var(--event-recurring-internal-color) !important; + } + + &.fc-daygrid-block-event { + background-color: var(--event-recurring-internal-color) !important; + border-color: var(--event-recurring-internal-color) !important; + } + +} + +.unpublished.recurring { + .fc-daygrid-event-dot { + border-color: var(--event-recurring-unpublished-color) !important; + } + + &.fc-daygrid-block-event { + background-color: var(--event-recurring-unpublished-color) !important; + border-color: var(--event-recurring-unpublished-color) !important; + } +}