MDL-64573 output: ajax form event
[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         ],
38         function(
39             $,
40             Event,
41             Str,
42             Notification,
43             Templates,
44             CustomEvents,
45             Modal,
46             ModalRegistry,
47             Fragment,
48             CalendarEvents,
49             Repository
50         ) {
52     var registered = false;
53     var SELECTORS = {
54         SAVE_BUTTON: '[data-action="save"]',
55         LOADING_ICON_CONTAINER: '[data-region="loading-icon-container"]',
56     };
58     /**
59      * Constructor for the Modal.
60      *
61      * @param {object} root The root jQuery element for the modal
62      */
63     var ModalEventForm = function(root) {
64         Modal.call(this, root);
65         this.eventId = null;
66         this.startTime = null;
67         this.courseId = null;
68         this.categoryId = null;
69         this.contextId = null;
70         this.reloadingBody = false;
71         this.reloadingTitle = false;
72         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
73     };
75     ModalEventForm.TYPE = 'core_calendar-modal_event_form';
76     ModalEventForm.prototype = Object.create(Modal.prototype);
77     ModalEventForm.prototype.constructor = ModalEventForm;
79     /**
80      * Set the context id to the given value.
81      *
82      * @method setContextId
83      * @param {Number} id The event id
84      */
85     ModalEventForm.prototype.setContextId = function(id) {
86         this.contextId = id;
87     };
89     /**
90      * Retrieve the current context id, if any.
91      *
92      * @method getContextId
93      * @return {Number|null} The event id
94      */
95     ModalEventForm.prototype.getContextId = function() {
96         return this.contextId;
97     };
99     /**
100      * Set the course id to the given value.
101      *
102      * @method setCourseId
103      * @param {int} id The event id
104      */
105     ModalEventForm.prototype.setCourseId = function(id) {
106         this.courseId = id;
107     };
109     /**
110      * Retrieve the current course id, if any.
111      *
112      * @method getCourseId
113      * @return {int|null} The event id
114      */
115     ModalEventForm.prototype.getCourseId = function() {
116         return this.courseId;
117     };
119     /**
120      * Set the category id to the given value.
121      *
122      * @method setCategoryId
123      * @param {int} id The event id
124      */
125     ModalEventForm.prototype.setCategoryId = function(id) {
126         this.categoryId = id;
127     };
129     /**
130      * Retrieve the current category id, if any.
131      *
132      * @method getCategoryId
133      * @return {int|null} The event id
134      */
135     ModalEventForm.prototype.getCategoryId = function() {
136         return this.categoryId;
137     };
139     /**
140      * Check if the modal has an course id.
141      *
142      * @method hasCourseId
143      * @return {bool}
144      */
145     ModalEventForm.prototype.hasCourseId = function() {
146         return this.courseId !== null;
147     };
149     /**
150      * Check if the modal has an category id.
151      *
152      * @method hasCategoryId
153      * @return {bool}
154      */
155     ModalEventForm.prototype.hasCategoryId = function() {
156         return this.categoryId !== null;
157     };
159     /**
160      * Set the event id to the given value.
161      *
162      * @method setEventId
163      * @param {int} id The event id
164      */
165     ModalEventForm.prototype.setEventId = function(id) {
166         this.eventId = id;
167     };
169     /**
170      * Retrieve the current event id, if any.
171      *
172      * @method getEventId
173      * @return {int|null} The event id
174      */
175     ModalEventForm.prototype.getEventId = function() {
176         return this.eventId;
177     };
179     /**
180      * Check if the modal has an event id.
181      *
182      * @method hasEventId
183      * @return {bool}
184      */
185     ModalEventForm.prototype.hasEventId = function() {
186         return this.eventId !== null;
187     };
189     /**
190      * Set the start time to the given value.
191      *
192      * @method setStartTime
193      * @param {int} time The start time
194      */
195     ModalEventForm.prototype.setStartTime = function(time) {
196         this.startTime = time;
197     };
199     /**
200      * Retrieve the current start time, if any.
201      *
202      * @method getStartTime
203      * @return {int|null} The start time
204      */
205     ModalEventForm.prototype.getStartTime = function() {
206         return this.startTime;
207     };
209     /**
210      * Check if the modal has start time.
211      *
212      * @method hasStartTime
213      * @return {bool}
214      */
215     ModalEventForm.prototype.hasStartTime = function() {
216         return this.startTime !== null;
217     };
219     /**
220      * Get the form element from the modal.
221      *
222      * @method getForm
223      * @return {object}
224      */
225     ModalEventForm.prototype.getForm = function() {
226         return this.getBody().find('form');
227     };
229     /**
230      * Disable the buttons in the footer.
231      *
232      * @method disableButtons
233      */
234     ModalEventForm.prototype.disableButtons = function() {
235         this.saveButton.prop('disabled', true);
236     };
238     /**
239      * Enable the buttons in the footer.
240      *
241      * @method enableButtons
242      */
243     ModalEventForm.prototype.enableButtons = function() {
244         this.saveButton.prop('disabled', false);
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))
276         .fail(Notification.exception);
278         return this.titlePromise;
279     };
281     /**
282      * Send a request to the server to get the event_form in a fragment
283      * and render the result in the body of the modal.
284      *
285      * If serialised form data is provided then it will be sent in the
286      * request to the server to have the form rendered with the data. This
287      * is used when the form had a server side error and we need the server
288      * to re-render it for us to display the error to the user.
289      *
290      * @method reloadBodyContent
291      * @param {string} formData The serialised form data
292      * @return {object} A promise resolved with the fragment html and js from
293      */
294     ModalEventForm.prototype.reloadBodyContent = function(formData) {
295         if (this.reloadingBody) {
296             return this.bodyPromise;
297         }
299         this.reloadingBody = true;
300         this.disableButtons();
302         var args = {};
304         if (this.hasEventId()) {
305             args.eventid = this.getEventId();
306         }
308         if (this.hasStartTime()) {
309             args.starttime = this.getStartTime();
310         }
312         if (this.hasCourseId()) {
313             args.courseid = this.getCourseId();
314         }
316         if (this.hasCategoryId()) {
317             args.categoryid = this.getCategoryId();
318         }
320         if (typeof formData !== 'undefined') {
321             args.formdata = formData;
322         }
324         this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', this.getContextId(), args);
326         this.setBody(this.bodyPromise);
328         this.bodyPromise.then(function() {
329             this.enableButtons();
330             return;
331         }.bind(this))
332         .fail(Notification.exception)
333         .always(function() {
334             this.reloadingBody = false;
335             return;
336         }.bind(this))
337         .fail(Notification.exception);
339         return this.bodyPromise;
340     };
342     /**
343      * Reload both the title and body content.
344      *
345      * @method reloadAllContent
346      * @return {object} promise
347      */
348     ModalEventForm.prototype.reloadAllContent = function() {
349         return $.when(this.reloadTitleContent(), this.reloadBodyContent());
350     };
352     /**
353      * Kick off a reload the modal content before showing it. This
354      * is to allow us to re-use the same modal for creating and
355      * editing different events within the page.
356      *
357      * We do the reload when showing the modal rather than hiding it
358      * to save a request to the server if the user closes the modal
359      * and never re-opens it.
360      *
361      * @method show
362      */
363     ModalEventForm.prototype.show = function() {
364         this.reloadAllContent();
365         Modal.prototype.show.call(this);
366     };
368     /**
369      * Clear the event id from the modal when it's closed so
370      * that it is loaded fresh next time it's displayed.
371      *
372      * The event id will be set by the calling code if it wants
373      * to edit a specific event.
374      *
375      * @method hide
376      */
377     ModalEventForm.prototype.hide = function() {
378         Modal.prototype.hide.call(this);
379         this.setEventId(null);
380         this.setStartTime(null);
381         this.setCourseId(null);
382         this.setCategoryId(null);
383     };
385     /**
386      * Get the serialised form data.
387      *
388      * @method getFormData
389      * @return {string} serialised form data
390      */
391     ModalEventForm.prototype.getFormData = function() {
392         return this.getForm().serialize();
393     };
395     /**
396      * Send the form data to the server to create or update
397      * an event.
398      *
399      * If there is a server side validation error then we re-request the
400      * rendered form (with the data) from the server in order to get the
401      * server side errors to display.
402      *
403      * On success the modal is hidden and the page is reloaded so that the
404      * new event will display.
405      *
406      * @method save
407      * @return {object} A promise
408      */
409     ModalEventForm.prototype.save = function() {
410         var invalid,
411             loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
413         // Now the change events have run, see if there are any "invalid" form fields.
414         invalid = this.getForm().find('[aria-invalid="true"]');
416         // If we found invalid fields, focus on the first one and do not submit via ajax.
417         if (invalid.length) {
418             invalid.first().focus();
419             return;
420         }
422         loadingContainer.removeClass('hidden');
423         this.disableButtons();
425         var formData = this.getFormData();
426         // Send the form data to the server for processing.
427         return Repository.submitCreateUpdateForm(formData)
428             .then(function(response) {
429                 if (response.validationerror) {
430                     // If there was a server side validation error then
431                     // we need to re-request the rendered form from the server
432                     // in order to display the error for the user.
433                     this.reloadBodyContent(formData);
434                     return;
435                 } else {
436                     // Check whether this was a new event or not.
437                     // The hide function unsets the form data so grab this before the hide.
438                     var isExisting = this.hasEventId();
440                     // No problemo! Our work here is done.
441                     this.hide();
443                     // Trigger the appropriate calendar event so that the view can be updated.
444                     if (isExisting) {
445                         $('body').trigger(CalendarEvents.updated, [response.event]);
446                     } else {
447                         $('body').trigger(CalendarEvents.created, [response.event]);
448                     }
449                 }
451                 return;
452             }.bind(this))
453             .always(function() {
454                 // Regardless of success or error we should always stop
455                 // the loading icon and re-enable the buttons.
456                 loadingContainer.addClass('hidden');
457                 this.enableButtons();
459                 return;
460             }.bind(this))
461             .fail(Notification.exception);
462     };
464     /**
465      * Set up all of the event handling for the modal.
466      *
467      * @method registerEventListeners
468      */
469     ModalEventForm.prototype.registerEventListeners = function() {
470         // Apply parent event listeners.
471         Modal.prototype.registerEventListeners.call(this);
473         // When the user clicks the save button we trigger the form submission. We need to
474         // trigger an actual submission because there is some JS code in the form that is
475         // listening for this event and doing some stuff (e.g. saving draft areas etc).
476         this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
477             this.getForm().submit();
478             data.originalEvent.preventDefault();
479             e.stopPropagation();
480         }.bind(this));
482         // Catch the submit event before it is actually processed by the browser and
483         // prevent the submission. We'll take it from here.
484         this.getModal().on('submit', function(e) {
485             Event.notifyFormSubmitAjax(this.getForm()[0]);
487             this.save();
489             // Stop the form from actually submitting and prevent it's
490             // propagation because we have already handled the event.
491             e.preventDefault();
492             e.stopPropagation();
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 });