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