aboutsummaryrefslogtreecommitdiff
path: root/resources
diff options
context:
space:
mode:
authorMoritz Strohm <strohm@data-quest.de>2026-01-16 09:36:16 +0000
committerMoritz Strohm <strohm@data-quest.de>2026-01-16 09:36:16 +0000
commitb58142fe5fa1ba1a99d850baa1465df6fa6e0d3b (patch)
treed73956252dc17dd054d0db8e0f167a4103b7ba83 /resources
parentc3e07e221b0bef64d3ad4da48c6371c75ca12cc3 (diff)
updated Fullcalendar to version 6, closes #4887
Closes #4887 Merge request studip/studip!4438
Diffstat (limited to 'resources')
-rw-r--r--resources/assets/javascripts/bootstrap/fullcalendar.js48
-rw-r--r--resources/assets/javascripts/bootstrap/resources.js195
-rw-r--r--resources/assets/javascripts/chunk-loader.js7
-rw-r--r--resources/assets/javascripts/chunks/fullcalendar.js11
-rw-r--r--resources/assets/javascripts/entry-base.js1
-rw-r--r--resources/assets/javascripts/init.js4
-rw-r--r--resources/assets/javascripts/lib/action.ts26
-rw-r--r--resources/assets/javascripts/lib/calendar.ts226
-rw-r--r--resources/assets/javascripts/lib/datetime.ts (renamed from resources/assets/javascripts/lib/datetime.js)92
-rw-r--r--resources/assets/javascripts/lib/fullcalendar.js977
-rw-r--r--resources/assets/javascripts/lib/holiday.ts254
-rw-r--r--resources/assets/stylesheets/fullcalendar.scss306
-rw-r--r--resources/assets/stylesheets/print.scss6
-rw-r--r--resources/assets/stylesheets/scss/calendar.scss47
-rw-r--r--resources/assets/stylesheets/scss/fullcalendar-print.scss165
-rw-r--r--resources/assets/stylesheets/scss/fullcalendar.scss341
-rw-r--r--resources/vue/apps/StudipCalendar.vue596
-rw-r--r--resources/vue/components/StudipDateTime.vue7
-rw-r--r--resources/vue/components/form_inputs/DateListInput.vue7
19 files changed, 1597 insertions, 1719 deletions
diff --git a/resources/assets/javascripts/bootstrap/fullcalendar.js b/resources/assets/javascripts/bootstrap/fullcalendar.js
deleted file mode 100644
index 44786d9..0000000
--- a/resources/assets/javascripts/bootstrap/fullcalendar.js
+++ /dev/null
@@ -1,48 +0,0 @@
-STUDIP.ready(function () {
- //Check if fullcalendar instances are to be displayed:
- $('*[data-fullcalendar="1"]').each(function () {
- STUDIP.loadChunk('fullcalendar').then(() => {
- if (this.calendar === undefined) {
- let calendar;
- if ($(this).hasClass('semester-plan')) {
- calendar = STUDIP.Fullcalendar.createSemesterCalendarFromNode(this);
- } else {
- calendar = STUDIP.Fullcalendar.createFromNode(this);
- }
-
- continuousRefresh(calendar, 10);
- }
- });
- });
-
- function continuousRefresh(calendar, ttl) {
- setTimeout(() => {
- calendar.updateSize();
- if (ttl > 0) {
- continuousRefresh(calendar, ttl - 1);
- }
- }, 200);
- }
-
- if ($('#event-color-picker > option').length <= 1) {
- var selectedColor = $('#selected-color').val();
- var colors = ['yellow', 'orange', 'red', 'violet', 'dark-violet', 'green', 'dark-green', 'petrol', 'brown'];
-
- var style = window.getComputedStyle(document.body);
- colors.forEach(color => {
- let real_color = style.getPropertyValue(`--${color}`).trim();
- $('#event-color-picker').append([
- $('<input type="radio" name="event_color">').attr({
- id: color,
- value: real_color,
- checked: selectedColor === real_color
- }),
- $('<label>').attr('for', color).css({
- backgroundColor: `var(--${color})`
- }),
- ]);
- });
- }
-
- jQuery(document).on('change', '#date_select[data-calendar-control]', STUDIP.Fullcalendar.submitDatePicker);
-});
diff --git a/resources/assets/javascripts/bootstrap/resources.js b/resources/assets/javascripts/bootstrap/resources.js
index caf9a95..ca16252 100644
--- a/resources/assets/javascripts/bootstrap/resources.js
+++ b/resources/assets/javascripts/bootstrap/resources.js
@@ -134,29 +134,6 @@ STUDIP.ready(function () {
);
//Event handlers for the individual booking plan print view:
- jQuery('#sidebar .colour-selector').draggable(
- {
- cursorAt: {
- left: 28, top: 15
- },
- appendTo: 'body',
- helper: function () {
- var dragged_item = jQuery(
- '<div class="dragged-colour"></div>'
- );
- jQuery(dragged_item).css(
- {
- backgroundColor: jQuery(this).css('background-color'),
- width: jQuery(this).css('width'),
- height: jQuery(this).css('height'),
- zIndex: 1000
- }
- );
- return dragged_item;
- },
- revert: true
- }
- );
jQuery(document).on(
'click',
@@ -177,35 +154,6 @@ STUDIP.ready(function () {
}
);
- jQuery(document).on(
- 'dragenter',
- '.individual-booking-plan .appointment-booking-plan',
- function (event) {
- jQuery(event.target).css('opacity', '0.7');
- }
- );
-
- jQuery(document).on(
- 'dragleave',
- '.individual-booking-plan .appointment-booking-plan',
- function (event) {
- jQuery(event.target).css('opacity', '1.0');
- }
- );
-
- jQuery(document).on(
- 'dragend',
- '.dragged-colour',
- function (event) {
- jQuery(event.target).css(
- {
- 'top': '0px',
- 'left': '0px'
- }
- );
- }
- );
-
//For the message functionality of the resource system:
jQuery(document).on(
@@ -442,24 +390,6 @@ STUDIP.ready(function () {
);
jQuery(document).on(
- 'click',
- '.fc-button',
- function () {
- if ($(this).hasClass('fc-dayGridMonth-button')) {
- updateViewURL('dayGridMonth')
- } else if ($(this).hasClass('fc-timeGridWeek-button')) {
- updateViewURL('timeGridWeek')
- } else if ($(this).hasClass('fc-timeGridDay-button')) {
- updateViewURL('timeGridDay')
- } else if ($(this).hasClass('fc-today-button')
- || $(this).hasClass('fc-prev-button')
- || $(this).hasClass('fc-next-button')) {
- STUDIP.Fullcalendar.updateDateURL();
- }
- }
- );
-
- jQuery(document).on(
'blur',
'.hasDatepicker',
function () {
@@ -553,137 +483,12 @@ STUDIP.ready(function () {
});
}
- function updateViewURL(defaultView) {
- const url = new URL(window.location.href);
- url.searchParams.set('defaultView', defaultView);
-
- // Push current view url to history
- history.pushState({}, null, url.toString());
-
- // Set links accordingly
- url.searchParams.delete('allday');
- $('.booking-plan-std_view').attr('href', url.toString());
-
- url.searchParams.set('allday', 1);
- $('.booking-plan-allday_view').attr('href', url.toString());
- }
-
-
- jQuery('#booking-plan-jmpdate').datepicker(
- {
- dateFormat: 'dd.mm.yy',
- onClose: STUDIP.Fullcalendar.submitDatePicker
- }
- );
jQuery('.resource-booking-time-fields input[type="date"]').datepicker(
{
dateFormat: 'yy-mm-dd'
}
);
- jQuery('.resource-plan[data-resources-fullcalendar="1"]').each(function () {
- STUDIP.loadChunk('fullcalendar').then(() => {
- //Get the default date from the sessionStorage, if it is set
- //and no date is specified in the url.
- let use_session_date = true;
- let url_param_string = window.location.search;
- if (url_param_string) {
- let url_params = new URLSearchParams(url_param_string);
- if (url_params.get('defaultDate')) {
- use_session_date = false;
- }
- }
- if (this.calendar === undefined) {
- if (jQuery(this).hasClass('semester-plan')) {
- STUDIP.Fullcalendar.createSemesterCalendarFromNode(
- this,
- {
- loading: function (isLoading) {
- if (!isLoading) {
- let h = jQuery('section.studip-fullcalendar-header');
- if (h) {
- jQuery(h).removeClass('invisible');
- jQuery(h).insertBefore(this);
- }
- }
- }
- }
- );
- } else {
- let config = {
- studip_functions: {
- drop_event:
- STUDIP.Resources.dropEventInRoomGroupBookingPlan,
- resize_event:
- STUDIP.Resources.resizeEventInRoomGroupBookingPlan
- },
- loading: function (isLoading) {
- if (!isLoading) {
- let h = jQuery('section.studip-fullcalendar-header');
- if (h) {
- jQuery(h).removeClass('invisible');
- jQuery(h).insertBefore(this);
- }
- }
- }
- };
- if (use_session_date) {
- let session_date_string = sessionStorage.getItem('booking_plan_date');
- if (session_date_string) {
- config.defaultDate = session_date_string;
- }
- }
- STUDIP.Fullcalendar.createFromNode(this, config);
- }
- }
- });
- });
-
- //Check if an individual booking plan is to be displayed:
- jQuery('.individual-booking-plan[data-resources-fullcalendar="1"]').each(function () {
- STUDIP.loadChunk('fullcalendar').then(() => {
- STUDIP.Fullcalendar.createFromNode(
- this,
- {
- eventPositioned: function (info) {
- jQuery(info.el).droppable({
- drop: function (event, ui_element) {
- event.preventDefault();
-
- let booking_plan_entry = event.target;
- let new_background_colour = jQuery(
- ui_element.helper
- ).css('background-color');
-
- jQuery(booking_plan_entry).css(
- 'background-color',
- new_background_colour
- );
- jQuery(booking_plan_entry).css(
- 'border-color',
- new_background_colour
- );
-
- jQuery(booking_plan_entry).find('dl').css({
- backgroundColor: new_background_colour,
- borderColor: new_background_colour
- });
- jQuery(booking_plan_entry).find('dt').css(
- 'background-color',
- new_background_colour
- );
- }
- });
- let h = jQuery('section.studip-fullcalendar-header').clone();
- if (h) {
- jQuery(h).removeClass('invisible');
- jQuery(h).insertBefore(this);
- }
- }
- }
- );
- });
- });
jQuery(document).on(
'click',
diff --git a/resources/assets/javascripts/chunk-loader.js b/resources/assets/javascripts/chunk-loader.js
index 8cb692c..e822b8e 100644
--- a/resources/assets/javascripts/chunk-loader.js
+++ b/resources/assets/javascripts/chunk-loader.js
@@ -47,13 +47,6 @@ export const loadChunk = function (chunk, { silent = false } = {}) {
).then(({ default: Chartist }) => Chartist);
break;
- case 'fullcalendar':
- promise = import(
- /* webpackChunkName: "fullcalendar" */
- './chunks/fullcalendar'
- );
- break;
-
case 'tablesorter':
promise = import(
/* webpackChunkName: "tablesorter" */
diff --git a/resources/assets/javascripts/chunks/fullcalendar.js b/resources/assets/javascripts/chunks/fullcalendar.js
deleted file mode 100644
index ee28099..0000000
--- a/resources/assets/javascripts/chunks/fullcalendar.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import '@fullcalendar/core/main.css';
-import '@fullcalendar/daygrid/main.css';
-import '@fullcalendar/timegrid/main.css';
-import '@fullcalendar/timeline/main.css';
-import '@fullcalendar/resource-timeline/main.css';
-
-import "../../stylesheets/fullcalendar.scss";
-
-import Fullcalendar from '../lib/fullcalendar.js';
-
-STUDIP.Fullcalendar = Fullcalendar;
diff --git a/resources/assets/javascripts/entry-base.js b/resources/assets/javascripts/entry-base.js
index 02fd471..ad114f1 100644
--- a/resources/assets/javascripts/entry-base.js
+++ b/resources/assets/javascripts/entry-base.js
@@ -67,7 +67,6 @@ import "./bootstrap/tabbable_widget.js"
import "./bootstrap/clipboard.js"
import "./bootstrap/resources.js"
import "./bootstrap/resource-tree-widget.js"
-import "./bootstrap/fullcalendar.js"
import "./bootstrap/gradebook.js"
import "./bootstrap/blubber.js"
import "./bootstrap/consultations.js"
diff --git a/resources/assets/javascripts/init.js b/resources/assets/javascripts/init.js
index 9fcfbb1..a48c028 100644
--- a/resources/assets/javascripts/init.js
+++ b/resources/assets/javascripts/init.js
@@ -20,7 +20,7 @@ import CourseWizard from './lib/course_wizard.js';
import { createURLHelper } from './lib/url_helper';
import CSS from './lib/css.js';
import Dates from './lib/dates.js';
-import DateTime from './lib/datetime.js';
+import DateTime from './lib/datetime.ts';
import Dialog from './lib/dialog.js';
import DragAndDropUpload from './lib/drag_and_drop_upload.js';
import enrollment from './lib/enrollment.js';
@@ -30,7 +30,6 @@ import Files from './lib/files.js';
import FilesDashboard from './lib/files_dashboard.js';
import Folders from './lib/folders.js';
import Forms from './lib/forms.js';
-import Fullcalendar from './lib/fullcalendar.js';
import Fullscreen from './lib/fullscreen.js';
import GlobalSearch from './lib/global_search.js';
import HeaderMagic from './lib/header_magic.js';
@@ -109,7 +108,6 @@ window.STUDIP = _.assign(window.STUDIP || {}, {
FilesDashboard,
Folders,
Forms,
- Fullcalendar,
Fullscreen,
Gettext,
GlobalSearch,
diff --git a/resources/assets/javascripts/lib/action.ts b/resources/assets/javascripts/lib/action.ts
new file mode 100644
index 0000000..4d247f6
--- /dev/null
+++ b/resources/assets/javascripts/lib/action.ts
@@ -0,0 +1,26 @@
+/**
+ * The Action class represents an action in Stud.IP with its usual attributes.
+ */
+class Action
+{
+ /**
+ * The URL for the action.
+ */
+ url: string;
+
+ label: string;
+
+ icon_name: string;
+
+ constructor(
+ url: string,
+ label: string,
+ icon_name: string = ''
+ ) {
+ this.url = url;
+ this.label = label;
+ this.icon_name = icon_name;
+ }
+}
+
+export {Action};
diff --git a/resources/assets/javascripts/lib/calendar.ts b/resources/assets/javascripts/lib/calendar.ts
new file mode 100644
index 0000000..05c2e99
--- /dev/null
+++ b/resources/assets/javascripts/lib/calendar.ts
@@ -0,0 +1,226 @@
+import {EventImpl} from "@fullcalendar/core/internal";
+import {CalendarOptions, PluginDef} from "@fullcalendar/core";
+import {getLocale} from "./gettext";
+import locale_de from "@fullcalendar/core/locales/de";
+import locale_en_gb from "@fullcalendar/core/locales/en-gb";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import timeGridPlugin from "@fullcalendar/timegrid";
+import resourceTimelinePlugin from "@fullcalendar/resource-timeline";
+import interactionPlugin from "@fullcalendar/interaction";
+
+/**
+ * The EventURLParameters class represents URL parameters for calendar events
+ * that are passed to Stud.IP controllers to do something with the event.
+ */
+class EventURLParameters
+{
+ /**
+ * The start date of the event.
+ */
+ start: Date | null;
+
+ /**
+ * The end date of the event.
+ */
+ end: Date | null;
+
+ /**
+ * Whether the event is an all-day event (true) or not (false).
+ */
+ all_day: boolean;
+
+ /**
+ * The optional resource-ID of the event. In the Stud.IP context, this can be the same
+ * as the ID of a Stud.IP resource.
+ */
+ resource_id: string | null;
+
+ /**
+ * Constructs an instance using a Fullcalendar Event object.
+ *
+ * @param event The Fullcalendar event object to construct URL parameters from.
+ */
+ constructor(event?: EventImpl) {
+ this.resource_id = null;
+ if (event) {
+ this.start = event.start;
+ this.end = event.end;
+ this.all_day = event.allDay;
+ } else {
+ this.start = null;
+ this.end = null;
+ this.all_day = false;
+ }
+ }
+
+ setStart(start: Date) {
+ this.start = start;
+ }
+
+ setEnd(end: Date) {
+ this.end = end;
+ }
+
+ setAllDay(all_day: boolean) {
+ this.all_day = all_day;
+ }
+
+ setResourceId(resource_id: string) : void {
+ this.resource_id = resource_id;
+ }
+
+ /**
+ * Converts the parameters to a plain JavaScript object to be used
+ * with the existing code in Stud.IP to fire requests or to open a dialog.
+ */
+ toObject(): object {
+ return {
+ start: this.start ? this.start.toISOString() : null,
+ end: this.end ? this.end.toISOString() : null,
+ all_day: this.all_day ? '1' : '0',
+ resource_id: this.resource_id,
+ };
+ }
+}
+
+/**
+ * The StudipCalendarConfig class provides default values for the Fullcalendar
+ * configuration that do not need to be set for each calendar instance.
+ */
+class StudipCalendarConfig
+{
+ fullcalendar_config: CalendarOptions = {};
+
+ constructor(config: CalendarOptions) {
+ this.fullcalendar_config = config;
+
+ //Set fixed options:
+ this.fullcalendar_config.firstDay = 1;
+ this.fullcalendar_config.weekNumberCalculation = 'ISO';
+ this.fullcalendar_config.height = 'auto';
+ this.fullcalendar_config.contentHeight = 'auto';
+ this.fullcalendar_config.schedulerLicenseKey = 'GPL-My-Project-Is-Open-Source';
+ this.fullcalendar_config.timeZone = 'local';
+
+ const all_views: Set<string> = new Set<string>();
+ if (this.fullcalendar_config.views) {
+ for (const view of Object.keys(this.fullcalendar_config.views)) {
+ all_views.add(view);
+ }
+ }
+ if (this.fullcalendar_config.initialView) {
+ all_views.add(this.fullcalendar_config.initialView);
+ }
+ //Load the plugins according to the views, if they are not
+ //explicitly set:
+ if (!this.fullcalendar_config.plugins) {
+ //Add the plugins here so that users of this component
+ //do not need to add them separately.
+ const active_plugins: Array<PluginDef> = [interactionPlugin];
+ for (const view_name of all_views) {
+ if (view_name === 'dayGridMonth') {
+ active_plugins.push(dayGridPlugin);
+ } else if (view_name === 'timeGridWeek' || view_name === 'timeGridDay') {
+ active_plugins.push(timeGridPlugin);
+ } else if (view_name === 'resourceTimelineWeek'
+ || view_name === 'resourceTimelineDay') {
+ active_plugins.push(resourceTimelinePlugin)
+ }
+ }
+ this.fullcalendar_config.plugins = active_plugins;
+ }
+
+ //Fullcalendar needs a short version of the locale:
+ let short_locale: string = getLocale();
+ if (short_locale) {
+ short_locale = short_locale.replace('_', '-');
+ } else {
+ short_locale = 'de-DE';
+ }
+ this.fullcalendar_config.locales = [locale_de, locale_en_gb];
+
+ //Make sure the event sources item is an array:
+ if (!this.fullcalendar_config.eventSources) {
+ this.fullcalendar_config.eventSources = [];
+ }
+
+ //Provide defaults for options that can be altered:
+ if (!this.fullcalendar_config.eventTimeFormat) {
+ this.fullcalendar_config.eventTimeFormat = {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit'
+ };
+ }
+ if (!this.fullcalendar_config.slotLabelFormat) {
+ this.fullcalendar_config.slotLabelFormat = {
+ hour: 'numeric',
+ minute: '2-digit',
+ omitZeroMinute: false
+ };
+ }
+
+ if (!this.fullcalendar_config.nowIndicator) {
+ this.fullcalendar_config.nowIndicator = true;
+ }
+ if (!this.fullcalendar_config.slotMinTime) {
+ this.fullcalendar_config.slotMinTime = '08:00';
+ }
+ if (!this.fullcalendar_config.slotMaxTime) {
+ this.fullcalendar_config.slotMaxTime = '20:00';
+ }
+ if (!this.fullcalendar_config.initialDate) {
+ this.fullcalendar_config.initialDate = new Date();
+ }
+ if (!this.fullcalendar_config.initialView && all_views.has('timeGridWeek')) {
+ //At the moment, there is only one good default for timeGridWeek.
+ this.fullcalendar_config.initialView = 'timeGridWeek';
+ }
+ if (this.fullcalendar_config.allDaySlot === undefined) {
+ this.fullcalendar_config.allDaySlot = false;
+ }
+ if (this.fullcalendar_config.allDayText === undefined) {
+ this.fullcalendar_config.allDayText = '';
+ }
+ if (this.fullcalendar_config.weekNumbers === undefined) {
+ this.fullcalendar_config.weekNumbers = true;
+ }
+ if (this.fullcalendar_config.headerToolbar) {
+ //Check if the start and end item in the toolbar is empty.
+ //In that case, navigation is not desired because it is
+ //a semester calendar.
+ const toolbar = this.fullcalendar_config.headerToolbar;
+ if ((!toolbar.start || toolbar.start.length < 1)
+ && (!toolbar.end || toolbar.end.length < 1)
+ && this.fullcalendar_config.views) {
+
+ //No navigation items present and no day header format set.
+ //This is a semester calendar. Deactivate the display of
+ //specific dates and display only the day(s) of the week.
+ if (this.fullcalendar_config.views.timeGridWeek) {
+ this.fullcalendar_config.views.timeGridWeek.dayHeaderFormat = {
+ weekday: 'long'
+ };
+ }
+ if (this.fullcalendar_config.views.timeGridDay) {
+ this.fullcalendar_config.views.timeGridDay.dayHeaderFormat = {
+ weekday: 'long'
+ };
+ }
+ }
+ } else {
+ //No toolbar defined. Define a standard one:
+ this.fullcalendar_config.headerToolbar = {
+ start: Array.from(all_views).join(','),
+ center: 'title',
+ end: 'prev,today,next'
+ };
+ }
+ }
+
+ getConfig() : CalendarOptions {
+ return this.fullcalendar_config;
+ }
+}
+
+export {EventURLParameters, StudipCalendarConfig};
diff --git a/resources/assets/javascripts/lib/datetime.js b/resources/assets/javascripts/lib/datetime.ts
index 04d7260..aa8a0bc 100644
--- a/resources/assets/javascripts/lib/datetime.js
+++ b/resources/assets/javascripts/lib/datetime.ts
@@ -1,16 +1,18 @@
-import { $gettext } from "./gettext.ts";
+import {$gettext, $gettextInterpolate} from "./gettext";
-const DateTime = {
+class DateTime
+{
/**
- * A helper method for padding strings with leading zeros.
- * @param what The date to pad.
- * @param length The length of the string to output.
+ * A helper method for padding parts of dates with leading zeros.
+ * @param item The part of a date string to pad.
+ * @param target_length The length of the string to output.
* @returns {string} A padded version of $what.
*/
- pad(what, length = 2) {
- return `00000000${what}`.substr(-length);
- },
+ pad(item: number, target_length: number = 2) : string {
+ const target: string = `00000000${item}`;
+ return target.substring(target.length - target_length);
+ }
/**
* Returns an ISO representation of the specified Date object.
@@ -19,9 +21,9 @@ const DateTime = {
* @param date The Date object to format as ISO date.
* @returns {string} The ISO date string of the Date object.
*/
- getISODate(date) {
- return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + date.getDate();
- },
+ getISODate(date: Date) : string {
+ return date.getFullYear() + '-' + this.pad(date.getMonth() + 1) + '-' + this.pad(date.getDate());
+ }
/**
* Returns a formatted version of the specified Date object
@@ -36,16 +38,21 @@ const DateTime = {
*
* @returns {*|string} The date, formatted according to the Stud.IP format for dates.
*/
- getStudipDate(date, relative_value = false, date_only = false, html = false) {
+ getStudipDate(
+ date: Date,
+ relative_value: boolean = false,
+ date_only: boolean = false,
+ html: boolean = false) : string {
if (relative_value) {
- let now = Date.now();
- if (now - date < 1 * 60 * 1000) {
+ const now: number = Date.now();
+ const date_ts: number = date.getMilliseconds();
+ if (now - date_ts < 60 * 1000) {
return $gettext('Jetzt');
}
- if (now - date < 2 * 60 * 60 * 1000) {
- return $gettext(
+ if (now - date_ts < 2 * 60 * 60 * 1000) {
+ return $gettextInterpolate(
'Vor %{ minutes } Minuten',
- {minutes: Math.floor((now - date) / (1000 * 60))}
+ {minutes: Math.floor((now - date_ts) / (1000 * 60))}
);
}
return this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
@@ -79,7 +86,56 @@ const DateTime = {
return this.pad(date.getDate()) + '.' + this.pad(date.getMonth() + 1) + '.' + date.getFullYear() + ' ' + this.pad(date.getHours()) + ':' + this.pad(date.getMinutes());
}
}
-};
+
+ getDayOfWeekName(dow: number, short: boolean = false): string {
+ if (dow === 0 || dow === 7) {
+ if (short) {
+ return $gettext('So.');
+ } else {
+ return $gettext('Sonntag');
+ }
+ } else if (dow === 1) {
+ if (short) {
+ return $gettext('Mo.');
+ } else {
+ return $gettext('Montag');
+ }
+ } else if (dow === 2) {
+ if (short) {
+ return $gettext('Di.');
+ } else {
+ return $gettext('Dienstag');
+ }
+ } else if (dow === 3) {
+ if (short) {
+ return $gettext('Mi.');
+ } else {
+ return $gettext('Mittwoch');
+ }
+ } else if (dow === 4) {
+ if (short) {
+ return $gettext('Do.');
+ } else {
+ return $gettext('Donnerstag');
+ }
+ } else if (dow === 5) {
+ if (short) {
+ return $gettext('Fr.');
+ } else {
+ return $gettext('Freitag');
+ }
+ } else if (dow === 6) {
+ if (short) {
+ return $gettext('Sa.');
+ } else {
+ return $gettext('Samstag');
+ }
+ } else {
+ return '';
+ }
+ }
+}
export default DateTime;
+export const datetime: DateTime = new DateTime();
diff --git a/resources/assets/javascripts/lib/fullcalendar.js b/resources/assets/javascripts/lib/fullcalendar.js
deleted file mode 100644
index 5882460..0000000
--- a/resources/assets/javascripts/lib/fullcalendar.js
+++ /dev/null
@@ -1,977 +0,0 @@
-/**
- * This class contains Stud.IP specific code for the fullcalendar package.
- */
-
-import { Calendar } from '@fullcalendar/core';
-import deLocale from '@fullcalendar/core/locales/de';
-import enLocale from '@fullcalendar/core/locales/en-gb';
-import interactionPlugin from '@fullcalendar/interaction';
-import { Draggable } from '@fullcalendar/interaction';
-import dayGridPlugin from '@fullcalendar/daygrid';
-import timeGridPlugin from '@fullcalendar/timegrid';
-import resourceCommonPlugin from '@fullcalendar/resource-common';
-import resourceTimeGridPlugin from '@fullcalendar/resource-timegrid';
-import resourceTimelinePlugin from '@fullcalendar/resource-timeline';
-
-import { jsPDF } from 'jspdf';
-import html2canvas from 'html2canvas';
-import Responsive from "./responsive";
-
-Date.prototype.getWeekNumber = function () {
- var d = new Date(Date.UTC(this.getFullYear(), this.getMonth(), this.getDate()));
- var dayNum = d.getUTCDay() || 7;
- d.setUTCDate(d.getUTCDate() + 4 - dayNum);
- var yearStart = new Date(Date.UTC(d.getUTCFullYear(),0,1));
- return Math.ceil((((d - yearStart) / 86400000) + 1)/7);
-};
-
-function pad(what, length = 2, char = '0') {
- let padding = new Array(length + 1).join(char);
- return `${padding}${what}`.substr(-length);
-}
-
-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
- * for the nodes specified in the parameter node.
- *
- * @param DOMElement|string node The node which shall have a full calendar.
- * This must either be a DOMElement or a string
- * containing a CSS selector.
- */
- static init(node, fullcalendar_options = null)
- {
- // Convert css selector to actual dom element
- node = $(node)[0];
-
- if (!node) {
- //We need a CSS selector or a node!
- return;
- }
-
- if (document.getElementById('external-events')) {
- new Draggable(document.getElementById('external-events'), {
- itemSelector: '.fc-event',
- eventData (eventEl) {
- return {
- title: eventEl.dataset.eventTitle,
- duration: eventEl.dataset.eventDuration,
- course_id: eventEl.dataset.eventCourse,
- tooltip: eventEl.dataset.eventTooltip,
- studip_api_urls: {drop: eventEl.dataset.eventDropUrl},
- studip_view_urls: {edit: eventEl.dataset.eventDetailsUrl}
- };
- }
- });
- }
-
- var calendar = new Calendar(node, fullcalendar_options);
- node.calendar = calendar;
- calendar.render();
-
- return calendar;
- }
-
- /**
- * Converts semester events to the default fullcalendar event format.
- * The begin and end date are converted to fit into the current week.
- */
- static convertSemesterEvents(event_data, fake_week_start = Date())
- {
- if (!event_data) {
- return {};
- }
-
- var start = String(event_data.start).split('T');
- var end = String(event_data.end).split('T');
-
- //start and end must be transformed to the current week.
- //Therefore, we need the ISO weekdays for begin and end.
- var fake_start = new Date(fake_week_start);
- fake_start.setHours(12);
- fake_start.setMinutes(0);
- fake_start.setSeconds(0);
- var fake_end = new Date(fake_week_start);
- fake_end.setHours(12);
- fake_end.setMinutes(0);
- fake_end.setSeconds(0);
-
- //Calculcate the week day of the current week for the event
- //from the current day and convert sunday to ISO format
- var start_day_diff = fake_start.getDay() || 7;
- var end_day_diff = fake_end.getDay() || 7;
-
- start_day_diff = start_day_diff - event_data.studip_weekday_begin;
- end_day_diff = end_day_diff - event_data.studip_weekday_end;
-
- fake_start = new Date(
- fake_start.getTime() - start_day_diff * 24 * 60 * 60 * 1000
- );
- fake_end = new Date(
- fake_end.getTime() - end_day_diff * 24 * 60 * 60 * 1000
- );
-
- //Output the modified begin and end date in the correct ISO format:
- event_data.start =`${fake_start.getFullYear()}-${pad(fake_start.getMonth() + 1)}-${pad(fake_start.getDate())}T${start[1]}`;
- event_data.end = `${fake_end.getFullYear()}-${pad(fake_end.getMonth() + 1)}-${pad(fake_end.getDate())}T${end[1]}`;
-
- return event_data;
- }
-
-
- static createSemesterCalendarFromNode(node, additional_config = {})
- {
- if (!node) {
- //Ain't no fullcalendar when the node's gone!
- return;
- }
-
- var config = $.extend(
- {},
- $(node).data('config') || {},
- additional_config
- );
-
- if (Array.isArray(config.eventSources)) {
- config.eventSources = config.eventSources.map((s) => {
- if (s.url !== undefined) {
- return s;
- }
- });
- }
-
- return this.createFromNode(node, config);
- }
-
-
- static defaultResizeEventHandler(info)
- {
- if (!info.event.durationEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be resized!
- info.revert();
- return;
- }
-
- if (info.event.extendedProps.studip_api_urls.resize) {
- $.post({
- url: info.event.extendedProps.studip_api_urls.resize,
- async: false,
- data: {
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- } else if (info.event.extendedProps.studip_api_urls.resize_dialog) {
- STUDIP.Dialog.fromURL(
- info.event.extendedProps.studip_api_urls.resize_dialog,
- {
- data: {
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }
- );
- }
- }
-
- static downloadPDF(format = 'landscape', withWeekend = false)
- {
- $('*[data-fullcalendar="1"]').each(function () {
- if (this.calendar != undefined) {
- $(this).addClass('print-view').toggleClass('without-weekend', !withWeekend);
-
- var title = $(this).data('title');
- let print_title = $('<h1>').text(title).prependTo(this);
-
- window.scrollTo(0, 0);
-
- html2canvas(this).then(canvas => {
- var imgData = canvas.toDataURL('image/jpeg');
- var pdf = new jsPDF({
- orientation: format === 'landscape' ? 'landscape' : 'portrait'
- });
- if (format === 'landscape') {
- pdf.addImage(imgData, 'JPEG', 20, 20, 250, 250, 'i1', 'NONE', 0);
- } else {
- pdf.addImage(imgData, 'JPEG', 25, 20, 160, 190, 'i1', 'NONE', 0);
- }
- pdf.save(title + '.pdf');
- });
-
- print_title.remove();
- $(this).removeClass('print-view without-weekend');
- }
- });
- }
-
- static toRFC3339String(date)
- {
- var timezone_offset_min = date.getTimezoneOffset();
- var offset_hrs = parseInt(Math.abs(timezone_offset_min / 60), 10);
- var offset_min = Math.abs(timezone_offset_min%60);
- var timezone_standard;
-
- offset_hrs = pad(offset_hrs);
- offset_min = pad(offset_min);
-
- // Add an opposite sign to the offset
- // If offset is 0, it means timezone is UTC
- if (timezone_offset_min < 0) {
- timezone_standard = `+${offset_hrs}:${offset_min}`;
- } else if (timezone_offset_min > 0) {
- timezone_standard = `-${offset_hrs}:${offset_min}`;
- } else {
- timezone_standard = '+00:00';
- }
-
- var current_date = pad(date.getDate());
- var current_month = pad(date.getMonth() + 1);
- var current_year = date.getFullYear();
- var current_hrs = pad(date.getHours());
- var current_mins = pad(date.getMinutes());
- var current_secs = pad(date.getSeconds());
- var current_datetime;
-
- // Current datetime
- // String such as 2016-07-16T19:20:30
- current_datetime = `${current_year}-${current_month}-${current_date}T${current_hrs}:${current_mins}:${current_secs}`;
-
- return current_datetime + timezone_standard;
- }
-
- static defaultDropEventHandler(info)
- {
- // The logic from fullcalendar is inversed here:
- // If the calendar isn't editable, the event isn't either.
- if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be dragged and dropped!
- info.revert();
- return;
- }
-
- var drop_resource_id = info.newResource ? info.newResource.id : info.event.extendedProps.studip_range_id;
-
- if (info.event.extendedProps.studip_api_urls.move || info.event.extendedProps.studip_api_urls.move_dialog) {
- let move_dialog = info.event.extendedProps.studip_api_urls.move_dialog;
- if (info.event.allDay) {
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
- end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start.setHours(0, 0, 0)),
- end: this.toRFC3339String(info.event.start.setHours(23, 59, 59))
- }
- }).fail(info.revert);
- }
- } else if (info.event.end === null) {
- let real_end = new Date();
- real_end.setTime(info.event.start.getTime());
- real_end.setHours(info.event.start.getHours()+2);
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(real_end)
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(real_end)
- }
- }).fail(info.revert);
- }
- } else {
- if (move_dialog) {
- STUDIP.Dialog.fromURL(
- move_dialog,
- {
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- },
- size: 'auto'
- }
- );
- } else {
- jQuery.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- resource_id: drop_resource_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- }
- }
- }
- }
-
- static institutePlanDropEventHandler(info)
- {
- //The logic from fullcalendar is inversed here:
- if (info.newResource) {
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- cycle_id: info.event.id,
- resource_id: info.newResource.id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- } else {
- //If the calendar isn't editable, the event isn't either.
- if (!info.event.startEditable || !info.view.viewSpec.options.editable) {
- //Read-only events cannot be dragged and dropped!
- info.revert();
- return;
- }
-
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.move,
- data: {
- cycle_id: info.event.id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end)
- }
- }).fail(info.revert);
- }
- }
-
- static institutePlanExternalDropEventHandler(info)
- {
- var resourceIds = info.event.getResources().map(resource => resource.id);
-
- $.post({
- async: false,
- url: info.event.extendedProps.studip_api_urls.drop,
- data: {
- course_id: info.event.extendedProps.course_id,
- begin: this.toRFC3339String(info.event.start),
- end: this.toRFC3339String(info.event.end),
- resource_id: resourceIds[0]
- }
- }).done(data => {
- if (data) {
- info.view.context.calendar.addEvent(JSON.parse(data));
- info.event.remove();
- }
- });
- }
-
- static createFromNode(node, additional_config = {})
- {
- if (!node) {
- //No node? No fullcalendar!
- return;
- }
-
- let config = $(node).data('config');
-
- let defaultView = 'timeGridWeek';
- if (Responsive.isResponsive() && config.responsiveDefaultView !== undefined) {
- defaultView = config.responsiveDefaultView;
- } else if (config.defaultView !== undefined) {
- defaultView = config.defaultView;
- }
-
- //Make sure the default values are set, if they are not found
- //in the additional_config object:
- config = $.extend({
- plugins: [ interactionPlugin, dayGridPlugin, timeGridPlugin, resourceCommonPlugin, resourceTimeGridPlugin, resourceTimelinePlugin ],
- schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
- header: {
- left: 'dayGridMonth,timeGridWeek,timeGridDay'
- },
- minTime: '08:00:00',
- maxTime: '20:00:00',
- validRange: {
- start: '1970-01-01'
- },
- height: 'auto',
- contentHeight: 'auto',
- firstDay: 1,
- weekNumberCalculation: 'ISO',
- locales: [enLocale, deLocale ],
- locale: String.locale === 'de-DE' ? 'de' : 'en-gb',
- timeFormat: 'H:mm',
- slotLabelFormat: {
- hour: 'numeric',
- minute: '2-digit',
- omitZeroMinute: false
- },
- columnHeaderHtml: STUDIP.Fullcalendar.renderDateForColumn,
- nowIndicator: true,
- timeZone: 'local',
- studip_functions: [],
- resourceAreaWidth: '20%',
- select (selectionInfo) {
- let calendar_config = JSON.parse(selectionInfo.view.context.calendar.el.dataset.config);
- let dialog_size = 'auto';
- if (calendar_config.dialog_size !== undefined) {
- dialog_size = calendar_config.dialog_size;
- }
-
- if (!selectionInfo.view.viewSpec.options.editable || !selectionInfo.view.viewSpec.options.studip_urls) {
- //The calendar isn't editable.
- return;
- }
- if (selectionInfo.view.viewSpec.options.studip_urls.add) {
- if (selectionInfo.resource) {
- STUDIP.Dialog.fromURL( selectionInfo.view.viewSpec.options.studip_urls.add, {
- data: {
- begin: selectionInfo.start.getTime()/1000,
- end: selectionInfo.end.getTime()/1000,
- ressource_id: selectionInfo.resource.id,
- all_day: selectionInfo.allDay ? '1' : '0'
- },
- size: dialog_size
- });
- } else {
- STUDIP.Dialog.fromURL(selectionInfo.view.viewSpec.options.studip_urls.add, {
- data: {
- begin: selectionInfo.start.getTime()/1000,
- end: selectionInfo.end.getTime()/1000,
- all_day: selectionInfo.allDay ? '1' : '0'
- },
- size: dialog_size
- });
- }
- }
- },
- eventClick (eventClickInfo) {
- var event = eventClickInfo.event;
- var extended_props = event.extendedProps;
- if ($(eventClickInfo.jsEvent.target).hasClass('event-colorpicker')) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/pick_color/' + extended_props.metadate_id + '/' + config.actionCalled),
- {'size': '400x400'}
- );
- return false;
- }
-
-
- if ($(eventClickInfo.event._calendar.el).hasClass('request-plan')) {
- if (extended_props.request_id && extended_props.studip_view_urls.edit) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit)
- );
- } else if(extended_props.studip_parent_object_class == 'ResourceBooking' && $.inArray('for-course', event._def.ui.classNames) != -1) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL('dispatch.php/resources/room_request/rerequest_booking/' + extended_props.studip_parent_object_id)
- );
- }
- return false;
- }
-
- if (extended_props.studip_view_urls === undefined) {
- return;
- }
- let calendar_config = JSON.parse(eventClickInfo.view.context.calendar.el.dataset.config);
- let dialog_size = 'auto';
- if (calendar_config.dialog_size !== undefined) {
- //Use the configured default dialog size for the fullcalendar instance:
- dialog_size = calendar_config.dialog_size;
- }
- if (extended_props.dialog_size !== undefined) {
- //Use the dialog size of the event:
- dialog_size = extended_props.dialog_size;
- }
- if (!event.startEditable && extended_props.studip_view_urls.show) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
- {size: dialog_size}
- );
- } else if (event.startEditable) {
- if (extended_props.studip_view_urls.edit) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.edit),
- {size: dialog_size}
- );
- } else if (extended_props.studip_view_urls.show) {
- STUDIP.Dialog.fromURL(
- STUDIP.URLHelper.getURL(extended_props.studip_view_urls.show),
- {size: dialog_size}
- );
- }
- }
- return false;
- },
- eventResize (info) {
- // The logic from fullcalendar is inversed here:
- // If the calendar isn't editable, the event isn't either.
- if (info.view.viewSpec.options.studip_functions.resize_event) {
- info.view.viewSpec.options.studip_functions.resize_event(info);
- } else {
- STUDIP.Fullcalendar.defaultResizeEventHandler(info);
- }
- info.event.source.refetch();
- },
- eventDrop (info) {
- let handle_drop = function() {
- if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
- var start = info.event.start;
- var cal_start = info.view.activeStart;
- if ((start.getHours() - cal_start.getHours()) % 2 === 1) {
- info.event.moveDates('-01:00');
- }
- STUDIP.Fullcalendar.institutePlanDropEventHandler(info);
- } else {
- if (info.view.viewSpec.options.studip_functions.drop_event) {
- info.view.viewSpec.options.studip_functions.drop_event(info);
- } else {
- STUDIP.Fullcalendar.defaultDropEventHandler(info);
- }
- info.event.source.refetch();
- }
- };
-
- let calendar_config = JSON.parse(info.view.context.calendar.el.dataset.config);
- if (calendar_config.confirm) {
- if (calendar_config.confirm.drop) {
- STUDIP.Dialog.confirm(calendar_config.confirm.drop)
- .done(handle_drop)
- .fail(function() {
- //Revert the dropped element:
- info.revert();
- });
- } else {
- handle_drop();
- }
- } else {
- handle_drop();
- }
- },
- eventRender (info) {
- 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);
- if (event.extendedProps.content_fields) {
- for (const [css_class, field] of Object.entries(event.extendedProps.content_fields)) {
- $(eventElement).find('.fc-content').append(
- $('<div>').css({
- width: 'calc(100% - 21px)',
- height: '100%',
- wordBreak: 'break-word'
- }).text(field)
- .addClass(css_class + ' fc-title')
- );
- }
- }
- $(eventElement).find('.fc-content').append(
- $('<button class="event-colorpicker">').addClass(iconColor)
- );
- } else {
- $(eventElement).attr('title', event.title);
- }
-
- if (event.extendedProps.icon) {
- //Check if there is more than one icon:
- let event_icons = event.extendedProps.icon.split(',');
- let title = $(eventElement).find('.fc-title');
- for (let icon of event_icons) {
- //Check if the icon is already a URL or just the name of an icon.
- let iconUrl = '';
- if (icon.includes('://')) {
- //The icon is already a URL.
- iconUrl = icon;
- } else {
- //The icon is just referenced by its name. We do not need a specific color here, background-color is currentColor.
- iconUrl = `${STUDIP.ASSETS_URL}images/icons/black/${icon}.svg`
- }
- //Add the icons as spans in front of the content:
- let icon_element = $('<span class="icon"></span>');
- icon_element.css('--icon-url', `url('${iconUrl}')`);
- 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')) {
- $(content).each(function(i, event_data){
- STUDIP.Fullcalendar.convertSemesterEvents(event_data, config.defaultDate);
- });
- }
- return content;
- },
- loading (isLoading) {
- if (isLoading) {
- if (!$('#loading-spinner').length) {
- jQuery('#content').append(
- $('<div id="loading-spinner" style="position: absolute; top: calc(50% - 55px); left: calc(50% + 135px); z-index: 9001;">').html(
- $('<img>').attr('src', STUDIP.ASSETS_URL + 'images/loading-indicator.svg')
- .css({
- width: 32,
- height: 32
- })
- )
- );
- }
- } else {
- $('#loading-spinner').remove();
- this.updateSize();
- }
- },
- datesRender (info) {
- if ($(node).hasClass('schedule') && info.view.type !== 'timeGridDay') {
- document.querySelector('.fc-right').style.display = 'none';
- }
-
- if ($(node).hasClass('semester-plan')) {
- //Nothing to do in the semester plan, since it already displays
- //all the information on loading.
- return;
- }
- let activeRange = info.view.props.dateProfile.activeRange;
- let timestamp = activeRange.start.getTime() / 1000;
- if ($(info.el).hasClass('institute-plan')) {
- $('.fc-slats tr:odd .fc-widget-content:not(.fc-axis)').remove();
- }
-
- if (document.getElementById('booking-plan-header-semname') === null) {
- return;
- }
-
- $.getJSON(
- STUDIP.URLHelper.getURL(`dispatch.php/resources/ajax/semester_week/${timestamp}`)
- ).done((data) => {
- if (data) {
- $('#booking-plan-header-semname').text(data.semester_name);
- if (data.sem_week) {
- $('#booking-plan-header-semweek').text(data.sem_week);
- $('#booking-plan-header-semweek-part').show();
- } else {
- $('#booking-plan-header-semweek').text('');
- $('#booking-plan-header-semweek-part').hide();
- }
- $('#booking-plan-header-semrow').show();
- $('#booking-plan-header-calweek').text(data.week_number);
- $('#booking-plan-header-calbegin').text('(' + data.current_day + ')');
- } else {
- $('#booking-plan-header-semrow').hide();
- $('#booking-plan-header-semweek-part').hide();
- }
- });
- },
- resourceRender (renderInfo) {
- if ($(renderInfo.view.context.calendar.el).hasClass('room-group-booking-plan')) {
- let action = $(renderInfo.view.context.calendar.el).hasClass('semester-plan') ? 'semester' : 'booking';
- let url = STUDIP.URLHelper.getURL(`dispatch.php/resources/room_planning/${action}_plan/${renderInfo.resource.id}`);
- $(renderInfo.el).find('.fc-cell-text').html(
- $('<a>').attr('href', url).text(renderInfo.resource.title)
- );
- } else if ($("*[data-fullcalendar='1']").hasClass('institute-plan') && renderInfo.resource.id > 0) {
- const icon = '<span class="btn-icon btn-icon--edit icon-role-clickable" aria-label="edit"></span>';
- $(renderInfo.el).append(
- '<a href="'
- + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/rename_column/'
- + renderInfo.resource.id
- +'/'
- + renderInfo.view.activeStart.getDay())
- + '" data-dialog="size=auto"> '
- + icon
- + '</a>'
- );
- }
- },
- drop (dropInfo) {
- $(dropInfo.draggedEl).remove();
- },
- eventReceive (info) {
- if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
- STUDIP.Fullcalendar.institutePlanExternalDropEventHandler(info);
- }
- }
- }, config);
-
- //Special treatment: If a general column header format is set,
- //in the configuration, it shall be used for all columns in all views
- //by using a special columnHeaderHtml function.
- if (config.columnHeaderFormat) {
- config.columnHeaderHtml = function (date) {
- if ($("*[data-fullcalendar='1']").hasClass('institute-plan')) {
- return '<a href="' + STUDIP.URLHelper.getURL('dispatch.php/admin/courseplanning/weekday/' + date.getDay()) + '">' + date.toLocaleDateString('de-DE', config.columnHeaderFormat) + '</a>';
- } else {
- return date.toLocaleDateString('de-DE', config.columnHeaderFormat);
- }
- };
- }
-
- 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);
- }
-
- static submitDatePicker() {
- let picked_date = jQuery('#booking-plan-jmpdate').val();
- if (!picked_date) {
- //Not a booking plan date selector.
- picked_date = jQuery('#date_select').val();
- }
- let iso_date_string = '';
- if (picked_date) {
- if (picked_date.includes('.')) {
- let [day, month, year] = picked_date.split('.');
- iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
- } else if (picked_date.includes('/')) {
- let [day, month, year] = picked_date.split('/');
- iso_date_string = year.padStart(4, '20') + '-' + month.padStart(2, '0') + '-' + day.padStart(2, '0');
- } else if (picked_date.includes('-')) {
- iso_date_string = picked_date;
- }
- }
- if (iso_date_string) {
- jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
- this.calendar.gotoDate(iso_date_string);
- });
- Fullcalendar.updateDateURL();
- }
- }
-
- static updateDateURL() {
- let changedMoment;
- jQuery('[data-fullcalendar="1"],[data-resources-fullcalendar="1"]').each(function () {
- changedMoment = this.calendar.getDate();
- });
- if (changedMoment) {
- let changed_date = STUDIP.Fullcalendar.toRFC3339String(changedMoment).split('T')[0];
- //Get the timestamp:
- let timestamp = changedMoment.getTime() / 1000;
-
- jQuery('a.resource-bookings-actions, a.calendar-action').each(function () {
- const url = new URL(this.href);
- url.searchParams.set('timestamp', timestamp.toString())
- url.searchParams.set('defaultDate', changed_date)
- this.href = url.toString();
- });
- jQuery('.sidebar-widget.calendar-action').each(function() {
- //Each sidebar widget is different. The placement of the defaultDate URL parameter
- //has to reflect that.
- jQuery(this).find('button[formaction]').each(function() {
- //Modify the formaction attribute:
- let url = new URL(jQuery(this).attr('formaction'));
- url.searchParams.set('defaultDate', changed_date);
- jQuery(this).attr('formaction', url.toString());
- });
- jQuery(this).find('form[action]').each(function() {
- //Add a hidden input with the defaultDate:
- let hidden_input = jQuery(this).find('input[name="defaultDate"]')[0];
- if (!hidden_input) {
- hidden_input = jQuery('<input type="hidden" name="defaultDate">');
- jQuery(this).append(hidden_input);
- }
- jQuery(hidden_input).val(changed_date);
- });
- });
-
- // Now change the URL of the window.
- const url = new URL(window.location.href);
- url.searchParams.set('defaultDate', changed_date);
-
- // Update url in history
- history.pushState({}, null, url.toString());
-
- // Adjust links accordingly
- url.searchParams.delete('allday');
- jQuery('.booking-plan-std_view').attr('href', url.toString());
-
- url.searchParams.set('allday', 1);
- jQuery('.booking-plan-allday_view').attr('href', url.toString());
-
- // Update sidebar value
- let element = jQuery('#booking-plan-jmpdate,#date_select').first();
- let padded_date = pad(changedMoment.getDate(), 2, '0')
- + '.' + pad(changedMoment.getMonth() + 1, 2, '0')
- + '.' + changedMoment.getFullYear();
- element.val(padded_date);
- if (element.is('#booking-plan-jmpdate')) {
- //Store the date in the sessionStorage:
- sessionStorage.setItem('booking_plan_date', changed_date);
- }
- }
- }
-
- static renderDateForColumn(date) {
- let format = new Intl.DateTimeFormat(String.locale, {weekday: 'short'});
- let date_html = STUDIP.DateTime.getStudipDate(date, false, true, true);
- return '<span class="dow">' + format.format(date) + '.</span> ' + date_html;
- }
-}
-
-export default Fullcalendar;
diff --git a/resources/assets/javascripts/lib/holiday.ts b/resources/assets/javascripts/lib/holiday.ts
new file mode 100644
index 0000000..2363eb1
--- /dev/null
+++ b/resources/assets/javascripts/lib/holiday.ts
@@ -0,0 +1,254 @@
+import {jsonapi} from "./jsonapi";
+import {datetime} from "./datetime";
+
+/**
+ * The Holiday class represents a holiday in Stud.IP
+ */
+class Holiday
+{
+ /**
+ * The day of the holiday.
+ */
+ day: Date;
+
+ /**
+ * The name of the holiday.
+ */
+ name: string;
+
+ /**
+ * Whether the holiday is an official holiday (true) or not (false).
+ * The latter means that it can be part of a vacation period.
+ */
+ official: boolean;
+
+ constructor(day: Date, name: string, official: boolean = true) {
+ this.day = day;
+ this.name = name;
+ this.official = official;
+ }
+}
+
+
+/**
+ * The HolidayCache class handles retrieving and caching holidays and vacations.
+ */
+class HolidayCache
+{
+ holiday_cache: Map<number, Array<Holiday>>;
+ vacation_cache: Map<number, Array<Holiday>>;
+
+ /**
+ * The constructor has code to load cached holidays and vacations from the session storage.
+ */
+ constructor() {
+ //Attempt to restore the cache from the session:
+ const holiday_cache_str = sessionStorage.getItem('fullcalendar_holidays');
+ this.holiday_cache = new Map<number, Array<Holiday>>();
+ if (holiday_cache_str != null) {
+ for (const [year, raw_array] of Object.entries(JSON.parse(holiday_cache_str))) {
+ const holidays: Array<Holiday> = [];
+ if (Array.isArray(raw_array)) {
+ for (const raw_holiday of raw_array) {
+ holidays.push(
+ new Holiday(new Date(raw_holiday.day), raw_holiday.name, raw_holiday.official)
+ );
+ }
+ this.holiday_cache.set(parseInt(year), holidays);
+ }
+ }
+ }
+
+ const vacation_cache_str = sessionStorage.getItem('fullcalendar_vacations');
+ this.vacation_cache = new Map<number, Array<Holiday>>();
+ if (vacation_cache_str != null) {
+ for (const [year, raw_array] of Object.entries(JSON.parse(vacation_cache_str))) {
+ const vacations: Array<Holiday> = [];
+ if (Array.isArray(raw_array)) {
+ for (const raw_vacation of raw_array) {
+ vacations.push(
+ new Holiday(new Date(raw_vacation.day), raw_vacation.name, raw_vacation.official)
+ );
+ }
+ this.vacation_cache.set(parseInt(year), vacations);
+ }
+ }
+ }
+ }
+
+ /**
+ * Loads the holidays of a year and stores them in the cache.
+ *
+ * @param year The year for which to load holidays.
+ */
+ loadHolidays(year: number) : void {
+ const existing_cache = this.holiday_cache.get(year);
+ if (existing_cache) {
+ return;
+ }
+
+ jsonapi.withPromises().GET('holidays', {
+ data: { 'filter[year]': year }
+ }).then(response => {
+ if (!response) {
+ return;
+ }
+ const events: Array<Holiday> = [];
+ for (const [date, data] of Object.entries(response)) {
+ const day = new Date(date);
+ events.push(new Holiday(day, data.holiday, data.mandatory));
+ }
+
+ this.holiday_cache.set(year, events);
+ //Update the session storage item:
+ sessionStorage.setItem('fullcalendar_holidays', JSON.stringify(Object.fromEntries(this.holiday_cache)));
+ });
+ }
+
+ /**
+ * Loads the vacation days of a year and stores them in the cache.
+ *
+ * @param year The year for which to load vacations.
+ */
+ loadVacations(year: number): void {
+ const existing_cache = this.vacation_cache.get(year);
+ if (existing_cache) {
+ return;
+ }
+ jsonapi.withPromises().get('vacations', {
+ data: {'filter[year]': year}
+ }).then(response => {
+ if (!response) {
+ return;
+ }
+ const events: Array<Holiday> = [];
+ for (const vacation_data of Object.values(response)) {
+ for (let i = parseInt(vacation_data.start); i < parseInt(vacation_data.end); i += 86400) {
+ const day = new Date(i * 1000);
+ events.push(new Holiday(day, vacation_data.name, false));
+ }
+ }
+
+ this.vacation_cache.set(year, events);
+ //Update the session storage item:
+ sessionStorage.setItem('fullcalendar_vacations', JSON.stringify(Object.fromEntries(this.vacation_cache)));
+ });
+ }
+
+ /**
+ * Returns a vacation day from the cache.
+ *
+ * @param date The day of the vacation.
+ *
+ * @returns Holiday|null If a vacation exists on the specified day, a Holiday object
+ * for it is returned. Otherwise, null is returned.
+ */
+ getVacation(date: Date) : Holiday|null {
+ const year_vacation_cache = this.vacation_cache.get(date.getFullYear());
+ if (!year_vacation_cache) {
+ return null;
+ }
+ for (const vacation of year_vacation_cache) {
+ if (datetime.getISODate(date) === datetime.getISODate(vacation.day)) {
+ //A vacation day has been found.
+ return vacation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Checks if there is a vacation day on the specified date.
+ *
+ * @param date The date to check.
+ *
+ * @returns boolean True, if the specified day is a vacation day.
+ * Otherwise, false is returned.
+ */
+ isVacation(date: Date) {
+ return this.getVacation(date) !== null;
+ }
+
+ /**
+ * Returns the name of the vacation day on the specified date.
+ *
+ * @param date The date for which to get the vacation name.
+ *
+ * @returns string The name of the vacation, if any.
+ * If there is no vacation day on the specified date, the string is empty.
+ */
+ getVacationName(date: Date) {
+ const vacation = this.getVacation(date);
+ if (vacation) {
+ return vacation.name;
+ }
+ return '';
+ }
+
+ /**
+ * Returns a holiday / vacation day from the cache.
+ *
+ * @param date The date of the holiday.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns Holiday|null If a holiday can be found, it is returned. Otherwise,
+ * null is returned.
+ */
+ getHoliday(date: Date, regard_vacations: boolean = false): Holiday|null {
+ //Check if an entry for the date exists in the holiday or the vacation list:
+ const year_holiday_cache = this.holiday_cache.get(date.getFullYear());
+ if (!year_holiday_cache) {
+ return null;
+ }
+ for (const holiday of year_holiday_cache) {
+ if (datetime.getISODate(date) === datetime.getISODate(holiday.day)) {
+ //A holiday has been found.
+ return holiday;
+ }
+ }
+ if (regard_vacations) {
+ return this.getVacation(date);
+ }
+ return null;
+ }
+
+ /**
+ * Checks if there is a holiday on the specified date.
+ *
+ * @param date The date to check.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns boolean True, if the specified day is a holiday / vacation day.
+ * Otherwise, false is returned.
+ */
+ isHoliday(date: Date, regard_vacations: boolean = false) : boolean {
+ return this.getHoliday(date, regard_vacations) !== null;
+ }
+
+ /**
+ * Returns the name of the holiday on the specified date.
+ *
+ * @param date The date for which to get the holiday name.
+ *
+ * @param regard_vacations Whether to regard vacations in addition to holidays (true)
+ * or to just regard holidays (false). Defaults to false.
+ *
+ * @returns string The name of the holiday / vacation, if any.
+ * If there is no holiday on the specified date, the string is empty.
+ */
+ getHolidayName(date: Date, regard_vacations: boolean = false) : string {
+ const holiday = this.getHoliday(date, regard_vacations);
+ if (holiday) {
+ return holiday.name;
+ }
+ return '';
+ }
+}
+
+
+export {Holiday, HolidayCache};
+export const holiday_cache: HolidayCache = new HolidayCache();
diff --git a/resources/assets/stylesheets/fullcalendar.scss b/resources/assets/stylesheets/fullcalendar.scss
deleted file mode 100644
index 70ed244..0000000
--- a/resources/assets/stylesheets/fullcalendar.scss
+++ /dev/null
@@ -1,306 +0,0 @@
-@import "scss/variables";
-@import "scss/buttons";
-@import "mixins";
-
-a.fc-event,
-td.fc-event {
- border-radius: 0;
-
- .fc-time {
- background-color: rgba(255, 255, 255, 0.2);
- font-weight: bold;
- }
-
- .fc-title .icon {
- content: '';
- display: inline-block;
- vertical-align: text-bottom;
- margin-right: 3px;
- width: 12px;
- height: 12px;
- mask-size: contain;
- mask-repeat: no-repeat;
- mask-position: center;
- background-color: currentColor;
- mask-image: var(--icon-url);
- }
-}
-
-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;
- }
-
- .fc-button-group {
- height: 30px;
-
- .fc-button {
- @include button;
- margin-top: 0;
- margin-bottom: 0;
- padding: 0;
-
- &:last-of-type {
- margin-right: 0;
- }
-
- .fc-icon {
- /* Unset rules that are set in the fullcalendar default stylesheet: */
- line-height: unset;
- height: unset;
- }
- }
- }
-}
-
-/* Put the year in a new line in the mobile view: */
-html.responsive-display .fc .fc-view:not(.fc-timeGridDay-view) .fc-day-header {
- span.dow, span.year {
- display: block;
- }
-}
-
-
-.fc-button-primary:not(:disabled):active,
-.fc-button-primary:not(:disabled).fc-button-active,
-.fc button.fc-button.fc-state-active {
- box-shadow: none;
-
- background-color: var(--base-color) !important;
- color: var(--white);
-}
-
-/* adjust height: */
-/* .fc-scroller.fc-time-grid-container {
- height: auto !important;
- min-height: 0 !important;
-}*/
-
-.studip-fullcalendar-header {
- /* This shall look like a table caption. */
- background-color: transparent;
- color: var(--base-gray);
- font-size: 1.4em;
- text-align: left;
-
- &.fullcalendar-dialog{
- width: calc(100% - 550px);
- vertical-align: middle;
- display: inline-block;
- margin-right: 275px;
- }
-}
-
-.fullcalendar-dialogwidget-container {
- border-left: 0;
- display: inline-block;
- flex: 0 0 auto;
- margin-bottom: 1em;
- position: relative;
-
- $width: 270px;
-
- padding-bottom: 7px;
- width: $width;
- z-index: 2;
-
-
- .fullcalendar-dialogwidget-widget {
- background: var(--white);
- border: 1px solid var(--content-color-40);
- margin: 15px 0 0;
- }
-
- .fullcalendar-dialogwidget-widget-header {
- @include clearfix;
- background: var(--content-color-20);
- color: var(--base-color);
- font-weight: bold;
- padding: 4px;
- }
-
- select.fullcalendar-dialogwidget-selectlist {
- overflow-y: auto;
- width: 100%;
- }
-
- .fullcalendar-dialogwidget-widget-content {
- border-top: 1px solid var(--content-color-40);
- padding: 4px;
- transition: all 0.5s;
- }
-}
-
-.institute-plan .fc-slats tr {
- height: 100px;
-}
-
-#external-events{
- td.fc-event {
- border-radius: 0;
- margin: 2px 0;
- background-color: var(--content-color);
- border: 1px solid var(--brand-color-light);
- }
-}
-
-.institute-plan {
- .fc-bg td.fc-today {
- background: none;
- }
-
- th.fc-day-header, .fc-axis, th.fc-resource-cell {
- background-color: var(--content-color-10);
- }
-}
-
-.calendar-caption {
- background-color: transparent;
- padding-top: 0;
- color: var(--base-gray);
- font-size: 1.4em;
- text-align: left;
- margin-bottom: -10px;
-}
-
-#event-color-picker {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- column-gap: 16px;
- width: 200px;
- height: 204px;
- margin: 10px 0 10px calc(50% - 100px);
-
- input {
- opacity: 0;
- position: absolute;
-
- + label {
- cursor: pointer;
-
- &::before {
- background-repeat: no-repeat;
- content: ' ';
- display: inline-block;
- margin: 0 1px 1px 1px;
- vertical-align: text-top;
- background-image: none;
- background-size: 100%;
- height: 100%;
- width: calc(100% - 4px);
- }
- }
-
- &:checked + label {
- @include icon(before, checkbox-checked, $size: 100%);
- &::before {
- background-color: var(--color--font-inverted);
- }
- }
- }
-}
-
-.event-colorpicker {
- background: none;
- border: 0;
- cursor: pointer;
- padding: 0;
-
- width: 20px;
- height: 20px;
-
- position: absolute;
- top: 0;
- right: 0;
-
- &.white {
- @include icon(before, group4, $size: 100%);
- &::before {
- background-color: var(--color--font-inverted);
- }
- }
- &.black {
- @include icon(before, group4, $size: 100%);
- &::before {
- background-color: var(--color--font-primary);
- }
- }
-}
-
-.fc[data-fullcalendar="1"].print-view {
- position: absolute;
- top: 0;
- left: 0;
- height: 2000px;
- width: 2000px;
-
- .fc-resource-cell img,
- .event-colorpicker {
- display: none;
- }
- th span a {
- color: var(--black);
- }
-
- td.fc-today {
- background: none;
- }
- .fc-now-indicator {
- border: 0;
- }
-
- &.without-weekend {
- .fc-day-header,
- .fc-day,
- .fc-content-skeleton td {
- &:last-child,
- &:nth-last-child(2) {
- display: none;
- }
- }
- }
-}
-
-
-// Responsive
-.responsive-display {
- .fc-header-toolbar {
- flex-direction: column;
-
- .fc-left,
- .fc-center,
- .fc-right {
- width: 100%;
- text-align: center;
- }
- .fc-left,
- .fc-center {
- margin-bottom: $line-height-computed - $font-size-base;
- }
-
- }
-
- .fc-view-container {
- .fc-day-header {
- overflow: hidden;
- }
- }
-}
diff --git a/resources/assets/stylesheets/print.scss b/resources/assets/stylesheets/print.scss
index fd93a75..7b2c5d8 100644
--- a/resources/assets/stylesheets/print.scss
+++ b/resources/assets/stylesheets/print.scss
@@ -1,8 +1,6 @@
@import "mixins";
@import "scss/breakpoints";
@import "scss/visibility";
-@import "scss/fullcalendar-print";
-@import "scss/resources-print";
/*******************************************************************************
@@ -224,3 +222,7 @@ a.link-extern {
font-style: oblique;
}
}
+
+.print-hidden {
+ display: none;
+}
diff --git a/resources/assets/stylesheets/scss/calendar.scss b/resources/assets/stylesheets/scss/calendar.scss
index 356cb3e..480c028 100644
--- a/resources/assets/stylesheets/scss/calendar.scss
+++ b/resources/assets/stylesheets/scss/calendar.scss
@@ -1,10 +1,13 @@
-.fc-body {
+.fc-view {
.fc-event {
background-color: #fff;
- color: var(--color--font-primary);
border-width: 1px;
+ .fc-event-main {
+ color: var(--color--font-primary);
+ }
+
&:hover {
color: var(--color--font-primary);
}
@@ -16,10 +19,6 @@
&:hover {
background-color: lighten($group-color-0, 50%);
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-0;
- }
}
&.course-color-1 {
@@ -29,10 +28,6 @@
&:hover {
background-color: lighten($group-color-1, 50%);
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-1;
- }
}
&.course-color-2 {
@@ -42,10 +37,6 @@
&:hover {
background-color: lighten($group-color-2, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-2;
- }
}
&.course-color-3 {
@@ -55,10 +46,6 @@
&:hover {
background-color: lighten($group-color-3, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-3;
- }
}
&.course-color-4 {
@@ -68,10 +55,6 @@
&:hover {
background-color: lighten($group-color-4, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-4;
- }
}
&.course-color-5 {
@@ -81,10 +64,6 @@
&:hover {
background-color: lighten($group-color-5, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-5;
- }
}
&.course-color-6 {
@@ -94,10 +73,6 @@
&:hover {
background-color: lighten($group-color-6, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-6;
- }
}
&.course-color-7 {
@@ -107,10 +82,6 @@
&:hover {
background-color: lighten($group-color-7, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-7;
- }
}
&.course-color-8 {
@@ -120,19 +91,11 @@
&:hover {
background-color: lighten($group-color-8, 50%);;
}
-
- .fc-time {
- border-bottom: 1px solid $group-color-8;
- }
}
&.marked-course {
border-color: var(--black);
background-color: var(--white);
-
- .fc-time {
- border-bottom: 1px solid var(--black);
- }
}
&.marked-course,
diff --git a/resources/assets/stylesheets/scss/fullcalendar-print.scss b/resources/assets/stylesheets/scss/fullcalendar-print.scss
index 9e52dbd..c3145da 100644
--- a/resources/assets/stylesheets/scss/fullcalendar-print.scss
+++ b/resources/assets/stylesheets/scss/fullcalendar-print.scss
@@ -1,115 +1,74 @@
+/*
+ * Fullcalendar rules for printing calendars.
+ */
+@media print {
+
+ .fc {
+ /* Disable page breaks inside the calendar: */
+ page-break-inside: avoid;
+
+ /*
+ * Hide the buttons:
+ */
+ .fc-button-group {
+ display: none;
+ }
-a.fc-event {
- border-radius: 0;
- color: var(--black);
+ /*
+ * Hide the now indicator:
+ */
+ .fc-timegrid .fc-timegrid-now-indicator-container {
+ display: none;
+ }
- .fc-time {
- text-decoration: underline;
- color: var(--black);
- }
-}
+ /*
+ * In case the calendar is smaller than the page width,
+ * it shall not look like there is some content missing on the right side:
+ */
+ .fc-scrollgrid {
+ width: unset;
+ max-width: 100%;
+ }
-div.fc-toolbar {
- display: none;
-}
+ /*
+ * Make sure that events are printed in black and white
+ * when their colours are not set explicitly:
+ */
+ .fc-event-main, .fc-v-event {
+ background-color: $white;
+ border-color: $black;
+
+ /*
+ * Set the text colour for event content to black:
+ */
+ .event-content {
+ color: $black;
+ }
-/* the "now" indicator: */
-.fc-now-indicator.fc-now-indicator-line,
-.fc-now-indicator.fc-now-indicator-arrow {
- display: none;
-}
-/* adjust height: */
-.fc-resourceTimelineDay-view {
- .fc-widget-header{
- .fc-scroller {
- height: 40px !important;
- .fc-content > table{
- height: 26px !important;
+ /*
+ * Position icons with the text and make them black:
+ */
+ .icons {
+ display: inline-block;
+ vertical-align: top;
+ color: $black;
}
}
- }
-}
-.fc-resourceTimelineWeek-view {
- .fc-widget-header{
- .fc-scroller {
- height: 60px !important;
- .fc-content > table{
- height: 26px !important;
- }
- }
- }
-}
-.fc-scroller.fc-time-grid-container {
- height: auto !important;
- min-height: 0 !important;
-}
-/* disable page break for calendar: */
-div.fc-view-container {
- page-break-inside: avoid;
-}
-
-/* a potential existing map key: */
-.map-key-list {
- display: none;
-}
-
-.studip-fullcalendar-header {
- &.fullcalendar-dialog{
- display: block;
- width: 100%;
- margin-top: 2em;
- margin-right: 0;
- }
-}
-
-.fullcalendar-dialogwidget-container {
- display:none !important;
-}
-
-div.fc-timeGrid-view th.fc-axis, td.fc-axis {
- display: table-cell !important;
-}
-
-div.fc-slats, div.fc-time-grid hr {
- display: block !important;
-}
-
-section.fc th, section.fc td, section.fc hr, section.fc thead, section.fc tbody, .fc-row {
- background: none !important;
-}
-
-div.fc-content-skeleton {
- & table {
- height: 100% !important;
- }
- height: 100%;
-}
-
-div.fc-time-grid a.fc-event {
- position: absolute !important;
- margin: 0 0 1px 0 !important;
-
- img {
- filter: brightness(0);
+ /*
+ * Do not colour the current day in case someone wants to print
+ * with backgrounds:
+ */
+ .fc-timegrid-col.fc-day-today {
+ background-color: unset;
+ }
}
-}
-a.fc-timeline-event {
- img {
- filter: brightness(0);
- }
-}
-
-.fc-slats table tr {
- line-height: 22px;
-}
-
-.fc-resource-area {
- .fc-cell-content {
- a > img {
- display: none;
- }
+ /*
+ * Hide a potential existing map key list:
+ */
+ .map-key-list {
+ display: none;
}
}
diff --git a/resources/assets/stylesheets/scss/fullcalendar.scss b/resources/assets/stylesheets/scss/fullcalendar.scss
new file mode 100644
index 0000000..c3972bc
--- /dev/null
+++ b/resources/assets/stylesheets/scss/fullcalendar.scss
@@ -0,0 +1,341 @@
+@import "buttons";
+@import "variables";
+
+.fc {
+ .fc-toolbar.fc-header-toolbar {
+ margin-bottom: 0.5em;
+
+ /* Make sure that all the elements of the header stay inside the area of the calendar: */
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ /* Styles for the buttons above the calendar: */
+ .fc-button-group {
+
+ height: 30px;
+
+ .fc-button.fc-button-primary {
+ @include button;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding: 0;
+
+ &.fc-button-active {
+ @extend .active;
+ }
+
+ /* Remove the default gray "aura" around a focused button. */
+ &:focus {
+ box-shadow: var(--brand-color-dark) 0 0 0 1px;
+ }
+
+ /* The last button on the right shall align with the calendar border: */
+ &:last-of-type {
+ margin-right: 0;
+ }
+
+ /* A button with an icon as content. */
+ .fc-icon {
+ /* Unset rules that are set in the fullcalendar default stylesheet: */
+ line-height: unset;
+ height: unset;
+ }
+ }
+ }
+
+ /* Styles for header columns (dates or days of the week) */
+
+ /* Column header text shall not be displayed as link: */
+ @mixin calendar-header-column-content {
+ a {
+ color: var(--text-color);
+
+ /* overwrite the default Stud.IP style for links: */
+ &:visited, &:active, &:hover {
+ text-decoration: none;
+ color: var(--text-color);
+ }
+ }
+ }
+
+ /* Grid view header columns: */
+ table.fc-col-header thead {
+ background-color: var(--table-header-color);
+
+ .fc-col-header-cell, .fc-timegrid-axis {
+ @include calendar-header-column-content;
+ }
+ }
+
+ /* Timeline view header columns: */
+ .fc-resource-timeline table.fc-scrollgrid-sync-table th {
+ background-color: var(--table-header-color);
+
+ .fc-timeline-slot-frame {
+ @include calendar-header-column-content;
+ }
+ }
+
+ .fc-timegrid {
+
+ /*
+ There shall be no lines with empty labels for the minor time divisions
+ like half hours in a calendar where only the full hours have labels.
+ */
+ .fc-timegrid-slot-label.fc-timegrid-slot-minor {
+ border-top-style: none;
+ }
+
+ /* The "now indicator" shall have an orange colour: */
+ .fc-timegrid-now-indicator-arrow {
+ border-color: var(--orange);
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+ }
+ .fc-timegrid-now-indicator-line {
+ border-color: var(--orange);
+ }
+ }
+
+ .fc-daygrid, .fc-timegrid {
+ /*
+ Disable the short day of week by default:
+ */
+ .fc-col-header-cell {
+ .dow {
+ display: block;
+ }
+
+ .dow-short {
+ display: none;
+ }
+ }
+ }
+
+ /* Display the week number at the top of the cell in the top left corner: */
+ &.fc-liquid-hack div.fc-timegrid-axis-frame-liquid {
+ position: unset;
+ a.fc-timegrid-axis-cushion {
+ padding: 2px 4px;
+ }
+ }
+
+
+ /* Styles for regular calendar events: */
+ a.fc-event,
+ td.fc-event {
+ /* no round corners for events: */
+ border-radius: 0;
+
+ /* extra thick border on the left side:*/
+ border-left-width: 8px;
+
+ /* Hide overflowing text: */
+ overflow: hidden;
+
+ /* Remove the default minus character for short events: */
+ .fc-timegrid-event-short .fc-event-time:after {
+ content: "";
+ }
+
+ /* The time or location in an event. */
+ .fc-event-time, .fc-event-location {
+ opacity: 70%;
+ }
+
+ /* The content shall span over the whole height of the event,
+ even if it doesn't need all the space. */
+ .event-content {
+ height: 100%;
+ }
+
+ /* The title of an event: */
+ .fc-event-title {
+ overflow-wrap: break-word;
+ }
+
+ /* The icons inside an event: */
+ .fc-event-title-container {
+
+ .action-icons {
+ position: absolute;
+ right: 0;
+
+ button {
+ border: none;
+ background: none;
+ cursor: pointer;
+ }
+ }
+
+ .icons .studip-icon {
+ width: $font-size-base;
+ height: $font-size-base;
+ }
+ }
+ }
+}
+
+/*
+ * Special rules for the responsive view.
+ */
+html.responsive-display .fc {
+ .fc-toolbar.fc-header-toolbar {
+ flex-direction: column;
+
+ .fc-toolbar-chunk {
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: $line-height-computed - $font-size-base;
+ }
+ }
+ }
+
+ .fc-timegrid:not(.fc-timeGridDay-view) {
+ /*
+ Use extra lines for the day of week, day and month and the year
+ in the week view:
+ */
+ .fc-day {
+ .dow, .year {
+ display: block;
+ }
+ }
+ }
+
+ /*
+ Disable the long day of week for the month and the week view:
+ */
+ .fc-daygrid, .fc-timegrid:not(.fc-timeGridDay-view) {
+ .fc-col-header-cell {
+ .dow {
+ display: none;
+ }
+
+ .dow-short {
+ display: block;
+ }
+ }
+ }
+}
+
+/*
+ * Special rules for calendars that display holidays:
+ */
+.fc.with-holidays {
+ .fc-timegrid {
+ /*
+ * Header cells in time grid views shall have a fixed size to avoid jumping between
+ * weeks with and without holidays. The latter shall also be displayed in normal font weight
+ * and with a smaller font. All of this shall only happen when the calendar displays holidays.
+ */
+ .fc-col-header-cell {
+ height: 2.85em;
+
+ .day {
+ /* The year may move into the next line of text in case that there is
+ * not enough space for displaying the date in one line.
+ */
+ .year {
+ display: inline-block;
+ }
+
+ /* In case the day of week is too long, it shall be broken into the next line to prevent overflow. */
+ .dow {
+ word-break: break-all;
+ }
+
+ /*
+ * Holiday names are displayed a bit smaller. The name also must not overflow.
+ * Instead, words shall be broken.
+ */
+ .holiday {
+ font-weight: normal;
+ font-size: 0.75em;
+ word-break: break-all;
+ }
+ }
+ }
+ }
+}
+
+/* Special styles: */
+
+/* Events that are displayed in a Fullcalendar
+ * that is diplayed inside a Stud.IP contentbox need to have
+ * a padding of 0 to prevent them from being unnecessary empty.
+ */
+section.contentbox .fc .fc-event-main section
+{
+ padding: 0;
+}
+
+/* The institute plan has larger cells than normal calendars: */
+div.fc.institute-plan {
+ .fc-timegrid-slot {
+ height: 100px;
+ }
+}
+
+/*
+ * The room management uses special headers that shall look like
+ * table captions:
+ */
+.studip-fullcalendar-header {
+ background-color: transparent;
+ color: var(--base-gray);
+ font-size: 1.4em;
+ text-align: left;
+
+ &.fullcalendar-dialog{
+ width: calc(100% - 550px);
+ vertical-align: middle;
+ display: inline-block;
+ margin-right: 275px;
+ }
+}
+
+
+/*
+ * Special rules for the colour picker in the course planning view
+ * (admin/coursplanning/index):
+ */
+#event-colour-picker {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ column-gap: 16px;
+ width: 200px;
+ height: 204px;
+ margin: 10px 0 10px calc(50% - 100px);
+
+ input {
+ opacity: 0;
+ position: absolute;
+
+ + label {
+ cursor: pointer;
+
+ &::before {
+ background-repeat: no-repeat;
+ content: ' ';
+ display: inline-block;
+ margin: 0 1px 1px 1px;
+ vertical-align: text-top;
+ background-image: none;
+ background-size: 100%;
+ }
+ }
+
+ &:checked + label {
+ @include icon(before, checkbox-checked, $size: 90%);
+ &::before {
+ background-color: var(--color--font-inverted);
+ }
+ }
+ }
+}
+
+
+@import "fullcalendar-print";
diff --git a/resources/vue/apps/StudipCalendar.vue b/resources/vue/apps/StudipCalendar.vue
new file mode 100644
index 0000000..c2689e5
--- /dev/null
+++ b/resources/vue/apps/StudipCalendar.vue
@@ -0,0 +1,596 @@
+<template>
+ <section class="studip-fullcalendar">
+ <Teleport v-if="eventColourPicker" to="#sidebar-calendar-colour-picker"
+ name="sidebar-calendar-colour-picker">
+ <section>
+ <p class="info-text">
+ {{ $gettext('Im unteren Bereich können Sie bis zu 4 Farben frei wählen und diese via Drag & Drop auf Termine ziehen. Wenn Sie fertig sind, klicken Sie auf das Drucken-Symbol unter den Farbwählern.') }}
+ </p>
+ <div v-for="i in 4" class="colour-selector" style="background-color: #000000;"
+ :key="i" draggable="true"
+ @dragstart="startColourDragging($event)">
+ <input type="color" value="#000000" class="big-colour-input">
+ </div>
+ <StudipIcon class="text-bottom print-action" shape="print"
+ @click="printCalendar"
+ :title="$gettext('Individuelle Druckansicht drucken')"></StudipIcon>
+ </section>
+ </Teleport>
+ <FullCalendar :options="calendar_options"
+ :class="all_extra_classes"
+ ref="fullCalendar">
+ <template v-slot:dayHeaderContent="arg">
+ <div v-if="['timeGridDay', 'timeGridWeek', 'resourceTimelineWeek', 'resourceTimelineDay'].includes(arg.view.type)"
+ class="day">
+ <div class="dow-short" v-html="getColumnDow(arg.date, true)"></div>
+ <div class="dow" v-html="getColumnDow(arg.date)"></div>
+ <template v-if="!isSemesterView(arg.view)">
+ <span class="date" v-html="getColumnDate(arg.date)"></span>
+ <div class="holiday" v-if="isHoliday(arg.date)">
+ {{ getHolidayName(arg.date) }}
+ </div>
+ </template>
+ </div>
+ <template v-if="arg.view.type === 'dayGridMonth'">
+ <div class="dow-short" v-html="getColumnDow(arg.date, true)"></div>
+ <div class="dow" v-html="getColumnDow(arg.date)"></div>
+ </template>
+ </template>
+ <template v-slot:eventContent="arg">
+ <section v-if="arg.event.display === 'auto' && ['timeGridDay', 'timeGridWeek'].includes(arg.view.type)"
+ :title="arg.event.title" class="event-content"
+ @drop="handleElementDropOnEvent($event)"
+ @dragenter.prevent @dragover.prevent>
+ <div class="fc-event-title-container">
+ <span v-if="arg.event.extendedProps['icons']" class="icons">
+ <StudipIcon v-for="icon of arg.event.extendedProps['icons']"
+ :shape="icon" v-bind:key="icon"
+ :style="{color: arg.event.textColor}"
+ class="text-bottom"></StudipIcon>
+ </span>
+ <div v-if="arg.event.extendedProps['action-icons']"
+ class="action-icons" ref="action_icons">
+ <span v-for="[key, action] of Object.entries(arg.event.extendedProps['action-icons'] as Array<Action>)"
+ v-bind:key="key">
+ <button :title="action.label"
+ @click.stop="openActionIconUrlAsDialog(action)">
+ <StudipIcon v-if="action.icon_name"
+ :shape="action.icon_name"
+ :style="{color: arg.event.textColor}"
+ class="text-bottom"></StudipIcon>
+ </button>
+ </span>
+ </div>
+ <span v-if="arg.event.title" class="fc-event-title">
+ {{ arg.event.title }}
+ </span>
+ <span v-if="arg.event.extendedProps['title-lines']"
+ class="fc-event-title">
+ <div v-for="[key, line] of Object.entries(arg.event.extendedProps['title-lines'])"
+ v-bind:key="key">
+ {{ line }}
+ </div>
+ </span>
+ </div>
+ <div v-if="!arg.event.allDay" class="fc-event-time">
+ {{ arg.timeText }}
+ </div>
+ </section>
+ <div v-if="arg.event.display === 'auto' && ['dayGridMonth', 'resourceTimelineWeek', 'resourceTimelineDay'].includes(arg.view.type)"
+ :style="{color: arg.event.textColor, backgroundColor: arg.event.backgroundColor, borderColor: arg.event.borderColor, width: '100%'}"
+ :title="arg.event.title">
+ <span v-if="['dayGridMonth', 'dayGridYear'].includes(arg.view.type)"
+ class="fc-event-time">
+ {{ arg.timeText }}
+ </span>
+ <span class="fc-event-title-container">
+ <span v-if="arg.event.extendedProps['icons']" class="icons">
+ <StudipIcon v-for="icon of arg.event.extendedProps['icons']" :shape="icon" v-bind:key="icon"
+ class="text-bottom"></StudipIcon>
+ </span>
+ <span class="fc-event-title">
+ {{ arg.event.title }}
+ </span>
+ </span>
+ </div>
+ <div v-if="arg.event.display === 'background'"
+ :title="arg.event.title">
+ <div v-if="arg.event.extendedProps['generate-title']"
+ class="title">
+ {{ arg.event.title }}
+ </div>
+ </div>
+ </template>
+ </FullCalendar>
+ </section>
+</template>
+<script lang="ts">
+import {defineComponent} from 'vue';
+import FullCalendar from "@fullcalendar/vue3";
+import {
+ CalendarOptions, CalendarApi,
+ DateSelectionApi, DatesSetArg,
+ EventClickArg, EventDropArg, EventInput, ViewApi,
+} from '@fullcalendar/core';
+import {Draggable, EventReceiveArg, EventResizeDoneArg} from '@fullcalendar/interaction';
+
+import {Action} from "../../assets/javascripts/lib/action";
+import {holiday_cache} from "../../assets/javascripts/lib/holiday";
+import {datetime} from "../../assets/javascripts/lib/datetime";
+import Dialog from "../../assets/javascripts/lib/dialog.js";
+import StudipIcon from "../components/StudipIcon.vue";
+import {EventURLParameters, StudipCalendarConfig} from "../../assets/javascripts/lib/calendar";
+
+export default defineComponent({
+ name: "StudipCalendar",
+ computed: {
+ Dialog() {
+ return Dialog
+ }
+ },
+ components: {
+ StudipIcon,
+ FullCalendar
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ default: () => ({})
+ },
+ actionUrls: {
+ type: Object,
+ required: false,
+ default: () => ({})
+ },
+ dialogSize: {
+ type: String,
+ required: false,
+ default: 'auto'
+ },
+ customEventHandlers: {
+ type: Object,
+ required: false,
+ default: () => ({})
+ },
+ displayHolidays: {
+ type: Boolean,
+ required: false,
+ default: true
+ },
+ displayVacations: {
+ type: Boolean,
+ required: false,
+ default: true
+ },
+ extraClasses: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ externalDroppableContainerId: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ externalDroppableEventSelector: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ eventColourPicker: {
+ type: Boolean,
+ required: false,
+ default: false
+ }
+ },
+ emits: {
+ eventDropped: (payload: EventDropArg) => payload,
+ eventReceived: (payload: EventReceiveArg) => payload,
+ eventResized: (payload: EventResizeDoneArg) => payload
+ },
+ setup() {
+ const printCalendar = () => {
+ window.print();
+ };
+ return {printCalendar};
+ },
+ data() {
+ //Make sure that defaults are set for the calendar:
+ const full_config = new StudipCalendarConfig(this.config);
+ //Convert the configuration to an object that can be passed
+ //to Fullcalendar later:
+ const calendar_options = full_config.getConfig();
+ const all_extra_classes : Array<string> = [];
+ if (this.extraClasses.length > 0) {
+ all_extra_classes.push(this.extraClasses);
+ }
+ if (this.displayVacations || this.displayHolidays) {
+ all_extra_classes.push('with-holidays');
+ }
+
+ //Check if the responsive-design class is present in the HTML DOM node.
+ //If so, start in the day view (if present) instead of the week view (if present).
+ if (calendar_options.views !== undefined
+ && calendar_options.views.timeGridDay !== undefined
+ && calendar_options.views.timeGridWeek !== undefined
+ && calendar_options.initialView === 'timeGridWeek') {
+ const nodes = document.getElementsByTagName('html');
+ if (nodes.length >= 1) {
+ //Regard only the first node.
+ const html_node = nodes[0];
+ if (html_node.classList.contains('responsive-display')) {
+ //Start in day view:
+ calendar_options.initialView = 'timeGridDay';
+ }
+ }
+ }
+
+ //Now the event handlers for this component are set:
+ const event_handlers = {
+ datesSet: this.handleCalendarRangeUpdate,
+ eventDrop: calendar_options.editable ? this.handleEventDrop : undefined,
+ eventResize: calendar_options.editable ? this.handleEventResize : undefined,
+ select: calendar_options.selectable ? this.handleSelection : undefined,
+ eventClick: this.handleEventClick,
+ eventReceive: this.handleEventReceive
+ } as CalendarOptions;
+
+ //Return the calendar options with other data:
+ return {
+ calendar_api: null as CalendarApi|null,
+ calendar_options: {...calendar_options, ...event_handlers},
+ all_extra_classes: all_extra_classes.join(' ')
+ }
+ },
+ mounted() {
+ const calendar = this.$refs.fullCalendar as typeof FullCalendar;
+ if (calendar) {
+ this.calendar_api = calendar.getApi();
+ }
+ this.initExternalDraggableItems();
+ if (this.$refs.action_icons) {
+ const element = this.$refs.action_icons as HTMLDivElement;
+ element.addEventListener<"click">('click', function(event: Event) {
+ event.preventDefault();
+ });
+ }
+ //Check if there is a date selector with calendar control enabled.
+ //In that case, the calendar shall change its date when the
+ //date selector changes its value.
+ const date_picker = document.querySelector('#date_select[data-calendar-control]') as HTMLElement;
+ if (date_picker) {
+ date_picker.onchange = this.useDateFromDatePicker;
+ }
+ },
+ methods: {
+ openActionIconUrlAsDialog(action: Action) {
+ if (action.url) {
+ Dialog.fromURL(action.url, {size: 'auto'});
+ }
+ },
+ handleElementDropOnEvent(event: DragEvent) {
+ if (!event.dataTransfer || !event.target) {
+ return;
+ }
+ const drop_target = event.target as HTMLElement;
+ const colour = event.dataTransfer.getData('colour');
+ if (colour && drop_target) {
+ //Colour the drop target:
+ drop_target.style.backgroundColor = colour;
+ //Colour the surrounding .fc-event element (for the border):
+ const fc_event = drop_target.closest('.fc-event') as HTMLElement;
+ if (fc_event) {
+ fc_event.style.backgroundColor = colour;
+ fc_event.style.borderColor = colour;
+ }
+ }
+ },
+ useDateFromDatePicker(event: Event) {
+ if (!event.target || !this.calendar_api) {
+ //Nothing to do.
+ return;
+ }
+ const target = event.target as HTMLInputElement;
+ if (!target) {
+ //Still nothing to do.
+ return;
+ }
+ if (!target.value) {
+ //Positively still nothing to do.
+ return;
+ }
+ const date_str = target.value;
+ //The date format should be in the format dd.mm.YYYY.
+ //But d.m.YYYY could also be acceptable.
+ if (date_str.length < 8) {
+ //Strange unsupported date format.
+ return;
+ }
+ //The date string needs to be split into three parts and then used
+ //as date parts for a Date object.
+ const date_parts = date_str.split('.');
+ if (date_parts.length != 3) {
+ //Invalid date format.
+ return;
+ }
+ const date = new Date();
+ date.setFullYear(parseInt(date_parts[2]));
+ date.setMonth(parseInt(date_parts[1]) - 1);
+ date.setDate(parseInt(date_parts[0]));
+ date.setHours(12);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ this.calendar_api.gotoDate(date);
+ },
+ startColourDragging(event: DragEvent) {
+ if (!event.dataTransfer || !event.target) {
+ return;
+ }
+ const colour_el = event.target as HTMLElement;
+ if (!colour_el.style.backgroundColor) {
+ return;
+ }
+ event.dataTransfer.dropEffect = 'move';
+ event.dataTransfer.effectAllowed = 'move';
+ event.dataTransfer.setData('colour', colour_el.style.backgroundColor);
+ },
+ initExternalDraggableItems() {
+ if (this.externalDroppableContainerId.length > 0
+ && this.externalDroppableEventSelector.length > 0) {
+ const container = document.getElementById(this.externalDroppableContainerId);
+ if (container) {
+ new Draggable(
+ container,
+ {
+ itemSelector: this.externalDroppableEventSelector,
+ eventData: function (event_element) {
+ const event_attr = event_element.getAttribute('data-event');
+ if (event_attr) {
+ return JSON.parse(event_attr);
+ }
+ return null;
+ }
+ }
+ );
+ }
+ }
+ },
+ getColumnDate(date: Date) {
+ return datetime.getStudipDate(date, false, true, true);
+ },
+ getColumnDow(date: Date, short: boolean = false) {
+ return datetime.getDayOfWeekName(date.getDay(), short);
+ },
+ isSemesterView(view: ViewApi) {
+ if (['dayGridMonth', 'resourceTimelineWeek', 'resourceTimelineDay'].includes(view.type)) {
+ //These views do not exist in semester views.
+ return false;
+ }
+ const day_header_format = view.getOption('dayHeaderFormat');
+ if (day_header_format === undefined) {
+ //No day header format defined. Assume a semester view:
+ return true;
+ }
+ if (day_header_format.standardDateProps.year && day_header_format.standardDateProps.month && day_header_format.standardDateProps.day) {
+ //Year, month and day shall be displayed. This is not a semester view.
+ return false;
+ }
+ //In all other cases, assume a semester view:
+ return true;
+ },
+ isHoliday(date: Date) {
+ if (date === undefined) {
+ return false;
+ }
+ if (!this.displayHolidays && !this.displayVacations) {
+ //Holidays are not displayed at all.
+ return false;
+ }
+ if (this.displayHolidays) {
+ return holiday_cache.isHoliday(date, this.displayVacations);
+ } else {
+ return holiday_cache.isVacation(date);
+ }
+ },
+ getHolidayName(date: Date) {
+ if (date === undefined) {
+ return '';
+ }
+ if (!this.displayHolidays && !this.displayVacations) {
+ //Holidays are not displayed at all.
+ return '';
+ }
+ if (this.displayHolidays) {
+ return holiday_cache.getHolidayName(date, this.displayVacations);
+ } else {
+ return holiday_cache.getVacationName(date);
+ }
+ },
+ handleCalendarRangeUpdate: function(arg: DatesSetArg) : void {
+ if (this.displayHolidays || this.displayVacations) {
+ //Make sure that all holidays and vacations are loaded for the range.
+ //NOTE: This works only for views that span over a maximum of two years
+ //like week and month views at the start/end of a year.
+ if (this.displayHolidays) {
+ holiday_cache.loadHolidays(arg.view.activeStart.getFullYear());
+ holiday_cache.loadHolidays(arg.view.activeEnd.getFullYear());
+ }
+ if (this.displayVacations) {
+ holiday_cache.loadVacations(arg.view.activeStart.getFullYear());
+ holiday_cache.loadVacations(arg.view.activeEnd.getFullYear());
+ }
+ }
+ if (this.isSemesterView(arg.view)) {
+ //Remove the navigation button if the view is not timeGridDay:
+ const end_nav = document.querySelector(':nth-last-child(1 of .fc-toolbar-chunk)') as HTMLElement;
+ if (end_nav) {
+ if (arg.view.type === 'timeGridDay') {
+ //Show the navigation buttons.
+ end_nav.style.display = 'initial';
+ } else {
+ //Hide the navigation buttons.
+ end_nav.style.display = 'none';
+ }
+ }
+ }
+ },
+ handleSelection: function(selection: DateSelectionApi) {
+ if (!this.calendar_options.editable || this.actionUrls.length < 1) {
+ //The calendar isn't editable.
+ return;
+ }
+ const data = new EventURLParameters();
+ data.start = selection.start;
+ data.end = selection.end;
+ data.all_day = selection.allDay;
+ if (selection.resource) {
+ data.setResourceId(selection.resource.id);
+ }
+ if (this.actionUrls['add']) {
+ //Add the selected time range to the URL and load it
+ //in a dialog:
+ Dialog.fromURL(
+ this.actionUrls['add'],
+ {
+ data: data.toObject(),
+ size: this.dialogSize
+ }
+ );
+ }
+ },
+ handleEventClick: function(event_data: EventClickArg) {
+ if (!event_data.event.extendedProps.studip_view_urls
+ || !event_data.event.extendedProps.studip_view_urls.show) {
+ //Nothing to do.
+ return;
+ }
+ //Load the dialog:
+ Dialog.fromURL(
+ event_data.event.extendedProps.studip_view_urls.show,
+ {
+ size: this.dialogSize
+ }
+ );
+ },
+ handleEventDrop: function(drop_arg: EventDropArg) {
+ if (!this.calendar_options.editable || !drop_arg.event.startEditable
+ || !drop_arg.event.start ||
+ (drop_arg.oldEvent.allDay === drop_arg.event.allDay && !drop_arg.event.end)) {
+ //Nothing to do.
+ return;
+ }
+ const data = new EventURLParameters(drop_arg.event);
+ if (data.start === null) {
+ //Something went wrong. We cannot continue.
+ return;
+ }
+ if (drop_arg.oldEvent.allDay && !drop_arg.event.allDay) {
+ //An all-day event has become a date with a time range.
+ //Construct an end date for it 1 hour after the start.
+ data.setEnd(new Date(data.start.getTime() + 3600000));
+ //Set the end to the event object, too so that it can be dragged some more:
+ drop_arg.event.setEnd(data.end);
+ }
+ if (drop_arg.newResource) {
+ data.setResourceId(drop_arg.newResource.id);
+ }
+ if (drop_arg.event.extendedProps.studip_api_urls.move) {
+ //Call the move URL as HTTP POST:
+ $.post({
+ async: false,
+ url: drop_arg.event.extendedProps.studip_api_urls.move,
+ data: data.toObject()
+ })
+ .fail(drop_arg.revert)
+ .done(() => {
+ //Reload all calendar events so that their
+ //move-URLs are also updated.
+ drop_arg.view.calendar.refetchEvents();
+ });
+ } else if (drop_arg.event.extendedProps.studip_view_urls.move_dialog) {
+ //Show the move dialog:
+ Dialog.fromURL(
+ drop_arg.event.extendedProps.studip_view_urls.move_dialog,
+ {
+ data: data.toObject(),
+ size: this.dialogSize
+ }
+ );
+ }
+ this.$emit('eventDropped', drop_arg as EventDropArg);
+ },
+ handleEventResize: function(resize_arg: EventResizeDoneArg) {
+ if (!this.calendar_options.editable || !resize_arg.event.startEditable
+ || !resize_arg.event.start || !resize_arg.event.end) {
+ //Nothing to do.
+ return;
+ }
+ const data = new EventURLParameters(resize_arg.event);
+ if (resize_arg.event.extendedProps.studip_api_urls.resize) {
+ //Call the move URL as HTTP POST:
+ $.post({
+ async: false,
+ url: resize_arg.event.extendedProps.studip_api_urls.resize,
+ data: data.toObject()
+ })
+ .fail(resize_arg.revert)
+ .done(() => {
+ //Reload all calendar events so that their
+ //resize-URLs are also updated.
+ resize_arg.view.calendar.refetchEvents();
+ });
+ } else if (resize_arg.event.extendedProps.studip_view_urls.resize_dialog) {
+ Dialog.fromURL(
+ resize_arg.event.extendedProps.studip_view_urls.resize_dialog,
+ {
+ data: data.toObject(),
+ size: this.dialogSize
+ }
+ );
+ }
+ this.$emit('eventResized', resize_arg as EventResizeDoneArg);
+ },
+ handleEventReceive(receive_arg: EventReceiveArg) {
+ if (!receive_arg.event || !receive_arg.event.start || !receive_arg.event.end) {
+ //Nothing to do except of reverting the event:
+ receive_arg.revert();
+ return;
+ }
+ const data = new EventURLParameters();
+ data.setStart(receive_arg.event.start);
+ data.setEnd(receive_arg.event.end);
+ data.setAllDay(receive_arg.event.allDay);
+ if (receive_arg.event.extendedProps.studip_api_urls.receive) {
+ $.post({
+ async: false,
+ url: receive_arg.event.extendedProps.studip_api_urls.receive,
+ data: data.toObject()
+ })
+ .fail(receive_arg.revert)
+ .done(data => {
+ //Add the event that has been created and remove the
+ //temporary event from the drop.
+ const event_data = JSON.parse(data);
+ if (event_data) {
+ receive_arg.view.calendar.addEvent(event_data as EventInput);
+ receive_arg.event.remove();
+ }
+ });
+ } else if (receive_arg.event.extendedProps.studip_view_urls.receive_dialog) {
+ Dialog.fromURL(
+ receive_arg.event.extendedProps.studip_view_urls.receive_dialog,
+ {
+ data: data.toObject(),
+ size: this.dialogSize
+ }
+ );
+ }
+ this.$emit('eventReceived', receive_arg);
+ }
+ }
+})
+</script>
+<style lang="scss">
+@import '../../assets/stylesheets/scss/fullcalendar';
+</style>
diff --git a/resources/vue/components/StudipDateTime.vue b/resources/vue/components/StudipDateTime.vue
index 4f4ab72..0bed4cf 100644
--- a/resources/vue/components/StudipDateTime.vue
+++ b/resources/vue/components/StudipDateTime.vue
@@ -1,5 +1,6 @@
<script setup>
import { ref, computed, onMounted } from "vue"
+import {datetime} from "../../assets/javascripts/lib/datetime";
const props = defineProps({
timestamp: {
@@ -32,7 +33,7 @@ const date = computed(() => {
return null
})
-const datetime = computed(() => (date.value ? date.value.toISOString() : ''))
+const current_datetime = computed(() => (date.value ? date.value.toISOString() : ''))
const displayRelative = () => {
if (!date.value || !props.relative) {
@@ -47,7 +48,7 @@ const formattedDate = (forceAbsolute = false) => {
return 'Invalid date'
}
const relativeValue = !forceAbsolute && props.relative && displayRelative()
- return STUDIP.DateTime.getStudipDate(date.value, relativeValue, props.date_only)
+ return datetime.getStudipDate(date.value, relativeValue, props.date_only)
}
onMounted(() => {
@@ -58,7 +59,7 @@ onMounted(() => {
</script>
<template>
- <time :datetime="datetime" v-if="date" :title="title">
+ <time :datetime="current_datetime" v-if="date" :title="title">
{{ formattedDate() }}
</time>
</template>
diff --git a/resources/vue/components/form_inputs/DateListInput.vue b/resources/vue/components/form_inputs/DateListInput.vue
index c2ad1ac..72efab2 100644
--- a/resources/vue/components/form_inputs/DateListInput.vue
+++ b/resources/vue/components/form_inputs/DateListInput.vue
@@ -22,6 +22,7 @@
<script>
import StudipDateTime from "../StudipDateTime.vue";
+import {datetime} from "../../../assets/javascripts/lib/datetime";
export default {
name: "date-list-input",
@@ -40,7 +41,7 @@ export default {
},
data () {
return {
- selected_date_value: STUDIP.DateTime.getStudipDate(new Date(), false, true),
+ selected_date_value: datetime.getStudipDate(new Date(), false, true),
selected_date_list: this.selected_dates.map(date => new Date(date)),
input_name: this.name,
};
@@ -85,11 +86,11 @@ export default {
this.$refs.list_message_field.innerText = this.$gettext(
'Datum %{date} entfernt',
- { date: STUDIP.DateTime.getStudipDate(date, false, true) }
+ { date: datetime.getStudipDate(date, false, true) }
);
},
getISODate(date) {
- return STUDIP.DateTime.getISODate(date);
+ return datetime.getISODate(date);
}
}
}