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