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