[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 /** 2 * Resource and activity toolbox class. 3 * 4 * This class is responsible for managing AJAX interactions with activities and resources 5 * when viewing a course in editing mode. 6 * 7 * @module moodle-course-toolboxes 8 * @namespace M.course.toolboxes 9 */ 10 11 /** 12 * Resource and activity toolbox class. 13 * 14 * This is a class extending TOOLBOX containing code specific to resources 15 * 16 * This class is responsible for managing AJAX interactions with activities and resources 17 * when viewing a course in editing mode. 18 * 19 * @class resources 20 * @constructor 21 * @extends M.course.toolboxes.toolbox 22 */ 23 var RESOURCETOOLBOX = function() { 24 RESOURCETOOLBOX.superclass.constructor.apply(this, arguments); 25 }; 26 27 Y.extend(RESOURCETOOLBOX, TOOLBOX, { 28 /** 29 * No groups are being used. 30 * 31 * @property GROUPS_NONE 32 * @protected 33 * @type Number 34 */ 35 GROUPS_NONE: 0, 36 37 /** 38 * Separate groups are being used. 39 * 40 * @property GROUPS_SEPARATE 41 * @protected 42 * @type Number 43 */ 44 GROUPS_SEPARATE: 1, 45 46 /** 47 * Visible groups are being used. 48 * 49 * @property GROUPS_VISIBLE 50 * @protected 51 * @type Number 52 */ 53 GROUPS_VISIBLE: 2, 54 55 /** 56 * An Array of events added when editing a title. 57 * These should all be detached when editing is complete. 58 * 59 * @property edittitleevents 60 * @protected 61 * @type Array 62 * @protected 63 */ 64 edittitleevents: [], 65 66 /** 67 * Initialize the resource toolbox 68 * 69 * For each activity the commands are updated and a reference to the activity is attached. 70 * This way it doesn't matter where the commands are going to called from they have a reference to the 71 * activity that they relate to. 72 * This is essential as some of the actions are displayed in an actionmenu which removes them from the 73 * page flow. 74 * 75 * This function also creates a single event delegate to manage all AJAX actions for all activities on 76 * the page. 77 * 78 * @method initializer 79 * @protected 80 */ 81 initializer: function() { 82 M.course.coursebase.register_module(this); 83 BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this); 84 Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this); 85 }, 86 87 /** 88 * Handles the delegation event. When this is fired someone has triggered an action. 89 * 90 * Note not all actions will result in an AJAX enhancement. 91 * 92 * @protected 93 * @method handle_data_action 94 * @param {EventFacade} ev The event that was triggered. 95 * @return {boolean} 96 */ 97 handle_data_action: function(ev) { 98 // We need to get the anchor element that triggered this event. 99 var node = ev.target; 100 if (!node.test('a')) { 101 node = node.ancestor(SELECTOR.ACTIVITYACTION); 102 } 103 104 // From the anchor we can get both the activity (added during initialisation) and the action being 105 // performed (added by the UI as a data attribute). 106 var action = node.getData('action'), 107 activity = node.ancestor(SELECTOR.ACTIVITYLI); 108 109 if (!node.test('a') || !action || !activity) { 110 // It wasn't a valid action node. 111 return; 112 } 113 114 // Switch based upon the action and do the desired thing. 115 switch (action) { 116 case 'edittitle': 117 // The user wishes to edit the title of the event. 118 this.edit_title(ev, node, activity, action); 119 break; 120 case 'moveleft': 121 case 'moveright': 122 // The user changing the indent of the activity. 123 this.change_indent(ev, node, activity, action); 124 break; 125 case 'delete': 126 // The user is deleting the activity. 127 this.delete_with_confirmation(ev, node, activity, action); 128 break; 129 case 'duplicate': 130 // The user is duplicating the activity. 131 this.duplicate(ev, node, activity, action); 132 break; 133 case 'hide': 134 case 'show': 135 // The user is changing the visibility of the activity. 136 this.change_visibility(ev, node, activity, action); 137 break; 138 case 'groupsseparate': 139 case 'groupsvisible': 140 case 'groupsnone': 141 // The user is changing the group mode. 142 callback = 'change_groupmode'; 143 this.change_groupmode(ev, node, activity, action); 144 break; 145 case 'move': 146 case 'update': 147 case 'duplicate': 148 case 'assignroles': 149 break; 150 default: 151 // Nothing to do here! 152 break; 153 } 154 }, 155 156 /** 157 * Add a loading icon to the specified activity. 158 * 159 * The icon is added within the action area. 160 * 161 * @method add_spinner 162 * @param {Node} activity The activity to add a loading icon to 163 * @return {Node|null} The newly created icon, or null if the action area was not found. 164 */ 165 add_spinner: function(activity) { 166 var actionarea = activity.one(SELECTOR.ACTIONAREA); 167 if (actionarea) { 168 return M.util.add_spinner(Y, actionarea); 169 } 170 return null; 171 }, 172 173 /** 174 * Change the indent of the activity or resource. 175 * 176 * @method change_indent 177 * @protected 178 * @param {EventFacade} ev The event that was fired. 179 * @param {Node} button The button that triggered this action. 180 * @param {Node} activity The activity node that this action will be performed on. 181 * @param {String} action The action that has been requested. Will be 'moveleft' or 'moveright'. 182 */ 183 change_indent: function(ev, button, activity, action) { 184 // Prevent the default button action 185 ev.preventDefault(); 186 187 var direction = (action === 'moveleft') ? -1: 1; 188 189 // And we need to determine the current and new indent level 190 var indentdiv = activity.one(SELECTOR.MODINDENTDIV), 191 indent = indentdiv.getAttribute('class').match(/mod-indent-(\d{1,})/), 192 oldindent = 0, 193 newindent; 194 195 if (indent) { 196 oldindent = parseInt(indent[1], 10); 197 } 198 newindent = oldindent + parseInt(direction, 10); 199 200 if (newindent < INDENTLIMITS.MIN || newindent > INDENTLIMITS.MAX) { 201 return; 202 } 203 204 if (indent) { 205 indentdiv.removeClass(indent[0]); 206 } 207 208 // Perform the move 209 indentdiv.addClass(CSS.MODINDENTCOUNT + newindent); 210 var data = { 211 'class': 'resource', 212 'field': 'indent', 213 'value': newindent, 214 'id': Y.Moodle.core_course.util.cm.getId(activity) 215 }; 216 var spinner = this.add_spinner(activity); 217 this.send_request(data, spinner); 218 219 var remainingmove; 220 221 // Handle removal/addition of the moveleft button. 222 if (newindent === INDENTLIMITS.MIN) { 223 button.addClass('hidden'); 224 remainingmove = activity.one('.editing_moveright'); 225 } else if (newindent > INDENTLIMITS.MIN && oldindent === INDENTLIMITS.MIN) { 226 button.ancestor('.menu').one('[data-action=moveleft]').removeClass('hidden'); 227 } 228 229 if (newindent === INDENTLIMITS.MAX) { 230 button.addClass('hidden'); 231 remainingmove = activity.one('.editing_moveleft'); 232 } else if (newindent < INDENTLIMITS.MAX && oldindent === INDENTLIMITS.MAX) { 233 button.ancestor('.menu').one('[data-action=moveright]').removeClass('hidden'); 234 } 235 236 // Handle massive indentation to match non-ajax display 237 var hashugeclass = indentdiv.hasClass(CSS.MODINDENTHUGE); 238 if (newindent > 15 && !hashugeclass) { 239 indentdiv.addClass(CSS.MODINDENTHUGE); 240 } else if (newindent <= 15 && hashugeclass) { 241 indentdiv.removeClass(CSS.MODINDENTHUGE); 242 } 243 244 if (ev.type && ev.type === "key" && remainingmove) { 245 remainingmove.focus(); 246 } 247 }, 248 249 /** 250 * Deletes the given activity or resource after confirmation. 251 * 252 * @protected 253 * @method delete_with_confirmation 254 * @param {EventFacade} ev The event that was fired. 255 * @param {Node} button The button that triggered this action. 256 * @param {Node} activity The activity node that this action will be performed on. 257 * @chainable 258 */ 259 delete_with_confirmation: function(ev, button, activity) { 260 // Prevent the default button action 261 ev.preventDefault(); 262 263 // Get the element we're working on 264 var element = activity, 265 // Create confirm string (different if element has or does not have name) 266 confirmstring = '', 267 plugindata = { 268 type: M.util.get_string('pluginname', element.getAttribute('class').match(/modtype_([^\s]*)/)[1]) 269 }; 270 if (Y.Moodle.core_course.util.cm.getName(element) !== null) { 271 plugindata.name = Y.Moodle.core_course.util.cm.getName(element); 272 confirmstring = M.util.get_string('deletechecktypename', 'moodle', plugindata); 273 } else { 274 confirmstring = M.util.get_string('deletechecktype', 'moodle', plugindata); 275 } 276 277 // Create the confirmation dialogue. 278 var confirm = new M.core.confirm({ 279 question: confirmstring, 280 modal: true 281 }); 282 283 // If it is confirmed. 284 confirm.on('complete-yes', function() { 285 286 // Actually remove the element. 287 element.remove(); 288 var data = { 289 'class': 'resource', 290 'action': 'DELETE', 291 'id': Y.Moodle.core_course.util.cm.getId(element) 292 }; 293 this.send_request(data); 294 if (M.core.actionmenu && M.core.actionmenu.instance) { 295 M.core.actionmenu.instance.hideMenu(); 296 } 297 298 }, this); 299 300 return this; 301 }, 302 303 /** 304 * Duplicates the activity. 305 * 306 * @method duplicate 307 * @protected 308 * @param {EventFacade} ev The event that was fired. 309 * @param {Node} button The button that triggered this action. 310 * @param {Node} activity The activity node that this action will be performed on. 311 * @chainable 312 */ 313 duplicate: function(ev, button, activity) { 314 // Prevent the default button action 315 ev.preventDefault(); 316 317 // Get the element we're working on 318 var element = activity; 319 320 // Add the lightbox. 321 var section = activity.ancestor(M.course.format.get_section_selector(Y)), 322 lightbox = M.util.add_lightbox(Y, section).show(); 323 324 // Build and send the request. 325 var data = { 326 'class': 'resource', 327 'field': 'duplicate', 328 'id': Y.Moodle.core_course.util.cm.getId(element), 329 'sr': button.getData('sr') 330 }; 331 this.send_request(data, lightbox, function(response) { 332 var newcm = Y.Node.create(response.fullcontent); 333 334 // Append to the section? 335 activity.insert(newcm, 'after'); 336 Y.use('moodle-course-coursebase', function() { 337 M.course.coursebase.invoke_function('setup_for_resource', newcm); 338 }); 339 if (M.core.actionmenu && M.core.actionmenu.newDOMNode) { 340 M.core.actionmenu.newDOMNode(newcm); 341 } 342 }); 343 return this; 344 }, 345 346 /** 347 * Changes the visibility of this activity or resource. 348 * 349 * @method change_visibility 350 * @protected 351 * @param {EventFacade} ev The event that was fired. 352 * @param {Node} button The button that triggered this action. 353 * @param {Node} activity The activity node that this action will be performed on. 354 * @param {String} action The action that has been requested. 355 * @chainable 356 */ 357 change_visibility: function(ev, button, activity, action) { 358 // Prevent the default button action 359 ev.preventDefault(); 360 361 // Get the element we're working on 362 var element = activity; 363 var value = this.handle_resource_dim(button, activity, action); 364 365 // Send the request 366 var data = { 367 'class': 'resource', 368 'field': 'visible', 369 'value': value, 370 'id': Y.Moodle.core_course.util.cm.getId(element) 371 }; 372 var spinner = this.add_spinner(element); 373 this.send_request(data, spinner); 374 375 return this; 376 }, 377 378 /** 379 * Handles the UI aspect of dimming the activity or resource. 380 * 381 * @method handle_resource_dim 382 * @protected 383 * @param {Node} button The button that triggered the action. 384 * @param {Node} activity The activity node that this action will be performed on. 385 * @param {String} action 'show' or 'hide'. 386 * @return {Number} 1 if we changed to visible, 0 if we were hiding. 387 */ 388 handle_resource_dim: function(button, activity, action) { 389 var toggleclass = CSS.DIMCLASS, 390 dimarea = activity.one([ 391 SELECTOR.ACTIVITYLINK, 392 SELECTOR.CONTENTWITHOUTLINK 393 ].join(', ')), 394 availabilityinfo = activity.one(CSS.AVAILABILITYINFODIV), 395 nextaction = (action === 'hide') ? 'show': 'hide', 396 buttontext = button.one('span'), 397 newstring = M.util.get_string(nextaction, 'moodle'), 398 buttonimg = button.one('img'); 399 400 // Update button info. 401 buttonimg.setAttrs({ 402 'src': M.util.image_url('t/' + nextaction) 403 }); 404 405 if (Y.Lang.trim(button.getAttribute('title'))) { 406 button.setAttribute('title', newstring); 407 } 408 409 if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { 410 buttonimg.setAttribute('alt', newstring); 411 } 412 413 button.replaceClass('editing_'+action, 'editing_'+nextaction); 414 button.setData('action', nextaction); 415 if (buttontext) { 416 buttontext.set('text', newstring); 417 } 418 419 if (activity.one(SELECTOR.CONTENTWITHOUTLINK)) { 420 dimarea = activity.one(SELECTOR.CONTENTWITHOUTLINK); 421 toggleclass = CSS.DIMMEDTEXT; 422 } 423 424 // If activity is conditionally hidden, then don't toggle. 425 if (!dimarea.hasClass(CSS.CONDITIONALHIDDEN)) { 426 // Change the UI. 427 dimarea.toggleClass(toggleclass); 428 // We need to toggle dimming on the description too. 429 activity.all(SELECTOR.CONTENTAFTERLINK).toggleClass(CSS.DIMMEDTEXT); 430 } 431 // Toggle availablity info for conditional activities. 432 if (availabilityinfo) { 433 availabilityinfo.toggleClass(CSS.HIDE); 434 } 435 return (action === 'hide') ? 0: 1; 436 }, 437 438 /** 439 * Changes the groupmode of the activity to the next groupmode in the sequence. 440 * 441 * @method change_groupmode 442 * @protected 443 * @param {EventFacade} ev The event that was fired. 444 * @param {Node} button The button that triggered this action. 445 * @param {Node} activity The activity node that this action will be performed on. 446 * @chainable 447 */ 448 change_groupmode: function(ev, button, activity) { 449 // Prevent the default button action. 450 ev.preventDefault(); 451 452 // Current Mode 453 var groupmode = parseInt(button.getData('nextgroupmode'), 10), 454 newtitle = '', 455 iconsrc = '', 456 newtitlestr, 457 data, 458 spinner, 459 nextgroupmode = groupmode + 1, 460 buttonimg = button.one('img'); 461 462 if (nextgroupmode > 2) { 463 nextgroupmode = 0; 464 } 465 466 if (groupmode === this.GROUPS_NONE) { 467 newtitle = 'groupsnone'; 468 iconsrc = M.util.image_url('i/groupn', 'moodle'); 469 } else if (groupmode === this.GROUPS_SEPARATE) { 470 newtitle = 'groupsseparate'; 471 iconsrc = M.util.image_url('i/groups', 'moodle'); 472 } else if (groupmode === this.GROUPS_VISIBLE) { 473 newtitle = 'groupsvisible'; 474 iconsrc = M.util.image_url('i/groupv', 'moodle'); 475 } 476 newtitlestr = M.util.get_string('clicktochangeinbrackets', 'moodle', M.util.get_string(newtitle, 'moodle')); 477 478 // Change the UI 479 buttonimg.setAttrs({ 480 'src': iconsrc 481 }); 482 if (Y.Lang.trim(button.getAttribute('title'))) { 483 button.setAttribute('title', newtitlestr).setData('action', newtitle).setData('nextgroupmode', nextgroupmode); 484 } 485 486 if (Y.Lang.trim(buttonimg.getAttribute('alt'))) { 487 buttonimg.setAttribute('alt', newtitlestr); 488 } 489 490 // And send the request 491 data = { 492 'class': 'resource', 493 'field': 'groupmode', 494 'value': groupmode, 495 'id': Y.Moodle.core_course.util.cm.getId(activity) 496 }; 497 498 spinner = this.add_spinner(activity); 499 this.send_request(data, spinner); 500 return this; 501 }, 502 503 /** 504 * Edit the title for the resource 505 * 506 * @method edit_title 507 * @protected 508 * @param {EventFacade} ev The event that was fired. 509 * @param {Node} button The button that triggered this action. 510 * @param {Node} activity The activity node that this action will be performed on. 511 * @param {String} action The action that has been requested. 512 * @chainable 513 */ 514 edit_title: function(ev, button, activity) { 515 // Get the element we're working on 516 var activityid = Y.Moodle.core_course.util.cm.getId(activity), 517 instancename = activity.one(SELECTOR.INSTANCENAME), 518 instance = activity.one(SELECTOR.ACTIVITYINSTANCE), 519 currenttitle = instancename.get('firstChild'), 520 oldtitle = currenttitle.get('data'), 521 titletext = oldtitle, 522 thisevent, 523 anchor = instancename.ancestor('a'),// Grab the anchor so that we can swap it with the edit form. 524 data = { 525 'class': 'resource', 526 'field': 'gettitle', 527 'id': activityid 528 }; 529 530 // Prevent the default actions. 531 ev.preventDefault(); 532 533 this.send_request(data, null, function(response) { 534 if (M.core.actionmenu && M.core.actionmenu.instance) { 535 M.core.actionmenu.instance.hideMenu(); 536 } 537 538 // Try to retrieve the existing string from the server 539 if (response.instancename) { 540 titletext = response.instancename; 541 } 542 543 // Create the editor and submit button 544 var editform = Y.Node.create('<form action="#" />'); 545 var editinstructions = Y.Node.create('<span class="'+CSS.EDITINSTRUCTIONS+'" id="id_editinstructions" />') 546 .set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle')); 547 var editor = Y.Node.create('<input name="title" type="text" class="'+CSS.TITLEEDITOR+'" />').setAttrs({ 548 'value': titletext, 549 'autocomplete': 'off', 550 'aria-describedby': 'id_editinstructions', 551 'maxLength': '255' 552 }); 553 554 // Clear the existing content and put the editor in 555 editform.appendChild(activity.one(SELECTOR.ACTIVITYICON).cloneNode()); 556 editform.appendChild(editor); 557 editform.setData('anchor', anchor); 558 instance.insert(editinstructions, 'before'); 559 anchor.replace(editform); 560 561 // Force the editing instruction to match the mod-indent position. 562 var padside = 'left'; 563 if (right_to_left()) { 564 padside = 'right'; 565 } 566 567 // We hide various components whilst editing: 568 activity.addClass(CSS.EDITINGTITLE); 569 570 // Focus and select the editor text 571 editor.focus().select(); 572 573 // Cancel the edit if we lose focus or the escape key is pressed. 574 thisevent = editor.on('blur', this.edit_title_cancel, this, activity, false); 575 this.edittitleevents.push(thisevent); 576 thisevent = editor.on('key', this.edit_title_cancel, 'esc', this, activity, true); 577 this.edittitleevents.push(thisevent); 578 579 // Handle form submission. 580 thisevent = editform.on('submit', this.edit_title_submit, this, activity, oldtitle); 581 this.edittitleevents.push(thisevent); 582 }); 583 return this; 584 }, 585 586 /** 587 * Handles the submit event when editing the activity or resources title. 588 * 589 * @method edit_title_submit 590 * @protected 591 * @param {EventFacade} ev The event that triggered this. 592 * @param {Node} activity The activity whose title we are altering. 593 * @param {String} originaltitle The original title the activity or resource had. 594 */ 595 edit_title_submit: function(ev, activity, originaltitle) { 596 // We don't actually want to submit anything 597 ev.preventDefault(); 598 599 var newtitle = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYTITLE).get('value')); 600 this.edit_title_clear(activity); 601 var spinner = this.add_spinner(activity); 602 if (newtitle !== null && newtitle !== "" && newtitle !== originaltitle) { 603 var data = { 604 'class': 'resource', 605 'field': 'updatetitle', 606 'title': newtitle, 607 'id': Y.Moodle.core_course.util.cm.getId(activity) 608 }; 609 this.send_request(data, spinner, function(response) { 610 if (response.instancename) { 611 activity.one(SELECTOR.INSTANCENAME).setContent(response.instancename); 612 } 613 }); 614 } 615 }, 616 617 /** 618 * Handles the cancel event when editing the activity or resources title. 619 * 620 * @method edit_title_cancel 621 * @protected 622 * @param {EventFacade} ev The event that triggered this. 623 * @param {Node} activity The activity whose title we are altering. 624 * @param {Boolean} preventdefault If true we should prevent the default action from occuring. 625 */ 626 edit_title_cancel: function(ev, activity, preventdefault) { 627 if (preventdefault) { 628 ev.preventDefault(); 629 } 630 this.edit_title_clear(activity); 631 }, 632 633 /** 634 * Handles clearing the editing UI and returning things to the original state they were in. 635 * 636 * @method edit_title_clear 637 * @protected 638 * @param {Node} activity The activity whose title we were altering. 639 */ 640 edit_title_clear: function(activity) { 641 // Detach all listen events to prevent duplicate triggers 642 new Y.EventHandle(this.edittitleevents).detach(); 643 644 var editform = activity.one(SELECTOR.ACTIVITYFORM), 645 instructions = activity.one('#id_editinstructions'); 646 if (editform) { 647 editform.replace(editform.getData('anchor')); 648 } 649 if (instructions) { 650 instructions.remove(); 651 } 652 653 // Remove the editing class again to revert the display. 654 activity.removeClass(CSS.EDITINGTITLE); 655 656 // Refocus the link which was clicked originally so the user can continue using keyboard nav. 657 Y.later(100, this, function() { 658 activity.one(SELECTOR.EDITTITLE).focus(); 659 }); 660 }, 661 662 /** 663 * Set the visibility of the specified resource to match the visible parameter. 664 * 665 * Note: This is not a toggle function and only changes the visibility 666 * in the browser (no ajax update is performed). 667 * 668 * @method set_visibility_resource_ui 669 * @param {object} args An object containing the required information to trigger a change. 670 * @param {Node} args.element The resource to toggle 671 * @param {Boolean} args.visible The target visibility 672 */ 673 set_visibility_resource_ui: function(args) { 674 var element = args.element, 675 buttonnode = element.one(SELECTOR.HIDE), 676 // By default we assume that the item is visible and we're going to hide it. 677 currentVisibility = true, 678 targetVisibility = false; 679 680 if (!buttonnode) { 681 // If the buttonnode was not found, try to find the HIDE button 682 // and change the target visibility setting to false. 683 buttonnode = element.one(SELECTOR.SHOW); 684 currentVisibility = false; 685 targetVisibility = true; 686 } 687 688 if (typeof args.visible !== 'undefined') { 689 // If we were provided with a visibility argument, use that instead. 690 targetVisibility = args.visible; 691 } 692 693 // Only trigger a change if necessary. 694 if (currentVisibility !== targetVisibility) { 695 var action = 'hide'; 696 if (targetVisibility) { 697 action = 'show'; 698 } 699 700 this.handle_resource_dim(buttonnode, element, action); 701 } 702 } 703 }, { 704 NAME: 'course-resource-toolbox', 705 ATTRS: { 706 } 707 }); 708 709 M.course.resource_toolbox = null; 710 M.course.init_resource_toolbox = function(config) { 711 M.course.resource_toolbox = new RESOURCETOOLBOX(config); 712 return M.course.resource_toolbox; 713 };
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 20:29:05 2014 | Cross-referenced by PHPXref 0.7.1 |