MDL-54852 assignment: Suppress "leave page" warning after save
[moodle.git] / mod / assign / amd / src / grading_panel.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  * Javascript controller for the "Grading" panel at the right of the page.
18  *
19  * @module     mod_assign/grading_panel
20  * @package    mod_assign
21  * @class      GradingPanel
22  * @copyright  2016 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      3.1
25  */
26 define(['jquery', 'core/yui', 'core/notification', 'core/templates', 'core/fragment',
27         'core/ajax', 'core/str', 'mod_assign/grading_form_change_checker',
28         'mod_assign/grading_events'],
29        function($, Y, notification, templates, fragment, ajax, str, checker, GradingEvents) {
31     /**
32      * GradingPanel class.
33      *
34      * @class GradingPanel
35      * @param {String} selector The selector for the page region containing the user navigation.
36      */
37     var GradingPanel = function(selector) {
38         this._regionSelector = selector;
39         this._region = $(selector);
40         this._userCache = [];
42         this.registerEventListeners();
43     };
45     /** @type {String} Selector for the page region containing the user navigation. */
46     GradingPanel.prototype._regionSelector = null;
48     /** @type {Integer} Remember the last user id to prevent unnessecary reloads. */
49     GradingPanel.prototype._lastUserId = 0;
51     /** @type {Integer} Remember the last attempt number to prevent unnessecary reloads. */
52     GradingPanel.prototype._lastAttemptNumber = -1;
54     /** @type {JQuery} JQuery node for the page region containing the user navigation. */
55     GradingPanel.prototype._region = null;
57     /**
58      * Fade the dom node out, update it, and fade it back.
59      *
60      * @private
61      * @method _niceReplaceNodeContents
62      * @param {JQuery} node
63      * @param {String} html
64      * @param {String} js
65      * @return {Deferred} promise resolved when the animations are complete.
66      */
67     GradingPanel.prototype._niceReplaceNodeContents = function(node, html, js) {
68         var promise = $.Deferred();
70         node.fadeOut("fast", function() {
71             templates.replaceNodeContents(node, html, js);
72             node.fadeIn("fast", function() {
73                 promise.resolve();
74             });
75         });
77         return promise.promise();
78     };
80     /**
81      * Make sure all form fields have the latest saved state.
82      * @private
83      * @method _saveFormState
84      */
85     GradingPanel.prototype._saveFormState = function() {
86         // Grrrrr! TinyMCE you know what you did.
87         if (typeof window.tinyMCE !== 'undefined') {
88             window.tinyMCE.triggerSave();
89         }
91         // Copy data from notify students checkbox which was moved out of the form.
92         var checked = $('[data-region="grading-actions-form"] [name="sendstudentnotifications"]').val();
93         $('.gradeform [name="sendstudentnotifications"]').val(checked);
94     };
96     /**
97      * Make form submit via ajax.
98      *
99      * @private
100      * @param {Object} event
101      * @param {Integer} nextUserId
102      * @method _submitForm
103      */
104     GradingPanel.prototype._submitForm = function(event, nextUserId) {
105         // The form was submitted - send it via ajax instead.
106         var form = $(this._region.find('form.gradeform'));
108         $('[data-region="overlay"]').show();
110         // We call this, so other modules can update the form with the latest state.
111         form.trigger('save-form-state');
113         // Now we get all the current values from the form.
114         var data = form.serialize();
115         var assignmentid = this._region.attr('data-assignmentid');
117         // Now we can continue...
118         ajax.call([{
119             methodname: 'mod_assign_submit_grading_form',
120             args: {assignmentid: assignmentid, userid: this._lastUserId, jsonformdata: JSON.stringify(data)},
121             done: this._handleFormSubmissionResponse.bind(this, data, nextUserId),
122             fail: notification.exception
123         }]);
124     };
126     /**
127      * Handle form submission response.
128      *
129      * @private
130      * @method _handleFormSubmissionResponse
131      * @param {Array} formdata - submitted values
132      * @param {Integer} nextUserId - optional. The id of the user to load after the form is saved.
133      * @param {Array} response List of errors.
134      */
135     GradingPanel.prototype._handleFormSubmissionResponse = function(formdata, nextUserId, response) {
136         if (typeof nextUserId === "undefined") {
137             nextUserId = this._lastUserId;
138         }
139         if (response.length) {
140             // There was an error saving the grade. Re-render the form using the submitted data so we can show
141             // validation errors.
142             $(document).trigger('reset', [this._lastUserId, formdata]);
143         } else {
144             str.get_strings([
145                 {key: 'changessaved', component: 'core'},
146                 {key: 'gradechangessaveddetail', component: 'mod_assign'},
147             ]).done(function(strs) {
148                 notification.alert(strs[0], strs[1]);
149             }).fail(notification.exception);
150             Y.use('moodle-core-formchangechecker', function() {
151                 M.core_formchangechecker.reset_form_dirty_state();
152             });
153             if (nextUserId == this._lastUserId) {
154                 $(document).trigger('reset', nextUserId);
155             } else {
156                 $(document).trigger('user-changed', nextUserId);
157             }
158         }
159         $('[data-region="overlay"]').hide();
160     };
162     /**
163      * Refresh form with default values.
164      *
165      * @private
166      * @method _resetForm
167      * @param {Event} e
168      * @param {Integer} userid
169      * @param {Array} formdata
170      */
171     GradingPanel.prototype._resetForm = function(e, userid, formdata) {
172         // The form was cancelled - refresh with default values.
173         var event = $.Event("custom");
174         if (typeof userid == "undefined") {
175             userid = this._lastUserId;
176         }
177         this._lastUserId = 0;
178         this._refreshGradingPanel(event, userid, formdata);
179     };
181     /**
182      * Open a picker to choose an older attempt.
183      *
184      * @private
185      * @param {Object} e
186      * @method _chooseAttempt
187      */
188     GradingPanel.prototype._chooseAttempt = function(e) {
189         // Show a dialog.
191         // The form is in the element pointed to by data-submissions.
192         var link = $(e.target);
193         var submissionsId = link.data('submissions');
194         var submissionsform = $(document.getElementById(submissionsId));
195         var formcopy = submissionsform.clone();
196         var formhtml = formcopy.wrap($('<form/>')).html();
198         str.get_strings([
199             {key: 'viewadifferentattempt', component: 'mod_assign'},
200             {key: 'view', component: 'core'},
201             {key: 'cancel', component: 'core'},
202         ]).done(function(strs) {
203             notification.confirm(strs[0], formhtml, strs[1], strs[2], function() {
204                 var attemptnumber = $("input:radio[name='select-attemptnumber']:checked").val();
206                 this._refreshGradingPanel(null, this._lastUserId, '', attemptnumber);
207             }.bind(this));
208         }.bind(this)).fail(notification.exception);
209     };
211     /**
212      * Add popout buttons
213      *
214      * @private
215      * @method _addPopoutButtons
216      * @param {JQuery} selector The region selector to add popout buttons to.
217      */
218     GradingPanel.prototype._addPopoutButtons = function(selector) {
219         var region = $(selector);
221         templates.render('mod_assign/popout_button', {}).done(function(html) {
222             var parents = region.find('[data-fieldtype="filemanager"],[data-fieldtype="editor"],[data-fieldtype="grading"]')
223                     .closest('.fitem');
224             parents.addClass('has-popout').find('label').parent().append(html);
226             region.on('click', '[data-region="popout-button"]', this._togglePopout.bind(this));
227         }.bind(this)).fail(notification.exception);
228     };
230     /**
231      * Make a div "popout" or "popback".
232      *
233      * @private
234      * @method _togglePopout
235      * @param {Event} event
236      */
237     GradingPanel.prototype._togglePopout = function(event) {
238         event.preventDefault();
239         var container = $(event.target).closest('.fitem');
240         if (container.hasClass('popout')) {
241             $('.popout').removeClass('popout');
242         } else {
243             $('.popout').removeClass('popout');
244             container.addClass('popout');
245             container.addClass('moodle-has-zindex');
246         }
247     };
249     /**
250      * Get the user context - re-render the template in the page.
251      *
252      * @private
253      * @method _refreshGradingPanel
254      * @param {Event} event
255      * @param {Number} userid
256      * @param {String} submissiondata serialised submission data.
257      * @param {Integer} attemptnumber
258      */
259     GradingPanel.prototype._refreshGradingPanel = function(event, userid, submissiondata, attemptnumber) {
260         var contextid = this._region.attr('data-contextid');
261         if (typeof submissiondata === 'undefined') {
262             submissiondata = '';
263         }
264         if (typeof attemptnumber === 'undefined') {
265             attemptnumber = -1;
266         }
267         // Skip reloading if it is the same user.
268         if (this._lastUserId == userid && this._lastAttemptNumber == attemptnumber && submissiondata === '') {
269             return;
270         }
271         this._lastUserId = userid;
272         this._lastAttemptNumber = attemptnumber;
273         $(document).trigger('start-loading-user');
274         // Tell behat to back off too.
275         window.M.util.js_pending('mod-assign-loading-user');
276         // First insert the loading template.
277         templates.render('mod_assign/loading', {}).done(function(html, js) {
278             // Update the page.
279             this._niceReplaceNodeContents(this._region, html, js).done(function() {
280                 if (userid > 0) {
281                     this._region.show();
282                     // Reload the grading form "fragment" for this user.
283                     var params = {userid: userid, attemptnumber: attemptnumber, jsonformdata: JSON.stringify(submissiondata)};
284                     fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) {
285                         this._niceReplaceNodeContents(this._region, html, js)
286                         .done(function() {
287                             checker.saveFormState('[data-region="grade-panel"] .gradeform');
288                             $(document).on('editor-content-restored', function() {
289                                 // If the editor has some content that has been restored
290                                 // then save the form state again for comparison.
291                                 checker.saveFormState('[data-region="grade-panel"] .gradeform');
292                             });
293                             $('[data-region="attempt-chooser"]').on('click', this._chooseAttempt.bind(this));
294                             this._addPopoutButtons('[data-region="grade-panel"] .gradeform');
295                             $(document).trigger('finish-loading-user');
296                             // Tell behat we are friends again.
297                             window.M.util.js_complete('mod-assign-loading-user');
298                         }.bind(this))
299                         .fail(notification.exception);
300                     }.bind(this)).fail(notification.exception);
301                 } else {
302                     this._region.hide();
303                     var reviewPanel = $('[data-region="review-panel"]');
304                     if (reviewPanel.length) {
305                         this._niceReplaceNodeContents(reviewPanel, '', '');
306                     }
307                     $(document).trigger('finish-loading-user');
308                     // Tell behat we are friends again.
309                     window.M.util.js_complete('mod-assign-loading-user');
310                 }
311             }.bind(this));
312         }.bind(this)).fail(notification.exception);
313     };
315     /**
316      * Get the grade panel element.
317      *
318      * @method getPanelElement
319      * @return {jQuery}
320      */
321     GradingPanel.prototype.getPanelElement = function() {
322         return $('[data-region="grade-panel"]');
323     };
325     /**
326      * Hide the grade panel.
327      *
328      * @method collapsePanel
329      */
330     GradingPanel.prototype.collapsePanel = function() {
331         this.getPanelElement().addClass('collapsed');
332     };
334     /**
335      * Show the grade panel.
336      *
337      * @method expandPanel
338      */
339     GradingPanel.prototype.expandPanel = function() {
340         this.getPanelElement().removeClass('collapsed');
341     };
343     /**
344      * Register event listeners for the grade panel.
345      *
346      * @method registerEventListeners
347      */
348     GradingPanel.prototype.registerEventListeners = function() {
349         var docElement = $(document);
351         docElement.on('user-changed', this._refreshGradingPanel.bind(this));
352         docElement.on('save-changes', this._submitForm.bind(this));
353         docElement.on('reset', this._resetForm.bind(this));
355         docElement.on('save-form-state', this._saveFormState.bind(this));
357         docElement.on(GradingEvents.COLLAPSE_GRADE_PANEL, function() {
358             this.collapsePanel();
359         }.bind(this));
361         // We should expand if the review panel is collapsed.
362         docElement.on(GradingEvents.COLLAPSE_REVIEW_PANEL, function() {
363             this.expandPanel();
364         }.bind(this));
366         docElement.on(GradingEvents.EXPAND_GRADE_PANEL, function() {
367             this.expandPanel();
368         }.bind(this));
369     };
371     return GradingPanel;
372 });