ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç src/modal_event_form.js000064400000032172152265477470011241 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Contain the logic for the quick add or update event modal. * * @module core_calendar/modal_event_form * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import * as FormEvents from 'core_form/events'; import CalendarEvents from './events'; import * as Str from 'core/str'; import * as Notification from 'core/notification'; import * as Fragment from 'core/fragment'; import * as Repository from 'core_calendar/repository'; const SELECTORS = { SAVE_BUTTON: '[data-action="save"]', LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]', }; export default class ModalEventForm extends Modal { static TYPE = 'core_calendar-modal_event_form'; static TEMPLATE = 'calendar/modal_event_form'; /** * Constructor for the Modal. * * @param {object} root The root jQuery element for the modal */ constructor(root) { super(root); this.eventId = null; this.startTime = null; this.courseId = null; this.categoryId = null; this.contextId = null; this.reloadingBody = false; this.reloadingTitle = false; this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON); } configure(modalConfig) { modalConfig.large = true; super.configure(modalConfig); } /** * Set the context id to the given value. * * @method setContextId * @param {Number} id The event id */ setContextId(id) { this.contextId = id; } /** * Retrieve the current context id, if any. * * @method getContextId * @return {Number|null} The event id */ getContextId() { return this.contextId; } /** * Set the course id to the given value. * * @method setCourseId * @param {Number} id The event id */ setCourseId(id) { this.courseId = id; } /** * Retrieve the current course id, if any. * * @method getCourseId * @return {Number|null} The event id */ getCourseId() { return this.courseId; } /** * Set the category id to the given value. * * @method setCategoryId * @param {Number} id The event id */ setCategoryId(id) { this.categoryId = id; } /** * Retrieve the current category id, if any. * * @method getCategoryId * @return {Number|null} The event id */ getCategoryId() { return this.categoryId; } /** * Check if the modal has an course id. * * @method hasCourseId * @return {bool} */ hasCourseId() { return this.courseId !== null; } /** * Check if the modal has an category id. * * @method hasCategoryId * @return {bool} */ hasCategoryId() { return this.categoryId !== null; } /** * Set the event id to the given value. * * @method setEventId * @param {Number} id The event id */ setEventId(id) { this.eventId = id; } /** * Retrieve the current event id, if any. * * @method getEventId * @return {Number|null} The event id */ getEventId() { return this.eventId; } /** * Check if the modal has an event id. * * @method hasEventId * @return {bool} */ hasEventId() { return this.eventId !== null; } /** * Set the start time to the given value. * * @method setStartTime * @param {Number} time The start time */ setStartTime(time) { this.startTime = time; } /** * Retrieve the current start time, if any. * * @method getStartTime * @return {Number|null} The start time */ getStartTime() { return this.startTime; } /** * Check if the modal has start time. * * @method hasStartTime * @return {bool} */ hasStartTime() { return this.startTime !== null; } /** * Get the form element from the modal. * * @method getForm * @return {object} */ getForm() { return this.getBody().find('form'); } /** * Disable the buttons in the footer. * * @method disableButtons */ disableButtons() { this.saveButton.prop('disabled', true); } /** * Enable the buttons in the footer. * * @method enableButtons */ enableButtons() { this.saveButton.prop('disabled', false); } /** * Reload the title for the modal to the appropriate value * depending on whether we are creating a new event or * editing an existing event. * * @method reloadTitleContent * @return {object} A promise resolved with the new title text */ reloadTitleContent() { if (this.reloadingTitle) { return this.titlePromise; } this.reloadingTitle = true; if (this.hasEventId()) { this.titlePromise = Str.get_string('editevent', 'calendar'); } else { this.titlePromise = Str.get_string('newevent', 'calendar'); } this.titlePromise.then((string) => { this.setTitle(string); return string; }) .catch(Notification.exception) .always(() => { this.reloadingTitle = false; return; }); return this.titlePromise; } /** * Send a request to the server to get the event_form in a fragment * and render the result in the body of the modal. * * If serialised form data is provided then it will be sent in the * request to the server to have the form rendered with the data. This * is used when the form had a server side error and we need the server * to re-render it for us to display the error to the user. * * @method reloadBodyContent * @param {string} formData The serialised form data * @return {object} A promise resolved with the fragment html and js from */ reloadBodyContent(formData) { if (this.reloadingBody) { return this.bodyPromise; } this.reloadingBody = true; this.disableButtons(); const args = {}; if (this.hasEventId()) { args.eventid = this.getEventId(); } if (this.hasStartTime()) { args.starttime = this.getStartTime(); } if (this.hasCourseId()) { args.courseid = this.getCourseId(); } if (this.hasCategoryId()) { args.categoryid = this.getCategoryId(); } if (typeof formData !== 'undefined') { args.formdata = formData; } this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', this.getContextId(), args); this.setBody(this.bodyPromise); this.bodyPromise.then(() => { this.enableButtons(); return; }) .catch(Notification.exception) .always(() => { this.reloadingBody = false; return; }); return this.bodyPromise; } /** * Reload both the title and body content. * * @method reloadAllContent * @return {object} promise */ reloadAllContent() { return $.when(this.reloadTitleContent(), this.reloadBodyContent()); } /** * Kick off a reload the modal content before showing it. This * is to allow us to re-use the same modal for creating and * editing different events within the page. * * We do the reload when showing the modal rather than hiding it * to save a request to the server if the user closes the modal * and never re-opens it. * * @method show */ show() { this.reloadAllContent(); super.show(this); } /** * Clear the event id from the modal when it's closed so * that it is loaded fresh next time it's displayed. * * The event id will be set by the calling code if it wants * to edit a specific event. * * @method hide */ hide() { super.hide(this); this.setEventId(null); this.setStartTime(null); this.setCourseId(null); this.setCategoryId(null); } /** * Get the serialised form data. * * @method getFormData * @return {string} serialised form data */ getFormData() { return this.getForm().serialize(); } /** * Send the form data to the server to create or update * an event. * * If there is a server side validation error then we re-request the * rendered form (with the data) from the server in order to get the * server side errors to display. * * On success the modal is hidden and the page is reloaded so that the * new event will display. * * @method save * @return {object} A promise */ save() { const loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER); // Now the change events have run, see if there are any "invalid" form fields. const invalid = this.getForm().find('[aria-invalid="true"]'); // If we found invalid fields, focus on the first one and do not submit via ajax. if (invalid.length) { invalid.first().focus(); return Promise.resolve(); } loadingContainer.removeClass('hidden'); this.disableButtons(); const formData = this.getFormData(); // Send the form data to the server for processing. return Repository.submitCreateUpdateForm(formData) .then((response) => { if (response.validationerror) { // If there was a server side validation error then // we need to re-request the rendered form from the server // in order to display the error for the user. this.reloadBodyContent(formData); return; } else { // Check whether this was a new event or not. // The hide function unsets the form data so grab this before the hide. const isExisting = this.hasEventId(); // No problemo! Our work here is done. this.hide(); // Trigger the appropriate calendar event so that the view can be updated. if (isExisting) { $('body').trigger(CalendarEvents.updated, [response.event]); } else { $('body').trigger(CalendarEvents.created, [response.event]); } } return; }) .catch(Notification.exception) .always(() => { // Regardless of success or error we should always stop // the loading icon and re-enable the buttons. loadingContainer.addClass('hidden'); this.enableButtons(); return; }); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners * @fires event:uploadStarted * @fires event:formSubmittedByJavascript */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); // When the user clicks the save button we trigger the form submission. We need to // trigger an actual submission because there is some JS code in the form that is // listening for this event and doing some stuff (e.g. saving draft areas etc). this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, (e, data) => { this.getForm().submit(); data.originalEvent.preventDefault(); e.stopPropagation(); }); // Catch the submit event before it is actually processed by the browser and // prevent the submission. We'll take it from here. this.getModal().on('submit', (e) => { FormEvents.notifyFormSubmittedByJavascript(this.getForm()[0]); this.save(); // Stop the form from actually submitting and prevent it's // propagation because we have already handled the event. e.preventDefault(); e.stopPropagation(); }); } } ModalEventForm.registerModalType(); src/crud.js000064400000021532152265477470006654 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A module to handle CRUD operations within the UI. * * @module core_calendar/crud * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/str', 'core/notification', 'core/modal_events', 'core_calendar/modal_event_form', 'core_calendar/repository', 'core_calendar/events', 'core_calendar/modal_delete', 'core_calendar/selectors', 'core/pending', 'core/modal_save_cancel', 'core/config', ], function( $, Str, Notification, ModalEvents, ModalEventForm, CalendarRepository, CalendarEvents, CalendarModalDelete, CalendarSelectors, Pending, ModalSaveCancel, Config, ) { /** * Prepares the action for the summary modal's delete action. * * @param {Number} eventId The ID of the event. * @param {string} eventTitle The event title. * @param {Number} eventCount The number of events in the series. * @return {Promise} */ function confirmDeletion(eventId, eventTitle, eventCount) { var pendingPromise = new Pending('core_calendar/crud:confirmDeletion'); var deleteStrings = [ { key: 'deleteevent', component: 'calendar' }, ]; eventCount = parseInt(eventCount, 10); var deletePromise; var isRepeatedEvent = eventCount > 1; if (isRepeatedEvent) { deleteStrings.push({ key: 'confirmeventseriesdelete', component: 'calendar', param: { name: eventTitle, count: eventCount, }, }); deletePromise = CalendarModalDelete.create(); } else { deleteStrings.push({ key: 'confirmeventdelete', component: 'calendar', param: eventTitle }); deletePromise = ModalSaveCancel.create(); } var stringsPromise = Str.get_strings(deleteStrings); var finalPromise = $.when(stringsPromise, deletePromise) .then(function(strings, deleteModal) { deleteModal.setRemoveOnClose(true); deleteModal.setTitle(strings[0]); deleteModal.setBody(strings[1]); if (!isRepeatedEvent) { deleteModal.setSaveButtonText(strings[0]); } deleteModal.show(); deleteModal.getRoot().on(ModalEvents.save, function() { var pendingPromise = new Pending('calendar/crud:initModal:deletedevent'); CalendarRepository.deleteEvent(eventId, false) .then(function() { $('body').trigger(CalendarEvents.deleted, [eventId, false]); return; }) .then(pendingPromise.resolve) .catch(Notification.exception); }); deleteModal.getRoot().on(CalendarEvents.deleteAll, function() { var pendingPromise = new Pending('calendar/crud:initModal:deletedallevent'); CalendarRepository.deleteEvent(eventId, true) .then(function() { $('body').trigger(CalendarEvents.deleted, [eventId, true]); return; }) .then(pendingPromise.resolve) .catch(Notification.exception); }); return deleteModal; }) .then(function(modal) { pendingPromise.resolve(); return modal; }) .catch(Notification.exception); return finalPromise; } /** * Create the event form modal for creating new events and * editing existing events. * * @method registerEventFormModal * @param {object} root The calendar root element * @return {object} The create modal promise */ var registerEventFormModal = function(root) { var eventFormPromise = ModalEventForm.create(); // Bind click event on the new event button. root.on('click', CalendarSelectors.actions.create, function(e) { eventFormPromise.then(function(modal) { var wrapper = root.find(CalendarSelectors.wrapper); var categoryId = wrapper.data('categoryid'); const courseId = wrapper.data('courseid'); if (typeof categoryId !== 'undefined' && courseId != Config.siteId) { modal.setCategoryId(categoryId); } // Attempt to find the cell for today. // If it can't be found, then use the start time of the first day on the calendar. var today = root.find(CalendarSelectors.today); var firstDay = root.find(CalendarSelectors.day); if (!today.length && firstDay.length) { modal.setStartTime(firstDay.data('newEventTimestamp')); } modal.setContextId(wrapper.data('contextId')); modal.setCourseId(wrapper.data('courseid')); modal.show(); return; }) .catch(Notification.exception); e.preventDefault(); }); root.on('click', CalendarSelectors.actions.edit, function(e) { e.preventDefault(); var target = $(e.currentTarget), calendarWrapper = target.closest(CalendarSelectors.wrapper), eventWrapper = target.closest(CalendarSelectors.eventItem); eventFormPromise.then(function(modal) { // When something within the calendar tells us the user wants // to edit an event then show the event form modal. modal.setEventId(eventWrapper.data('eventId')); modal.setContextId(calendarWrapper.data('contextId')); modal.setCourseId(eventWrapper.data('courseId')); modal.show(); e.stopImmediatePropagation(); return; }).catch(Notification.exception); }); return eventFormPromise; }; /** * Register the listeners required to remove the event. * * @param {jQuery} root */ function registerRemove(root) { root.on('click', CalendarSelectors.actions.remove, function(e) { // Fetch the event title, count, and pass them into the new dialogue. var eventSource = $(this).closest(CalendarSelectors.eventItem); var eventId = eventSource.data('eventId'), eventTitle = eventSource.data('eventTitle'), eventCount = eventSource.data('eventCount'); confirmDeletion(eventId, eventTitle, eventCount); e.preventDefault(); }); } /** * Register the listeners required to edit the event. * * @param {jQuery} root * @param {Promise} eventFormModalPromise * @returns {Promise} */ function registerEditListeners(root, eventFormModalPromise) { var pendingPromise = new Pending('core_calendar/crud:registerEditListeners'); return eventFormModalPromise .then(function(modal) { // When something within the calendar tells us the user wants // to edit an event then show the event form modal. $('body').on(CalendarEvents.editEvent, function(e, eventId) { var target = root.find(`[data-event-id=${eventId}]`), calendarWrapper = root.find(CalendarSelectors.wrapper); modal.setEventId(eventId); modal.setContextId(calendarWrapper.data('contextId')); modal.setReturnElement(target); modal.show(); e.stopImmediatePropagation(); }); return modal; }) .then(function(modal) { pendingPromise.resolve(); return modal; }) .catch(Notification.exception); } return { registerRemove: registerRemove, registerEditListeners: registerEditListeners, registerEventFormModal: registerEventFormModal }; }); src/month_view_drag_drop.js000064400000032054152265477470012120 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A javascript module to handle calendar drag and drop in the calendar * month view. * * @module core_calendar/month_view_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/notification', 'core/str', 'core_calendar/events', 'core_calendar/drag_drop_data_store' ], function( $, Notification, Str, CalendarEvents, DataStore ) { var SELECTORS = { ROOT: "[data-region='calendar']", DRAGGABLE: '[draggable="true"][data-region="event-item"]', DROP_ZONE: '[data-drop-zone="month-view-day"]', WEEK: '[data-region="month-view-week"]', }; var INVALID_DROP_ZONE_CLASS = 'bg-faded'; var INVALID_HOVER_CLASS = 'bg-danger text-white'; var VALID_HOVER_CLASS = 'bg-primary text-white'; var ALL_CLASSES = INVALID_DROP_ZONE_CLASS + ' ' + INVALID_HOVER_CLASS + ' ' + VALID_HOVER_CLASS; /* @var {bool} registered If the event listeners have been added */ var registered = false; /** * Get the correct drop zone element from the given javascript * event. * * @param {event} e The javascript event * @return {object|null} */ var getDropZoneFromEvent = function(e) { var dropZone = $(e.target).closest(SELECTORS.DROP_ZONE); return (dropZone.length) ? dropZone : null; }; /** * Determine if the given dropzone element is within the acceptable * time range. * * The drop zone timestamp is midnight on that day so we should check * that the event's acceptable timestart value * * @param {object} dropZone The drop zone day from the calendar * @return {bool} */ var isValidDropZone = function(dropZone) { var dropTimestamp = dropZone.attr('data-day-timestamp'); var minTimestart = DataStore.getMinTimestart(); var maxTimestart = DataStore.getMaxTimestart(); if (minTimestart && minTimestart > dropTimestamp) { return false; } if (maxTimestart && maxTimestart < dropTimestamp) { return false; } return true; }; /** * Get the error string to display for a given drop zone element * if it is invalid. * * @param {object} dropZone The drop zone day from the calendar * @return {string} */ var getDropZoneError = function(dropZone) { var dropTimestamp = dropZone.attr('data-day-timestamp'); var minTimestart = DataStore.getMinTimestart(); var maxTimestart = DataStore.getMaxTimestart(); if (minTimestart && minTimestart > dropTimestamp) { return DataStore.getMinError(); } if (maxTimestart && maxTimestart < dropTimestamp) { return DataStore.getMaxError(); } return null; }; /** * Remove all of the styling from each of the drop zones in the calendar. */ var clearAllDropZonesState = function() { $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { dropZone = $(dropZone); dropZone.removeClass(ALL_CLASSES); }); }; /** * Update the hover state for the event in the calendar to reflect * which days the event will be moved to. * * If the drop zone is not being hovered then it will apply some * styling to reflect whether the drop zone is a valid or invalid * drop place for the current dragging event. * * This funciton supports events spanning multiple days and will * recurse to highlight (or remove highlight) each of the days * that the event will be moved to. * * For example: An event with a duration of 3 days will have * 3 days highlighted when it's dragged elsewhere in the calendar. * The current drag target and the 2 days following it (including * wrapping to the next week if necessary). * * @param {string|object} dropZone The drag target element * @param {bool} hovered If the target is hovered or not * @param {Number} count How many days to highlight (default to duration) */ var updateHoverState = function(dropZone, hovered, count) { if (typeof count === 'undefined') { // This is how many days we need to highlight. count = DataStore.getDurationDays(); } var valid = isValidDropZone(dropZone); dropZone.removeClass(ALL_CLASSES); if (hovered) { if (valid) { dropZone.addClass(VALID_HOVER_CLASS); } else { dropZone.addClass(INVALID_HOVER_CLASS); } } else { dropZone.removeClass(VALID_HOVER_CLASS + ' ' + INVALID_HOVER_CLASS); if (!valid) { dropZone.addClass(INVALID_DROP_ZONE_CLASS); } } count--; // If we've still got days to highlight then we should // find the next day. if (count > 0) { var nextDropZone = dropZone.next(); // If there are no more days in this week then we // need to move down to the next week in the calendar. if (!nextDropZone.length) { var nextWeek = dropZone.closest(SELECTORS.WEEK).next(); if (nextWeek.length) { nextDropZone = nextWeek.children(SELECTORS.DROP_ZONE).first(); } } // If we found another day then let's recursively // update it's hover state. if (nextDropZone.length) { updateHoverState(nextDropZone, hovered, count); } } }; /** * Find all of the calendar event drop zones in the calendar and update the display * for the user to indicate which zones are valid and invalid. */ var updateAllDropZonesState = function() { $(SELECTORS.ROOT).find(SELECTORS.DROP_ZONE).each(function(index, dropZone) { dropZone = $(dropZone); if (!isValidDropZone(dropZone)) { updateHoverState(dropZone, false); } }); }; /** * Set up the module level variables to track which event is being * dragged and how many days it spans. * * @param {event} e The dragstart event */ var dragstartHandler = function(e) { var target = $(e.target); var draggableElement = target.closest(SELECTORS.DRAGGABLE); if (!draggableElement.length) { return; } var eventElement = draggableElement.find('[data-event-id]'); var eventId = eventElement.attr('data-event-id'); var minTimestart = draggableElement.attr('data-min-day-timestamp'); var maxTimestart = draggableElement.attr('data-max-day-timestamp'); var minError = draggableElement.attr('data-min-day-error'); var maxError = draggableElement.attr('data-max-day-error'); var eventsSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; var duration = $(eventsSelector).length; DataStore.setEventId(eventId); DataStore.setDurationDays(duration); if (minTimestart) { DataStore.setMinTimestart(minTimestart); } if (maxTimestart) { DataStore.setMaxTimestart(maxTimestart); } if (minError) { DataStore.setMinError(minError); } if (maxError) { DataStore.setMaxError(maxError); } e.dataTransfer.effectAllowed = "move"; e.dataTransfer.dropEffect = "move"; // Firefox requires a value to be set here or the drag won't // work and the dragover handler won't fire. e.dataTransfer.setData('text/plain', eventId); e.dropEffect = "move"; updateAllDropZonesState(); }; /** * Update the hover state of the target day element when * the user is dragging an event over it. * * This will add a visual indicator to the calendar UI to * indicate which day(s) the event will be moved to. * * @param {event} e The dragstart event */ var dragoverHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } e.preventDefault(); var dropZone = getDropZoneFromEvent(e); if (!dropZone) { return; } updateHoverState(dropZone, true); }; /** * Update the hover state of the target day element that was * previously dragged over but has is no longer a drag target. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var dropZone = getDropZoneFromEvent(e); if (!dropZone) { return; } updateHoverState(dropZone, false); e.preventDefault(); }; /** * Determines the event element, origin day, and destination day * once the user drops the calendar event. These three bits of data * are provided as the payload to the "moveEvent" calendar javascript * event that is fired. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dropHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var dropZone = getDropZoneFromEvent(e); if (!dropZone) { DataStore.clearAll(); clearAllDropZonesState(); return; } if (isValidDropZone(dropZone)) { var eventId = DataStore.getEventId(); var eventElementSelector = SELECTORS.ROOT + ' [data-event-id="' + eventId + '"]'; var eventElement = $(eventElementSelector); var origin = null; if (eventElement.length) { origin = eventElement.closest(SELECTORS.DROP_ZONE); } $('body').trigger(CalendarEvents.moveEvent, [eventId, origin, dropZone]); } else { // If the drop zone is not valid then there is not need for us to // try to process it. Instead we can just show an error to the user. var message = getDropZoneError(dropZone); Str.get_string('errorinvaliddate', 'calendar').then(function(string) { Notification.exception({ name: string, message: message || string }); }); } DataStore.clearAll(); clearAllDropZonesState(); e.preventDefault(); }; /** * Clear the data store and remove the drag indicators from the UI * when the drag event has finished. */ var dragendHandler = function() { DataStore.clearAll(); clearAllDropZonesState(); }; /** * Re-render the drop zones in the new month to highlight * which areas are or aren't acceptable to drop the calendar * event. */ var calendarMonthChangedHandler = function() { updateAllDropZonesState(); }; return { /** * Initialise the event handlers for the drag events. */ init: function() { if (!registered) { // These handlers are only added the first time the module // is loaded because we don't want to have a new listener // added each time the "init" function is called otherwise we'll // end up with lots of stale handlers. document.addEventListener('dragstart', dragstartHandler, false); document.addEventListener('dragover', dragoverHandler, false); document.addEventListener('dragleave', dragleaveHandler, false); document.addEventListener('drop', dropHandler, false); document.addEventListener('dragend', dragendHandler, false); $('body').on(CalendarEvents.monthChanged, calendarMonthChangedHandler); registered = true; } }, }; }); src/events.js000064400000003650152265477470007224 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Javascript events for the `core_editor` subsystem. * * @module core_editor/events * @copyright 2021 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 4.0 */ import {dispatchEvent} from 'core/event_dispatcher'; /** * Events for the `core_editor` subsystem. * * @constant * @property {String} editorContentRestored See {@link event:editorContentRestored} */ export const eventTypes = { /** * An event triggered when an editor restores auto-saved content. * * @event editorContentRestored */ editorContentRestored: 'core_editor/contentRestored', }; /** * Trigger an event to indicate that editor content was restored. * * @method notifyEditorContentRestored * @param {HTMLElement|null} editor The element that was modified * @returns {CustomEvent} * @fires editorContentRestored */ export const notifyEditorContentRestored = editor => { if (!editor) { window.console.warn( `The HTMLElement representing the editor that was modified should be provided to notifyEditorContentRestored.` ); } return dispatchEvent( eventTypes.editorContentRestored, {}, editor || document ); }; src/modal_delete.js000064400000005742152265477470010342 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * Contain the logic for the delete modal. * * @module core_calendar/modal_delete * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import ModalEvents from 'core/modal_events'; import CalendarEvents from './events'; const SELECTORS = { DELETE_ONE_BUTTON: '[data-action="deleteone"]', DELETE_ALL_BUTTON: '[data-action="deleteall"]', CANCEL_BUTTON: '[data-action="cancel"]', }; /** * Constructor for the Modal. * * @class * @param {object} root The root jQuery element for the modal */ export default class ModalDelete extends Modal { static TYPE = 'core_calendar-modal_delete'; static TEMPLATE = 'calendar/event_delete_modal'; constructor(root) { super(root); this.setRemoveOnClose(true); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ONE_BUTTON, (e, data) => { const saveEvent = $.Event(ModalEvents.save); this.getRoot().trigger(saveEvent, this); if (!saveEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); this.getModal().on(CustomEvents.events.activate, SELECTORS.DELETE_ALL_BUTTON, (e, data) => { const saveEvent = $.Event(CalendarEvents.deleteAll); this.getRoot().trigger(saveEvent, this); if (!saveEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); this.getModal().on(CustomEvents.events.activate, SELECTORS.CANCEL_BUTTON, (e, data) => { const cancelEvent = $.Event(ModalEvents.cancel); this.getRoot().trigger(cancelEvent, this); if (!cancelEvent.isDefaultPrevented()) { this.hide(); data.originalEvent.preventDefault(); } }); } } ModalDelete.registerModalType(); src/calendar.js000064400000030731152265477470007471 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core/templates', 'core/notification', 'core_calendar/repository', 'core_calendar/events', 'core_calendar/view_manager', 'core_calendar/crud', 'core_calendar/selectors', 'core/url', 'core/str', ], function( $, Templates, Notification, CalendarRepository, CalendarEvents, CalendarViewManager, CalendarCrud, CalendarSelectors, Url, Str, ) { var SELECTORS = { ROOT: "[data-region='calendar']", DAY: "[data-region='day']", NEW_EVENT_BUTTON: "[data-action='new-event-button']", DAY_CONTENT: "[data-region='day-content']", LOADING_ICON: '.loading-icon', VIEW_DAY_LINK: "[data-action='view-day-link']", CALENDAR_MONTH_WRAPPER: ".calendarwrapper", TODAY: '.today', DAY_NUMBER_CIRCLE: '.day-number-circle', DAY_NUMBER: '.day-number', SCREEN_READER_ANNOUNCEMENTS: '.calendar-announcements', CURRENT_MONTH: '.calendar-controls .current' }; /** * Handler for the drag and drop move event. Provides a loading indicator * while the request is sent to the server to update the event start date. * * Triggers a eventMoved calendar javascript event if the event was successfully * updated. * * @param {event} e The calendar move event * @param {int} eventId The event id being moved * @param {object|null} originElement The jQuery element for where the event is moving from * @param {object} destinationElement The jQuery element for where the event is moving to */ var handleMoveEvent = function(e, eventId, originElement, destinationElement) { var originTimestamp = null; var destinationTimestamp = destinationElement.attr('data-day-timestamp'); if (originElement) { originTimestamp = originElement.attr('data-day-timestamp'); } // If the event has actually changed day. if (!originElement || originTimestamp != destinationTimestamp) { Templates.render('core/loading', {}) .then(function(html, js) { // First we show some loading icons in each of the days being affected. destinationElement.find(SELECTORS.DAY_CONTENT).addClass('hidden'); Templates.appendNodeContents(destinationElement, html, js); if (originElement) { originElement.find(SELECTORS.DAY_CONTENT).addClass('hidden'); Templates.appendNodeContents(originElement, html, js); } return; }) .then(function() { // Send a request to the server to make the change. return CalendarRepository.updateEventStartDay(eventId, destinationTimestamp); }) .then(function() { // If the update was successful then broadcast an event letting the calendar // know that an event has been moved. $('body').trigger(CalendarEvents.eventMoved, [eventId, originElement, destinationElement]); return; }) .always(function() { // Always remove the loading icons regardless of whether the update // request was successful or not. var destinationLoadingElement = destinationElement.find(SELECTORS.LOADING_ICON); destinationElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden'); Templates.replaceNode(destinationLoadingElement, '', ''); if (originElement) { var originLoadingElement = originElement.find(SELECTORS.LOADING_ICON); originElement.find(SELECTORS.DAY_CONTENT).removeClass('hidden'); Templates.replaceNode(originLoadingElement, '', ''); } return; }) .catch(Notification.exception); } }; /** * Listen to and handle any calendar events fired by the calendar UI. * * @method registerCalendarEventListeners * @param {object} root The calendar root element * @param {object} eventFormModalPromise A promise reolved with the event form modal */ var registerCalendarEventListeners = function(root, eventFormModalPromise) { var body = $('body'); body.on(CalendarEvents.created, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.deleted, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.updated, function() { CalendarViewManager.reloadCurrentMonth(root); }); body.on(CalendarEvents.editActionEvent, function(e, url) { // Action events needs to be edit directly on the course module. window.location.assign(url); }); // Handle the event fired by the drag and drop code. body.on(CalendarEvents.moveEvent, handleMoveEvent); // When an event is successfully moved we should updated the UI. body.on(CalendarEvents.eventMoved, function() { CalendarViewManager.reloadCurrentMonth(root); }); // Announce the newly loaded month to screen readers. body.on(CalendarEvents.monthChanged, root, async function() { const monthName = body.find(SELECTORS.CURRENT_MONTH).text(); const monthAnnoucement = await Str.get_string('newmonthannouncement', 'calendar', monthName); body.find(SELECTORS.SCREEN_READER_ANNOUNCEMENTS).html(monthAnnoucement); }); CalendarCrud.registerEditListeners(root, eventFormModalPromise); }; /** * Register event listeners for the module. * * @param {object} root The calendar root element * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ var registerEventListeners = function(root, isCalendarBlock) { const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView); // Listen the click on the day link to render the day view. root.on('click', SELECTORS.VIEW_DAY_LINK, function(e) { var dayLink = $(e.target).closest(SELECTORS.VIEW_DAY_LINK); var year = dayLink.data('year'), month = dayLink.data('month'), day = dayLink.data('day'), courseId = dayLink.data('courseid'), categoryId = dayLink.data('categoryid'); const urlParams = { view: 'day', time: dayLink.data('timestamp'), course: courseId, }; if (viewingFullCalendar) { // Construct the URL parameter string from the urlParams object. const urlParamString = Object.entries(urlParams) .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) .join('&'); CalendarViewManager.refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day', isCalendarBlock).then(function() { e.preventDefault(); // Update the URL if it's not calendar block. if (!isCalendarBlock) { CalendarViewManager.updateUrl('?' + urlParamString); } return; }).catch(Notification.exception); } else { window.location.assign(Url.relativeUrl('calendar/view.php', urlParams)); } }); root.on('change', CalendarSelectors.elements.courseSelector, function() { var selectElement = $(this); var courseId = selectElement.val(); const courseName = $("option:selected", selectElement).text(); CalendarViewManager.reloadCurrentMonth(root, courseId, null) .then(function() { // We need to get the selector again because the content has changed. return root.find(CalendarSelectors.elements.courseSelector).val(courseId); }) .then(function() { CalendarViewManager.updateUrl('?view=month&course=' + courseId); CalendarViewManager.handleCourseChange(Number(courseId), courseName); return; }) .catch(Notification.exception); }); var eventFormPromise = CalendarCrud.registerEventFormModal(root), contextId = $(SELECTORS.CALENDAR_MONTH_WRAPPER).data('context-id'); registerCalendarEventListeners(root, eventFormPromise); if (contextId) { // Bind click events to calendar days. root.on('click', SELECTORS.DAY, function(e) { var target = $(e.target); const displayingSmallBlockCalendar = root.parents('aside').data('blockregion') === 'side-pre'; if (!viewingFullCalendar && displayingSmallBlockCalendar) { const dateContainer = target.closest(SELECTORS.DAY); const wrapper = target.closest(CalendarSelectors.wrapper); const courseId = wrapper.data('courseid'); const params = { view: 'day', time: dateContainer.data('day-timestamp'), course: courseId, }; window.location.assign(Url.relativeUrl('calendar/view.php', params)); } else { const hasViewDayLink = target.closest(SELECTORS.VIEW_DAY_LINK).length; const shouldShowNewEventModal = !hasViewDayLink; if (shouldShowNewEventModal) { var startTime = $(this).attr('data-new-event-timestamp'); eventFormPromise.then(function(modal) { var wrapper = target.closest(CalendarSelectors.wrapper); modal.setCourseId(wrapper.data('courseid')); var categoryId = wrapper.data('categoryid'); if (typeof categoryId !== 'undefined') { modal.setCategoryId(categoryId); } modal.setContextId(wrapper.data('contextId')); modal.setStartTime(startTime); modal.show(); return; }).catch(Notification.exception); } } e.preventDefault(); }); } }; return { /** * Initializes the calendar view manager and registers event listeners. * * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached. * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. */ init: function(root, isCalendarBlock = false) { root = $(root); CalendarViewManager.init(root, 'month', isCalendarBlock); registerEventListeners(root, isCalendarBlock); } }; }); src/calendar_filter.js000064400000007211152265477470011033 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module is responsible for the calendar filter. * * @module core_calendar/calendar_filter * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', 'core/str', 'core/templates', ], function( $, CalendarSelectors, CalendarEvents, Str, Templates ) { var registerEventListeners = function(root) { root.on('click', CalendarSelectors.eventFilterItem, function(e) { var target = $(e.currentTarget); toggleFilter(target); e.preventDefault(); }); $('body').on(CalendarEvents.viewUpdated, function() { var filters = root.find(CalendarSelectors.eventFilterItem); filters.each(function(i, filter) { filter = $(filter); if (filter.data('eventtype-hidden')) { var data = getFilterData(filter); fireFilterChangedEvent(data); } }); }); }; var toggleFilter = function(target) { var data = getFilterData(target); // Toggle the hidden. We need to render the template before we change the value. data.hidden = !data.hidden; M.util.js_pending("core_calendar/calendar_filter:toggleFilter"); return Str.get_string('eventtype' + data.eventtype, 'calendar') .then(function(nameStr) { data.name = nameStr; data.icon = true; data.key = 'i/' + data.eventtype + 'event'; data.component = 'core'; return data; }) .then(function(context) { return Templates.render('core_calendar/event_filter_key', context); }) .then(function(html, js) { return Templates.replaceNode(target, html, js); }) .then(function() { fireFilterChangedEvent(data); M.util.js_complete("core_calendar/calendar_filter:toggleFilter"); return; }); }; /** * Fire the filterChanged event for the specified data. * * @param {object} data The data to include */ var fireFilterChangedEvent = function(data) { M.util.js_pending("month-mini-filterChanged"); $('body').trigger(CalendarEvents.filterChanged, { type: data.eventtype, hidden: data.hidden, }); M.util.js_complete("month-mini-filterChanged"); }; /** * Get the filter data for the specified target. * * @param {jQuery} target The target node * @return {Object} */ var getFilterData = function(target) { return { eventtype: target.data('eventtype'), hidden: target.data('eventtype-hidden'), }; }; return { init: function(root) { root = $(root); registerEventListeners(root); } }; }); src/calendar_mini.js000064400000007646152265477470010516 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * This module is the highest level module for the calendar. It is * responsible for initialising all of the components required for * the calendar to run. It also coordinates the interaction between * components by listening for and responding to different events * triggered within the calendar UI. * * @module core_calendar/calendar_mini * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/selectors', 'core_calendar/events', 'core_calendar/view_manager', ], function( $, CalendarSelectors, CalendarEvents, CalendarViewManager ) { /** * Listen to and handle any calendar events fired by the calendar UI. * * @method registerCalendarEventListeners * @param {object} root The calendar root element */ var registerCalendarEventListeners = function(root) { var body = $('body'); var namespace = '.' + root.attr('id'); body.on(CalendarEvents.created + namespace, root, reloadMonth); body.on(CalendarEvents.deleted + namespace, root, reloadMonth); body.on(CalendarEvents.updated + namespace, root, reloadMonth); body.on(CalendarEvents.eventMoved + namespace, root, reloadMonth); }; /** * Reload the month view in this month. * * @param {EventFacade} e */ var reloadMonth = function(e) { var root = e.data; var body = $('body'); var namespace = '.' + root.attr('id'); if (root.is(':visible')) { CalendarViewManager.reloadCurrentMonth(root); } else { // The root has been removed. // Remove all events in the namespace. body.off(CalendarEvents.created + namespace); body.off(CalendarEvents.deleted + namespace); body.off(CalendarEvents.updated + namespace); body.off(CalendarEvents.eventMoved + namespace); } }; var registerEventListeners = function(root) { $('body').on(CalendarEvents.filterChanged, function(e, data) { var daysWithEvent = root.find(CalendarSelectors.eventType[data.type]); daysWithEvent.toggleClass('calendar_event_' + data.type, !data.hidden); }); var namespace = '.' + root.attr('id'); $('body').on('change' + namespace, CalendarSelectors.elements.courseSelector, function() { if (root.is(':visible')) { var selectElement = $(this); var courseId = selectElement.val(); var categoryId = null; CalendarViewManager.reloadCurrentMonth(root, courseId, categoryId); } else { $('body').off('change' + namespace); } }); }; return { init: function(root, loadOnInit) { root = $(root); CalendarViewManager.init(root); registerEventListeners(root); registerCalendarEventListeners(root); if (loadOnInit) { // The calendar hasn't yet loaded it's events so we // should load them as soon as we've initialised. CalendarViewManager.reloadCurrentMonth(root); } } }; }); src/event_form.js000064400000006215152265477470010064 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A javascript module to enhance the event form. * * @module core_calendar/event_form * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core_calendar/repository', 'core/notification'], function($, CalendarRepository, Notification) { var SELECTORS = { EVENT_GROUP_COURSE_ID: '[name="groupcourseid"]', EVENT_GROUP_ID: '[name="groupid"]', SELECT_OPTION: 'option' }; /** * Listen for when the user changes the group course when configuring * a group event and filter the options in the group select to only * show the groups available within the course the user has selected. * * @method addCourseGroupSelectListeners * @param {object} formElement The root form element */ var addCourseGroupSelectListeners = function(formElement) { var courseGroupSelect = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID); var loadGroupSelectOptions = function(groups) { var groupSelect = formElement.find(SELECTORS.EVENT_GROUP_ID), groupSelectOptions = groupSelect.find(SELECTORS.SELECT_OPTION), courseGroups = $(groups); // Let's clear all options first. groupSelectOptions.remove(); groupSelect.prop("disabled", false); courseGroups.each(function(id, group) { $(groupSelect).append($("").attr("value", group.id).text(group.name)); }); }; // If the user choose a course in the selector do a WS request to get groups. courseGroupSelect.on('change', function() { var courseId = formElement.find(SELECTORS.EVENT_GROUP_COURSE_ID).val(); if (isNaN(courseId) || courseId <= 0) { loadGroupSelectOptions([]); } else { CalendarRepository.getCourseGroupsData(courseId) .then(function(groups) { return loadGroupSelectOptions(groups); }) .catch(Notification.exception); } }); }; /** * Initialise all of the form enhancements. * * @method init * @param {string} formId The value of the form's id attribute */ var init = function(formId) { var formElement = $('#' + formId); addCourseGroupSelectListeners(formElement); }; return { init: init, }; }); src/summary_modal.js000064400000013665152265477470010600 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A javascript module to handle summary modal. * * @module core_calendar/summary_modal * @copyright 2017 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import * as CustomEvents from 'core/custom_interaction_events'; import Modal from 'core/modal'; import CalendarEvents from './events'; import * as CalendarCrud from 'core_calendar/crud'; import * as ModalEvents from 'core/modal_events'; const SELECTORS = { ROOT: "[data-region='summary-modal-container']", EDIT_BUTTON: '[data-action="edit"]', DELETE_BUTTON: '[data-action="delete"]', }; export default class ModalEventSummary extends Modal { static TEMPLATE = 'core_calendar/event_summary_modal'; static TYPE = 'core_calendar-event_summary'; /** * Get the edit button element from the footer. The button is cached * as it's not expected to change. * * @method getEditButton * @return {object} button element */ getEditButton() { if (typeof this.editButton == 'undefined') { this.editButton = this.getFooter().find(SELECTORS.EDIT_BUTTON); } return this.editButton; } /** * Get the delete button element from the footer. The button is cached * as it's not expected to change. * * @method getDeleteButton * @return {object} button element */ getDeleteButton() { if (typeof this.deleteButton == 'undefined') { this.deleteButton = this.getFooter().find(SELECTORS.DELETE_BUTTON); } return this.deleteButton; } /** * Get the id for the event being shown in this modal. This value is * not cached because it will change depending on which event is * being displayed. * * @method getEventId * @return {int} */ getEventId() { return this.getBody().find(SELECTORS.ROOT).attr('data-event-id'); } /** * Get the title for the event being shown in this modal. This value is * not cached because it will change depending on which event is * being displayed. * * @method getEventTitle * @return {String} */ getEventTitle() { return this.getBody().find(SELECTORS.ROOT).attr('data-event-title'); } /** * Get the number of events in the series for the event being shown in * this modal. This value is not cached because it will change * depending on which event is being displayed. * * @method getEventCount * @return {int} */ getEventCount() { return this.getBody().find(SELECTORS.ROOT).attr('data-event-count'); } /** * Get the url for the event being shown in this modal. * * @method getEventUrl * @return {String} */ getEditUrl() { return this.getBody().find(SELECTORS.ROOT).attr('data-edit-url'); } /** * Is this an action event. * * @method getEventUrl * @return {String} */ isActionEvent() { return (this.getBody().find(SELECTORS.ROOT).attr('data-action-event') == 'true'); } /** * Set up all of the event handling for the modal. * * @method registerEventListeners */ registerEventListeners() { // Apply parent event listeners. super.registerEventListeners(this); // We have to wait for the modal to finish rendering in order to ensure that // the data-event-title property is available to use as the modal title. M.util.js_pending('core_calendar/summary_modal:registerEventListeners:bodyRendered'); this.getRoot().on(ModalEvents.bodyRendered, function() { this.getModal().data({ eventTitle: this.getEventTitle(), eventId: this.getEventId(), eventCount: this.getEventCount(), }) .attr('data-type', 'event'); CalendarCrud.registerRemove(this.getModal()); M.util.js_complete('core_calendar/summary_modal:registerEventListeners:bodyRendered'); }.bind(this)); $('body').on(CalendarEvents.deleted, function() { // Close the dialogue on delete. this.hide(); }.bind(this)); CustomEvents.define(this.getEditButton(), [ CustomEvents.events.activate ]); this.getEditButton().on(CustomEvents.events.activate, function(e, data) { if (this.isActionEvent()) { // Action events cannot be edited on the event form and must be redirected to the module UI. $('body').trigger(CalendarEvents.editActionEvent, [this.getEditUrl()]); } else { // When the edit button is clicked we fire an event for the calendar UI to handle. // We don't care how the UI chooses to handle it. $('body').trigger(CalendarEvents.editEvent, [this.getEventId()]); } // There is nothing else for us to do so let's hide. this.hide(); // We've handled this event so no need to propagate it. e.preventDefault(); e.stopPropagation(); data.originalEvent.preventDefault(); data.originalEvent.stopPropagation(); }.bind(this)); } } ModalEventSummary.registerModalType(); src/month_navigation_drag_drop.js000064400000016445152265477470013313 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A javascript module to handle calendar drag and drop in the calendar * month view navigation. * * This code is run each time the calendar month view is re-rendered. We * only register the event handlers once per page load so that the in place * DOM updates that happen on month change don't continue to register handlers. * * @module core_calendar/month_navigation_drag_drop * @copyright 2017 Ryan Wyllie * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define([ 'jquery', 'core_calendar/drag_drop_data_store', ], function( $, DataStore ) { var SELECTORS = { DRAGGABLE: '[draggable="true"][data-region="event-item"]', DROP_ZONE: '[data-drop-zone="nav-link"]', }; var HOVER_CLASS = 'bg-primary text-white'; var TARGET_CLASS = 'drop-target'; var HOVER_TIME = 1000; // 1 second hover to change month. // We store some static variables at the module level because this // module is called each time the calendar month view is reloaded but // we want some actions to only occur ones. /* @var {bool} registered If the event listeners have been added */ var registered = false; /* @var {int} hoverTimer The timeout id of any timeout waiting for hover */ var hoverTimer = null; /* @var {object} root The root nav element we're operating on */ var root = null; /** * Add or remove the appropriate styling to indicate whether * the drop target is being hovered over. * * @param {object} target The target drop zone element * @param {bool} hovered If the element is hovered over ot not */ var updateHoverState = function(target, hovered) { if (hovered) { target.addClass(HOVER_CLASS); } else { target.removeClass(HOVER_CLASS); } }; /** * Add some styling to the UI to indicate that the nav links * are an acceptable drop target. */ var addDropZoneIndicator = function() { root.find(SELECTORS.DROP_ZONE).addClass(TARGET_CLASS); }; /** * Remove the styling from the nav links. */ var removeDropZoneIndicator = function() { root.find(SELECTORS.DROP_ZONE).removeClass(TARGET_CLASS); }; /** * Get the drop zone target from the event, if one is found. * * @param {event} e Javascript event * @return {object|null} */ var getTargetFromEvent = function(e) { var target = $(e.target).closest(SELECTORS.DROP_ZONE); return (target.length) ? target : null; }; /** * This will add a visual indicator to the calendar UI to * indicate which nav link is a valid drop zone. * * @param {Event} e */ var dragstartHandler = function(e) { // Make sure the drag event is for a calendar event. var eventElement = $(e.target).closest(SELECTORS.DRAGGABLE); if (eventElement.length) { addDropZoneIndicator(); } }; /** * Update the hover state of the target nav element when * the user is dragging an event over it. * * This will add a visual indicator to the calendar UI to * indicate which nav link is being hovered. * * @param {event} e The dragover event */ var dragoverHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } e.preventDefault(); var target = getTargetFromEvent(e); if (!target) { return; } // If we're not draggin a calendar event then // ignore it. if (!DataStore.hasEventId()) { return; } if (!hoverTimer) { hoverTimer = setTimeout(function() { target.click(); hoverTimer = null; }, HOVER_TIME); } updateHoverState(target, true); removeDropZoneIndicator(); }; /** * Update the hover state of the target nav element that was * previously dragged over but has is no longer a drag target. * * This will remove the visual indicator from the calendar UI * that was added by the dragoverHandler. * * @param {event} e The dragstart event */ var dragleaveHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } var target = getTargetFromEvent(e); if (!target) { return; } if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } updateHoverState(target, false); addDropZoneIndicator(); e.preventDefault(); }; /** * Remove the visual indicator from the calendar UI that was * added by the dragoverHandler. * * @param {event} e The drop event */ var dropHandler = function(e) { // Ignore dragging of non calendar events. if (!DataStore.hasEventId()) { return; } removeDropZoneIndicator(); var target = getTargetFromEvent(e); if (!target) { return; } updateHoverState(target, false); e.preventDefault(); }; return { /** * Initialise the event handlers for the drag events. * * @param {object} rootElement The element containing calendar nav links */ init: function(rootElement) { // Only register the handlers once on the first load. if (!registered) { // These handlers are only added the first time the module // is loaded because we don't want to have a new listener // added each time the "init" function is called otherwise we'll // end up with lots of stale handlers. document.addEventListener('dragstart', dragstartHandler, false); document.addEventListener('dragover', dragoverHandler, false); document.addEventListener('dragleave', dragleaveHandler, false); document.addEventListener('drop', dropHandler, false); document.addEventListener('dragend', removeDropZoneIndicator, false); registered = true; } // Update the module variable to operate on the given // root element. root = $(rootElement); // If we're currently dragging then add the indicators. if (DataStore.hasEventId()) { addDropZoneIndicator(); } }, }; }); src/view_manager.js000064400000054753152265477470010376 0ustar00// This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . /** * A javascript module to handler calendar view changes. * * @module core_calendar/view_manager * @copyright 2017 Andrew Nicols * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import Templates from 'core/templates'; import Notification from 'core/notification'; import * as CalendarRepository from 'core_calendar/repository'; import CalendarEvents from 'core_calendar/events'; import * as CalendarSelectors from 'core_calendar/selectors'; import ModalEvents from 'core/modal_events'; import SummaryModal from 'core_calendar/summary_modal'; import CustomEvents from 'core/custom_interaction_events'; import {getString} from 'core/str'; import Pending from 'core/pending'; import {prefetchStrings} from 'core/prefetch'; import Url from 'core/url'; import Config from 'core/config'; /** * Limit number of events per day * */ const LIMIT_DAY_EVENTS = 5; /** * Hide day events if more than 5. * */ export const foldDayEvents = () => { const root = $(CalendarSelectors.elements.monthDetailed); const days = root.find(CalendarSelectors.day); if (days.length === 0) { return; } days.each(function() { const dayContainer = $(this); const eventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-eventtype]`; const filteredEventsSelector = `${CalendarSelectors.elements.dateContent} ul li[data-event-filtered="true"]`; const moreEventsSelector = `${CalendarSelectors.elements.dateContent} [data-action="view-more-events"]`; const events = dayContainer.find(eventsSelector); if (events.length === 0) { return; } const filteredEvents = dayContainer.find(filteredEventsSelector); const numberOfFiltered = filteredEvents.length; const numberOfEvents = events.length - numberOfFiltered; let count = 1; events.each(function() { const event = $(this); const isNotFiltered = event.attr('data-event-filtered') !== 'true'; const offset = (numberOfEvents === LIMIT_DAY_EVENTS) ? 0 : 1; if (isNotFiltered) { if (count > LIMIT_DAY_EVENTS - offset) { event.attr('data-event-folded', 'true'); event.hide(); } else { event.attr('data-event-folded', 'false'); event.show(); count++; } } else { // It's being filtered out. event.attr('data-event-folded', 'false'); } }); const moreEventsLink = dayContainer.find(moreEventsSelector); if (numberOfEvents > LIMIT_DAY_EVENTS) { const numberOfHiddenEvents = numberOfEvents - LIMIT_DAY_EVENTS + 1; moreEventsLink.show(); getString('moreevents', 'calendar', numberOfHiddenEvents).then(str => { const link = moreEventsLink.find('strong a'); moreEventsLink.attr('data-event-folded', 'false'); link.text(str); return str; }).catch(Notification.exception); } else { moreEventsLink.hide(); } }); }; /** * Register and handle month calendar events. * * @param {string} pendingId pending id. */ export const registerEventListenersForMonthDetailed = (pendingId) => { const events = `${CalendarEvents.viewUpdated}`; $('body').on(events, function(e) { foldDayEvents(e); }); foldDayEvents(); $('body').on(CalendarEvents.filterChanged, function(e, data) { const root = $(CalendarSelectors.elements.monthDetailed); const pending = new Pending(pendingId); const target = root.find(CalendarSelectors.eventType[data.type]); const transitionPromise = $.Deferred(); if (data.hidden) { transitionPromise.then(function() { target.attr('data-event-filtered', 'true'); return target.hide().promise(); }).fail(); } else { transitionPromise.then(function() { target.attr('data-event-filtered', 'false'); return target.show().promise(); }).fail(); } transitionPromise.then(function() { foldDayEvents(); return; }) .always(pending.resolve) .fail(); transitionPromise.resolve(); }); }; /** * Register event listeners for the module. * * @param {object} root The root element. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ const registerEventListeners = (root, isCalendarBlock) => { root = $(root); // Bind click events to event links. root.on('click', CalendarSelectors.links.eventLink, (e) => { const target = e.target; let eventLink = null; let eventId = null; const pendingPromise = new Pending('core_calendar/view_manager:eventLink:click'); if (target.matches(CalendarSelectors.actions.viewEvent)) { eventLink = target; } else { eventLink = target.closest(CalendarSelectors.actions.viewEvent); } if (eventLink) { eventId = eventLink.dataset.eventId; } else { eventId = target.querySelector(CalendarSelectors.actions.viewEvent).dataset.eventId; } if (eventId) { // A link was found. Show the modal. e.preventDefault(); // We've handled the event so stop it from bubbling // and causing the day click handler to fire. e.stopPropagation(); renderEventSummaryModal(eventId) .then(pendingPromise.resolve) .catch(); } else { pendingPromise.resolve(); } }); root.on('click', CalendarSelectors.links.navLink, (e) => { const wrapper = root.find(CalendarSelectors.wrapper); const view = wrapper.data('view'); const courseId = wrapper.data('courseid'); const categoryId = wrapper.data('categoryid'); const link = e.currentTarget; if (view === 'month' || view === 'monthblock') { changeMonth(root, link.href, link.dataset.year, link.dataset.month, courseId, categoryId, link.dataset.day, isCalendarBlock); e.preventDefault(); } else if (view === 'day') { changeDay(root, link.href, link.dataset.year, link.dataset.month, link.dataset.day, courseId, categoryId, isCalendarBlock); e.preventDefault(); } }); const viewSelector = root.find(CalendarSelectors.viewSelector); CustomEvents.define(viewSelector, [CustomEvents.events.activate]); viewSelector.on( CustomEvents.events.activate, (e) => { e.preventDefault(); const option = e.target; if (option.classList.contains('active')) { return; } const view = option.dataset.view, year = option.dataset.year, month = option.dataset.month, day = option.dataset.day, courseId = option.dataset.courseid, categoryId = option.dataset.categoryid; if (view == 'month') { refreshMonthContent(root, year, month, courseId, categoryId, root, 'core_calendar/calendar_month', day) .then(() => { updateUrl('?view=month&course=' + courseId); return; }).fail(Notification.exception); } else if (view == 'day') { refreshDayContent(root, year, month, day, courseId, categoryId, root, 'core_calendar/calendar_day') .then(() => { updateUrl('?view=day&course=' + courseId); return; }).fail(Notification.exception); } else if (view == 'upcoming') { reloadCurrentUpcoming(root, courseId, categoryId, root, 'core_calendar/calendar_upcoming') .then(() => { updateUrl('?view=upcoming&course=' + courseId); return; }).fail(Notification.exception); } } ); }; /** * Refresh the month content. * * @param {object} root The root element. * @param {number} year Year * @param {number} month Month * @param {number} courseId The id of the course whose events are shown * @param {number} categoryId The id of the category whose events are shown * @param {object} target The element being replaced. If not specified, the calendarwrapper is used. * @param {string} template The template to be rendered. * @param {number} day Day (optional) * @return {promise} */ export const refreshMonthContent = (root, year, month, courseId, categoryId, target = null, template = '', day = 1) => { startLoading(root); target = target || root.find(CalendarSelectors.wrapper); template = template || root.attr('data-template'); M.util.js_pending([root.get('id'), year, month, courseId].join('-')); const includenavigation = root.data('includenavigation'); const mini = root.data('mini'); const viewMode = target.data('view'); return CalendarRepository.getCalendarMonthData(year, month, courseId, categoryId, includenavigation, mini, day, viewMode) .then(context => { return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(() => { M.util.js_complete([root.get('id'), year, month, courseId].join('-')); return stopLoading(root); }) .fail(Notification.exception); }; /** * Handle changes to the current calendar view. * * @param {object} root The container element * @param {string} url The calendar url to be shown * @param {number} year Year * @param {number} month Month * @param {number} courseId The id of the course whose events are shown * @param {number} categoryId The id of the category whose events are shown * @param {number} day Day (optional) * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. * @return {promise} */ export const changeMonth = (root, url, year, month, courseId, categoryId, day = 1, isCalendarBlock = false) => { return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day) .then((...args) => { if (url.length && url !== '#' && !isCalendarBlock) { updateUrl(url); } return args; }) .then((...args) => { $('body').trigger(CalendarEvents.monthChanged, [year, month, courseId, categoryId, day, isCalendarBlock]); return args; }); }; /** * Reload the current month view data. * * @param {object} root The container element. * @param {number} courseId The course id. * @param {number} categoryId The id of the category whose events are shown * @return {promise} */ export const reloadCurrentMonth = (root, courseId = 0, categoryId = 0) => { const year = root.find(CalendarSelectors.wrapper).data('year'); const month = root.find(CalendarSelectors.wrapper).data('month'); const day = root.find(CalendarSelectors.wrapper).data('day'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid'); return refreshMonthContent(root, year, month, courseId, categoryId, null, '', day). then((...args) => { $('body').trigger(CalendarEvents.courseChanged, [year, month, courseId, categoryId]); return args; }); }; /** * Refresh the day content. * * @param {object} root The root element. * @param {number} year Year * @param {number} month Month * @param {number} day Day * @param {number} courseId The id of the course whose events are shown * @param {number} categoryId The id of the category whose events are shown * @param {object} target The element being replaced. If not specified, the calendarwrapper is used. * @param {string} template The template to be rendered. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. * * @return {promise} */ export const refreshDayContent = (root, year, month, day, courseId, categoryId, target = null, template = '', isCalendarBlock = false) => { startLoading(root); if (!target || target.length == 0){ target = root.find(CalendarSelectors.wrapper); } template = template || root.attr('data-template'); M.util.js_pending([root.get('id'), year, month, day, courseId, categoryId].join('-')); const includenavigation = root.data('includenavigation'); return CalendarRepository.getCalendarDayData(year, month, day, courseId, categoryId, includenavigation) .then((context) => { context.viewingday = true; context.showviewselector = true; context.iscalendarblock = isCalendarBlock; return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(() => { M.util.js_complete([root.get('id'), year, month, day, courseId, categoryId].join('-')); return stopLoading(root); }) .fail(Notification.exception); }; /** * Reload the current day view data. * * @param {object} root The container element. * @param {number} courseId The course id. * @param {number} categoryId The id of the category whose events are shown * @return {promise} */ export const reloadCurrentDay = (root, courseId = 0, categoryId = 0) => { const wrapper = root.find(CalendarSelectors.wrapper); const year = wrapper.data('year'); const month = wrapper.data('month'); const day = wrapper.data('day'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid'); return refreshDayContent(root, year, month, day, courseId, categoryId); }; /** * Handle changes to the current calendar view. * * @param {object} root The root element. * @param {String} url The calendar url to be shown * @param {Number} year Year * @param {Number} month Month * @param {Number} day Day * @param {Number} courseId The id of the course whose events are shown * @param {Number} categoryId The id of the category whose events are shown * @param {boolean} [isCalendarBlock=false] - A flag indicating whether this is a calendar block. * @return {promise} */ export const changeDay = (root, url, year, month, day, courseId, categoryId, isCalendarBlock = false) => { return refreshDayContent(root, year, month, day, courseId, categoryId, null, '', isCalendarBlock) .then((...args) => { if (url.length && url !== '#' && !isCalendarBlock) { updateUrl(url); } return args; }) .then((...args) => { $('body').trigger(CalendarEvents.dayChanged, [year, month, courseId, categoryId, isCalendarBlock]); return args; }); }; /** * Update calendar URL. * * @param {String} url The calendar url to be updated. */ export const updateUrl = (url) => { const viewingFullCalendar = document.getElementById(CalendarSelectors.fullCalendarView); // We want to update the url only if the user is viewing the full calendar. if (viewingFullCalendar) { window.history.pushState({}, '', url); } }; /** * Set the element state to loading. * * @param {object} root The container element * @method startLoading */ const startLoading = (root) => { const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon); loadingIconContainer.removeClass('hidden'); }; /** * Remove the loading state from the element. * * @param {object} root The container element * @method stopLoading */ const stopLoading = (root) => { const loadingIconContainer = root.find(CalendarSelectors.containers.loadingIcon); loadingIconContainer.addClass('hidden'); }; /** * Reload the current month view data. * * @param {object} root The container element. * @param {number} courseId The course id. * @param {number} categoryId The id of the category whose events are shown * @param {object} target The element being replaced. If not specified, the calendarwrapper is used. * @param {string} template The template to be rendered. * @return {promise} */ export const reloadCurrentUpcoming = (root, courseId = 0, categoryId = 0, target = null, template = '') => { startLoading(root); target = target || root.find(CalendarSelectors.wrapper); template = template || root.attr('data-template'); courseId = courseId || root.find(CalendarSelectors.wrapper).data('courseid'); categoryId = categoryId || root.find(CalendarSelectors.wrapper).data('categoryid'); return CalendarRepository.getCalendarUpcomingData(courseId, categoryId) .then((context) => { context.viewingupcoming = true; context.showviewselector = true; return Templates.render(template, context); }) .then((html, js) => { return Templates.replaceNode(target, html, js); }) .then(() => { document.querySelector('body').dispatchEvent(new CustomEvent(CalendarEvents.viewUpdated)); return; }) .always(function() { return stopLoading(root); }) .fail(Notification.exception); }; /** * Get the CSS class to apply for the given event type. * * @param {string} eventType The calendar event type * @return {string} */ const getEventTypeClassFromType = (eventType) => { return 'calendar_event_' + eventType; }; /** * Render the event summary modal. * * @param {Number} eventId The calendar event id. * @returns {Promise} */ const renderEventSummaryModal = (eventId) => { const pendingPromise = new Pending('core_calendar/view_manager:renderEventSummaryModal'); // Calendar repository promise. return CalendarRepository.getEventById(eventId) .then((getEventResponse) => { if (!getEventResponse.event) { throw new Error('Error encountered while trying to fetch calendar event with ID: ' + eventId); } return getEventResponse.event; }) .then(eventData => { // Build the modal parameters from the event data. const modalParams = { title: eventData.name, body: Templates.render('core_calendar/event_summary_body', eventData), templateContext: { canedit: eventData.canedit, candelete: eventData.candelete, headerclasses: getEventTypeClassFromType(eventData.normalisedeventtype), isactionevent: eventData.isactionevent, url: eventData.url, action: eventData.action } }; // Create the modal. return SummaryModal.create(modalParams); }) .then(modal => { // Handle hidden event. modal.getRoot().on(ModalEvents.hidden, function() { // Destroy when hidden. modal.destroy(); }); // Finally, render the modal! modal.show(); return modal; }) .then(modal => { pendingPromise.resolve(); return modal; }) .catch(Notification.exception); }; /** * Initializes the calendar component by prefetching strings, folding day events, * and registering event listeners. * * @param {HTMLElement} root - The root element where the calendar view manager and event listeners will be attached. * @param {string} view - A flag indicating whether this is a calendar block. * @param {boolean} isCalendarBlock - A flag indicating whether this is a calendar block. */ export const init = (root, view, isCalendarBlock) => { prefetchStrings('calendar', ['moreevents']); foldDayEvents(); registerEventListeners(root, isCalendarBlock); const calendarTable = root.find(CalendarSelectors.elements.monthDetailed); if (calendarTable.length) { const pendingId = `month-detailed-${calendarTable.id}-filterChanged`; registerEventListenersForMonthDetailed(calendarTable, pendingId); } }; /** * Handles the change of course and updates the relevant elements on the page. * * @param {integer} courseId - The ID of the new course. * @param {string} courseName - The name of the new course. * @returns {Promise} - A promise that resolves after the updates are applied. */ export const handleCourseChange = async(courseId, courseName) => { // Select the