MDL-60430 assignment: Assignment save and show next for 1 user
[moodle.git] / mod / assign / amd / src / grading_navigation.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 to handle changing users via the user selector in the header.
18  *
19  * @module     mod_assign/grading_navigation
20  * @package    mod_assign
21  * @copyright  2016 Damyon Wiese <damyon@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  * @since      3.1
24  */
25 define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',
26         'core/ajax', 'mod_assign/grading_form_change_checker'],
27        function($, notification, str, autocomplete, ajax, checker) {
29     /**
30      * GradingNavigation class.
31      *
32      * @class GradingNavigation
33      * @param {String} selector The selector for the page region containing the user navigation.
34      */
35     var GradingNavigation = function(selector) {
36         this._regionSelector = selector;
37         this._region = $(selector);
38         this._filters = [];
39         this._users = [];
40         this._filteredUsers = [];
42         // Get the current user list from a webservice.
43         this._loadAllUsers();
45         // Attach listeners to the select and arrow buttons.
47         this._region.find('[data-action="previous-user"]').on('click', this._handlePreviousUser.bind(this));
48         this._region.find('[data-action="next-user"]').on('click', this._handleNextUser.bind(this));
49         this._region.find('[data-action="change-user"]').on('change', this._handleChangeUser.bind(this));
50         this._region.find('[data-region="user-filters"]').on('click', this._toggleExpandFilters.bind(this));
52         $(document).on('user-changed', this._refreshSelector.bind(this));
53         $(document).on('done-saving-show-next', this._handleNextUser.bind(this));
55         // Position the configure filters panel under the link that expands it.
56         var toggleLink = this._region.find('[data-region="user-filters"]');
57         var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
59         configPanel.on('change', '[type="checkbox"]', this._filterChanged.bind(this));
61         var userid = $('[data-region="grading-navigation-panel"]').data('first-userid');
62         if (userid) {
63             this._selectUserById(userid);
64         }
66         str.get_string('changeuser', 'mod_assign').done(function(s) {
67                 autocomplete.enhance('[data-action=change-user]', false, 'mod_assign/participant_selector', s);
68             }
69         ).fail(notification.exception);
71         // We do not allow navigation while ajax requests are pending.
73         $(document).bind("start-loading-user", function() {
74             this._isLoading = true;
75         }.bind(this));
76         $(document).bind("finish-loading-user", function() {
77             this._isLoading = false;
78         }.bind(this));
79     };
81     /** @type {Boolean} Boolean tracking active ajax requests. */
82     GradingNavigation.prototype._isLoading = false;
84     /** @type {String} Selector for the page region containing the user navigation. */
85     GradingNavigation.prototype._regionSelector = null;
87     /** @type {Array} The list of active filter keys */
88     GradingNavigation.prototype._filters = null;
90     /** @type {Array} The list of users */
91     GradingNavigation.prototype._users = null;
93     /** @type {JQuery} JQuery node for the page region containing the user navigation. */
94     GradingNavigation.prototype._region = null;
96     /**
97      * Load the list of all users for this assignment.
98      *
99      * @private
100      * @method _loadAllUsers
101      */
102     GradingNavigation.prototype._loadAllUsers = function() {
103         var select = this._region.find('[data-action=change-user]');
104         var assignmentid = select.attr('data-assignmentid');
105         var groupid = select.attr('data-groupid');
107         ajax.call([{
108             methodname: 'mod_assign_list_participants',
109             args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true},
110             done: this._usersLoaded.bind(this),
111             fail: notification.exception
112         }]);
113     };
115     /**
116      * Call back to rebuild the user selector and x of y info when the user list is updated.
117      *
118      * @private
119      * @method _usersLoaded
120      * @param {Array} users
121      */
122     GradingNavigation.prototype._usersLoaded = function(users) {
123         this._filteredUsers = this._users = users;
124         if (this._users.length) {
125             // Position the configure filters panel under the link that expands it.
126             var toggleLink = this._region.find('[data-region="user-filters"]');
127             var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
129             configPanel.find('[type="checkbox"]').trigger('change');
130         } else {
131             this._selectNoUser();
132         }
133         this._triggerNextUserEvent();
134     };
136     /**
137      * Close the configure filters panel if a click is detected outside of it.
138      *
139      * @private
140      * @method _checkClickOutsideConfigureFilters
141      * @param {Event} event
142      */
143     GradingNavigation.prototype._checkClickOutsideConfigureFilters = function(event) {
144         var configPanel = this._region.find('[data-region="configure-filters"]');
146         if (!configPanel.is(event.target) && configPanel.has(event.target).length === 0) {
147             var toggleLink = this._region.find('[data-region="user-filters"]');
149             configPanel.hide();
150             configPanel.attr('aria-hidden', 'true');
151             toggleLink.attr('aria-expanded', 'false');
152             $(document).unbind('click.mod_assign_grading_navigation');
153         }
154     };
156     /**
157      * Turn a filter on or off.
158      *
159      * @private
160      * @method _filterChanged
161      * @param {Event} event
162      */
163     GradingNavigation.prototype._filterChanged = function(event) {
164         var name = $(event.target).attr('name');
165         var key = name.split('_').pop();
166         var enabled = $(event.target).prop('checked');
168         if (enabled) {
169             if (this._filters.indexOf(key) == -1) {
170                 this._filters[this._filters.length] = key;
171             }
172         } else {
173             var index = this._filters.indexOf(key);
174             if (index != -1) {
175                 this._filters.splice(index, 1);
176             }
177         }
179         // Update the active filter string.
180         var filterlist = [];
181         this._region.find('[data-region="configure-filters"]').find('[type="checkbox"]').each(function(idx, ele) {
182             if ($(ele).prop('checked')) {
183                 filterlist[filterlist.length] = $(ele).closest('label').text();
184             }
185         });
186         if (filterlist.length) {
187             this._region.find('[data-region="user-filters"] span').text(filterlist.join(', '));
188         } else {
189             str.get_string('nofilters', 'mod_assign').done(function(s) {
190                 this._region.find('[data-region="user-filters"] span').text(s);
191             }.bind(this)).fail(notification.exception);
192         }
194         // Filter the options in the select box that do not match the current filters.
196         var select = this._region.find('[data-action=change-user]');
197         var userid = select.attr('data-selected');
198         var foundIndex = 0;
200         this._filteredUsers = [];
202         $.each(this._users, function(index, user) {
203             var show = true;
204             $.each(this._filters, function(filterindex, filter) {
205                 if (filter == "submitted") {
206                     if (user.submitted == "0") {
207                         show = false;
208                     }
209                 } else if (filter == "notsubmitted") {
210                     if (user.submitted == "1") {
211                         show = false;
212                     }
213                 } else if (filter == "requiregrading") {
214                     if (user.requiregrading == "0") {
215                         show = false;
216                     }
217                 } else if (filter == "grantedextension") {
218                     if (user.grantedextension == "0") {
219                         show = false;
220                     }
221                 }
222             });
224             if (show) {
225                 this._filteredUsers[this._filteredUsers.length] = user;
226                 if (userid == user.id) {
227                     foundIndex = (this._filteredUsers.length - 1);
228                 }
229             }
230         }.bind(this));
232         if (this._filteredUsers.length) {
233             this._selectUserById(this._filteredUsers[foundIndex].id);
234         } else {
235             this._selectNoUser();
236         }
237         this._triggerNextUserEvent();
238     };
240     /**
241      * Select no users, because no users match the filters.
242      *
243      * @private
244      * @method _selectNoUser
245      */
246     GradingNavigation.prototype._selectNoUser = function() {
247         // Detect unsaved changes, and offer to save them - otherwise change user right now.
248         if (this._isLoading) {
249             return;
250         }
251         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
252             // Form has changes, so we need to confirm before switching users.
253             str.get_strings([
254                 {key: 'unsavedchanges', component: 'mod_assign'},
255                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
256                 {key: 'saveandcontinue', component: 'mod_assign'},
257                 {key: 'cancel', component: 'core'},
258             ]).done(function(strs) {
259                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
260                     $(document).trigger('save-changes', -1);
261                 });
262             });
263         } else {
264             $(document).trigger('user-changed', -1);
265         }
266     };
268     /**
269      * Select the specified user by id.
270      *
271      * @private
272      * @method _selectUserById
273      * @param {Number} userid
274      */
275     GradingNavigation.prototype._selectUserById = function(userid) {
276         var select = this._region.find('[data-action=change-user]');
277         var useridnumber = parseInt(userid, 10);
279         // Detect unsaved changes, and offer to save them - otherwise change user right now.
280         if (this._isLoading) {
281             return;
282         }
283         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
284             // Form has changes, so we need to confirm before switching users.
285             str.get_strings([
286                 {key: 'unsavedchanges', component: 'mod_assign'},
287                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
288                 {key: 'saveandcontinue', component: 'mod_assign'},
289                 {key: 'cancel', component: 'core'},
290             ]).done(function(strs) {
291                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
292                     $(document).trigger('save-changes', useridnumber);
293                 });
294             });
295         } else {
296             select.attr('data-selected', userid);
298             if (!isNaN(useridnumber) && useridnumber > 0) {
299                 $(document).trigger('user-changed', userid);
300             }
301         }
302     };
304     /**
305      * Expand or collapse the filter config panel.
306      *
307      * @private
308      * @method _toggleExpandFilters
309      * @param {Event} event
310      */
311     GradingNavigation.prototype._toggleExpandFilters = function(event) {
312         event.preventDefault();
313         var toggleLink = $(event.target).closest('[data-region="user-filters"]');
314         var expanded = toggleLink.attr('aria-expanded') == 'true';
315         var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
317         if (expanded) {
318             configPanel.hide();
319             configPanel.attr('aria-hidden', 'true');
320             toggleLink.attr('aria-expanded', 'false');
321             $(document).unbind('click.mod_assign_grading_navigation');
322         } else {
323             configPanel.css('display', 'inline-block');
324             configPanel.attr('aria-hidden', 'false');
325             toggleLink.attr('aria-expanded', 'true');
326             event.stopPropagation();
327             $(document).on('click.mod_assign_grading_navigation', this._checkClickOutsideConfigureFilters.bind(this));
328         }
329     };
331     /**
332      * Change to the previous user in the grading list.
333      *
334      * @private
335      * @method _handlePreviousUser
336      * @param {Event} e
337      */
338     GradingNavigation.prototype._handlePreviousUser = function(e) {
339         e.preventDefault();
340         var select = this._region.find('[data-action=change-user]');
341         var currentUserId = select.attr('data-selected');
342         var i = 0;
343         var currentIndex = 0;
345         for (i = 0; i < this._filteredUsers.length; i++) {
346             if (this._filteredUsers[i].id == currentUserId) {
347                 currentIndex = i;
348                 break;
349             }
350         }
352         var count = this._filteredUsers.length;
353         var newIndex = (currentIndex - 1);
354         if (newIndex < 0) {
355             newIndex = count - 1;
356         }
358         if (count) {
359             this._selectUserById(this._filteredUsers[newIndex].id);
360         }
361     };
363     /**
364      * Change to the next user in the grading list.
365      *
366      * @param {Event} e
367      * @param {Boolean} saved Has the form already been saved? Skips checking for changes if true.
368      */
369     GradingNavigation.prototype._handleNextUser = function(e, saved) {
370         e.preventDefault();
371         var select = this._region.find('[data-action=change-user]');
372         var currentUserId = select.attr('data-selected');
373         var i = 0;
374         var currentIndex = 0;
376         for (i = 0; i < this._filteredUsers.length; i++) {
377             if (this._filteredUsers[i].id == currentUserId) {
378                 currentIndex = i;
379                 break;
380             }
381         }
383         var count = this._filteredUsers.length;
384         var newIndex = (currentIndex + 1) % count;
386         if (saved && count) {
387             // If we've already saved the grade, skip checking if we've made any changes.
388             var userid = this._filteredUsers[newIndex].id;
389             var useridnumber = parseInt(userid, 10);
390             select.attr('data-selected', userid);
391             if (!isNaN(useridnumber) && useridnumber > 0) {
392                 $(document).trigger('user-changed', userid);
393             }
394         } else if (count) {
395             this._selectUserById(this._filteredUsers[newIndex].id);
396         }
397     };
399     /**
400      * Rebuild the x of y string.
401      *
402      * @private
403      * @method _refreshCount
404      */
405     GradingNavigation.prototype._refreshCount = function() {
406         var select = this._region.find('[data-action=change-user]');
407         var userid = select.attr('data-selected');
408         var i = 0;
409         var currentIndex = 0;
411         if (isNaN(userid) || userid <= 0) {
412             this._region.find('[data-region="user-count"]').hide();
413         } else {
414             this._region.find('[data-region="user-count"]').show();
416             for (i = 0; i < this._filteredUsers.length; i++) {
417                 if (this._filteredUsers[i].id == userid) {
418                     currentIndex = i;
419                     break;
420                 }
421             }
422             var count = this._filteredUsers.length;
423             if (count) {
424                 currentIndex += 1;
425             }
426             var param = {x: currentIndex, y: count};
428             str.get_string('xofy', 'mod_assign', param).done(function(s) {
429                 this._region.find('[data-region="user-count-summary"]').text(s);
430             }.bind(this)).fail(notification.exception);
431         }
432     };
434     /**
435      * Respond to a user-changed event by updating the selector.
436      *
437      * @private
438      * @method _refreshSelector
439      * @param {Event} event
440      * @param {String} userid
441      */
442     GradingNavigation.prototype._refreshSelector = function(event, userid) {
443         var select = this._region.find('[data-action=change-user]');
444         userid = parseInt(userid, 10);
446         if (!isNaN(userid) && userid > 0) {
447             select.attr('data-selected', userid);
448         }
449         this._refreshCount();
450     };
452     /**
453      * Trigger the next user event depending on the number of filtered users
454      *
455      * @private
456      * @method _triggerNextUserEvent
457      */
458     GradingNavigation.prototype._triggerNextUserEvent = function() {
459         if (this._filteredUsers.length > 1) {
460             $(document).trigger('next-user', {nextUserId: null, nextUser: true});
461         } else {
462             $(document).trigger('next-user', {nextUser: false});
463         }
464     };
466     /**
467      * Change to a different user in the grading list.
468      *
469      * @private
470      * @method _handleChangeUser
471      * @param {Event} event
472      */
473     GradingNavigation.prototype._handleChangeUser = function() {
474         var select = this._region.find('[data-action=change-user]');
475         var userid = parseInt(select.val(), 10);
477         if (this._isLoading) {
478             return;
479         }
480         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
481             // Form has changes, so we need to confirm before switching users.
482             str.get_strings([
483                 {key: 'unsavedchanges', component: 'mod_assign'},
484                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
485                 {key: 'saveandcontinue', component: 'mod_assign'},
486                 {key: 'cancel', component: 'core'},
487             ]).done(function(strs) {
488                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
489                     $(document).trigger('save-changes', userid);
490                 });
491             });
492         } else {
493             if (!isNaN(userid) && userid > 0) {
494                 select.attr('data-selected', userid);
496                 $(document).trigger('user-changed', userid);
497             }
498         }
499     };
501     return GradingNavigation;
502 });