/** * Grader report namespace */ M.gradereport_grader = { /** * @namespace M.gradereport_grader * @param {Object} reports A collection of classes used by the grader report module */ classes : {}, /** * Instantiates a new grader report * * @function * @param {YUI} Y * @param {Object} cfg A configuration object * @param {Array} An array of items in the report * @param {Array} An array of users on the report * @param {Array} An array of feedback objects * @param {Array} An array of student grades */ init_report : function(Y, cfg, items, users, feedback, grades) { // Create the actual report new this.classes.report(Y, cfg, items, users, feedback, grades); } }; /** * Initialises the JavaScript for the gradebook grader report * * The functions fall into 3 groups: * M.gradereport_grader.classes.ajax Used when editing is off and fields are dynamically added and removed * M.gradereport_grader.classes.existingfield Used when editing is on meaning all fields are already displayed * M.gradereport_grader.classes.report Common to both of the above * * @class report * @constructor * @this {M.gradereport_grader} * @param {YUI} Y * @param {Object} cfg Configuration variables * @param {Array} items An array containing grade items * @param {Array} users An array containing user information * @param {Array} feedback An array containing feedback information */ M.gradereport_grader.classes.report = function(Y, cfg, items, users, feedback, grades) { this.Y = Y; this.isediting = (cfg.isediting); this.ajaxenabled = (cfg.ajaxenabled); this.items = items; this.users = users; this.feedback = feedback; this.table = Y.one('#user-grades'); this.grades = grades; // If ajax is enabled then initialise the ajax component if (this.ajaxenabled) { this.ajax = new M.gradereport_grader.classes.ajax(this, cfg); } }; /** * Extend the report class with the following methods and properties */ M.gradereport_grader.classes.report.prototype.table = null; // YUI Node for the reports main table M.gradereport_grader.classes.report.prototype.items = []; // Array containing grade items M.gradereport_grader.classes.report.prototype.users = []; // Array containing user information M.gradereport_grader.classes.report.prototype.feedback = []; // Array containing feedback items M.gradereport_grader.classes.report.prototype.ajaxenabled = false; // True is AJAX is enabled for the report M.gradereport_grader.classes.report.prototype.ajax = null; // An instance of the ajax class or null /** * Builds an object containing information at the relevant cell given either * the cell to get information for or an array containing userid and itemid * * @function * @this {M.gradereport_grader} * @param {Y.Node|Array} arg Either a YUI Node instance or an array containing * the userid and itemid to reference * @return {Object} */ M.gradereport_grader.classes.report.prototype.get_cell_info = function(arg) { var userid= null; var itemid = null; var feedback = ''; // Don't default feedback to null or string comparisons become error prone var cell = null; var i = null; if (arg instanceof this.Y.Node) { if (arg.get('nodeName').toUpperCase() !== 'TD') { arg = arg.ancestor('td.cell'); } var regexp = /^u(\d+)i(\d+)$/; var parts = regexp.exec(arg.getAttribute('id')); userid = parts[1]; itemid = parts[2]; cell = arg; } else { userid = arg[0]; itemid = arg[1]; cell = this.Y.one('#u'+userid+'i'+itemid); } if (!cell) { return null; } for (i in this.feedback) { if (this.feedback[i] && this.feedback[i].user == userid && this.feedback[i].item == itemid) { feedback = this.feedback[i].content; break; } } return { id : cell.getAttribute('id'), userid : userid, username : this.users[userid], itemid : itemid, itemname : this.items[itemid].name, itemtype : this.items[itemid].type, itemscale : this.items[itemid].scale, itemdp : this.items[itemid].decimals, feedback : feedback, cell : cell }; }; /** * Updates or creates the feedback JS structure for the given user/item * * @function * @this {M.gradereport_grader} * @param {Int} userid * @param {Int} itemid * @param {String} newfeedback * @return {Bool} */ M.gradereport_grader.classes.report.prototype.update_feedback = function(userid, itemid, newfeedback) { for (var i in this.feedback) { if (this.feedback[i].user == userid && this.feedback[i].item == itemid) { this.feedback[i].content = newfeedback; return true; } } this.feedback.push({user:userid,item:itemid,content:newfeedback}); return true; }; /** * Initialises the AJAX component of this report * @class ajax * @constructor * @this {M.gradereport_grader.ajax} * @param {M.gradereport_grader.classes.report} report * @param {Object} cfg */ M.gradereport_grader.classes.ajax = function(report, cfg) { this.report = report; this.courseid = cfg.courseid || null; this.feedbacktrunclength = cfg.feedbacktrunclength || null; this.studentsperpage = cfg.studentsperpage || null; this.showquickfeedback = cfg.showquickfeedback || false; this.scales = cfg.scales || null; this.existingfields = []; if (!report.isediting) { report.table.all('.clickable').on('click', this.make_editable, this); } else { for (var userid in report.users) { if (!this.existingfields[userid]) { this.existingfields[userid] = []; } for (var itemid in report.items) { this.existingfields[userid][itemid] = new M.gradereport_grader.classes.existingfield(this, userid, itemid); } } // Disable the Update button as we're saving using ajax. submitbutton = this.report.Y.one('#gradersubmit'); submitbutton.set('disabled', true); } }; /** * Extend the ajax class with the following methods and properties */ M.gradereport_grader.classes.ajax.prototype.report = null; // A reference to the report class this object will use M.gradereport_grader.classes.ajax.prototype.courseid = null; // The id for the course being viewed M.gradereport_grader.classes.ajax.prototype.feedbacktrunclength = null; // The length to truncate feedback to M.gradereport_grader.classes.ajax.prototype.studentsperpage = null; // The number of students shown per page M.gradereport_grader.classes.ajax.prototype.showquickfeedback = null; // True if feedback editing should be shown M.gradereport_grader.classes.ajax.prototype.current = null; // The field being currently editing M.gradereport_grader.classes.ajax.prototype.pendingsubmissions = []; // Array containing pending IO transactions M.gradereport_grader.classes.ajax.prototype.scales = []; // An array of scales used in this report /** * Makes a cell editable * @function * @this {M.gradereport_grader.classes.ajax} */ M.gradereport_grader.classes.ajax.prototype.make_editable = function(e) { var node = e; if (e.halt) { e.halt(); node = e.target; } if (node.get('nodeName').toUpperCase() !== 'TD') { node = node.ancestor('td'); } this.report.Y.detach('click', this.make_editable, node); if (this.current) { // Current is already set! this.process_editable_field(node); return; } // Sort out the field type var fieldtype = 'value'; if (node.hasClass('grade_type_scale')) { fieldtype = 'scale'; } else if (node.hasClass('grade_type_text')) { fieldtype = 'text'; } // Create the appropriate field widget switch (fieldtype) { case 'scale': this.current = new M.gradereport_grader.classes.scalefield(this.report, node); break; case 'text': this.current = new M.gradereport_grader.classes.feedbackfield(this.report, node); break; default: this.current = new M.gradereport_grader.classes.textfield(this.report, node); break; } this.current.replace().attach_key_events(); // Fire the global resized event for the gradereport_grader to update the table row/column sizes. Y.Global.fire('moodle-gradereport_grader:resized'); }; /** * Callback function for the user pressing the enter key on an editable field * * @function * @this {M.gradereport_grader.classes.ajax} * @param {Event} e */ M.gradereport_grader.classes.ajax.prototype.keypress_enter = function(e) { this.process_editable_field(null); }; /** * Callback function for the user pressing Tab or Shift+Tab * * @function * @this {M.gradereport_grader.classes.ajax} * @param {Event} e * @param {Bool} ignoreshift If true and shift is pressed then don't exec */ M.gradereport_grader.classes.ajax.prototype.keypress_tab = function(e, ignoreshift) { e.preventDefault(); var next = null; if (e.shiftKey) { if (ignoreshift) { return; } next = this.get_above_cell(); } else { next = this.get_below_cell(); } this.process_editable_field(next); }; /** * Callback function for the user pressing an CTRL + an arrow key * * @function * @this {M.gradereport_grader.classes.ajax} */ M.gradereport_grader.classes.ajax.prototype.keypress_arrows = function(e) { e.preventDefault(); var next = null; switch (e.keyCode) { case 37: // Left next = this.get_prev_cell(); break; case 38: // Up next = this.get_above_cell(); break; case 39: // Right next = this.get_next_cell(); break; case 40: // Down next = this.get_below_cell(); break; } this.process_editable_field(next); }; /** * Processes an editable field an does what ever is required to update it * * @function * @this {M.gradereport_grader.classes.ajax} * @param {Y.Node|null} next The next node to make editable (chaining) */ M.gradereport_grader.classes.ajax.prototype.process_editable_field = function(next) { if (this.current.has_changed()) { var properties = this.report.get_cell_info(this.current.node); var values = this.current.commit(); this.current.revert(); this.submit(properties, values); } else { this.current.revert(); } this.current = null; if (next) { this.make_editable(next, null); } // Fire the global resized event for the gradereport_grader to update the table row/column sizes. Y.Global.fire('moodle-gradereport_grader:resized'); }; /** * Gets the next cell that is editable (right) * @function * @this {M.gradereport_grader.classes.ajax} * @param {Y.Node} cell * @return {Y.Node} */ M.gradereport_grader.classes.ajax.prototype.get_next_cell = function(cell) { var n = cell || this.current.node; var next = n.next('td'); var tr = null; if (!next && (tr = n.ancestor('tr').next('tr'))) { next = tr.all('.grade').item(0); } if (!next) { return this.current.node; } // Continue on until we find a navigable cell if (!next.hasClass('gbnavigable')) { return this.get_next_cell(next); } return next; }; /** * Gets the previous cell that is editable (left) * @function * @this {M.gradereport_grader.classes.ajax} * @param {Y.Node} cell * @return {Y.Node} */ M.gradereport_grader.classes.ajax.prototype.get_prev_cell = function(cell) { var n = cell || this.current.node; var next = n.previous('.grade'); var tr = null; if (!next && (tr = n.ancestor('tr').previous('tr'))) { var cells = tr.all('.grade'); next = cells.item(cells.size()-1); } if (!next) { return this.current.node; } // Continue on until we find a navigable cell if (!next.hasClass('gbnavigable')) { return this.get_prev_cell(next); } return next; }; /** * Gets the cell above if it is editable (up) * @function * @this {M.gradereport_grader.classes.ajax} * @param {Y.Node} cell * @return {Y.Node} */ M.gradereport_grader.classes.ajax.prototype.get_above_cell = function(cell) { var n = cell || this.current.node; var tr = n.ancestor('tr').previous('tr'); var next = null; if (tr) { var column = 0; var ntemp = n; while (ntemp = ntemp.previous('td.cell')) { column++; } next = tr.all('td.cell').item(column); } if (!next) { return this.current.node; } // Continue on until we find a navigable cell if (!next.hasClass('gbnavigable')) { return this.get_above_cell(next); } return next; }; /** * Gets the cell below if it is editable (down) * @function * @this {M.gradereport_grader.classes.ajax} * @param {Y.Node} cell * @return {Y.Node} */ M.gradereport_grader.classes.ajax.prototype.get_below_cell = function(cell) { var n = cell || this.current.node; var tr = n.ancestor('tr').next('tr'); var next = null; if (tr && !tr.hasClass('avg')) { var column = 0; var ntemp = n; while (ntemp = ntemp.previous('td.cell')) { column++; } next = tr.all('td.cell').item(column); } if (!next) { return this.current.node; } // Continue on until we find a navigable cell if (!next.hasClass('gbnavigable')) { return this.get_below_cell(next); } return next; }; /** * Submits changes for update * * @function * @this {M.gradereport_grader.classes.ajax} * @param {Object} properties Properties of the cell being edited * @param {Object} values Object containing old + new values */ M.gradereport_grader.classes.ajax.prototype.submit = function(properties, values) { // Stop the IO queue so we can add to it this.report.Y.io.queue.stop(); // If the grade has changed add an IO transaction to update it to the queue if (values.grade !== values.oldgrade) { this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', { method : 'POST', data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.grade+'&type='+properties.itemtype+'&sesskey='+M.cfg.sesskey, on : { complete : this.submission_outcome }, context : this, arguments : { properties : properties, values : values, type : 'grade' } }),complete:false,outcome:null}); } // If feedback is editable and has changed add to the IO queue for it if (values.editablefeedback && values.feedback !== values.oldfeedback) { values.feedback = encodeURIComponent(values.feedback); this.pendingsubmissions.push({transaction:this.report.Y.io.queue(M.cfg.wwwroot+'/grade/report/grader/ajax_callbacks.php', { method : 'POST', data : 'id='+this.courseid+'&userid='+properties.userid+'&itemid='+properties.itemid+'&action=update&newvalue='+values.feedback+'&type=feedback&sesskey='+M.cfg.sesskey, on : { complete : this.submission_outcome }, context : this, arguments : { properties : properties, values : values, type : 'feedback' } }),complete:false,outcome:null}); } // Process the IO queue this.report.Y.io.queue.start(); }; /** * Callback function for IO transaction completions * * Uses a synchronous queue to ensure we maintain some sort of order * * @function * @this {M.gradereport_grader.classes.ajax} * @param {Int} tid Transaction ID * @param {Object} outcome * @param {Mixed} args */ M.gradereport_grader.classes.ajax.prototype.submission_outcome = function(tid, outcome, args) { // Parse the response as JSON try { outcome = this.report.Y.JSON.parse(outcome.responseText); } catch(e) { var message = M.util.get_string('ajaxfailedupdate', 'gradereport_grader'); message = message.replace(/\[1\]/, args.type); message = message.replace(/\[2\]/, this.report.users[args.properties.userid]); this.display_submission_error(message, args.properties.cell); return; } // Quick reference for the grader report var i = null; // Check the outcome if (outcome.result == 'success') { // Iterate through each row in the result object for (i in outcome.row) { if (outcome.row[i] && outcome.row[i].userid && outcome.row[i].itemid) { // alias it, we use it quite a bit var r = outcome.row[i]; // Get the cell referred to by this result object var info = this.report.get_cell_info([r.userid, r.itemid]); if (!info) { continue; } // Calculate the final grade for the cell var finalgrade = ''; var scalegrade = -1; if (!r.finalgrade) { if (this.report.isediting) { // In edit mode don't put hyphens in the grade text boxes finalgrade = ''; } else { // In non-edit mode put a hyphen in the grade cell finalgrade = '-'; } } else { if (r.scale) { scalegrade = parseFloat(r.finalgrade); finalgrade = this.scales[r.scale][scalegrade-1]; } else { finalgrade = parseFloat(r.finalgrade).toFixed(info.itemdp); } } if (this.report.isediting) { var grade = info.cell.one('#grade_'+r.userid+'_'+r.itemid); if (grade) { // This means the item has a input element to update. var parent = grade.ancestor('td'); if (parent.hasClass('grade_type_scale')) { grade.all('option').each(function(option) { if (option.get('value') == scalegrade) { option.setAttribute('selected', 'selected'); } else { option.removeAttribute('selected'); } }); } else { grade.set('value', finalgrade); } } else if (info.cell.one('.gradevalue')) { // This means we are updating a value for something without editing boxed (locked, etc). info.cell.one('.gradevalue').set('innerHTML', finalgrade); } } else { // If there is no currently editing field or if this cell is not being currently edited if (!this.current || info.cell.get('id') != this.current.node.get('id')) { // Update the value var node = info.cell.one('.gradevalue'); var td = node.ancestor('td'); // Only scale and value type grades should have their content updated in this way. if (td.hasClass('grade_type_value') || td.hasClass('grade_type_scale')) { node.set('innerHTML', finalgrade); } } else if (this.current && info.cell.get('id') == this.current.node.get('id')) { // If we are here the grade value of the cell currently being edited has changed !!!!!!!!! // If the user has not actually changed the old value yet we will automatically correct it // otherwise we will prompt the user to choose to use their value or the new value! if (!this.current.has_changed() || confirm(M.util.get_string('ajaxfieldchanged', 'gradereport_grader'))) { this.current.set_grade(finalgrade); if (this.current.grade) { this.current.grade.set('value', finalgrade); } } } } } } // Flag the changed cell as overridden by ajax args.properties.cell.addClass('ajaxoverridden'); } else { var p = args.properties; if (args.type == 'grade') { var oldgrade = args.values.oldgrade; p.cell.one('.gradevalue').set('innerHTML',oldgrade); } else if (args.type == 'feedback') { this.report.update_feedback(p.userid, p.itemid, args.values.oldfeedback); } this.display_submission_error(outcome.message, p.cell); } // Check if all IO transactions in the queue are complete yet var allcomplete = true; for (i in this.pendingsubmissions) { if (this.pendingsubmissions[i]) { if (this.pendingsubmissions[i].transaction.id == tid) { this.pendingsubmissions[i].complete = true; this.pendingsubmissions[i].outcome = outcome; this.report.Y.io.queue.remove(this.pendingsubmissions[i].transaction); } if (!this.pendingsubmissions[i].complete) { allcomplete = false; } } } if (allcomplete) { this.pendingsubmissions = []; } }; /** * Displays a submission error within a overlay on the cell that failed update * * @function * @this {M.gradereport_grader.classes.ajax} * @param {String} message * @param {Y.Node} cell */ M.gradereport_grader.classes.ajax.prototype.display_submission_error = function(message, cell) { var erroroverlay = new this.report.Y.Overlay({ headerContent : '
'+M.util.get_string('ajaxerror', 'gradereport_grader')+' '+M.util.get_string('ajaxclicktoclose', 'gradereport_grader')+'
', bodyContent : message, visible : false, zIndex : 3 }); erroroverlay.set('xy', [cell.getX()+10,cell.getY()+10]); erroroverlay.render(this.report.table.ancestor('div')); erroroverlay.show(); erroroverlay.get('boundingBox').on('click', function(){ this.get('boundingBox').setStyle('visibility', 'hidden'); this.hide(); this.destroy(); }, erroroverlay); erroroverlay.get('boundingBox').setStyle('visibility', 'visible'); }; /** * A class for existing fields * This class is used only when the user is in editing mode * * @class existingfield * @constructor * @param {M.gradereport_grader.classes.report} report * @param {Int} userid * @param {Int} itemid */ M.gradereport_grader.classes.existingfield = function(ajax, userid, itemid) { this.report = ajax.report; this.userid = userid; this.itemid = itemid; this.editfeedback = ajax.showquickfeedback; this.grade = this.report.Y.one('#grade_'+userid+'_'+itemid); var i = 0; if (this.grade) { for (i = 0; i < this.report.grades.length; i++) { if (this.report.grades[i]['user'] == this.userid && this.report.grades[i]['item'] == this.itemid) { this.oldgrade = this.report.grades[i]['grade']; } } if (!this.oldgrade) { // Assigning an empty string makes determining whether the grade has been changed easier // This value is never sent to the server this.oldgrade = ''; } // On blur save any changes in the grade field this.grade.on('blur', this.submit, this); } // Check if feedback is enabled if (this.editfeedback) { // Get the feedback fields this.feedback = this.report.Y.one('#feedback_'+userid+'_'+itemid); if (this.feedback) { for(i = 0; i < this.report.feedback.length; i++) { if (this.report.feedback[i]['user'] == this.userid && this.report.feedback[i]['item'] == this.itemid) { this.oldfeedback = this.report.feedback[i]['content']; } } if(!this.oldfeedback) { // Assigning an empty string makes determining whether the feedback has been changed easier // This value is never sent to the server this.oldfeedback = ''; } // On blur save any changes in the feedback field this.feedback.on('blur', this.submit, this); // Override the default tab movements when moving between cells // Handle Tab. this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.feedback, 'press:9', this, true)); // Handle the Enter key being pressed. this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.feedback, 'press:13', this)); // Handle CTRL + arrow keys. this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.feedback, 'press:37,38,39,40+ctrl', this)); if (this.grade) { // Override the default tab movements when moving between cells // Handle Shift+Tab. this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'press:9+shift', this)); // Override the default tab movements for fields in the same cell this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();this.grade.focus();}, this.feedback, 'press:9+shift', this)); this.keyevents.push(this.report.Y.on('key', function(e){if (e.shiftKey) {return;}e.preventDefault();this.feedback.focus();}, this.grade, 'press:9', this)); } } } else if (this.grade) { // Handle Tab and Shift+Tab. this.keyevents.push(this.report.Y.on('key', this.keypress_tab, this.grade, 'down:9', this)); } if (this.grade) { // Handle the Enter key being pressed. this.keyevents.push(this.report.Y.on('key', this.keypress_enter, this.grade, 'up:13', this)); // Handle CTRL + arrow keys. this.keyevents.push(this.report.Y.on('key', this.keypress_arrows, this.grade, 'down:37,38,39,40+ctrl', this)); } }; /** * Attach the required properties and methods to the existing field class * via prototyping */ M.gradereport_grader.classes.existingfield.prototype.userid = null; M.gradereport_grader.classes.existingfield.prototype.itemid = null; M.gradereport_grader.classes.existingfield.prototype.editfeedback = false; M.gradereport_grader.classes.existingfield.prototype.grade = null; M.gradereport_grader.classes.existingfield.prototype.oldgrade = null; M.gradereport_grader.classes.existingfield.prototype.keyevents = []; /** * Handles saving of changed on keypress * * @function * @this {M.gradereport_grader.classes.existingfield} * @param {Event} e */ M.gradereport_grader.classes.existingfield.prototype.keypress_enter = function(e) { e.preventDefault(); this.submit(); }; /** * Handles setting the correct focus if the user presses tab * * @function * @this {M.gradereport_grader.classes.existingfield} * @param {Event} e * @param {Bool} ignoreshift */ M.gradereport_grader.classes.existingfield.prototype.keypress_tab = function(e, ignoreshift) { e.preventDefault(); var next = null; if (e.shiftKey) { if (ignoreshift) { return; } next = this.report.ajax.get_above_cell(this.grade.ancestor('td')); } else { next = this.report.ajax.get_below_cell(this.grade.ancestor('td')); } this.move_focus(next); }; /** * Handles setting the correct focus when the user presses CTRL+arrow keys * * @function * @this {M.gradereport_grader.classes.existingfield} * @param {Event} e */ M.gradereport_grader.classes.existingfield.prototype.keypress_arrows = function(e) { e.preventDefault(); var next = null; switch (e.keyCode) { case 37: // Left next = this.report.ajax.get_prev_cell(this.grade.ancestor('td')); break; case 38: // Up next = this.report.ajax.get_above_cell(this.grade.ancestor('td')); break; case 39: // Right next = this.report.ajax.get_next_cell(this.grade.ancestor('td')); break; case 40: // Down next = this.report.ajax.get_below_cell(this.grade.ancestor('td')); break; } this.move_focus(next); }; /** * Move the focus to the node * @function * @this {M.gradereport_grader.classes.existingfield} * @param {Y.Node} node */ M.gradereport_grader.classes.existingfield.prototype.move_focus = function(node) { if (node) { var properties = this.report.get_cell_info(node); this.report.ajax.current = node; switch(properties.itemtype) { case 'scale': properties.cell.one('select.select').focus(); break; case 'value': default: properties.cell.one('input.text').focus(); break; } } }; /** * Checks if the values for the field have changed * * @function * @this {M.gradereport_grader.classes.existingfield} * @return {Bool} */ M.gradereport_grader.classes.existingfield.prototype.has_changed = function() { if (this.grade) { if (this.grade.get('value') !== this.oldgrade) { return true; } } if (this.editfeedback && this.feedback) { if (this.feedback.get('value') !== this.oldfeedback) { return true; } } return false; }; /** * Submits any changes and then updates the fields accordingly * * @function * @this {M.gradereport_grader.classes.existingfield} */ M.gradereport_grader.classes.existingfield.prototype.submit = function() { if (!this.has_changed()) { return; } var properties = this.report.get_cell_info([this.userid,this.itemid]); var values = (function(f){ var feedback, oldfeedback, grade, oldgrade = null; if (f.editfeedback && f.feedback) { feedback = f.feedback.get('value'); oldfeedback = f.oldfeedback; } if (f.grade) { grade = f.grade.get('value'); oldgrade = f.oldgrade; } return { editablefeedback : f.editfeedback, grade : grade, oldgrade : oldgrade, feedback : feedback, oldfeedback : oldfeedback }; })(this); this.oldgrade = values.grade; if (values.editablefeedback && values.feedback != values.oldfeedback) { this.report.update_feedback(this.userid, this.itemid, values.feedback); this.oldfeedback = values.feedback; } this.report.ajax.submit(properties, values); }; /** * Textfield class * This classes gets used in conjunction with the report running with AJAX enabled * and is used to manage a cell that has a grade requiring a textfield for input * * @class textfield * @constructor * @this {M.gradereport_grader.classes.textfield} * @param {M.gradereport_grader.classes.report} report * @param {Y.Node} node */ M.gradereport_grader.classes.textfield = function(report, node) { this.report = report; this.node = node; this.gradespan = node.one('.gradevalue'); this.inputdiv = this.report.Y.Node.create('
'); this.editfeedback = this.report.ajax.showquickfeedback; this.grade = this.report.Y.Node.create(''); this.gradetype = 'value'; this.inputdiv.append(this.grade); if (this.report.ajax.showquickfeedback) { this.feedback = this.report.Y.Node.create(''); this.inputdiv.append(this.feedback); } }; /** * Extend the textfield class with the following methods and properties */ M.gradereport_grader.classes.textfield.prototype.keyevents = []; M.gradereport_grader.classes.textfield.prototype.editable = false; M.gradereport_grader.classes.textfield.prototype.gradetype = null; M.gradereport_grader.classes.textfield.prototype.grade = null; M.gradereport_grader.classes.textfield.prototype.report = null; M.gradereport_grader.classes.textfield.prototype.node = null; M.gradereport_grader.classes.textfield.prototype.gradespam = null; M.gradereport_grader.classes.textfield.prototype.inputdiv = null; M.gradereport_grader.classes.textfield.prototype.editfeedback = false; /** * Replaces the cell contents with the controls to enable editing * * @function * @this {M.gradereport_grader.classes.textfield} * @return {M.gradereport_grader.classes.textfield} */ M.gradereport_grader.classes.textfield.prototype.replace = function() { this.set_grade(this.get_grade()); if (this.editfeedback) { this.set_feedback(this.get_feedback()); } this.node.replaceChild(this.inputdiv, this.gradespan); if (this.grade) { this.grade.focus(); } else if (this.feedback) { this.feedback.focus(); } this.editable = true; return this; }; /** * Commits the changes within a cell and returns a result object of new + old values * @function * @this {M.gradereport_grader.classes.textfield} * @return {Object} */ M.gradereport_grader.classes.textfield.prototype.commit = function() { // Produce an anonymous result object contianing all values var result = (function(field){ // Editable false lets us get the pre-update values. field.editable = false; var oldgrade = field.get_grade(); if (oldgrade == '-') { oldgrade = ''; } var feedback = null; var oldfeedback = null; if (field.editfeedback) { oldfeedback = field.get_feedback(); } // Now back to editable gives us the values in the edit areas. field.editable = true; if (field.editfeedback) { feedback = field.get_feedback(); } return { gradetype : field.gradetype, editablefeedback : field.editfeedback, grade : field.get_grade(), oldgrade : oldgrade, feedback : feedback, oldfeedback : oldfeedback }; })(this); // Set the changes in stone this.set_grade(result.grade); if (this.editfeedback) { this.set_feedback(result.feedback); } // Return the result object return result; }; /** * Reverts a cell back to its static contents * @function * @this {M.gradereport_grader.classes.textfield} */ M.gradereport_grader.classes.textfield.prototype.revert = function() { this.node.replaceChild(this.gradespan, this.inputdiv); for (var i in this.keyevents) { if (this.keyevents[i]) { this.keyevents[i].detach(); } } this.keyevents = []; this.node.on('click', this.report.ajax.make_editable, this.report.ajax); }; /** * Gets the grade for current cell * * @function * @this {M.gradereport_grader.classes.textfield} * @return {Mixed} */ M.gradereport_grader.classes.textfield.prototype.get_grade = function() { if (this.editable) { return this.grade.get('value'); } return this.gradespan.get('innerHTML'); }; /** * Sets the grade for the current cell * @function * @this {M.gradereport_grader.classes.textfield} * @param {Mixed} value */ M.gradereport_grader.classes.textfield.prototype.set_grade = function(value) { if (!this.editable) { if (value == '-') { value = ''; } this.grade.set('value', value); } else { if (value == '') { value = '-'; } this.gradespan.set('innerHTML', value); } }; /** * Gets the feedback for the current cell * @function * @this {M.gradereport_grader.classes.textfield} * @return {String} */ M.gradereport_grader.classes.textfield.prototype.get_feedback = function() { if (this.editable) { if (this.feedback) { return this.feedback.get('value'); } else { return null; } } var properties = this.report.get_cell_info(this.node); if (properties) { return properties.feedback; } return ''; }; /** * Sets the feedback for the current cell * @function * @this {M.gradereport_grader.classes.textfield} * @param {Mixed} value */ M.gradereport_grader.classes.textfield.prototype.set_feedback = function(value) { if (!this.editable) { if (this.feedback) { this.feedback.set('value', value); } } else { var properties = this.report.get_cell_info(this.node); this.report.update_feedback(properties.userid, properties.itemid, value); } }; /** * Checks if the current cell has changed at all * @function * @this {M.gradereport_grader.classes.textfield} * @return {Bool} */ M.gradereport_grader.classes.textfield.prototype.has_changed = function() { // If its not editable it has not changed if (!this.editable) { return false; } // If feedback is being edited then it has changed if either grade or feedback have changed if (this.editfeedback) { var properties = this.report.get_cell_info(this.node); if (this.get_feedback() != properties.feedback) { return true; } } if (this.grade) { return (this.get_grade() != this.gradespan.get('innerHTML')); } else { return false; } }; /** * Attaches the key listeners for the editable fields and stored the event references * against the textfield * * @function * @this {M.gradereport_grader.classes.textfield} */ M.gradereport_grader.classes.textfield.prototype.attach_key_events = function() { var a = this.report.ajax; // Setup the default key events for tab and enter if (this.editfeedback) { if (this.grade) { // Handle Shift+Tab. this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'down:9+shift', a)); } // Handle Tab. this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.feedback, 'down:9', a, true)); // Handle the Enter key being pressed. this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.feedback, 'up:13', a)); } else { if (this.grade) { // Handle Tab and Shift+Tab. this.keyevents.push(this.report.Y.on('key', a.keypress_tab, this.grade, 'down:9', a)); } } // Setup the arrow key events. // Handle CTRL + arrow keys. this.keyevents.push(this.report.Y.on('key', a.keypress_arrows, this.inputdiv.ancestor('td'), 'down:37,38,39,40+ctrl', a)); if (this.grade) { // Handle the Enter key being pressed. this.keyevents.push(this.report.Y.on('key', a.keypress_enter, this.grade, 'up:13', a)); // Prevent the default key action on all fields for arrow keys on all key events! // Note: this still does not work in FF!!!!! this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'down:37,38,39,40+ctrl')); this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'press:37,38,39,40+ctrl')); this.keyevents.push(this.report.Y.on('key', function(e){e.preventDefault();}, this.grade, 'up:37,38,39,40+ctrl')); } }; /** * Feedback field class * This classes gets used in conjunction with the report running with AJAX enabled * and is used to manage a cell that no editable grade, only possibly feedback * * @class feedbackfield * @constructor * @this {M.gradereport_grader.classes.feedbackfield} * @param {M.gradereport_grader.classes.report} report * @param {Y.Node} node */ M.gradereport_grader.classes.feedbackfield = function(report, node) { this.report = report; this.node = node; this.gradespan = node.one('.gradevalue'); this.inputdiv = this.report.Y.Node.create('
'); this.editfeedback = this.report.ajax.showquickfeedback; this.gradetype = 'text'; if (this.report.ajax.showquickfeedback) { this.feedback = this.report.Y.Node.create(''); this.inputdiv.append(this.feedback); } }; /** * Gets the grade for current cell (which will always be null) * * @function * @this {M.gradereport_grader.classes.feedbackfield} * @return {Mixed} */ M.gradereport_grader.classes.feedbackfield.prototype.get_grade = function() { return null; }; /** * Overrides the set_grade function of textfield so that it can ignore the set-grade * for grade cells without grades * * @function * @this {M.gradereport_grader.classes.feedbackfield} * @param {String} value */ M.gradereport_grader.classes.feedbackfield.prototype.set_grade = function() { return; }; /** * Manually extend the feedbackfield class with the properties and methods of the * textfield class that have not been defined */ for (var i in M.gradereport_grader.classes.textfield.prototype) { if (!M.gradereport_grader.classes.feedbackfield.prototype[i]) { M.gradereport_grader.classes.feedbackfield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i]; } } /** * An editable scale field * * @class scalefield * @constructor * @inherits M.gradereport_grader.classes.textfield * @base M.gradereport_grader.classes.textfield * @this {M.gradereport_grader.classes.scalefield} * @param {M.gradereport_grader.classes.report} report * @param {Y.Node} node */ M.gradereport_grader.classes.scalefield = function(report, node) { this.report = report; this.node = node; this.gradespan = node.one('.gradevalue'); this.inputdiv = this.report.Y.Node.create('
'); this.editfeedback = this.report.ajax.showquickfeedback; this.grade = this.report.Y.Node.create(''); this.gradetype = 'scale'; this.inputdiv.append(this.grade); if (this.editfeedback) { this.feedback = this.report.Y.Node.create(''); this.inputdiv.append(this.feedback); } var properties = this.report.get_cell_info(node); this.scale = this.report.ajax.scales[properties.itemscale]; for (var i in this.scale) { if (this.scale[i]) { this.grade.append(this.report.Y.Node.create('')); } } }; /** * Override + extend the scalefield class with the following properties * and methods */ /** * @property {Array} scale */ M.gradereport_grader.classes.scalefield.prototype.scale = []; /** * Extend the scalefield with the functions from the textfield */ /** * Overrides the get_grade function so that it can pick up the value from the * scales select box * * @function * @this {M.gradereport_grader.classes.scalefield} * @return {Int} the scale id */ M.gradereport_grader.classes.scalefield.prototype.get_grade = function(){ if (this.editable) { // Return the scale value return this.grade.all('option').item(this.grade.get('selectedIndex')).get('value'); } else { // Return the scale values id var value = this.gradespan.get('innerHTML'); for (var i in this.scale) { if (this.scale[i] == value) { return parseFloat(i)+1; } } return -1; } }; /** * Overrides the set_grade function of textfield so that it can set the scale * within the scale select box * * @function * @this {M.gradereport_grader.classes.scalefield} * @param {String} value */ M.gradereport_grader.classes.scalefield.prototype.set_grade = function(value) { if (!this.editable) { if (value == '-') { value = '-1'; } this.grade.all('option').each(function(node){ if (node.get('value') == value) { node.set('selected', true); } }); } else { if (value == '' || value == '-1') { value = '-'; } else { value = this.scale[parseFloat(value)-1]; } this.gradespan.set('innerHTML', value); } }; /** * Checks if the current cell has changed at all * @function * @this {M.gradereport_grader.classes.scalefield} * @return {Bool} */ M.gradereport_grader.classes.scalefield.prototype.has_changed = function() { if (!this.editable) { return false; } var gradef = this.get_grade(); this.editable = false; var gradec = this.get_grade(); this.editable = true; if (this.editfeedback) { var properties = this.report.get_cell_info(this.node); var feedback = properties.feedback; return (gradef != gradec || this.get_feedback() != feedback); } return (gradef != gradec); }; /** * Manually extend the scalefield class with the properties and methods of the * textfield class that have not been defined */ for (var i in M.gradereport_grader.classes.textfield.prototype) { if (!M.gradereport_grader.classes.scalefield.prototype[i]) { M.gradereport_grader.classes.scalefield.prototype[i] = M.gradereport_grader.classes.textfield.prototype[i]; } }