MDL-47353 gradereport_grader: Only respect fixed headers
[moodle.git] / grade / report / grader / module.js
1 /**
2  * Grader report namespace
3  */
4 M.gradereport_grader = {
5     /**
6      * @namespace M.gradereport_grader
7      * @param {Object} reports A collection of classes used by the grader report module
8      */
9     classes : {},
10     /**
11      * Instantiates a new grader report
12      *
13      * @function
14      * @param {YUI} Y
15      * @param {Object} cfg A configuration object
16      * @param {Array} An array of items in the report
17      * @param {Array} An array of users on the report
18      * @param {Array} An array of feedback objects
19      * @param {Array} An array of student grades
20      */
21     init_report : function(Y, cfg, items, users, feedback, grades) {
22         // Create the actual report
23         new this.classes.report(Y, cfg, items, users, feedback, grades);
24     }
25 };
27 /**
28  * Initialises the JavaScript for the gradebook grader report
29  *
30  * The functions fall into 3 groups:
31  * M.gradereport_grader.classes.ajax Used when editing is off and fields are dynamically added and removed
32  * M.gradereport_grader.classes.existingfield Used when editing is on meaning all fields are already displayed
33  * M.gradereport_grader.classes.report Common to both of the above
34  *
35  * @class report
36  * @constructor
37  * @this {M.gradereport_grader}
38  * @param {YUI} Y
39  * @param {Object} cfg Configuration variables
40  * @param {Array} items An array containing grade items
41  * @param {Array} users An array containing user information
42  * @param {Array} feedback An array containing feedback information
43  */
44 M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) {
45     this.Y = Y;
46     this.isediting = (cfg.isediting);
47     this.ajaxenabled = (cfg.ajaxenabled);
48     this.items = items;
49     this.users = users;
50     this.feedback = feedback;
51     this.table = Y.one('#user-grades');
52     this.grades = grades;
54     // If ajax is enabled then initialise the ajax component
55     if (this.ajaxenabled) {
56         this.ajax = new M.gradereport_grader.classes.ajax(this, cfg);
57     }
58 };
59 /**
60  * Extend the report class with the following methods and properties
61  */
62 M.gradereport_grader.classes.report.prototype.table = null;           // YUI Node for the reports main table
63 M.gradereport_grader.classes.report.prototype.items = [];             // Array containing grade items
64 M.gradereport_grader.classes.report.prototype.users = [];             // Array containing user information
65 M.gradereport_grader.classes.report.prototype.feedback = [];          // Array containing feedback items
66 M.gradereport_grader.classes.report.prototype.ajaxenabled = false;    // True is AJAX is enabled for the report
67 M.gradereport_grader.classes.report.prototype.ajax = null;            // An instance of the ajax class or null
68 /**
69  * Builds an object containing information at the relevant cell given either
70  * the cell to get information for or an array containing userid and itemid
71  *
72  * @function
73  * @this {M.gradereport_grader}
74  * @param {Y.Node|Array} arg Either a YUI Node instance or an array containing
75  *                           the userid and itemid to reference
76  * @return {Object}
77  */
78 M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
80     var userid= null;
81     var itemid = null;
82     var feedback = ''; // Don't default feedback to null or string comparisons become error prone
83     var cell = null;
84     var i = null;
86     if (arg instanceof this.Y.Node) {
87         if (arg.get('nodeName').toUpperCase() !== 'TD') {
88             arg = arg.ancestor('td.cell');
89         }
90         var regexp = /^u(\d+)i(\d+)$/;
91         var parts = regexp.exec(arg.getAttribute('id'));
92         userid = parts[1];
93         itemid = parts[2];
94         cell = arg;
95     } else {
96         userid = arg[0];
97         itemid = arg[1];
98         cell = this.Y.one('#u'+userid+'i'+itemid);
99     }
101     if (!cell) {
102         return null;
103     }
105     return {
106         id : cell.getAttribute('id'),
107         userid : userid,
108         username : this.users[userid],
109         itemid : itemid,
110         itemname : this.items[itemid].name,
111         itemtype : this.items[itemid].type,
112         itemscale : this.items[itemid].scale,
113         itemdp : this.items[itemid].decimals,
114         cell : cell
115     };
116 };
117 /**
118  * Updates or creates the feedback JS structure for the given user/item
119  *
120  * @function
121  * @this {M.gradereport_grader}
122  * @param {Int} userid
123  * @param {Int} itemid
124  * @param {String} newfeedback
125  * @return {Bool}
126  */
127 M.gradereport_grader.classes.report.prototype.update_feedback = function(userid, itemid, newfeedback) {
128     for (var i in this.feedback) {
129         if (this.feedback[i].user == userid && this.feedback[i].item == itemid) {
130             this.feedback[i].content = newfeedback;
131             return true;
132         }
133     }
134     this.feedback.push({user:userid,item:itemid,content:newfeedback});
135     return true;
136 };
137 /**
138  * Initialises the AJAX component of this report
139  * @class ajax
140  * @constructor
141  * @this {M.gradereport_grader.ajax}
142  * @param {M.gradereport_grader.classes.report} report
143  * @param {Object} cfg
144  */
145 M.gradereport_grader.classes.ajax = function(report, cfg) {
146     this.report = report;
147     this.courseid = cfg.courseid || null;
148     this.feedbacktrunclength = cfg.feedbacktrunclength || null;
149     this.studentsperpage = cfg.studentsperpage || null;
150     this.showquickfeedback = cfg.showquickfeedback || false;
151     this.scales = cfg.scales || null;
152     this.existingfields = [];
154     if (!report.isediting) {
155         report.table.all('.clickable').on('click', this.make_editable, this);
156     } else {
157         for (var userid in report.users) {
158             if (!this.existingfields[userid]) {
159                 this.existingfields[userid] = [];
160             }
161             for (var itemid in report.items) {
162                 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
163             }
164         }
165         // Disable the Update button as we're saving using ajax.
166         submitbutton = this.report.Y.one('#gradersubmit');
167         submitbutton.set('disabled', true);
168     }
169 };
170 /**
171  * Extend the ajax class with the following methods and properties
172  */
173 M.gradereport_grader.classes.ajax.prototype.report = null;                  // A reference to the report class this object will use
174 M.gradereport_grader.classes.ajax.prototype.courseid = null;                // The id for the course being viewed
175 M.gradereport_grader.classes.ajax.prototype.feedbacktrunclength = null;     // The length to truncate feedback to
176 M.gradereport_grader.classes.ajax.prototype.studentsperpage = null;         // The number of students shown per page
177 M.gradereport_grader.classes.ajax.prototype.showquickfeedback = null;       // True if feedback editing should be shown
178 M.gradereport_grader.classes.ajax.prototype.current = null;                 // The field being currently editing
179 M.gradereport_grader.classes.ajax.prototype.pendingsubmissions = [];        // Array containing pending IO transactions
180 M.gradereport_grader.classes.ajax.prototype.scales = [];                    // An array of scales used in this report
181 /**
182  * Makes a cell editable
183  * @function
184  * @this {M.gradereport_grader.classes.ajax}
185  */
186 M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
187     var node = e;
188     if (e.halt) {
189         e.halt();
190         node = e.target;
191     }
192     if (node.get('nodeName').toUpperCase() !== 'TD') {
193         node = node.ancestor('td');
194     }
195     this.report.Y.detach('click', this.make_editable, node);
197     if (this.current) {
198         // Current is already set!
199         this.process_editable_field(node);
200         return;
201     }
203     // Sort out the field type
204     var fieldtype = 'text';
205     if (node.hasClass('grade_type_scale')) {
206         fieldtype = 'scale';
207     }
208     // Create the appropriate field widget
209     switch (fieldtype) {
210         case 'scale':
211             this.current = new M.gradereport_grader.classes.scalefield(this.report, node);
212             break;
213         case 'text':
214         default:
215             this.current = new M.gradereport_grader.classes.textfield(this.report, node);
216             break;
217     }
218     this.current.replace().attach_key_events();
220     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
221     Y.Global.fire('moodle-gradereport_grader:resized');
222 };
223 /**
224  * Callback function for the user pressing the enter key on an editable field
225  *
226  * @function
227  * @this {M.gradereport_grader.classes.ajax}
228  * @param {Event} e
229  */
230 M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) {
231     this.process_editable_field(null);
232 };
233 /**
234  * Callback function for the user pressing Tab or Shift+Tab
235  *
236  * @function
237  * @this {M.gradereport_grader.classes.ajax}
238  * @param {Event} e
239  * @param {Bool} ignoreshift If true and shift is pressed then don't exec
240  */
241 M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) {
242     var next = null;
243     if (e.shiftKey) {
244         if (ignoreshift) {
245             return;
246         }
247         next = this.get_above_cell();
248     } else {
249         next = this.get_below_cell();
250     }
251     this.process_editable_field(next);
252 };
253 /**
254  * Callback function for the user pressing an CTRL + an arrow key
255  *
256  * @function
257  * @this {M.gradereport_grader.classes.ajax}
258  */
259 M.gradereport_grader.classes.ajax.prototype.keypress_arrows = function(e) {
260     e.preventDefault();
261     var next = null;
262     switch (e.keyCode) {
263         case 37:    // Left
264             next = this.get_prev_cell();
265             break;
266         case 38:    // Up
267             next = this.get_above_cell();
268             break;
269         case 39:    // Right
270             next = this.get_next_cell();
271             break;
272         case 40:    // Down
273             next = this.get_below_cell();
274             break;
275     }
276     this.process_editable_field(next);
277 };
278 /**
279  * Processes an editable field an does what ever is required to update it
280  *
281  * @function
282  * @this {M.gradereport_grader.classes.ajax}
283  * @param {Y.Node|null} next The next node to make editable (chaining)
284  */
285 M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(next) {
286     if (this.current.has_changed()) {
287         var properties = this.report.get_cell_info(this.current.node);
288         var values = this.current.commit();
289         this.current.revert();
290         this.submit(properties, values);
291     } else {
292         this.current.revert();
293     }
294     this.current = null;
295     if (next) {
296         this.make_editable(next, null);
297     }
299     // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
300     Y.Global.fire('moodle-gradereport_grader:resized');
301 };
302 /**
303  * Gets the next cell that is editable (right)
304  * @function
305  * @this {M.gradereport_grader.classes.ajax}
306  * @param {Y.Node} cell
307  * @return {Y.Node}
308  */
309 M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
310     var n = cell || this.current.node;
311     var next = n.next('td');
312     var tr = null;
313     if (!next && (tr = n.ancestor('tr').next('tr'))) {
314         next = tr.all('.grade').item(0);
315     }
316     if (!next) {
317         next = this.current.node;
318     }
319     return next;
320 };
321 /**
322  * Gets the previous cell that is editable (left)
323  * @function
324  * @this {M.gradereport_grader.classes.ajax}
325  * @param {Y.Node} cell
326  * @return {Y.Node}
327  */
328 M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
329     var n = cell || this.current.node;
330     var next = n.previous('.grade');
331     var tr = null;
332     if (!next && (tr = n.ancestor('tr').previous('tr'))) {
333         var cells = tr.all('.grade');
334         next = cells.item(cells.size()-1);
335     }
336     if (!next) {
337         next = this.current.node;
338     }
339     return next;
340 };
341 /**
342  * Gets the cell above if it is editable (up)
343  * @function
344  * @this {M.gradereport_grader.classes.ajax}
345  * @param {Y.Node} cell
346  * @return {Y.Node}
347  */
348 M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
349     var n = cell || this.current.node;
350     var tr = n.ancestor('tr').previous('tr');
351     var next = null;
352     if (tr) {
353         var column = 0;
354         var ntemp = n;
355         while (ntemp = ntemp.previous('td.cell')) {
356             column++;
357         }
358         next = tr.all('td.cell').item(column);
359     }
360     if (!next) {
361         next = this.current.node;
362     }
363     return next;
364 };
365 /**
366  * Gets the cell below if it is editable (down)
367  * @function
368  * @this {M.gradereport_grader.classes.ajax}
369  * @param {Y.Node} cell
370  * @return {Y.Node}
371  */
372 M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
373     var n = cell || this.current.node;
374     var tr = n.ancestor('tr').next('tr');
375     var next = null;
376     if (tr && !tr.hasClass('avg')) {
377         var column = 0;
378         var ntemp = n;
379         while (ntemp = ntemp.previous('td.cell')) {
380             column++;
381         }
382         next = tr.all('td.cell').item(column);
383     }
384     // next will be null when we get to the bottom of a column
385     return next;
386 };
387 /**
388  * Submits changes for update
389  *
390  * @function
391  * @this {M.gradereport_grader.classes.ajax}
392  * @param {Object} properties Properties of the cell being edited
393  * @param {Object} values Object containing old + new values
394  */
395 M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) {
396     // Stop the IO queue so we can add to it
397     this.report.Y.io.queue.stop();
398     // If the grade has changed add an IO transaction to update it to the queue
399     if (values.grade !== values.oldgrade) {
400         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
401             method : 'POST',
402             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.grade+'&type='+properties.itemtype+'&sesskey='+M.cfg.sesskey,
403             on : {
404                 complete : this.submission_outcome
405             },
406             context : this,
407             arguments : {
408                 properties : properties,
409                 values : values,
410                 type : 'grade'
411             }
412         }),complete:false,outcome:null});
413     }
414     // If feedback is editable and has changed add to the IO queue for it
415     if (values.editablefeedback && values.feedback !== values.oldfeedback) {
416         this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
417             method : 'POST',
418             data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.feedback+'&type=feedback&sesskey='+M.cfg.sesskey,
419             on : {
420                 complete : this.submission_outcome
421             },
422             context : this,
423             arguments : {
424                 properties : properties,
425                 values : values,
426                 type : 'feedback'
427             }
428         }),complete:false,outcome:null});
429     }
430     // Process the IO queue
431     this.report.Y.io.queue.start();
432 };
433 /**
434  * Callback function for IO transaction completions
435  *
436  * Uses a synchronous queue to ensure we maintain some sort of order
437  *
438  * @function
439  * @this {M.gradereport_grader.classes.ajax}
440  * @param {Int} tid Transaction ID
441  * @param {Object} outcome
442  * @param {Mixed} args
443  */
444 M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, outcome, args) {
445     // Parse the response as JSON
446     try {
447         outcome = this.report.Y.JSON.parse(outcome.responseText);
448     } catch(e) {
449         var message = M.str.gradereport_grader.ajaxfailedupdate;
450         message.replace(/\[1\]/, args.type);
451         message.replace(/\[2\]/, this.report.users[args.properties.userid]);
453         this.display_submission_error(message, args.properties.cell);
454         return;
455     }
457     // Quick reference for the grader report
458     var i = null;
459     // Check the outcome
460     if (outcome.result == 'success') {
461         // Iterate through each row in the result object
462         for (i in outcome.row) {
463             if (outcome.row[i] && outcome.row[i].userid && outcome.row[i].itemid) {
464                 // alias it, we use it quite a bit
465                 var r = outcome.row[i];
466                 // Get the cell referred to by this result object
467                 var info = this.report.get_cell_info([r.userid, r.itemid]);
468                 if (!info) {
469                     continue;
470                 }
471                 // Calculate the final grade for the cell
472                 var finalgrade = '';
473                 if (!r.finalgrade) {
474                     if (this.report.isediting) {
475                         // In edit mode don't put hyphens in the grade text boxes
476                         finalgrade = '';
477                     } else {
478                         // In non-edit mode put a hyphen in the grade cell
479                         finalgrade = '-';
480                     }
481                 } else {
482                     if (r.scale) {
483                         finalgrade = this.scales[r.scale][parseFloat(r.finalgrade)-1];
484                     } else {
485                         finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp);
486                     }
487                 }
488                 if (this.report.isediting) {
489                     if (args.properties.itemtype == 'scale') {
490                         info.cell.one('#grade_'+r.userid+'_'+r.itemid).all('options').each(function(option){
491                             if (option.get('value') == finalgrade) {
492                                 option.setAttribute('selected', 'selected');
493                             } else {
494                                 option.removeAttribute('selected');
495                             }
496                         });
497                     } else {
498                         info.cell.one('#grade_'+r.userid+'_'+r.itemid).set('value', finalgrade);
499                     }
500                 } else {
501                     // If there is no currently editing field or if this cell is not being currently edited
502                     if (!this.current || info.cell.get('id') != this.current.node.get('id')) {
503                         // Update the value
504                         info.cell.one('.gradevalue').set('innerHTML',finalgrade);
505                     } else if (this.current && info.cell.get('id') == this.current.node.get('id')) {
506                         // If we are here the grade value of the cell currently being edited has changed !!!!!!!!!
507                         // If the user has not actually changed the old value yet we will automatically correct it
508                         // otherwise we will prompt the user to choose to use their value or the new value!
509                         if (!this.current.has_changed() || confirm(M.str.gradereport_grader.ajaxfieldchanged)) {
510                             this.current.set_grade(finalgrade);
511                             this.current.grade.set('value', finalgrade);
512                         }
513                     }
514                 }
515             }
516         }
517         // Flag the changed cell as overridden by ajax
518         args.properties.cell.addClass('ajaxoverridden');
519     } else {
520         var p = args.properties;
521         if (args.type == 'grade') {
522             var oldgrade = args.values.oldgrade;
523             p.cell.one('.gradevalue').set('innerHTML',oldgrade);
524         } else if (args.type == 'feedback') {
525             this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback);
526         }
527         this.display_submission_error(outcome.message, p.cell);
528     }
529     // Check if all IO transactions in the queue are complete yet
530     var allcomplete = true;
531     for (i in this.pendingsubmissions) {
532         if (this.pendingsubmissions[i]) {
533             if (this.pendingsubmissions[i].transaction.id == tid) {
534                 this.pendingsubmissions[i].complete = true;
535                 this.pendingsubmissions[i].outcome = outcome;
536                 this.report.Y.io.queue.remove(this.pendingsubmissions[i].transaction);
537             }
538             if (!this.pendingsubmissions[i].complete) {
539                 allcomplete = false;
540             }
541         }
542     }
543     if (allcomplete) {
544         this.pendingsubmissions = [];
545     }
546 };
547 /**
548  * Displays a submission error within a overlay on the cell that failed update
549  *
550  * @function
551  * @this {M.gradereport_grader.classes.ajax}
552  * @param {String} message
553  * @param {Y.Node} cell
554  */
555 M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) {
556     var erroroverlay = new this.report.Y.Overlay({
557         headerContent : '<div><strong class="error">'+M.str.gradereport_grader.ajaxerror+'</strong>  <em>'+M.str.gradereport_grader.ajaxclicktoclose+'</em></div>',
558         bodyContent : message,
559         visible : false,
560         zIndex : 3
561     });
562     erroroverlay.set('xy', [cell.getX()+10,cell.getY()+10]);
563     erroroverlay.render(this.report.table.ancestor('div'));
564     erroroverlay.show();
565     erroroverlay.get('boundingBox').on('click', function(){
566         this.get('boundingBox').setStyle('visibility', 'hidden');
567         this.hide();
568         this.destroy();
569     }, erroroverlay);
570     erroroverlay.get('boundingBox').setStyle('visibility', 'visible');
571 };
572 /**
573  * A class for existing fields
574  * This class is used only when the user is in editing mode
575  *
576  * @class existingfield
577  * @constructor
578  * @param {M.gradereport_grader.classes.report} report
579  * @param {Int} userid
580  * @param {Int} itemid
581  */
582 M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
583     this.report = ajax.report;
584     this.userid = userid;
585     this.itemid = itemid;
586     this.editfeedback = ajax.showquickfeedback;
587     this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
589     if (this.report.grades) {
590         for (var i = 0; i < this.report.grades.length; i++) {
591             if (this.report.grades[i]['user']==this.userid && this.report.grades[i]['item']==this.itemid) {
592                 this.oldgrade = this.report.grades[i]['grade'];
593             }
594         }
595     }
597     if (!this.oldgrade) {
598         // Assigning an empty string makes determining whether the grade has been changed easier
599         // This value is never sent to the server
600         this.oldgrade = '';
601     }
603     // On blur save any changes in the grade field
604     this.grade.on('blur', this.submit, this);
606     // Check if feedback is enabled
607     if (this.editfeedback) {
608         // Get the feedback fields
609         this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
611         for(var i = 0; i < this.report.feedback.length; i++) {
612             if (this.report.feedback[i]['user']==this.userid && this.report.feedback[i]['item']==this.itemid) {
613                 this.oldfeedback = this.report.feedback[i]['content'];
614             }
615         }
617         if(!this.oldfeedback) {
618             // Assigning an empty string makes determining whether the feedback has been changed easier
619             // This value is never sent to the server
620             this.oldfeedback = '';
621         }
623         // On blur save any changes in the feedback field
624         this.feedback.on('blur', this.submit, this);
626         // Override the default tab movements when moving between cells
627         this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));                // Handle Shift+Tab
628         this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));                   // Handle Tab
629         this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));                // Handle the Enter key being pressed
630         this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this)); // Handle CTRL + arrow keys
632         // Override the default tab movements for fields in the same cell
633         this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();this.grade.focus();}, this.feedback, 'press:9+shift', this));
634         this.keyevents.push(this.report.Y.on('key', function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();}, this.grade, 'press:9', this));
635     } else {
636         this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));                      // Handle Tab and Shift+Tab
637     }
638     this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));                   // Handle the Enter key being pressed
639     this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));    // Handle CTRL + arrow keys
640 };
641 /**
642  * Attach the required properties and methods to the existing field class
643  * via prototyping
644  */
645 M.gradereport_grader.classes.existingfield.prototype.userid = null;
646 M.gradereport_grader.classes.existingfield.prototype.itemid = null;
647 M.gradereport_grader.classes.existingfield.prototype.editfeedback = false;
648 M.gradereport_grader.classes.existingfield.prototype.grade = null;
649 M.gradereport_grader.classes.existingfield.prototype.oldgrade = null;
650 M.gradereport_grader.classes.existingfield.prototype.keyevents = [];
651 /**
652  * Handles saving of changed on keypress
653  *
654  * @function
655  * @this {M.gradereport_grader.classes.existingfield}
656  * @param {Event} e
657  */
658 M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) {
659     e.preventDefault();
660     this.submit();
661 };
662 /**
663  * Handles setting the correct focus if the user presses tab
664  *
665  * @function
666  * @this {M.gradereport_grader.classes.existingfield}
667  * @param {Event} e
668  * @param {Bool} ignoreshift
669  */
670 M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e, ignoreshift) {
671     e.preventDefault();
672     var next = null;
673     if (e.shiftKey) {
674         if (ignoreshift) {
675             return;
676         }
677         next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
678     } else {
679         next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
680     }
681     this.move_focus(next);
682 };
683 /**
684  * Handles setting the correct focus when the user presses CTRL+arrow keys
685  *
686  * @function
687  * @this {M.gradereport_grader.classes.existingfield}
688  * @param {Event} e
689  */
690 M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) {
691     var next = null;
692     switch (e.keyCode) {
693         case 37:    // Left
694             next = this.report.ajax.get_prev_cell(this.grade.ancestor('td'));
695             break;
696         case 38:    // Up
697             next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
698             break;
699         case 39:    // Right
700             next = this.report.ajax.get_next_cell(this.grade.ancestor('td'));
701             break;
702         case 40:    // Down
703             next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
704             break;
705     }
706     this.move_focus(next);
707 };
708 /**
709  * Move the focus to the node
710  * @function
711  * @this {M.gradereport_grader.classes.existingfield}
712  * @param {Y.Node} node
713  */
714 M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) {
715     if (node) {
716         var properties = this.report.get_cell_info(node);
717         switch(properties.itemtype) {
718             case 'scale':
719                 properties.cell.one('select.select').focus();
720                 break;
721             case 'value':
722             default:
723                 properties.cell.one('input.text').focus();
724                 break;
725         }
726     }
727 };
728 /**
729  * Checks if the values for the field have changed
730  *
731  * @function
732  * @this {M.gradereport_grader.classes.existingfield}
733  * @return {Bool}
734  */
735 M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
736     if (this.editfeedback) {
737         return (this.grade.get('value') !== this.oldgrade || this.feedback.get('value') !== this.oldfeedback);
738     }
739     return (this.grade.get('value') !== this.oldgrade);
740 };
741 /**
742  * Submits any changes and then updates the fields accordingly
743  *
744  * @function
745  * @this {M.gradereport_grader.classes.existingfield}
746  */
747 M.gradereport_grader.classes.existingfield.prototype.submit = function() {
748     if (!this.has_changed()) {
749         return;
750     }
752     var properties = this.report.get_cell_info([this.userid,this.itemid]);
753     var values = (function(f){
754         var feedback, oldfeedback = null;
755         if (f.editfeedback) {
756             feedback = f.feedback.get('value');
757             oldfeedback = f.oldfeedback;
758         }
759         return {
760             editablefeedback : f.editfeedback,
761             grade : f.grade.get('value'),
762             oldgrade : f.oldgrade,
763             feedback : feedback,
764             oldfeedback : oldfeedback
765         };
766     })(this);
768     this.oldgrade = values.grade;
769     if (values.editablefeedback && values.feedback != values.oldfeedback) {
770         this.report.update_feedback(this.userid, this.itemid, values.feedback);
771         this.oldfeedback = values.feedback;
772     }
774     this.report.ajax.submit(properties, values);
775 };
777 /**
778  * Textfield class
779  * This classes gets used in conjunction with the report running with AJAX enabled
780  * and is used to manage a cell that has a grade requiring a textfield for input
781  *
782  * @class textfield
783  * @constructor
784  * @this {M.gradereport_grader.classes.textfield}
785  * @param {M.gradereport_grader.classes.report} report
786  * @param {Y.Node} node
787  */
788 M.gradereport_grader.classes.textfield = function(report, node) {
789     this.report = report;
790     this.node = node;
791     this.gradespan = node.one('.gradevalue');
792     this.inputdiv = this.report.Y.Node.create('<div></div>');
793     this.editfeedback = this.report.ajax.showquickfeedback;
794     this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" />');
795     this.gradetype = 'value';
796     this.inputdiv.append(this.grade);
797     if (this.report.ajax.showquickfeedback) {
798         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
799         this.inputdiv.append(this.feedback);
800     }
801 };
802 /**
803  * Extend the textfield class with the following methods and properties
804  */
805 M.gradereport_grader.classes.textfield.prototype.keyevents = [];
806 M.gradereport_grader.classes.textfield.prototype.editable = false;
807 M.gradereport_grader.classes.textfield.prototype.gradetype = null;
808 M.gradereport_grader.classes.textfield.prototype.grade = null;
809 M.gradereport_grader.classes.textfield.prototype.report = null;
810 M.gradereport_grader.classes.textfield.prototype.node = null;
811 M.gradereport_grader.classes.textfield.prototype.gradespam = null;
812 M.gradereport_grader.classes.textfield.prototype.inputdiv = null;
813 M.gradereport_grader.classes.textfield.prototype.editfeedback = false;
814 /**
815  * Replaces the cell contents with the controls to enable editing
816  *
817  * @function
818  * @this {M.gradereport_grader.classes.textfield}
819  * @return {M.gradereport_grader.classes.textfield}
820  */
821 M.gradereport_grader.classes.textfield.prototype.replace = function() {
822     this.set_grade(this.get_grade());
823     if (this.editfeedback) {
824         this.set_feedback(this.get_feedback());
825     }
826     this.node.replaceChild(this.inputdiv, this.gradespan);
827     this.grade.focus();
828     this.editable = true;
829     return this;
830 };
831 /**
832  * Commits the changes within a cell and returns a result object of new + old values
833  * @function
834  * @this {M.gradereport_grader.classes.textfield}
835  * @return {Object}
836  */
837 M.gradereport_grader.classes.textfield.prototype.commit = function() {
838     // Produce an anonymous result object contianing all values
839     var result = (function(field){
840         field.editable = false;
841         var oldgrade = field.get_grade();
842         if (oldgrade == '-') {
843             oldgrade = '';
844         }
845         var feedback = null;
846         var oldfeedback = null;
847         if (field.editfeedback) {
848             oldfeedback = field.get_feedback();
849         }
850         field.editable = true;
851         if (field.editfeedback) {
852             feedback = field.get_feedback();
853         }
854         return {
855             gradetype : field.gradetype,
856             editablefeedback : field.editfeedback,
857             grade : field.get_grade(),
858             oldgrade : oldgrade,
859             feedback : feedback,
860             oldfeedback : oldfeedback
861         };
862     })(this);
863     // Set the changes in stone
864     this.set_grade(result.grade);
865     if (this.editfeedback) {
866         this.set_feedback(result.feedback);
867     }
868     // Return the result object
869     return result;
870 };
871 /**
872  * Reverts a cell back to its static contents
873  * @function
874  * @this {M.gradereport_grader.classes.textfield}
875  */
876 M.gradereport_grader.classes.textfield.prototype.revert = function() {
877     this.node.replaceChild(this.gradespan, this.inputdiv);
878     for (var i in this.keyevents) {
879         if (this.keyevents[i]) {
880             this.keyevents[i].detach();
881         }
882     }
883     this.keyevents = [];
884     this.node.on('click', this.report.ajax.make_editable, this.report.ajax);
885 };
886 /**
887  * Gets the grade for current cell
888  *
889  * @function
890  * @this {M.gradereport_grader.classes.textfield}
891  * @return {Mixed}
892  */
893 M.gradereport_grader.classes.textfield.prototype.get_grade = function() {
894     if (this.editable) {
895         return this.grade.get('value');
896     }
897     return this.gradespan.get('innerHTML');
898 };
899 /**
900  * Sets the grade for the current cell
901  * @function
902  * @this {M.gradereport_grader.classes.textfield}
903  * @param {Mixed} value
904  */
905 M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) {
906     if (!this.editable) {
907         if (value == '-') {
908             value = '';
909         }
910         this.grade.set('value', value);
911     } else {
912         if (value == '') {
913             value = '-';
914         }
915         this.gradespan.set('innerHTML', value);
916     }
917 };
918 /**
919  * Gets the feedback for the current cell
920  * @function
921  * @this {M.gradereport_grader.classes.textfield}
922  * @return {String}
923  */
924 M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
925     if (this.editable) {
926         return this.feedback.get('value');
927     }
928     var properties = this.report.get_cell_info(this.node);
929     if (properties) {
930         return properties.feedback || '';
931     }
932     return '';
933 };
934 /**
935  * Sets the feedback for the current cell
936  * @function
937  * @this {M.gradereport_grader.classes.textfield}
938  * @param {Mixed} value
939  */
940 M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) {
941     if (!this.editable) {
942         this.feedback.set('value', value);
943     } else {
944         var properties = this.report.get_cell_info(this.node);
945         this.report.update_feedback(properties.userid, properties.itemid, value);
946     }
947 };
948 /**
949  * Checks if the current cell has changed at all
950  * @function
951  * @this {M.gradereport_grader.classes.textfield}
952  * @return {Bool}
953  */
954 M.gradereport_grader.classes.textfield.prototype.has_changed = function() {
955     // If its not editable it has not changed
956     if (!this.editable) {
957         return false;
958     }
959     // If feedback is being edited then it has changed if either grade or feedback have changed
960     if (this.editfeedback) {
961         var properties = this.report.get_cell_info(this.node);
962         if (this.get_feedback() != properties.feedback) {
963             return true;
964         }
965     }
966     return (this.get_grade() != this.gradespan.get('innerHTML'));
967 };
968 /**
969  * Attaches the key listeners for the editable fields and stored the event references
970  * against the textfield
971  *
972  * @function
973  * @this {M.gradereport_grader.classes.textfield}
974  */
975 M.gradereport_grader.classes.textfield.prototype.attach_key_events = function() {
976     var a = this.report.ajax;
977     // Setup the default key events for tab and enter
978     if (this.editfeedback) {
979         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));               // Handle Shift+Tab
980         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));            // Handle Tab
981         this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));               // Handle the Enter key being pressed
982     } else {
983         this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));                     // Handle Tab and Shift+Tab
984     }
985     this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));                      // Handle the Enter key being pressed
986     // Setup the arrow key events
987     this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.grade.ancestor('td'), 'down:37,38,39,40+ctrl', a));       // Handle CTRL + arrow keys
988     // Prevent the default key action on all fields for arrow keys on all key events!
989     // Note: this still does not work in FF!!!!!
990     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
991     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
992     this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
993 };
995 /**
996  * An editable scale field
997  *
998  * @class scalefield
999  * @constructor
1000  * @inherits M.gradereport_grader.classes.textfield
1001  * @base M.gradereport_grader.classes.textfield
1002  * @this {M.gradereport_grader.classes.scalefield}
1003  * @param {M.gradereport_grader.classes.report} report
1004  * @param {Y.Node} node
1005  */
1006 M.gradereport_grader.classes.scalefield = function(report, node) {
1007     this.report = report;
1008     this.node = node;
1009     this.gradespan = node.one('.gradevalue');
1010     this.inputdiv = this.report.Y.Node.create('<div></div>');
1011     this.editfeedback = this.report.ajax.showquickfeedback;
1012     this.grade = this.report.Y.Node.create('<select type="text" class="text" /><option value="-1">'+M.str.gradereport_grader.ajaxchoosescale+'</option></select>');
1013     this.gradetype = 'scale';
1014     this.inputdiv.append(this.grade);
1015     if (this.editfeedback) {
1016         this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" />');
1017         this.inputdiv.append(this.feedback);
1018     }
1019     var properties = this.report.get_cell_info(node);
1020     this.scale = this.report.ajax.scales[properties.itemscale];
1021     for (var i in this.scale) {
1022         if (this.scale[i]) {
1023             this.grade.append(this.report.Y.Node.create('<option value="'+(parseFloat(i)+1)+'">'+this.scale[i]+'</option>'));
1024         }
1025     }
1026 };
1027 /**
1028  * Override + extend the scalefield class with the following properties
1029  * and methods
1030  */
1031 /**
1032  * @property {Array} scale
1033  */
1034 M.gradereport_grader.classes.scalefield.prototype.scale = [];
1035 /**
1036  * Extend the scalefield with the functions from the textfield
1037  */
1038 /**
1039  * Overrides the get_grade function so that it can pick up the value from the
1040  * scales select box
1041  *
1042  * @function
1043  * @this {M.gradereport_grader.classes.scalefield}
1044  * @return {Int} the scale id
1045  */
1046 M.gradereport_grader.classes.scalefield.prototype.get_grade = function(){
1047     if (this.editable) {
1048         // Return the scale value
1049         return this.grade.all('option').item(this.grade.get('selectedIndex')).get('value');
1050     } else {
1051         // Return the scale values id
1052         var value = this.gradespan.get('innerHTML');
1053         for (var i in this.scale) {
1054             if (this.scale[i] == value) {
1055                 return parseFloat(i)+1;
1056             }
1057         }
1058         return -1;
1059     }
1060 };
1061 /**
1062  * Overrides the set_grade function of textfield so that it can set the scale
1063  * within the scale select box
1064  *
1065  * @function
1066  * @this {M.gradereport_grader.classes.scalefield}
1067  * @param {String} value
1068  */
1069 M.gradereport_grader.classes.scalefield.prototype.set_grade = function(value) {
1070     if (!this.editable) {
1071         if (value == '-') {
1072             value = '-1';
1073         }
1074         this.grade.all('option').each(function(node){
1075             if (node.get('value') == value) {
1076                 node.set('selected', true);
1077             }
1078         });
1079     } else {
1080         if (value == '' || value == '-1') {
1081             value = '-';
1082         } else {
1083             value = this.scale[parseFloat(value)-1];
1084         }
1085         this.gradespan.set('innerHTML', value);
1086     }
1087 };
1088 /**
1089  * Checks if the current cell has changed at all
1090  * @function
1091  * @this {M.gradereport_grader.classes.scalefield}
1092  * @return {Bool}
1093  */
1094 M.gradereport_grader.classes.scalefield.prototype.has_changed = function() {
1095     if (!this.editable) {
1096         return false;
1097     }
1098     var gradef = this.get_grade();
1099     this.editable = false;
1100     var gradec = this.get_grade();
1101     this.editable = true;
1102     if (this.editfeedback) {
1103         var properties = this.report.get_cell_info(this.node);
1104         var feedback = properties.feedback || '';
1105         return (gradef != gradec || this.get_feedback() != feedback);
1106     }
1107     return (gradef != gradec);
1108 };
1110 /**
1111  * Manually extend the scalefield class with the properties and methods of the
1112  * textfield class that have not been defined
1113  */
1114 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1115     if (!M.gradereport_grader.classes.scalefield.prototype[i]) {
1116         M.gradereport_grader.classes.scalefield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
1117     }