aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2025-10-15 14:45:09 +0000
committerMoritz Strohm <strohm@data-quest.de>2025-10-15 14:45:09 +0000
commit2fffc8a81a1f3c3665efac516aab93e823e6cd14 (patch)
tree012bbaa4c868f701c105fc8cd4ee5a551c8cdf73 /resources
parent894dd34eed77192d955c32783bc4d874c0716c2c (diff)
TIC 2832, closes #2832
Closes #2832 Merge request studip/studip!4453
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js147
-rw-r--r--resources/assets/stylesheets/fullcalendar.scss21
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;