diff options
| author | Moritz Strohm <strohm@data-quest.de> | 2025-10-15 14:45:09 +0000 |
|---|---|---|
| committer | Moritz Strohm <strohm@data-quest.de> | 2025-10-15 14:45:09 +0000 |
| commit | 2fffc8a81a1f3c3665efac516aab93e823e6cd14 (patch) | |
| tree | 012bbaa4c868f701c105fc8cd4ee5a551c8cdf73 /resources | |
| parent | 894dd34eed77192d955c32783bc4d874c0716c2c (diff) | |
TIC 2832, closes #2832
Closes #2832
Merge request studip/studip!4453
Diffstat (limited to 'resources')
| -rw-r--r-- | resources/assets/javascripts/lib/fullcalendar.js | 147 | ||||
| -rw-r--r-- | resources/assets/stylesheets/fullcalendar.scss | 21 |
2 files changed, 164 insertions, 4 deletions
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js index a6eedbe..5882460 100644 --- a/resources/assets/javascripts/lib/fullcalendar.js +++ b/resources/assets/javascripts/lib/fullcalendar.js @@ -32,6 +32,99 @@ function pad(what, length = 2, char = '0') { class Fullcalendar { + static holidayCache = sessionStorage.getItem('fullcalendar_holidays') ? JSON.parse(sessionStorage.getItem('fullcalendar_holidays')) : {}; + static vacationCache = sessionStorage.getItem('fullcalendar_vacations') ? JSON.parse(sessionStorage.getItem('fullcalendar_vacations')) : {}; + + static loadHolidays(year) { + if (this.holidayCache[year]) { + return Promise.resolve(this.holidayCache[year]); + } + + return STUDIP.jsonapi.withPromises().get('holidays', { + data: { 'filter[year]': year } + }).then(response => { + const events = []; + if (!response) { + return events; + } + + for (const [date, data] of Object.entries(response)) { + const classNames = ['holiday']; + if (data.mandatory) { + classNames.push('official'); + } + + const day = new Date(date); + events.push({ + // Note: Since allDay is set to true, the start and end time is ignored. + // See the documentation: https://fullcalendar.io/docs/v4/event-parsing + start: day, + end: day, + allDay: true, + title: data.holiday, + editable: false, + + classNames, + + // Note: Colours are set via SCSS. + textColor: '', + color: '', + borderColor: '', + + rendering: 'background' + }); + } + + this.holidayCache[year] = events; + + sessionStorage.setItem('fullcalendar_holidays', JSON.stringify(this.holidayCache)); + + return events; + }); + } + + static loadVacations(year) { + if (this.vacationCache[year]) { + return Promise.resolve(this.vacationCache[year]); + } + + return STUDIP.jsonapi.withPromises().get('vacations', { + data: {'filter[year]': year} + }).then(response => { + if (!response) { + return []; + } + + const items = []; + + for (const vacation_data of Object.values(response)) { + const start = new Date(parseInt(vacation_data.start) * 1000); + const end = new Date(parseInt(vacation_data.end) * 1000); + items.push({ + start, + end, + allDay: true, + title: vacation_data.name, + editable: false, + classNames: ['holiday'], + + // Note: Colours are set via SCSS. + textColor: '', + color: '', + borderColor: '', + + rendering: 'background' + }); + } + + this.vacationCache[year] = items; + + sessionStorage.setItem('fullcalendar_vacations', JSON.stringify(this.vacationCache)); + + return items; + }); + } + /** * The initialisation method. It loads the JS files for fullcalendar * in case they are not loaded and sets up a fullcalendar instance @@ -573,9 +666,9 @@ class Fullcalendar } }, eventRender (info) { - var event = info.event; - var eventElement = info.el; - var iconColor = event.textColor == '#000000' ? 'black' : 'white'; + let event = info.event; + let eventElement = info.el; + let iconColor = event.textColor === '#000000' ? 'black' : 'white'; if ($(info.view.context.calendar.el).hasClass('institute-plan')) { $(eventElement).attr('title', event.extendedProps.tooltip); @@ -618,6 +711,34 @@ class Fullcalendar title.prepend(icon_element); } } + + //If a background event with title shall be rendered, we have to check + //if an all-day slot is present or not. + let generate_title = false; + if (event.rendering === 'background' && event.title && event.allDay) { + if (info.view.viewSpec.options.allDaySlot === true) { + //An all-day slot is present in the calendar. + if (info.isStart === true || info.isEnd === true) { + + generate_title = true; + } + } else { + //No all-day slot in the calendar. Display a title + //at the start of the day in the calendar. + if (info.isStart === false || info.isEnd === false) { + generate_title = true; + } + } + } + if (generate_title) { + //Generate a visible title for the background element: + let element = $('<div class="title"></div>'); + $(element).text(event.title); + if (event.textColor) { + element.css('color', event.textColor); + } + $(eventElement).append(element); + } }, eventSourceSuccess: function(content) { if ($(node).hasClass('semester-plan')) { @@ -732,6 +853,26 @@ class Fullcalendar config = $.extend({}, config, additional_config); + if (!Array.isArray(config.eventSources)) { + config.eventSources = []; + } + config.eventSources.push({ + events: (fetchInfo, successCallback, failureCallback) => { + const startYear = fetchInfo.start.getFullYear(); + const endYear = fetchInfo.end.getFullYear(); + const requests = []; + for (let year = startYear; year <= endYear; year++) { + requests.push(this.loadHolidays(year)); + requests.push(this.loadVacations(year)); + } + Promise.all(requests).then(results => { + const events = [].concat(...results); + successCallback(events); + return results; + }).catch(failureCallback); + }, + }); + config.defaultView = defaultView; return this.init(node, config); diff --git a/resources/assets/stylesheets/fullcalendar.scss b/resources/assets/stylesheets/fullcalendar.scss index 8edf7b6..70ed244 100644 --- a/resources/assets/stylesheets/fullcalendar.scss +++ b/resources/assets/stylesheets/fullcalendar.scss @@ -2,7 +2,8 @@ @import "scss/buttons"; @import "mixins"; -a.fc-event, td.fc-event { +a.fc-event, +td.fc-event { border-radius: 0; .fc-time { @@ -25,6 +26,24 @@ a.fc-event, td.fc-event { } } +div.fc-bgevent, +td.fc-bgevent { + .title { + color: $text-color; + font-weight: bold; + text-align: center; + } + + &.holiday { + opacity: 1; + background-color: rgba($light-gray-color-60, 0.3); + + &.official { + background-color: rgba($base-gray, 0.3); + } + } +} + .fc { .fc-toolbar.fc-header-toolbar { margin-bottom: 0.5em; |
