62ed463550461cabc99bca341eebdfcca855b07e
[moodle.git] / calendar / amd / src / modal_event_form.js
1 // This file is part of Moodle - http://moodle.org/
2 //
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
7 //
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 // GNU General Public License for more details.
12 //
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16 /**
17  * Contain the logic for the quick add or update event modal.
18  *
19  * @module     calendar/modal_quick_add_event
20  * @class      modal_quick_add_event
21  * @package    core
22  * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 define([
26             'jquery',
27             'core/event',
28             'core/str',
29             'core/notification',
30             'core/templates',
31             'core/custom_interaction_events',
32             'core/modal',
33             'core/modal_registry',
34             'core/fragment',
35             'core_calendar/events',
36             'core_calendar/repository',
37             'core_calendar/event_form'
38         ],
39         function(
40             $,
41             Event,
42             Str,
43             Notification,
44             Templates,
45             CustomEvents,
46             Modal,
47             ModalRegistry,
48             Fragment,
49             CalendarEvents,
50             Repository,
51             EventForm
52         ) {
54     var registered = false;
55     var SELECTORS = {
56         MORELESS_BUTTON: '[data-action="more-less-toggle"]',
57         SAVE_BUTTON: '[data-action="save"]',
58         LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
59     };
61     /**
62      * Constructor for the Modal.
63      *
64      * @param {object} root The root jQuery element for the modal
65      */
66     var ModalEventForm = function(root) {
67         Modal.call(this, root);
68         this.eventId = null;
69         this.startTime = null;
70         this.courseId = null;
71         this.reloadingBody = false;
72         this.reloadingTitle = false;
73         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
74         this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_BUTTON);
75     };
77     ModalEventForm.TYPE = 'core_calendar-modal_event_form';
78     ModalEventForm.prototype = Object.create(Modal.prototype);
79     ModalEventForm.prototype.constructor = ModalEventForm;
80     
81     /**
82      * Set the course id to the given value.
83      *
84      * @method setCourseId
85      * @param {int} id The event id
86      */
87     ModalEventForm.prototype.setCourseId = function(id) {
88         this.courseId = id;
89     };
91     /**
92      * Retrieve the current course id, if any.
93      *
94      * @method getCourseId
95      * @return {int|null} The event id
96      */
97     ModalEventForm.prototype.getCourseId = function() {
98         return this.courseId;
99     };
101     /**
102      * Check if the modal has an course id.
103      *
104      * @method hasCourseId
105      * @return {bool}
106      */
107     ModalEventForm.prototype.hasCourseId = function() {
108         return this.courseId !== null;
109     };
111     /**
112      * Set the event id to the given value.
113      *
114      * @method setEventId
115      * @param {int} id The event id
116      */
117     ModalEventForm.prototype.setEventId = function(id) {
118         this.eventId = id;
119     };
121     /**
122      * Retrieve the current event id, if any.
123      *
124      * @method getEventId
125      * @return {int|null} The event id
126      */
127     ModalEventForm.prototype.getEventId = function() {
128         return this.eventId;
129     };
131     /**
132      * Check if the modal has an event id.
133      *
134      * @method hasEventId
135      * @return {bool}
136      */
137     ModalEventForm.prototype.hasEventId = function() {
138         return this.eventId !== null;
139     };
141     /**
142      * Set the start time to the given value.
143      *
144      * @method setStartTime
145      * @param {int} time The start time
146      */
147     ModalEventForm.prototype.setStartTime = function(time) {
148         this.startTime = time;
149     };
151     /**
152      * Retrieve the current start time, if any.
153      *
154      * @method getStartTime
155      * @return {int|null} The start time
156      */
157     ModalEventForm.prototype.getStartTime = function() {
158         return this.startTime;
159     };
161     /**
162      * Check if the modal has start time.
163      *
164      * @method hasStartTime
165      * @return {bool}
166      */
167     ModalEventForm.prototype.hasStartTime = function() {
168         return this.startTime !== null;
169     };
171     /**
172      * Get the form element from the modal.
173      *
174      * @method getForm
175      * @return {object}
176      */
177     ModalEventForm.prototype.getForm = function() {
178         return this.getBody().find('form');
179     };
181     /**
182      * Disable the buttons in the footer.
183      *
184      * @method disableButtons
185      */
186     ModalEventForm.prototype.disableButtons = function() {
187         this.saveButton.prop('disabled', true);
188         this.moreLessButton.prop('disabled', true);
189     };
191     /**
192      * Enable the buttons in the footer.
193      *
194      * @method enableButtons
195      */
196     ModalEventForm.prototype.enableButtons = function() {
197         this.saveButton.prop('disabled', false);
198         this.moreLessButton.prop('disabled', false);
199     };
201     /**
202      * Set the more/less button in the footer to the "more"
203      * state.
204      *
205      * @method setMoreButton
206      */
207     ModalEventForm.prototype.setMoreButton = function() {
208         this.moreLessButton.attr('data-collapsed', 'true');
209         Str.get_string('more', 'calendar').then(function(string) {
210             this.moreLessButton.text(string);
211             return;
212         }.bind(this));
213     };
215     /**
216      * Set the more/less button in the footer to the "less"
217      * state.
218      *
219      * @method setLessButton
220      */
221     ModalEventForm.prototype.setLessButton = function() {
222         this.moreLessButton.attr('data-collapsed', 'false');
223         Str.get_string('less', 'calendar').then(function(string) {
224             this.moreLessButton.text(string);
225             return;
226         }.bind(this));
227     };
229     /**
230      * Toggle the more/less button in the footer from the current
231      * state to it's opposite state.
232      *
233      * @method toggleMoreLessButton
234      */
235     ModalEventForm.prototype.toggleMoreLessButton = function() {
236         var form = this.getForm();
238         if (this.moreLessButton.attr('data-collapsed') == 'true') {
239             form.trigger(EventForm.events.SHOW_ADVANCED);
240             this.setLessButton();
241         } else {
242             form.trigger(EventForm.events.HIDE_ADVANCED);
243             this.setMoreButton();
244         }
245     };
247     /**
248      * Reload the title for the modal to the appropriate value
249      * depending on whether we are creating a new event or
250      * editing an existing event.
251      *
252      * @method reloadTitleContent
253      * @return {object} A promise resolved with the new title text
254      */
255     ModalEventForm.prototype.reloadTitleContent = function() {
256         if (this.reloadingTitle) {
257             return this.titlePromise;
258         }
260         this.reloadingTitle = true;
262         if (this.hasEventId()) {
263             this.titlePromise = Str.get_string('editevent', 'calendar');
264         } else {
265             this.titlePromise = Str.get_string('newevent', 'calendar');
266         }
268         this.titlePromise.then(function(string) {
269             this.setTitle(string);
270             return string;
271         }.bind(this))
272         .always(function() {
273             this.reloadingTitle = false;
274             return;
275         }.bind(this));
277         return this.titlePromise;
278     };
280     /**
281      * Send a request to the server to get the event_form in a fragment
282      * and render the result in the body of the modal.
283      *
284      * If serialised form data is provided then it will be sent in the
285      * request to the server to have the form rendered with the data. This
286      * is used when the form had a server side error and we need the server
287      * to re-render it for us to display the error to the user.
288      *
289      * @method reloadBodyContent
290      * @param {string} formData The serialised form data
291      * @param {bool} hasError True if we know the form data is erroneous
292      * @return {object} A promise resolved with the fragment html and js from
293      */
294     ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
295         if (this.reloadingBody) {
296             return this.bodyPromise;
297         }
299         this.reloadingBody = true;
300         this.disableButtons();
302         var contextId = this.saveButton.attr('data-context-id');
303         var args = {};
305         if (this.hasEventId()) {
306             args.eventid = this.getEventId();
307         }
309         if (this.hasStartTime()) {
310             args.starttime = this.getStartTime();
311         }
313         if(this.hasCourseId()) {
314             args.courseid = this.getCourseId();
315         }
317         if (typeof formData !== 'undefined') {
318             args.formdata = formData;
319         }
321         args.haserror = (typeof hasError == 'undefined') ? false : hasError;
323         this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
325         this.setBody(this.bodyPromise);
327         this.bodyPromise.then(function() {
328             this.enableButtons();
329             return;
330         }.bind(this))
331         .catch(Notification.exception)
332         .always(function() {
333             this.reloadingBody = false;
334             return;
335         }.bind(this));
337         return this.bodyPromise;
338     };
340     /**
341      * Reload both the title and body content.
342      *
343      * @method reloadAllContent
344      * @return {object} promise
345      */
346     ModalEventForm.prototype.reloadAllContent = function() {
347         return $.when(this.reloadTitleContent(), this.reloadBodyContent());
348     };
350     /**
351      * Kick off a reload the modal content before showing it. This
352      * is to allow us to re-use the same modal for creating and
353      * editing different events within the page.
354      *
355      * We do the reload when showing the modal rather than hiding it
356      * to save a request to the server if the user closes the modal
357      * and never re-opens it.
358      *
359      * @method show
360      */
361     ModalEventForm.prototype.show = function() {
362         this.reloadAllContent();
363         Modal.prototype.show.call(this);
364     };
366     /**
367      * Clear the event id from the modal when it's closed so
368      * that it is loaded fresh next time it's displayed.
369      *
370      * The event id will be set by the calling code if it wants
371      * to edit a specific event.
372      *
373      * @method hide
374      */
375     ModalEventForm.prototype.hide = function() {
376         Modal.prototype.hide.call(this);
377         this.setEventId(null);
378         this.setStartTime(null);
379     };
381     /**
382      * Get the serialised form data.
383      *
384      * @method getFormData
385      * @return {string} serialised form data
386      */
387     ModalEventForm.prototype.getFormData = function() {
388         return this.getForm().serialize();
389     };
391     /**
392      * Send the form data to the server to create or update
393      * an event.
394      *
395      * If there is a server side validation error then we re-request the
396      * rendered form (with the data) from the server in order to get the
397      * server side errors to display.
398      *
399      * On success the modal is hidden and the page is reloaded so that the
400      * new event will display.
401      *
402      * @method save
403      * @return {object} A promise
404      */
405     ModalEventForm.prototype.save = function() {
406         var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
408         loadingContainer.removeClass('hidden');
409         this.disableButtons();
411         var formData = this.getFormData();
412         // Send the form data to the server for processing.
413         return Repository.submitCreateUpdateForm(formData)
414             .then(function(response) {
415                 if (response.validationerror) {
416                     // If there was a server side validation error then
417                     // we need to re-request the rendered form from the server
418                     // in order to display the error for the user.
419                     return this.reloadBodyContent(formData, true);
420                 } else {
421                     // No problemo! Our work here is done.
422                     this.hide();
424                     // Trigger the appropriate calendar event so that the view can
425                     // be updated.
426                     if (this.hasEventId()) {
427                         $('body').trigger(CalendarEvents.updated, [response.event]);
428                     } else {
429                         $('body').trigger(CalendarEvents.created, [response.event]);
430                     }
431                 }
433                 return;
434             }.bind(this))
435             .always(function() {
436                 // Regardless of success or error we should always stop
437                 // the loading icon and re-enable the buttons.
438                 loadingContainer.addClass('hidden');
439                 this.enableButtons();
440             }.bind(this))
441             .catch(Notification.exception);
442     };
444     /**
445      * Set up all of the event handling for the modal.
446      *
447      * @method registerEventListeners
448      */
449     ModalEventForm.prototype.registerEventListeners = function() {
450         // Apply parent event listeners.
451         Modal.prototype.registerEventListeners.call(this);
453         // When the user clicks the save button we trigger the form submission. We need to
454         // trigger an actual submission because there is some JS code in the form that is
455         // listening for this event and doing some stuff (e.g. saving draft areas etc).
456         this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
457             this.getForm().submit();
458             data.originalEvent.preventDefault();
459             e.stopPropagation();
460         }.bind(this));
462         // Catch the submit event before it is actually processed by the browser and
463         // prevent the submission. We'll take it from here.
464         this.getModal().on('submit', function(e) {
465             this.save();
467             // Stop the form from actually submitting and prevent it's
468             // propagation because we have already handled the event.
469             e.preventDefault();
470             e.stopPropagation();
471         }.bind(this));
473         // Toggle the state of the more/less button in the footer.
474         this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
475             this.toggleMoreLessButton();
477             data.originalEvent.preventDefault();
478             e.stopPropagation();
479         }.bind(this));
481         // When the event form tells us that the advanced fields are shown
482         // then the more/less button should be set to less to allow the user
483         // to hide the advanced fields.
484         this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
485             this.setLessButton();
486         }.bind(this));
488         // When the event form tells us that the advanced fields are hidden
489         // then the more/less button should be set to more to allow the user
490         // to show the advanced fields.
491         this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
492             this.setMoreButton();
493         }.bind(this));
494     };
496     // Automatically register with the modal registry the first time this module is imported so that you can create modals
497     // of this type using the modal factory.
498     if (!registered) {
499         ModalRegistry.register(ModalEventForm.TYPE, ModalEventForm, 'calendar/modal_event_form');
500         registered = true;
501     }
503     return ModalEventForm;
504 });