2 * Grader report namespace
4 M.gradereport_grader = {
6 * @namespace M.gradereport_grader
7 * @param {Object} reports A collection of classes used by the grader report module
11 * Instantiates a new grader report
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
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);
28 * Initialises the JavaScript for the gradebook grader report
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
37 * @this {M.gradereport_grader}
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
44 M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) {
46 this.isediting = (cfg.isediting);
47 this.ajaxenabled = (cfg.ajaxenabled);
50 this.feedback = feedback;
51 this.table = Y.one('#user-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);
60 * Extend the report class with the following methods and properties
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
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
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
78 M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) {
82 var feedback = ''; // Don't default feedback to null or string comparisons become error prone
86 if (arg instanceof this.Y.Node) {
87 if (arg.get('nodeName').toUpperCase() !== 'TD') {
88 arg = arg.ancestor('td.cell');
90 var regexp = /^u(\d+)i(\d+)$/;
91 var parts = regexp.exec(arg.getAttribute('id'));
98 cell = this.Y.one('#u'+userid+'i'+itemid);
105 for (i in this.feedback) {
106 if (this.feedback[i] && this.feedback[i].user == userid && this.feedback[i].item == itemid) {
107 feedback = this.feedback[i].content;
113 id : cell.getAttribute('id'),
115 username : this.users[userid],
117 itemname : this.items[itemid].name,
118 itemtype : this.items[itemid].type,
119 itemscale : this.items[itemid].scale,
120 itemdp : this.items[itemid].decimals,
126 * Updates or creates the feedback JS structure for the given user/item
129 * @this {M.gradereport_grader}
130 * @param {Int} userid
131 * @param {Int} itemid
132 * @param {String} newfeedback
135 M.gradereport_grader.classes.report.prototype.update_feedback = function(userid, itemid, newfeedback) {
136 for (var i in this.feedback) {
137 if (this.feedback[i].user == userid && this.feedback[i].item == itemid) {
138 this.feedback[i].content = newfeedback;
142 this.feedback.push({user:userid,item:itemid,content:newfeedback});
146 * Initialises the AJAX component of this report
149 * @this {M.gradereport_grader.ajax}
150 * @param {M.gradereport_grader.classes.report} report
151 * @param {Object} cfg
153 M.gradereport_grader.classes.ajax = function(report, cfg) {
154 this.report = report;
155 this.courseid = cfg.courseid || null;
156 this.feedbacktrunclength = cfg.feedbacktrunclength || null;
157 this.studentsperpage = cfg.studentsperpage || null;
158 this.showquickfeedback = cfg.showquickfeedback || false;
159 this.scales = cfg.scales || null;
160 this.existingfields = [];
162 if (!report.isediting) {
163 report.table.all('.clickable').on('click', this.make_editable, this);
165 for (var userid in report.users) {
166 if (!this.existingfields[userid]) {
167 this.existingfields[userid] = [];
169 for (var itemid in report.items) {
170 this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid);
173 // Disable the Update button as we're saving using ajax.
174 submitbutton = this.report.Y.one('#gradersubmit');
175 submitbutton.set('disabled', true);
179 * Extend the ajax class with the following methods and properties
181 M.gradereport_grader.classes.ajax.prototype.report = null; // A reference to the report class this object will use
182 M.gradereport_grader.classes.ajax.prototype.courseid = null; // The id for the course being viewed
183 M.gradereport_grader.classes.ajax.prototype.feedbacktrunclength = null; // The length to truncate feedback to
184 M.gradereport_grader.classes.ajax.prototype.studentsperpage = null; // The number of students shown per page
185 M.gradereport_grader.classes.ajax.prototype.showquickfeedback = null; // True if feedback editing should be shown
186 M.gradereport_grader.classes.ajax.prototype.current = null; // The field being currently editing
187 M.gradereport_grader.classes.ajax.prototype.pendingsubmissions = []; // Array containing pending IO transactions
188 M.gradereport_grader.classes.ajax.prototype.scales = []; // An array of scales used in this report
190 * Makes a cell editable
192 * @this {M.gradereport_grader.classes.ajax}
194 M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) {
200 if (node.get('nodeName').toUpperCase() !== 'TD') {
201 node = node.ancestor('td');
203 this.report.Y.detach('click', this.make_editable, node);
206 // Current is already set!
207 this.process_editable_field(node);
211 // Sort out the field type
212 var fieldtype = 'value';
213 if (node.hasClass('grade_type_scale')) {
215 } else if (node.hasClass('grade_type_text')) {
218 // Create the appropriate field widget
221 this.current = new M.gradereport_grader.classes.scalefield(this.report, node);
224 this.current = new M.gradereport_grader.classes.feedbackfield(this.report, node);
227 this.current = new M.gradereport_grader.classes.textfield(this.report, node);
230 this.current.replace().attach_key_events();
232 // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
233 Y.Global.fire('moodle-gradereport_grader:resized');
236 * Callback function for the user pressing the enter key on an editable field
239 * @this {M.gradereport_grader.classes.ajax}
242 M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) {
243 this.process_editable_field(null);
246 * Callback function for the user pressing Tab or Shift+Tab
249 * @this {M.gradereport_grader.classes.ajax}
251 * @param {Bool} ignoreshift If true and shift is pressed then don't exec
253 M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) {
259 next = this.get_above_cell();
261 next = this.get_below_cell();
263 this.process_editable_field(next);
266 * Callback function for the user pressing an CTRL + an arrow key
269 * @this {M.gradereport_grader.classes.ajax}
271 M.gradereport_grader.classes.ajax.prototype.keypress_arrows = function(e) {
276 next = this.get_prev_cell();
279 next = this.get_above_cell();
282 next = this.get_next_cell();
285 next = this.get_below_cell();
288 this.process_editable_field(next);
291 * Processes an editable field an does what ever is required to update it
294 * @this {M.gradereport_grader.classes.ajax}
295 * @param {Y.Node|null} next The next node to make editable (chaining)
297 M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(next) {
298 if (this.current.has_changed()) {
299 var properties = this.report.get_cell_info(this.current.node);
300 var values = this.current.commit();
301 this.current.revert();
302 this.submit(properties, values);
304 this.current.revert();
308 this.make_editable(next, null);
311 // Fire the global resized event for the gradereport_grader to update the table row/column sizes.
312 Y.Global.fire('moodle-gradereport_grader:resized');
315 * Gets the next cell that is editable (right)
317 * @this {M.gradereport_grader.classes.ajax}
318 * @param {Y.Node} cell
321 M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) {
322 var n = cell || this.current.node;
323 var next = n.next('td');
325 if (!next && (tr = n.ancestor('tr').next('tr'))) {
326 next = tr.all('.grade').item(0);
329 return this.current.node;
331 // Continue on until we find a clickable cell
332 if (!next.hasClass('clickable')) {
333 return this.get_next_cell(next);
338 * Gets the previous cell that is editable (left)
340 * @this {M.gradereport_grader.classes.ajax}
341 * @param {Y.Node} cell
344 M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) {
345 var n = cell || this.current.node;
346 var next = n.previous('.grade');
348 if (!next && (tr = n.ancestor('tr').previous('tr'))) {
349 var cells = tr.all('.grade');
350 next = cells.item(cells.size()-1);
353 return this.current.node;
355 // Continue on until we find a clickable cell
356 if (!next.hasClass('clickable')) {
357 return this.get_prev_cell(next);
362 * Gets the cell above if it is editable (up)
364 * @this {M.gradereport_grader.classes.ajax}
365 * @param {Y.Node} cell
368 M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) {
369 var n = cell || this.current.node;
370 var tr = n.ancestor('tr').previous('tr');
375 while (ntemp = ntemp.previous('td.cell')) {
378 next = tr.all('td.cell').item(column);
381 return this.current.node;
383 // Continue on until we find a clickable cell
384 if (!next.hasClass('clickable')) {
385 return this.get_above_cell(next);
390 * Gets the cell below if it is editable (down)
392 * @this {M.gradereport_grader.classes.ajax}
393 * @param {Y.Node} cell
396 M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) {
397 var n = cell || this.current.node;
398 var tr = n.ancestor('tr').next('tr');
400 if (tr && !tr.hasClass('avg')) {
403 while (ntemp = ntemp.previous('td.cell')) {
406 next = tr.all('td.cell').item(column);
409 return this.current.node;
411 // Continue on until we find a clickable cell
412 if (!next.hasClass('clickable')) {
413 return this.get_below_cell(next);
418 * Submits changes for update
421 * @this {M.gradereport_grader.classes.ajax}
422 * @param {Object} properties Properties of the cell being edited
423 * @param {Object} values Object containing old + new values
425 M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) {
426 // Stop the IO queue so we can add to it
427 this.report.Y.io.queue.stop();
428 // If the grade has changed add an IO transaction to update it to the queue
429 if (values.grade !== values.oldgrade) {
430 this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
432 data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.grade+'&type='+properties.itemtype+'&sesskey='+M.cfg.sesskey,
434 complete : this.submission_outcome
438 properties : properties,
442 }),complete:false,outcome:null});
444 // If feedback is editable and has changed add to the IO queue for it
445 if (values.editablefeedback && values.feedback !== values.oldfeedback) {
446 this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', {
448 data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.feedback+'&type=feedback&sesskey='+M.cfg.sesskey,
450 complete : this.submission_outcome
454 properties : properties,
458 }),complete:false,outcome:null});
460 // Process the IO queue
461 this.report.Y.io.queue.start();
464 * Callback function for IO transaction completions
466 * Uses a synchronous queue to ensure we maintain some sort of order
469 * @this {M.gradereport_grader.classes.ajax}
470 * @param {Int} tid Transaction ID
471 * @param {Object} outcome
472 * @param {Mixed} args
474 M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, outcome, args) {
475 // Parse the response as JSON
477 outcome = this.report.Y.JSON.parse(outcome.responseText);
479 var message = M.util.get_string('ajaxfailedupdate', 'gradereport_grader');
480 message = message.replace(/\[1\]/, args.type);
481 message = message.replace(/\[2\]/, this.report.users[args.properties.userid]);
483 this.display_submission_error(message, args.properties.cell);
487 // Quick reference for the grader report
490 if (outcome.result == 'success') {
491 // Iterate through each row in the result object
492 for (i in outcome.row) {
493 if (outcome.row[i] && outcome.row[i].userid && outcome.row[i].itemid) {
494 // alias it, we use it quite a bit
495 var r = outcome.row[i];
496 // Get the cell referred to by this result object
497 var info = this.report.get_cell_info([r.userid, r.itemid]);
501 // Calculate the final grade for the cell
505 if (this.report.isediting) {
506 // In edit mode don't put hyphens in the grade text boxes
509 // In non-edit mode put a hyphen in the grade cell
514 scalegrade = parseFloat(r.finalgrade);
515 finalgrade = this.scales[r.scale][scalegrade-1];
517 finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp);
520 if (this.report.isediting) {
521 var grade = info.cell.one('#grade_'+r.userid+'_'+r.itemid);
523 // This means the item has a input element to update.
524 var parent = grade.ancestor('td');
525 if (parent.hasClass('grade_type_scale')) {
526 grade.all('option').each(function(option) {
527 if (option.get('value') == scalegrade) {
528 option.setAttribute('selected', 'selected');
530 option.removeAttribute('selected');
534 grade.set('value', finalgrade);
536 } else if (info.cell.one('.gradevalue')) {
537 // This means we are updating a value for something without editing boxed (locked, etc).
538 info.cell.one('.gradevalue').set('innerHTML', finalgrade);
541 // If there is no currently editing field or if this cell is not being currently edited
542 if (!this.current || info.cell.get('id') != this.current.node.get('id')) {
544 var node = info.cell.one('.gradevalue');
545 var td = node.ancestor('td');
546 // Only scale and value type grades should have their content updated in this way.
547 if (td.hasClass('grade_type_value') || td.hasClass('grade_type_scale')) {
548 node.set('innerHTML', finalgrade);
550 } else if (this.current && info.cell.get('id') == this.current.node.get('id')) {
551 // If we are here the grade value of the cell currently being edited has changed !!!!!!!!!
552 // If the user has not actually changed the old value yet we will automatically correct it
553 // otherwise we will prompt the user to choose to use their value or the new value!
554 if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) {
555 this.current.set_grade(finalgrade);
556 if (this.current.grade) {
557 this.current.grade.set('value', finalgrade);
564 // Flag the changed cell as overridden by ajax
565 args.properties.cell.addClass('ajaxoverridden');
567 var p = args.properties;
568 if (args.type == 'grade') {
569 var oldgrade = args.values.oldgrade;
570 p.cell.one('.gradevalue').set('innerHTML',oldgrade);
571 } else if (args.type == 'feedback') {
572 this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback);
574 this.display_submission_error(outcome.message, p.cell);
576 // Check if all IO transactions in the queue are complete yet
577 var allcomplete = true;
578 for (i in this.pendingsubmissions) {
579 if (this.pendingsubmissions[i]) {
580 if (this.pendingsubmissions[i].transaction.id == tid) {
581 this.pendingsubmissions[i].complete = true;
582 this.pendingsubmissions[i].outcome = outcome;
583 this.report.Y.io.queue.remove(this.pendingsubmissions[i].transaction);
585 if (!this.pendingsubmissions[i].complete) {
591 this.pendingsubmissions = [];
595 * Displays a submission error within a overlay on the cell that failed update
598 * @this {M.gradereport_grader.classes.ajax}
599 * @param {String} message
600 * @param {Y.Node} cell
602 M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) {
603 var erroroverlay = new this.report.Y.Overlay({
604 headerContent : '<div><strong class="error">'+M.util.get_string('ajaxerror', 'gradereport_grader')+'</strong> <em>'+M.util.get_string('ajaxclicktoclose', 'gradereport_grader')+'</em></div>',
605 bodyContent : message,
609 erroroverlay.set('xy', [cell.getX()+10,cell.getY()+10]);
610 erroroverlay.render(this.report.table.ancestor('div'));
612 erroroverlay.get('boundingBox').on('click', function(){
613 this.get('boundingBox').setStyle('visibility', 'hidden');
617 erroroverlay.get('boundingBox').setStyle('visibility', 'visible');
620 * A class for existing fields
621 * This class is used only when the user is in editing mode
623 * @class existingfield
625 * @param {M.gradereport_grader.classes.report} report
626 * @param {Int} userid
627 * @param {Int} itemid
629 M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) {
630 this.report = ajax.report;
631 this.userid = userid;
632 this.itemid = itemid;
633 this.editfeedback = ajax.showquickfeedback;
634 this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid);
638 for (i = 0; i < this.report.grades.length; i++) {
639 if (this.report.grades[i]['user'] == this.userid && this.report.grades[i]['item'] == this.itemid) {
640 this.oldgrade = this.report.grades[i]['grade'];
644 if (!this.oldgrade) {
645 // Assigning an empty string makes determining whether the grade has been changed easier
646 // This value is never sent to the server
650 // On blur save any changes in the grade field
651 this.grade.on('blur', this.submit, this);
654 // Check if feedback is enabled
655 if (this.editfeedback) {
656 // Get the feedback fields
657 this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid);
660 for(i = 0; i < this.report.feedback.length; i++) {
661 if (this.report.feedback[i]['user'] == this.userid && this.report.feedback[i]['item'] == this.itemid) {
662 this.oldfeedback = this.report.feedback[i]['content'];
666 if(!this.oldfeedback) {
667 // Assigning an empty string makes determining whether the feedback has been changed easier
668 // This value is never sent to the server
669 this.oldfeedback = '';
672 // On blur save any changes in the feedback field
673 this.feedback.on('blur', this.submit, this);
675 // Override the default tab movements when moving between cells
677 this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true));
678 // Handle the Enter key being pressed.
679 this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this));
680 // Handle CTRL + arrow keys.
681 this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this));
684 // Override the default tab movements when moving between cells
686 this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this));
688 // Override the default tab movements for fields in the same cell
689 this.keyevents.push(this.report.Y.on('key',
690 function(e){e.preventDefault();this.grade.focus();},
694 this.keyevents.push(this.report.Y.on('key',
695 function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();},
701 } else if (this.grade) {
702 // Handle Tab and Shift+Tab.
703 this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9', this));
706 // Handle the Enter key being pressed.
707 this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'press:13', this));
708 // Handle CTRL + arrow keys.
709 this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'press:37,38,39,40+ctrl', this));
713 * Attach the required properties and methods to the existing field class
716 M.gradereport_grader.classes.existingfield.prototype.userid = null;
717 M.gradereport_grader.classes.existingfield.prototype.itemid = null;
718 M.gradereport_grader.classes.existingfield.prototype.editfeedback = false;
719 M.gradereport_grader.classes.existingfield.prototype.grade = null;
720 M.gradereport_grader.classes.existingfield.prototype.oldgrade = null;
721 M.gradereport_grader.classes.existingfield.prototype.keyevents = [];
723 * Handles saving of changed on keypress
726 * @this {M.gradereport_grader.classes.existingfield}
729 M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) {
734 * Handles setting the correct focus if the user presses tab
737 * @this {M.gradereport_grader.classes.existingfield}
739 * @param {Bool} ignoreshift
741 M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e, ignoreshift) {
748 next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
750 next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
752 this.move_focus(next);
755 * Handles setting the correct focus when the user presses CTRL+arrow keys
758 * @this {M.gradereport_grader.classes.existingfield}
761 M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) {
765 next = this.report.ajax.get_prev_cell(this.grade.ancestor('td'));
768 next = this.report.ajax.get_above_cell(this.grade.ancestor('td'));
771 next = this.report.ajax.get_next_cell(this.grade.ancestor('td'));
774 next = this.report.ajax.get_below_cell(this.grade.ancestor('td'));
777 this.move_focus(next);
780 * Move the focus to the node
782 * @this {M.gradereport_grader.classes.existingfield}
783 * @param {Y.Node} node
785 M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) {
787 var properties = this.report.get_cell_info(node);
788 switch(properties.itemtype) {
790 properties.cell.one('select.select').focus();
794 properties.cell.one('input.text').focus();
800 * Checks if the values for the field have changed
803 * @this {M.gradereport_grader.classes.existingfield}
806 M.gradereport_grader.classes.existingfield.prototype.has_changed = function() {
808 if (this.grade.get('value') !== this.oldgrade) {
812 if (this.editfeedback && this.feedback) {
813 if (this.feedback.get('value') !== this.oldfeedback) {
820 * Submits any changes and then updates the fields accordingly
823 * @this {M.gradereport_grader.classes.existingfield}
825 M.gradereport_grader.classes.existingfield.prototype.submit = function() {
826 if (!this.has_changed()) {
830 var properties = this.report.get_cell_info([this.userid,this.itemid]);
831 var values = (function(f){
832 var feedback, oldfeedback, grade, oldgrade = null;
833 if (f.editfeedback && f.feedback) {
834 feedback = f.feedback.get('value');
835 oldfeedback = f.oldfeedback;
838 grade = f.grade.get('value');
839 oldgrade = f.oldgrade;
842 editablefeedback : f.editfeedback,
846 oldfeedback : oldfeedback
850 this.oldgrade = values.grade;
851 if (values.editablefeedback && values.feedback != values.oldfeedback) {
852 this.report.update_feedback(this.userid, this.itemid, values.feedback);
853 this.oldfeedback = values.feedback;
856 this.report.ajax.submit(properties, values);
861 * This classes gets used in conjunction with the report running with AJAX enabled
862 * and is used to manage a cell that has a grade requiring a textfield for input
866 * @this {M.gradereport_grader.classes.textfield}
867 * @param {M.gradereport_grader.classes.report} report
868 * @param {Y.Node} node
870 M.gradereport_grader.classes.textfield = function(report, node) {
871 this.report = report;
873 this.gradespan = node.one('.gradevalue');
874 this.inputdiv = this.report.Y.Node.create('<div></div>');
875 this.editfeedback = this.report.ajax.showquickfeedback;
876 this.grade = this.report.Y.Node.create('<input type="text" class="text" value="" name="ajaxgrade" />');
877 this.gradetype = 'value';
878 this.inputdiv.append(this.grade);
879 if (this.report.ajax.showquickfeedback) {
880 this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
881 this.inputdiv.append(this.feedback);
885 * Extend the textfield class with the following methods and properties
887 M.gradereport_grader.classes.textfield.prototype.keyevents = [];
888 M.gradereport_grader.classes.textfield.prototype.editable = false;
889 M.gradereport_grader.classes.textfield.prototype.gradetype = null;
890 M.gradereport_grader.classes.textfield.prototype.grade = null;
891 M.gradereport_grader.classes.textfield.prototype.report = null;
892 M.gradereport_grader.classes.textfield.prototype.node = null;
893 M.gradereport_grader.classes.textfield.prototype.gradespam = null;
894 M.gradereport_grader.classes.textfield.prototype.inputdiv = null;
895 M.gradereport_grader.classes.textfield.prototype.editfeedback = false;
897 * Replaces the cell contents with the controls to enable editing
900 * @this {M.gradereport_grader.classes.textfield}
901 * @return {M.gradereport_grader.classes.textfield}
903 M.gradereport_grader.classes.textfield.prototype.replace = function() {
904 this.set_grade(this.get_grade());
905 if (this.editfeedback) {
906 this.set_feedback(this.get_feedback());
908 this.node.replaceChild(this.inputdiv, this.gradespan);
911 } else if (this.feedback) {
912 this.feedback.focus();
914 this.editable = true;
918 * Commits the changes within a cell and returns a result object of new + old values
920 * @this {M.gradereport_grader.classes.textfield}
923 M.gradereport_grader.classes.textfield.prototype.commit = function() {
924 // Produce an anonymous result object contianing all values
925 var result = (function(field){
926 // Editable false lets us get the pre-update values.
927 field.editable = false;
928 var oldgrade = field.get_grade();
929 if (oldgrade == '-') {
933 var oldfeedback = null;
934 if (field.editfeedback) {
935 oldfeedback = field.get_feedback();
938 // Now back to editable gives us the values in the edit areas.
939 field.editable = true;
940 if (field.editfeedback) {
941 feedback = field.get_feedback();
944 gradetype : field.gradetype,
945 editablefeedback : field.editfeedback,
946 grade : field.get_grade(),
949 oldfeedback : oldfeedback
952 // Set the changes in stone
953 this.set_grade(result.grade);
954 if (this.editfeedback) {
955 this.set_feedback(result.feedback);
957 // Return the result object
961 * Reverts a cell back to its static contents
963 * @this {M.gradereport_grader.classes.textfield}
965 M.gradereport_grader.classes.textfield.prototype.revert = function() {
966 this.node.replaceChild(this.gradespan, this.inputdiv);
967 for (var i in this.keyevents) {
968 if (this.keyevents[i]) {
969 this.keyevents[i].detach();
973 this.node.on('click', this.report.ajax.make_editable, this.report.ajax);
976 * Gets the grade for current cell
979 * @this {M.gradereport_grader.classes.textfield}
982 M.gradereport_grader.classes.textfield.prototype.get_grade = function() {
984 return this.grade.get('value');
986 return this.gradespan.get('innerHTML');
989 * Sets the grade for the current cell
991 * @this {M.gradereport_grader.classes.textfield}
992 * @param {Mixed} value
994 M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) {
995 if (!this.editable) {
999 this.grade.set('value', value);
1004 this.gradespan.set('innerHTML', value);
1008 * Gets the feedback for the current cell
1010 * @this {M.gradereport_grader.classes.textfield}
1013 M.gradereport_grader.classes.textfield.prototype.get_feedback = function() {
1014 if (this.editable) {
1015 if (this.feedback) {
1016 return this.feedback.get('value');
1021 var properties = this.report.get_cell_info(this.node);
1023 return properties.feedback;
1028 * Sets the feedback for the current cell
1030 * @this {M.gradereport_grader.classes.textfield}
1031 * @param {Mixed} value
1033 M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) {
1034 if (!this.editable) {
1035 if (this.feedback) {
1036 this.feedback.set('value', value);
1039 var properties = this.report.get_cell_info(this.node);
1040 this.report.update_feedback(properties.userid, properties.itemid, value);
1044 * Checks if the current cell has changed at all
1046 * @this {M.gradereport_grader.classes.textfield}
1049 M.gradereport_grader.classes.textfield.prototype.has_changed = function() {
1050 // If its not editable it has not changed
1051 if (!this.editable) {
1054 // If feedback is being edited then it has changed if either grade or feedback have changed
1055 if (this.editfeedback) {
1056 var properties = this.report.get_cell_info(this.node);
1057 if (this.get_feedback() != properties.feedback) {
1063 return (this.get_grade() != this.gradespan.get('innerHTML'));
1069 * Attaches the key listeners for the editable fields and stored the event references
1070 * against the textfield
1073 * @this {M.gradereport_grader.classes.textfield}
1075 M.gradereport_grader.classes.textfield.prototype.attach_key_events = function() {
1076 var a = this.report.ajax;
1077 // Setup the default key events for tab and enter
1078 if (this.editfeedback) {
1080 // Handle Shift+Tab.
1081 this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9+shift', a));
1084 this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'press:9', a, true));
1085 // Handle the Enter key being pressed.
1086 this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'press:13', a));
1089 // Handle Tab and Shift+Tab.
1090 this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'press:9', a));
1094 // Setup the arrow key events.
1095 // Handle CTRL + arrow keys.
1096 this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.inputdiv.ancestor('td'), 'down:37,38,39,40+ctrl', a));
1099 // Handle the Enter key being pressed.
1100 this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'press:13', a));
1101 // Prevent the default key action on all fields for arrow keys on all key events!
1102 // Note: this still does not work in FF!!!!!
1103 this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl'));
1104 this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl'));
1105 this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl'));
1110 * Feedback field class
1111 * This classes gets used in conjunction with the report running with AJAX enabled
1112 * and is used to manage a cell that no editable grade, only possibly feedback
1114 * @class feedbackfield
1116 * @this {M.gradereport_grader.classes.feedbackfield}
1117 * @param {M.gradereport_grader.classes.report} report
1118 * @param {Y.Node} node
1120 M.gradereport_grader.classes.feedbackfield = function(report, node) {
1121 this.report = report;
1123 this.gradespan = node.one('.gradevalue');
1124 this.inputdiv = this.report.Y.Node.create('<div></div>');
1125 this.editfeedback = this.report.ajax.showquickfeedback;
1126 this.gradetype = 'text';
1127 if (this.report.ajax.showquickfeedback) {
1128 this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback" />');
1129 this.inputdiv.append(this.feedback);
1134 * Gets the grade for current cell (which will always be null)
1137 * @this {M.gradereport_grader.classes.feedbackfield}
1140 M.gradereport_grader.classes.feedbackfield.prototype.get_grade = function() {
1145 * Overrides the set_grade function of textfield so that it can ignore the set-grade
1146 * for grade cells without grades
1149 * @this {M.gradereport_grader.classes.feedbackfield}
1150 * @param {String} value
1152 M.gradereport_grader.classes.feedbackfield.prototype.set_grade = function() {
1157 * Manually extend the feedbackfield class with the properties and methods of the
1158 * textfield class that have not been defined
1160 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1161 if (!M.gradereport_grader.classes.feedbackfield.prototype[i]) {
1162 M.gradereport_grader.classes.feedbackfield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];
1167 * An editable scale field
1171 * @inherits M.gradereport_grader.classes.textfield
1172 * @base M.gradereport_grader.classes.textfield
1173 * @this {M.gradereport_grader.classes.scalefield}
1174 * @param {M.gradereport_grader.classes.report} report
1175 * @param {Y.Node} node
1177 M.gradereport_grader.classes.scalefield = function(report, node) {
1178 this.report = report;
1180 this.gradespan = node.one('.gradevalue');
1181 this.inputdiv = this.report.Y.Node.create('<div></div>');
1182 this.editfeedback = this.report.ajax.showquickfeedback;
1183 this.grade = this.report.Y.Node.create('<select type="text" class="text" name="ajaxgrade" /><option value="-1">'+
1184 M.util.get_string('ajaxchoosescale', 'gradereport_grader')+'</option></select>');
1185 this.gradetype = 'scale';
1186 this.inputdiv.append(this.grade);
1187 if (this.editfeedback) {
1188 this.feedback = this.report.Y.Node.create('<input type="text" class="quickfeedback" value="" name="ajaxfeedback"/>');
1189 this.inputdiv.append(this.feedback);
1191 var properties = this.report.get_cell_info(node);
1192 this.scale = this.report.ajax.scales[properties.itemscale];
1193 for (var i in this.scale) {
1194 if (this.scale[i]) {
1195 this.grade.append(this.report.Y.Node.create('<option value="'+(parseFloat(i)+1)+'">'+this.scale[i]+'</option>'));
1200 * Override + extend the scalefield class with the following properties
1204 * @property {Array} scale
1206 M.gradereport_grader.classes.scalefield.prototype.scale = [];
1208 * Extend the scalefield with the functions from the textfield
1211 * Overrides the get_grade function so that it can pick up the value from the
1215 * @this {M.gradereport_grader.classes.scalefield}
1216 * @return {Int} the scale id
1218 M.gradereport_grader.classes.scalefield.prototype.get_grade = function(){
1219 if (this.editable) {
1220 // Return the scale value
1221 return this.grade.all('option').item(this.grade.get('selectedIndex')).get('value');
1223 // Return the scale values id
1224 var value = this.gradespan.get('innerHTML');
1225 for (var i in this.scale) {
1226 if (this.scale[i] == value) {
1227 return parseFloat(i)+1;
1234 * Overrides the set_grade function of textfield so that it can set the scale
1235 * within the scale select box
1238 * @this {M.gradereport_grader.classes.scalefield}
1239 * @param {String} value
1241 M.gradereport_grader.classes.scalefield.prototype.set_grade = function(value) {
1242 if (!this.editable) {
1246 this.grade.all('option').each(function(node){
1247 if (node.get('value') == value) {
1248 node.set('selected', true);
1252 if (value == '' || value == '-1') {
1255 value = this.scale[parseFloat(value)-1];
1257 this.gradespan.set('innerHTML', value);
1261 * Checks if the current cell has changed at all
1263 * @this {M.gradereport_grader.classes.scalefield}
1266 M.gradereport_grader.classes.scalefield.prototype.has_changed = function() {
1267 if (!this.editable) {
1270 var gradef = this.get_grade();
1271 this.editable = false;
1272 var gradec = this.get_grade();
1273 this.editable = true;
1274 if (this.editfeedback) {
1275 var properties = this.report.get_cell_info(this.node);
1276 var feedback = properties.feedback;
1277 return (gradef != gradec || this.get_feedback() != feedback);
1279 return (gradef != gradec);
1283 * Manually extend the scalefield class with the properties and methods of the
1284 * textfield class that have not been defined
1286 for (var i in M.gradereport_grader.classes.textfield.prototype) {
1287 if (!M.gradereport_grader.classes.scalefield.prototype[i]) {
1288 M.gradereport_grader.classes.scalefield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i];