diff options
| author | Moritz Strohm <strohm@data-quest.de> | 2026-01-16 09:36:16 +0000 |
|---|---|---|
| committer | Moritz Strohm <strohm@data-quest.de> | 2026-01-16 09:36:16 +0000 |
| commit | b58142fe5fa1ba1a99d850baa1465df6fa6e0d3b (patch) | |
| tree | d73956252dc17dd054d0db8e0f167a4103b7ba83 /resources/assets/javascripts/lib | |
| parent | c3e07e221b0bef64d3ad4da48c6371c75ca12cc3 (diff) | |
updated Fullcalendar to version 6, closes #4887
Closes #4887
Merge request studip/studip!4438
Diffstat (limited to 'resources/assets/javascripts/lib')
| -rw-r--r-- | resources/assets/javascripts/lib/action.ts | 26 | ||||
| -rw-r--r-- | resources/assets/javascripts/lib/calendar.ts | 226 | ||||
| -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.js | 977 | ||||
| -rw-r--r-- | resources/assets/javascripts/lib/holiday.ts | 254 |
5 files changed, 580 insertions, 995 deletions
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(); |
