64d675aec253e7264c29b4a1ea1ce60ee118c53a
[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.reloadingBody = false;
70         this.reloadingTitle = false;
71         this.saveButton = this.getFooter().find(SELECTORS.SAVE_BUTTON);
72         this.moreLessButton = this.getFooter().find(SELECTORS.MORELESS_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 event id to the given value.
81      *
82      * @method setEventId
83      * @param {int} id The event id
84      */
85     ModalEventForm.prototype.setEventId = function(id) {
86         this.eventId = id;
87     };
89     /**
90      * Retrieve the current event id, if any.
91      *
92      * @method getEventId
93      * @return {int|null} The event id
94      */
95     ModalEventForm.prototype.getEventId = function() {
96         return this.eventId;
97     };
99     /**
100      * Check if the modal has an event id.
101      *
102      * @method hasEventId
103      * @return {bool}
104      */
105     ModalEventForm.prototype.hasEventId = function() {
106         return this.eventId !== null;
107     };
109     /**
110      * Get the form element from the modal.
111      *
112      * @method getForm
113      * @return {object}
114      */
115     ModalEventForm.prototype.getForm = function() {
116         return this.getBody().find('form');
117     };
119     /**
120      * Disable the buttons in the footer.
121      *
122      * @method disableButtons
123      */
124     ModalEventForm.prototype.disableButtons = function() {
125         this.saveButton.prop('disabled', true);
126         this.moreLessButton.prop('disabled', true);
127     };
129     /**
130      * Enable the buttons in the footer.
131      *
132      * @method enableButtons
133      */
134     ModalEventForm.prototype.enableButtons = function() {
135         this.saveButton.prop('disabled', false);
136         this.moreLessButton.prop('disabled', false);
137     };
139     /**
140      * Set the more/less button in the footer to the "more"
141      * state.
142      *
143      * @method setMoreButton
144      */
145     ModalEventForm.prototype.setMoreButton = function() {
146         this.moreLessButton.attr('data-collapsed', 'true');
147         Str.get_string('more', 'calendar').then(function(string) {
148             this.moreLessButton.text(string);
149         }.bind(this));
150     };
152     /**
153      * Set the more/less button in the footer to the "less"
154      * state.
155      *
156      * @method setLessButton
157      */
158     ModalEventForm.prototype.setLessButton = function() {
159         this.moreLessButton.attr('data-collapsed', 'false');
160         Str.get_string('less', 'calendar').then(function(string) {
161             this.moreLessButton.text(string);
162         }.bind(this));
163     };
165     /**
166      * Toggle the more/less button in the footer from the current
167      * state to it's opposite state.
168      *
169      * @method toggleMoreLessButton
170      */
171     ModalEventForm.prototype.toggleMoreLessButton = function() {
172         var form = this.getForm();
174         if (this.moreLessButton.attr('data-collapsed') == 'true') {
175             form.trigger(EventForm.events.SHOW_ADVANCED);
176             this.setLessButton();
177         } else {
178             form.trigger(EventForm.events.HIDE_ADVANCED);
179             this.setMoreButton();
180         }
181     };
183     /**
184      * Reload the title for the modal to the appropriate value
185      * depending on whether we are creating a new event or
186      * editing an existing event.
187      *
188      * @method reloadTitleContent
189      * @return {object} A promise resolved with the new title text
190      */
191     ModalEventForm.prototype.reloadTitleContent = function() {
192         if (this.reloadingTitle) {
193             return this.titlePromise;
194         }
196         this.reloadingTitle = true;
198         if (this.hasEventId()) {
199             this.titlePromise = Str.get_string('editevent', 'calendar');
200         } else {
201             this.titlePromise = Str.get_string('newevent', 'calendar');
202         }
204         this.titlePromise.then(function(string) {
205             this.setTitle(string);
206             return string;
207         }.bind(this))
208         .always(function() {
209             this.reloadingTitle = false;
210             return;
211         }.bind(this));
213         return this.titlePromise;
214     };
216     /**
217      * Send a request to the server to get the event_form in a fragment
218      * and render the result in the body of the modal.
219      *
220      * If serialised form data is provided then it will be sent in the
221      * request to the server to have the form rendered with the data. This
222      * is used when the form had a server side error and we need the server
223      * to re-render it for us to display the error to the user.
224      *
225      * @method reloadBodyContent
226      * @param {string} formData The serialised form data
227      * @param {bool} hasError True if we know the form data is erroneous
228      * @return {object} A promise resolved with the fragment html and js from
229      */
230     ModalEventForm.prototype.reloadBodyContent = function(formData, hasError) {
231         if (this.reloadingBody) {
232             return this.bodyPromise;
233         }
235         this.reloadingBody = true;
236         this.disableButtons();
238         var contextId = this.saveButton.attr('data-context-id');
239         var args = {};
241         if (this.hasEventId()) {
242             args.eventid = this.getEventId();
243         }
245         if (typeof formData !== 'undefined') {
246             args.formdata = formData;
247         }
249         args.haserror = (typeof hasError == 'undefined') ? false : hasError;
251         this.bodyPromise = Fragment.loadFragment('calendar', 'event_form', contextId, args);
253         this.setBody(this.bodyPromise);
255         this.bodyPromise.then(function() {
256             this.enableButtons();
257             return;
258         }.bind(this))
259         .catch(Notification.exception)
260         .always(function() {
261             this.reloadingBody = false;
262             return;
263         }.bind(this));
265         return this.bodyPromise;
266     };
268     /**
269      * Reload both the title and body content.
270      *
271      * @method reloadAllContent
272      * @return {object} promise
273      */
274     ModalEventForm.prototype.reloadAllContent = function() {
275         return $.when(this.reloadTitleContent(), this.reloadBodyContent());
276     };
278     /**
279      * Kick off a reload the modal content before showing it. This
280      * is to allow us to re-use the same modal for creating and
281      * editing different events within the page.
282      *
283      * We do the reload when showing the modal rather than hiding it
284      * to save a request to the server if the user closes the modal
285      * and never re-opens it.
286      *
287      * @method show
288      */
289     ModalEventForm.prototype.show = function() {
290         this.reloadAllContent();
291         Modal.prototype.show.call(this);
292     };
294     /**
295      * Clear the event id from the modal when it's closed so
296      * that it is loaded fresh next time it's displayed.
297      *
298      * The event id will be set by the calling code if it wants
299      * to edit a specific event.
300      *
301      * @method hide
302      */
303     ModalEventForm.prototype.hide = function() {
304         Modal.prototype.hide.call(this);
305         this.setEventId(null);
306     };
308     /**
309      * Get the serialised form data.
310      *
311      * @method getFormData
312      * @return {string} serialised form data
313      */
314     ModalEventForm.prototype.getFormData = function() {
315         return this.getForm().serialize();
316     };
318     /**
319      * Send the form data to the server to create or update
320      * an event.
321      *
322      * If there is a server side validation error then we re-request the
323      * rendered form (with the data) from the server in order to get the
324      * server side errors to display.
325      *
326      * On success the modal is hidden and the page is reloaded so that the
327      * new event will display.
328      *
329      * @method save
330      * @return {object} A promise
331      */
332     ModalEventForm.prototype.save = function() {
333         var loadingContainer = this.saveButton.find(SELECTORS.LOADING_ICON_CONTAINER);
335         loadingContainer.removeClass('hidden');
336         this.disableButtons();
338         var formData = this.getFormData();
339         // Send the form data to the server for processing.
340         return Repository.submitCreateUpdateForm(formData)
341             .then(function(response) {
342                 if (response.validationerror) {
343                     // If there was a server side validation error then
344                     // we need to re-request the rendered form from the server
345                     // in order to display the error for the user.
346                     return this.reloadBodyContent(formData, true);
347                 } else {
348                     // No problemo! Our work here is done.
349                     this.hide();
351                     // Trigger the appropriate calendar event so that the view can
352                     // be updated.
353                     if (this.hasEventId()) {
354                         $('body').trigger(CalendarEvents.updated, [response.event]);
355                     } else {
356                         $('body').trigger(CalendarEvents.created, [response.event]);
357                     }
358                 }
359             }.bind(this))
360             .always(function() {
361                 // Regardless of success or error we should always stop
362                 // the loading icon and re-enable the buttons.
363                 loadingContainer.addClass('hidden');
364                 this.enableButtons();
365             }.bind(this))
366             .catch(Notification.exception);
367     };
369     /**
370      * Set up all of the event handling for the modal.
371      *
372      * @method registerEventListeners
373      */
374     ModalEventForm.prototype.registerEventListeners = function() {
375         // Apply parent event listeners.
376         Modal.prototype.registerEventListeners.call(this);
378         // When the user clicks the save button we trigger the form submission. We need to
379         // trigger an actual submission because there is some JS code in the form that is
380         // listening for this event and doing some stuff (e.g. saving draft areas etc).
381         this.getModal().on(CustomEvents.events.activate, SELECTORS.SAVE_BUTTON, function(e, data) {
382             this.getForm().submit();
383             data.originalEvent.preventDefault();
384             e.stopPropagation();
385         }.bind(this));
387         // Catch the submit event before it is actually processed by the browser and
388         // prevent the submission. We'll take it from here.
389         this.getModal().on('submit', function(e) {
390             this.save();
392             // Stop the form from actually submitting and prevent it's
393             // propagation because we have already handled the event.
394             e.preventDefault();
395             e.stopPropagation();
396         }.bind(this));
398         // Toggle the state of the more/less button in the footer.
399         this.getModal().on(CustomEvents.events.activate, SELECTORS.MORELESS_BUTTON, function(e, data) {
400             this.toggleMoreLessButton();
402             data.originalEvent.preventDefault();
403             e.stopPropagation();
404         }.bind(this));
406         // When the event form tells us that the advanced fields are shown
407         // then the more/less button should be set to less to allow the user
408         // to hide the advanced fields.
409         this.getModal().on(EventForm.events.ADVANCED_SHOWN, function() {
410             this.setLessButton();
411         }.bind(this));
413         // When the event form tells us that the advanced fields are hidden
414         // then the more/less button should be set to more to allow the user
415         // to show the advanced fields.
416         this.getModal().on(EventForm.events.ADVANCED_HIDDEN, function() {
417             this.setMoreButton();
418         }.bind(this));
419     };
421     // Automatically register with the modal registry the first time this module is imported so that you can create modals
422     // of this type using the modal factory.
423     if (!registered) {
424         ModalRegistry.register(ModalEventForm.TYPE, ModalEventForm, 'calendar/modal_event_form');
425         registered = true;
426     }
428     return ModalEventForm;
429 });