Skip to content
Snippets Groups Projects
fullcalendar.js 33.8 KiB
Newer Older
/**
 * 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
{
    /**
     * 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))
                        }
                    );
                } 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)
                        }
                    );
                } 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);
                }
                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)
                        }
                    );
                } 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',
            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
            },
            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(
Jan-Hendrik Willms's avatar
Jan-Hendrik Willms committed
                            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();
                            });
                        handle_drop();
                } else {
                    handle_drop();
                }
            },
            eventRender (info) {
                var event = info.event;
                var eventElement = info.el;
                var iconColor = event.textColor == '#000000' ? 'black' : 'white';

                if ($(info.view.context.calendar.el).hasClass('institute-plan')) {
                    $(eventElement).attr('title', event.extendedProps.tooltip);
                    $(eventElement).find('.fc-title').html(
                        $('<div>').css({
                            width: 'calc(100% - 21px)',
                            height: '100%',
                            wordBreak: 'break-word'
                        }).text(eventElement.text)
                    );
                    $(eventElement).find('.fc-title').append(
                        $('<button class="event-colorpicker">').addClass(iconColor)
                    );
                } else {
                    $(eventElement).attr('title', event.title);
                }

                if (event.extendedProps.icon) {
                    $(eventElement).find('.fc-title').prepend(
                        $('<img>').attr('src', `${STUDIP.ASSETS_URL}images/icons/${iconColor}/${event.extendedProps.icon}.svg`)
                            .css({
                                verticalAlign: 'text-bottom',
                                marginRight: '3px',
                                width: 14,
                                height: 14
                            })
                    );
                }
            },
            eventSourceSuccess: function(content, xhr) {
                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')
                                    })
                            )
                        );
                    }
                } else {
                    $('#loading-spinner').remove();
                    this.updateSize();
                }
            },
            datesRender (info) {
                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) {
                    let icon = '<img class="text-bottom icon-role-clickable icon-shape-edit" width="20" height="20" src="' + STUDIP.URLHelper.getURL('assets/images/icons/blue/edit.svg') + '" alt="edit">';
                    $(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);

        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);
            });
        }
    }

    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);
            }
        }
    }
}

export default Fullcalendar;