f5a373c51956ba4bb7b67eda294646060e9b6344
[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     };
135     /**
136      * Close the configure filters panel if a click is detected outside of it.
137      *
138      * @private
139      * @method _checkClickOutsideConfigureFilters
140      * @param {Event} event
141      */
142     GradingNavigation.prototype._checkClickOutsideConfigureFilters = function(event) {
143         var configPanel = this._region.find('[data-region="configure-filters"]');
145         if (!configPanel.is(event.target) && configPanel.has(event.target).length === 0) {
146             var toggleLink = this._region.find('[data-region="user-filters"]');
148             configPanel.hide();
149             configPanel.attr('aria-hidden', 'true');
150             toggleLink.attr('aria-expanded', 'false');
151             $(document).unbind('click.mod_assign_grading_navigation');
152         }
153     };
155     /**
156      * Turn a filter on or off.
157      *
158      * @private
159      * @method _filterChanged
160      * @param {Event} event
161      */
162     GradingNavigation.prototype._filterChanged = function(event) {
163         var name = $(event.target).attr('name');
164         var key = name.split('_').pop();
165         var enabled = $(event.target).prop('checked');
167         if (enabled) {
168             if (this._filters.indexOf(key) == -1) {
169                 this._filters[this._filters.length] = key;
170             }
171         } else {
172             var index = this._filters.indexOf(key);
173             if (index != -1) {
174                 this._filters.splice(index, 1);
175             }
176         }
178         // Update the active filter string.
179         var filterlist = [];
180         this._region.find('[data-region="configure-filters"]').find('[type="checkbox"]').each(function(idx, ele) {
181             if ($(ele).prop('checked')) {
182                 filterlist[filterlist.length] = $(ele).closest('label').text();
183             }
184         });
185         if (filterlist.length) {
186             this._region.find('[data-region="user-filters"] span').text(filterlist.join(', '));
187         } else {
188             str.get_string('nofilters', 'mod_assign').done(function(s) {
189                 this._region.find('[data-region="user-filters"] span').text(s);
190             }.bind(this)).fail(notification.exception);
191         }
193         // Filter the options in the select box that do not match the current filters.
195         var select = this._region.find('[data-action=change-user]');
196         var userid = select.attr('data-selected');
197         var foundIndex = 0;
199         this._filteredUsers = [];
201         $.each(this._users, function(index, user) {
202             var show = true;
203             $.each(this._filters, function(filterindex, filter) {
204                 if (filter == "submitted") {
205                     if (user.submitted == "0") {
206                         show = false;
207                     }
208                 } else if (filter == "notsubmitted") {
209                     if (user.submitted == "1") {
210                         show = false;
211                     }
212                 } else if (filter == "requiregrading") {
213                     if (user.requiregrading == "0") {
214                         show = false;
215                     }
216                 } else if (filter == "grantedextension") {
217                     if (user.grantedextension == "0") {
218                         show = false;
219                     }
220                 }
221             });
223             if (show) {
224                 this._filteredUsers[this._filteredUsers.length] = user;
225                 if (userid == user.id) {
226                     foundIndex = (this._filteredUsers.length - 1);
227                 }
228             }
229         }.bind(this));
231         if (this._filteredUsers.length) {
232             this._selectUserById(this._filteredUsers[foundIndex].id);
233         } else {
234             this._selectNoUser();
235         }
236     };
238     /**
239      * Select no users, because no users match the filters.
240      *
241      * @private
242      * @method _selectNoUser
243      */
244     GradingNavigation.prototype._selectNoUser = function() {
245         // Detect unsaved changes, and offer to save them - otherwise change user right now.
246         if (this._isLoading) {
247             return;
248         }
249         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
250             // Form has changes, so we need to confirm before switching users.
251             str.get_strings([
252                 {key: 'unsavedchanges', component: 'mod_assign'},
253                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
254                 {key: 'saveandcontinue', component: 'mod_assign'},
255                 {key: 'cancel', component: 'core'},
256             ]).done(function(strs) {
257                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
258                     $(document).trigger('save-changes', -1);
259                 });
260             });
261         } else {
262             $(document).trigger('user-changed', -1);
263         }
264     };
266     /**
267      * Select the specified user by id.
268      *
269      * @private
270      * @method _selectUserById
271      * @param {Number} userid
272      */
273     GradingNavigation.prototype._selectUserById = function(userid) {
274         var select = this._region.find('[data-action=change-user]');
275         var useridnumber = parseInt(userid, 10);
277         // Detect unsaved changes, and offer to save them - otherwise change user right now.
278         if (this._isLoading) {
279             return;
280         }
281         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
282             // Form has changes, so we need to confirm before switching users.
283             str.get_strings([
284                 {key: 'unsavedchanges', component: 'mod_assign'},
285                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
286                 {key: 'saveandcontinue', component: 'mod_assign'},
287                 {key: 'cancel', component: 'core'},
288             ]).done(function(strs) {
289                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
290                     $(document).trigger('save-changes', useridnumber);
291                 });
292             });
293         } else {
294             select.attr('data-selected', userid);
296             if (!isNaN(useridnumber) && useridnumber > 0) {
297                 $(document).trigger('user-changed', userid);
298             }
299         }
300     };
302     /**
303      * Expand or collapse the filter config panel.
304      *
305      * @private
306      * @method _toggleExpandFilters
307      * @param {Event} event
308      */
309     GradingNavigation.prototype._toggleExpandFilters = function(event) {
310         event.preventDefault();
311         var toggleLink = $(event.target).closest('[data-region="user-filters"]');
312         var expanded = toggleLink.attr('aria-expanded') == 'true';
313         var configPanel = $(document.getElementById(toggleLink.attr('aria-controls')));
315         if (expanded) {
316             configPanel.hide();
317             configPanel.attr('aria-hidden', 'true');
318             toggleLink.attr('aria-expanded', 'false');
319             $(document).unbind('click.mod_assign_grading_navigation');
320         } else {
321             configPanel.css('display', 'inline-block');
322             configPanel.attr('aria-hidden', 'false');
323             toggleLink.attr('aria-expanded', 'true');
324             event.stopPropagation();
325             $(document).on('click.mod_assign_grading_navigation', this._checkClickOutsideConfigureFilters.bind(this));
326         }
327     };
329     /**
330      * Change to the previous user in the grading list.
331      *
332      * @private
333      * @method _handlePreviousUser
334      * @param {Event} e
335      */
336     GradingNavigation.prototype._handlePreviousUser = function(e) {
337         e.preventDefault();
338         var select = this._region.find('[data-action=change-user]');
339         var currentUserId = select.attr('data-selected');
340         var i = 0;
341         var currentIndex = 0;
343         for (i = 0; i < this._filteredUsers.length; i++) {
344             if (this._filteredUsers[i].id == currentUserId) {
345                 currentIndex = i;
346                 break;
347             }
348         }
350         var count = this._filteredUsers.length;
351         var newIndex = (currentIndex - 1);
352         if (newIndex < 0) {
353             newIndex = count - 1;
354         }
356         if (count) {
357             this._selectUserById(this._filteredUsers[newIndex].id);
358         }
359     };
361     /**
362      * Change to the next user in the grading list.
363      *
364      * @param {Event} e
365      * @param {Boolean} saved Has the form already been saved? Skips checking for changes if true.
366      */
367     GradingNavigation.prototype._handleNextUser = function(e, saved) {
368         e.preventDefault();
369         var select = this._region.find('[data-action=change-user]');
370         var currentUserId = select.attr('data-selected');
371         var i = 0;
372         var currentIndex = 0;
374         for (i = 0; i < this._filteredUsers.length; i++) {
375             if (this._filteredUsers[i].id == currentUserId) {
376                 currentIndex = i;
377                 break;
378             }
379         }
381         var count = this._filteredUsers.length;
382         var newIndex = (currentIndex + 1) % count;
384         if (saved && count) {
385             // If we've already saved the grade, skip checking if we've made any changes.
386             var userid = this._filteredUsers[newIndex].id;
387             var useridnumber = parseInt(userid, 10);
388             select.attr('data-selected', userid);
389             if (!isNaN(useridnumber) && useridnumber > 0) {
390                 $(document).trigger('user-changed', userid);
391             }
392         } else if (count) {
393             this._selectUserById(this._filteredUsers[newIndex].id);
394         }
395     };
397     /**
398      * Rebuild the x of y string.
399      *
400      * @private
401      * @method _refreshCount
402      */
403     GradingNavigation.prototype._refreshCount = function() {
404         var select = this._region.find('[data-action=change-user]');
405         var userid = select.attr('data-selected');
406         var i = 0;
407         var currentIndex = 0;
409         if (isNaN(userid) || userid <= 0) {
410             this._region.find('[data-region="user-count"]').hide();
411         } else {
412             this._region.find('[data-region="user-count"]').show();
414             for (i = 0; i < this._filteredUsers.length; i++) {
415                 if (this._filteredUsers[i].id == userid) {
416                     currentIndex = i;
417                     break;
418                 }
419             }
420             var count = this._filteredUsers.length;
421             if (count) {
422                 currentIndex += 1;
423             }
424             var param = {x: currentIndex, y: count};
426             str.get_string('xofy', 'mod_assign', param).done(function(s) {
427                 this._region.find('[data-region="user-count-summary"]').text(s);
428             }.bind(this)).fail(notification.exception);
429         }
430     };
432     /**
433      * Respond to a user-changed event by updating the selector.
434      *
435      * @private
436      * @method _refreshSelector
437      * @param {Event} event
438      * @param {String} userid
439      */
440     GradingNavigation.prototype._refreshSelector = function(event, userid) {
441         var select = this._region.find('[data-action=change-user]');
442         userid = parseInt(userid, 10);
444         if (!isNaN(userid) && userid > 0) {
445             select.attr('data-selected', userid);
446         }
447         this._refreshCount();
448     };
450     /**
451      * Change to a different user in the grading list.
452      *
453      * @private
454      * @method _handleChangeUser
455      * @param {Event} event
456      */
457     GradingNavigation.prototype._handleChangeUser = function() {
458         var select = this._region.find('[data-action=change-user]');
459         var userid = parseInt(select.val(), 10);
461         if (this._isLoading) {
462             return;
463         }
464         if (checker.checkFormForChanges('[data-region="grade-panel"] .gradeform')) {
465             // Form has changes, so we need to confirm before switching users.
466             str.get_strings([
467                 {key: 'unsavedchanges', component: 'mod_assign'},
468                 {key: 'unsavedchangesquestion', component: 'mod_assign'},
469                 {key: 'saveandcontinue', component: 'mod_assign'},
470                 {key: 'cancel', component: 'core'},
471             ]).done(function(strs) {
472                 notification.confirm(strs[0], strs[1], strs[2], strs[3], function() {
473                     $(document).trigger('save-changes', userid);
474                 });
475             });
476         } else {
477             if (!isNaN(userid) && userid > 0) {
478                 select.attr('data-selected', userid);
480                 $(document).trigger('user-changed', userid);
481             }
482         }
483     };
485     return GradingNavigation;
486 });