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