[ Index ] |
PHP Cross Reference of moodle-2.8 |
[Summary view] [Print] [Text view]
1 /** 2 * Provides interface for users to edit availability settings on the 3 * module/section editing form. 4 * 5 * The system works using this JavaScript plus form.js files inside each 6 * condition plugin. 7 * 8 * The overall concept is that data is held in a textarea in the form in JSON 9 * format. This JavaScript converts the textarea into a set of controls 10 * generated here and by the relevant plugins. 11 * 12 * (Almost) all data is held directly by the state of the HTML controls, and 13 * can be updated to the form field by calling the 'update' method, which 14 * this code and the plugins call if any HTML control changes. 15 * 16 * @module moodle-core_availability-form 17 */ 18 M.core_availability = M.core_availability || {}; 19 20 /** 21 * Core static functions for availability settings in editing form. 22 * 23 * @class M.core_availability.form 24 * @static 25 */ 26 M.core_availability.form = { 27 /** 28 * Object containing installed plugins. They are indexed by plugin name. 29 * 30 * @property plugins 31 * @type Object 32 */ 33 plugins : {}, 34 35 /** 36 * Availability field (textarea). 37 * 38 * @property field 39 * @type Y.Node 40 */ 41 field : null, 42 43 /** 44 * Main div that replaces the availability field. 45 * 46 * @property mainDiv 47 * @type Y.Node 48 */ 49 mainDiv : null, 50 51 /** 52 * Object that represents the root of the tree. 53 * 54 * @property rootList 55 * @type M.core_availability.List 56 */ 57 rootList : null, 58 59 /** 60 * Counter used when creating anything that needs an id. 61 * 62 * @property idCounter 63 * @type Number 64 */ 65 idCounter : 0, 66 67 /** 68 * Called to initialise the system when the page loads. This method will 69 * also call the init method for each plugin. 70 * 71 * @method init 72 */ 73 init : function(pluginParams) { 74 // Init all plugins. 75 for(var plugin in pluginParams) { 76 var params = pluginParams[plugin]; 77 var pluginClass = M[params[0]].form; 78 pluginClass.init.apply(pluginClass, params); 79 } 80 81 // Get the availability field, hide it, and replace with the main div. 82 this.field = Y.one('#id_availabilityconditionsjson'); 83 this.field.setAttribute('aria-hidden', 'true'); 84 // The fcontainer class here is inappropriate, but is necessary 85 // because otherwise it is impossible to make Behat work correctly on 86 // these controls as Behat incorrectly decides they're a moodleform 87 // textarea. IMO Behat should not know about moodleforms at all and 88 // should look purely at HTML elements on the page, but until it is 89 // fixed to do this or fixed in some other way to only detect moodleform 90 // elements that specifically match what those elements should look like, 91 // then there is no good solution. 92 this.mainDiv = Y.Node.create('<div class="availability-field fcontainer"></div>'); 93 this.field.insert(this.mainDiv, 'after'); 94 95 // Get top-level tree as JSON. 96 var value = this.field.get('value'); 97 var data = null; 98 if (value !== '') { 99 try { 100 data = Y.JSON.parse(value); 101 } catch(x) { 102 // If the JSON data is not valid, treat it as empty. 103 this.field.set('value', ''); 104 } 105 } 106 this.rootList = new M.core_availability.List(data, true); 107 this.mainDiv.appendChild(this.rootList.node); 108 109 // Update JSON value after loading (to reflect any changes that need 110 // to be made to make it valid). 111 this.update(); 112 this.rootList.renumber(); 113 114 // Mark main area as dynamically updated. 115 this.mainDiv.setAttribute('aria-live', 'polite'); 116 117 // Listen for form submission - to avoid having our made-up fields 118 // submitted, we need to disable them all before submit. 119 this.field.ancestor('form').on('submit', function() { 120 this.mainDiv.all('input,textarea,select').set('disabled', true); 121 }, this); 122 }, 123 124 /** 125 * Called at any time to update the hidden field value. 126 * 127 * This should be called whenever any value changes in the form settings. 128 * 129 * @method update 130 */ 131 update : function() { 132 // Convert tree to value. 133 var jsValue = this.rootList.getValue(); 134 135 // Store any errors (for form reporting) in 'errors' value if present. 136 var errors = []; 137 this.rootList.fillErrors(errors); 138 if (errors.length !== 0) { 139 jsValue.errors = errors; 140 } 141 142 // Set into hidden form field, JS-encoded. 143 this.field.set('value', Y.JSON.stringify(jsValue)); 144 } 145 }; 146 147 148 /** 149 * Base object for plugins. Plugins should use Y.Object to extend this class. 150 * 151 * @class M.core_availability.plugin 152 * @static 153 */ 154 M.core_availability.plugin = { 155 /** 156 * True if users are allowed to add items of this plugin at the moment. 157 * 158 * @property allowAdd 159 * @type Boolean 160 */ 161 allowAdd : false, 162 163 /** 164 * Called (from PHP) to initialise the plugin. Should usually not be 165 * overridden by child plugin. 166 * 167 * @method init 168 * @param {String} component Component name e.g. 'availability_date' 169 */ 170 init : function(component, allowAdd, params) { 171 var name = component.replace(/^availability_/, ''); 172 this.allowAdd = allowAdd; 173 M.core_availability.form.plugins[name] = this; 174 this.initInner.apply(this, params); 175 }, 176 177 /** 178 * Init method for plugin to override. (Default does nothing.) 179 * 180 * This method will receive any parameters defined in frontend.php 181 * get_javascript_init_params. 182 * 183 * @method initInner 184 * @protected 185 */ 186 initInner : function() { 187 }, 188 189 /** 190 * Gets a YUI node representing the controls for this plugin on the form. 191 * 192 * Must be implemented by sub-object; default throws an exception. 193 * 194 * @method getNode 195 * @return {Y.Node} YUI node 196 */ 197 getNode : function() { 198 throw 'getNode not implemented'; 199 }, 200 201 /** 202 * Fills in the value from this plugin's controls into a value object, 203 * which will later be converted to JSON and stored in the form field. 204 * 205 * Must be implemented by sub-object; default throws an exception. 206 * 207 * @method fillValue 208 * @param {Object} value Value object (to be written to) 209 * @param {Y.Node} node YUI node (same one returned from getNode) 210 */ 211 fillValue : function() { 212 throw 'fillValue not implemented'; 213 }, 214 215 /** 216 * Fills in any errors from this plugin's controls. If there are any 217 * errors, push them into the supplied array. 218 * 219 * Errors are Moodle language strings in format component:string, e.g. 220 * 'availability_date:error_date_past_end_of_world'. 221 * 222 * The default implementation does nothing. 223 * 224 * @method fillErrors 225 * @param {Array} errors Array of errors (push new errors here) 226 * @param {Y.Node} node YUI node (same one returned from getNode) 227 */ 228 fillErrors : function() { 229 }, 230 231 /** 232 * Focuses the first thing in the plugin after it has been added. 233 * 234 * The default implementation uses a simple algorithm to identify the 235 * first focusable input/select and then focuses it. 236 */ 237 focusAfterAdd : function(node) { 238 var target = node.one('input:not([disabled]),select:not([disabled])'); 239 target.focus(); 240 } 241 }; 242 243 244 /** 245 * Maintains a list of children and settings for how they are combined. 246 * 247 * @class M.core_availability.List 248 * @constructor 249 * @param {Object} json Decoded JSON value 250 * @param {Boolean} [false] root True if this is root level list 251 * @param {Boolean} [false] root True if parent is root level list 252 */ 253 M.core_availability.List = function(json, root, parentRoot) { 254 // Set default value for children. (You can't do this in the prototype 255 // definition, or it ends up sharing the same array between all of them.) 256 this.children = []; 257 258 if (root !== undefined) { 259 this.root = root; 260 } 261 var strings = M.str.availability; 262 // Create DIV structure (without kids). 263 this.node = Y.Node.create('<div class="availability-list"><h3 class="accesshide"></h3>' + 264 '<div class="availability-inner">' + 265 '<div class="availability-header">' + strings.listheader_sign_before + 266 ' <label><span class="accesshide">' + strings.label_sign + 267 ' </span><select class="availability-neg" title="' + strings.label_sign + '">' + 268 '<option value="">' + strings.listheader_sign_pos + '</option>' + 269 '<option value="!">' + strings.listheader_sign_neg + '</option></select></label> ' + 270 '<span class="availability-single">' + strings.listheader_single + '</span>' + 271 '<span class="availability-multi">' + strings.listheader_multi_before + 272 ' <label><span class="accesshide">' + strings.label_multi + ' </span>' + 273 '<select class="availability-op" title="' + strings.label_multi + '"><option value="&">' + 274 strings.listheader_multi_and + '</option>' + 275 '<option value="|">' + strings.listheader_multi_or + '</option></select></label> ' + 276 strings.listheader_multi_after + '</span></div>' + 277 '<div class="availability-children"></div>' + 278 '<div class="availability-none">' + M.str.moodle.none + '</div>' + 279 '<div class="availability-button"></div></div></div>'); 280 if (!root) { 281 this.node.addClass('availability-childlist'); 282 } 283 this.inner = this.node.one('> .availability-inner'); 284 285 var shown = true; 286 if (root) { 287 // If it's the root, add an eye icon as first thing in header. 288 if (json && json.show !== undefined) { 289 shown = json.show; 290 } 291 this.eyeIcon = new M.core_availability.EyeIcon(false, shown); 292 this.node.one('.availability-header').get('firstChild').insert( 293 this.eyeIcon.span, 'before'); 294 } else if (parentRoot) { 295 // When the parent is root, add an eye icon before the main list div. 296 if (json && json.showc !== undefined) { 297 shown = json.showc; 298 } 299 this.eyeIcon = new M.core_availability.EyeIcon(false, shown); 300 this.inner.insert(this.eyeIcon.span, 'before'); 301 } 302 303 if (!root) { 304 // If it's not the root, add a delete button to the 'none' option. 305 // You can only delete lists when they have no children so this will 306 // automatically appear at the correct time. 307 var deleteIcon = new M.core_availability.DeleteIcon(this); 308 var noneNode = this.node.one('.availability-none'); 309 noneNode.appendChild(document.createTextNode(' ')); 310 noneNode.appendChild(deleteIcon.span); 311 312 // Also if it's not the root, none is actually invalid, so add a label. 313 noneNode.appendChild(Y.Node.create('<span class="label label-warning">' + 314 M.str.availability.invalid + '</span>')); 315 } 316 317 // Create the button and add it. 318 var button = Y.Node.create('<button type="button" class="btn btn-default">' + 319 M.str.availability.addrestriction + '</button>'); 320 button.on("click", function() { this.clickAdd(); }, this); 321 this.node.one('div.availability-button').appendChild(button); 322 323 if (json) { 324 // Set operator from JSON data. 325 switch (json.op) { 326 case '&' : 327 case '|' : 328 this.node.one('.availability-neg').set('value', ''); 329 break; 330 case '!&' : 331 case '!|' : 332 this.node.one('.availability-neg').set('value', '!'); 333 break; 334 } 335 switch (json.op) { 336 case '&' : 337 case '!&' : 338 this.node.one('.availability-op').set('value', '&'); 339 break; 340 case '|' : 341 case '!|' : 342 this.node.one('.availability-op').set('value', '|'); 343 break; 344 } 345 346 // Construct children. 347 for (var i = 0; i < json.c.length; i++) { 348 var child = json.c[i]; 349 if (this.root && json && json.showc !== undefined) { 350 child.showc = json.showc[i]; 351 } 352 var newItem; 353 if (child.type !== undefined) { 354 // Plugin type. 355 newItem = new M.core_availability.Item(child, this.root); 356 } else { 357 // List type. 358 newItem = new M.core_availability.List(child, false, this.root); 359 } 360 this.addChild(newItem); 361 } 362 } 363 364 // Add update listeners to the dropdowns. 365 this.node.one('.availability-neg').on('change', function() { 366 // Update hidden field and HTML. 367 M.core_availability.form.update(); 368 this.updateHtml(); 369 }, this); 370 this.node.one('.availability-op').on('change', function() { 371 // Update hidden field. 372 M.core_availability.form.update(); 373 this.updateHtml(); 374 }, this); 375 376 // Update HTML to hide unnecessary parts. 377 this.updateHtml(); 378 }; 379 380 /** 381 * Adds a child to the end of the list (in HTML and stored data). 382 * 383 * @method addChild 384 * @private 385 * @param {M.core_availability.Item|M.core_availability.List} newItem Child to add 386 */ 387 M.core_availability.List.prototype.addChild = function(newItem) { 388 if (this.children.length > 0) { 389 // Create connecting label (text will be filled in later by updateHtml). 390 this.inner.one('.availability-children').appendChild(Y.Node.create( 391 '<div class="availability-connector">' + 392 '<span class="label"></span>' + 393 '</div>')); 394 } 395 // Add item to array and to HTML. 396 this.children.push(newItem); 397 this.inner.one('.availability-children').appendChild(newItem.node); 398 }; 399 400 /** 401 * Focuses something after a new list is added. 402 * 403 * @method focusAfterAdd 404 */ 405 M.core_availability.List.prototype.focusAfterAdd = function() { 406 this.inner.one('button').focus(); 407 }; 408 409 /** 410 * Checks whether this list uses the individual show icons or the single one. 411 * 412 * (Basically, AND and the equivalent NOT OR list can have individual show icons 413 * so that you hide the activity entirely if a user fails one condition, but 414 * may display it with information about the condition if they fail a different 415 * one. That isn't possible with OR and NOT AND because for those types, there 416 * is not really a concept of which single condition caused the user to fail 417 * it.) 418 * 419 * Method can only be called on the root list. 420 * 421 * @method isIndividualShowIcons 422 * @return {Boolean} True if using the individual icons 423 */ 424 M.core_availability.List.prototype.isIndividualShowIcons = function() { 425 if (!this.root) { 426 throw 'Can only call this on root list'; 427 } 428 var neg = this.node.one('.availability-neg').get('value') === '!'; 429 var isor = this.node.one('.availability-op').get('value') === '|'; 430 return (!neg && !isor) || (neg && isor); 431 }; 432 433 /** 434 * Renumbers the list and all children. 435 * 436 * @method renumber 437 * @param {String} parentNumber Number to use in heading for this list 438 */ 439 M.core_availability.List.prototype.renumber = function(parentNumber) { 440 // Update heading for list. 441 var headingParams = { count: this.children.length }; 442 var prefix; 443 if (parentNumber === undefined) { 444 headingParams.number = ''; 445 prefix = ''; 446 } else { 447 headingParams.number = parentNumber + ':'; 448 prefix = parentNumber + '.'; 449 } 450 var heading = M.util.get_string('setheading', 'availability', headingParams); 451 this.node.one('> h3').set('innerHTML', heading); 452 453 // Do children. 454 for (var i = 0; i < this.children.length; i++) { 455 var child = this.children[i]; 456 child.renumber(prefix + (i + 1)); 457 } 458 }; 459 460 /** 461 * Updates HTML for the list based on the current values, for example showing 462 * the 'None' text if there are no children. 463 * 464 * @method updateHtml 465 */ 466 M.core_availability.List.prototype.updateHtml = function() { 467 // Control children appearing or not appearing. 468 if (this.children.length > 0) { 469 this.inner.one('> .availability-children').removeAttribute('aria-hidden'); 470 this.inner.one('> .availability-none').setAttribute('aria-hidden', 'true'); 471 this.inner.one('> .availability-header').removeAttribute('aria-hidden'); 472 if (this.children.length > 1) { 473 this.inner.one('.availability-single').setAttribute('aria-hidden', 'true'); 474 this.inner.one('.availability-multi').removeAttribute('aria-hidden'); 475 } else { 476 this.inner.one('.availability-single').removeAttribute('aria-hidden'); 477 this.inner.one('.availability-multi').setAttribute('aria-hidden', 'true'); 478 } 479 } else { 480 this.inner.one('> .availability-children').setAttribute('aria-hidden', 'true'); 481 this.inner.one('> .availability-none').removeAttribute('aria-hidden'); 482 this.inner.one('> .availability-header').setAttribute('aria-hidden', 'true'); 483 } 484 485 // For root list, control eye icons. 486 if (this.root) { 487 var showEyes = this.isIndividualShowIcons(); 488 489 // Individual icons. 490 for (var i = 0; i < this.children.length; i++) { 491 var child = this.children[i]; 492 if (showEyes) { 493 child.eyeIcon.span.removeAttribute('aria-hidden'); 494 } else { 495 child.eyeIcon.span.setAttribute('aria-hidden', 'true'); 496 } 497 } 498 499 // Single icon is the inverse. 500 if (showEyes) { 501 this.eyeIcon.span.setAttribute('aria-hidden', 'true'); 502 } else { 503 this.eyeIcon.span.removeAttribute('aria-hidden'); 504 } 505 } 506 507 // Update connector text. 508 var connectorText; 509 if (this.inner.one('.availability-op').get('value') === '&') { 510 connectorText = M.str.availability.and; 511 } else { 512 connectorText = M.str.availability.or; 513 } 514 this.inner.all('> .availability-children > .availability-connector span.label').each(function(span) { 515 span.set('innerHTML', connectorText); 516 }); 517 }; 518 519 /** 520 * Deletes a descendant item (Item or List). Called when the user clicks a 521 * delete icon. 522 * 523 * This is a recursive function. 524 * 525 * @method deleteDescendant 526 * @param {M.core_availability.Item|M.core_availability.List} descendant Item to delete 527 * @return {Boolean} True if it was deleted 528 */ 529 M.core_availability.List.prototype.deleteDescendant = function(descendant) { 530 // Loop through children. 531 for (var i = 0; i < this.children.length; i++) { 532 var child = this.children[i]; 533 if (child === descendant) { 534 // Remove from internal array. 535 this.children.splice(i, 1); 536 var target = child.node; 537 // Remove one of the connector nodes around target (if any left). 538 if (this.children.length > 0) { 539 if (target.previous('.availability-connector')) { 540 target.previous('.availability-connector').remove(); 541 } else { 542 target.next('.availability-connector').remove(); 543 } 544 } 545 // Remove target itself. 546 this.inner.one('> .availability-children').removeChild(target); 547 // Update the form and the list HTML. 548 M.core_availability.form.update(); 549 this.updateHtml(); 550 // Focus add button for this list. 551 this.inner.one('> .availability-button').one('button').focus(); 552 return true; 553 } else if (child instanceof M.core_availability.List) { 554 // Recursive call. 555 var found = child.deleteDescendant(descendant); 556 if (found) { 557 return true; 558 } 559 } 560 } 561 562 return false; 563 }; 564 565 /** 566 * Shows the 'add restriction' dialogue box. 567 * 568 * @method clickAdd 569 */ 570 M.core_availability.List.prototype.clickAdd = function() { 571 var content = Y.Node.create('<div>' + 572 '<ul class="list-unstyled"></ul>' + 573 '<div class="availability-buttons mdl-align">' + 574 '<button type="button" class="btn btn-default">' + M.str.moodle.cancel + 575 '</button></div></div>'); 576 var cancel = content.one('button'); 577 578 // Make a list of all the dialog options. 579 var dialogRef = { dialog: null }; 580 var ul = content.one('ul'); 581 var li, id, button, label; 582 for (var type in M.core_availability.form.plugins) { 583 // Plugins might decide not to display their add button. 584 if (!M.core_availability.form.plugins[type].allowAdd) { 585 continue; 586 } 587 // Add entry for plugin. 588 li = Y.Node.create('<li class="clearfix"></li>'); 589 id = 'availability_addrestriction_' + type; 590 var pluginStrings = M.str['availability_' + type]; 591 button = Y.Node.create('<button type="button" class="btn btn-default"' + 592 'id="' + id + '">' + pluginStrings.title + '</button>'); 593 button.on('click', this.getAddHandler(type, dialogRef), this); 594 li.appendChild(button); 595 label = Y.Node.create('<label for="' + id + '">' + 596 pluginStrings.description + '</label>'); 597 li.appendChild(label); 598 ul.appendChild(li); 599 } 600 // Extra entry for lists. 601 li = Y.Node.create('<li class="clearfix"></li>'); 602 id = 'availability_addrestriction_list_'; 603 button = Y.Node.create('<button type="button" class="btn btn-default"' + 604 'id="' + id + '">' + M.str.availability.condition_group + '</button>'); 605 button.on('click', this.getAddHandler(null, dialogRef), this); 606 li.appendChild(button); 607 label = Y.Node.create('<label for="' + id + '">' + 608 M.str.availability.condition_group_info + '</label>'); 609 li.appendChild(label); 610 ul.appendChild(li); 611 612 var config = { 613 headerContent : M.str.availability.addrestriction, 614 bodyContent : content, 615 additionalBaseClass : 'availability-dialogue', 616 draggable : true, 617 modal : true, 618 closeButton : false, 619 width : '450px' 620 }; 621 dialogRef.dialog = new M.core.dialogue(config); 622 dialogRef.dialog.show(); 623 cancel.on('click', function() { 624 dialogRef.dialog.destroy(); 625 // Focus the button they clicked originally. 626 this.inner.one('> .availability-button').one('button').focus(); 627 }, this); 628 }; 629 630 /** 631 * Gets an add handler function used by the dialogue to add a particular item. 632 * 633 * @method getAddHandler 634 * @param {String|Null} type Type name of plugin or null to add lists 635 * @param {Object} dialogRef Reference to object that contains dialog 636 * @param {M.core.dialogue} dialogRef.dialog Dialog object 637 * @return {Function} Add handler function to call when adding that thing 638 */ 639 M.core_availability.List.prototype.getAddHandler = function(type, dialogRef) { 640 return function() { 641 if (type) { 642 // Create an Item object to represent the child. 643 newItem = new M.core_availability.Item({ type: type, creating: true }, this.root); 644 } else { 645 // Create a new List object to represent the child. 646 newItem = new M.core_availability.List({ c: [], showc: true }, false, this.root); 647 } 648 // Add to list. 649 this.addChild(newItem); 650 // Update the form and list HTML. 651 M.core_availability.form.update(); 652 M.core_availability.form.rootList.renumber(); 653 this.updateHtml(); 654 // Hide dialog. 655 dialogRef.dialog.destroy(); 656 newItem.focusAfterAdd(); 657 }; 658 }; 659 660 /** 661 * Gets the value of the list ready to convert to JSON and fill form field. 662 * 663 * @method getValue 664 * @return {Object} Value of list suitable for use in JSON 665 */ 666 M.core_availability.List.prototype.getValue = function() { 667 // Work out operator from selects. 668 var value = {}; 669 value.op = this.node.one('.availability-neg').get('value') + 670 this.node.one('.availability-op').get('value'); 671 672 // Work out children from list. 673 value.c = []; 674 var i; 675 for (i = 0; i < this.children.length; i++) { 676 value.c.push(this.children[i].getValue()); 677 } 678 679 // Work out show/showc for root level. 680 if (this.root) { 681 if (this.isIndividualShowIcons()) { 682 value.showc = []; 683 for (i = 0; i < this.children.length; i++) { 684 value.showc.push(!this.children[i].eyeIcon.isHidden()); 685 } 686 } else { 687 value.show = !this.eyeIcon.isHidden(); 688 } 689 } 690 return value; 691 }; 692 693 /** 694 * Checks whether this list has any errors (incorrect user input). If so, 695 * an error string identifier in the form langfile:langstring should be pushed 696 * into the errors array. 697 * 698 * @method fillErrors 699 * @param {Array} errors Array of errors so far 700 */ 701 M.core_availability.List.prototype.fillErrors = function(errors) { 702 // List with no items is an error (except root). 703 if (this.children.length === 0 && !this.root) { 704 errors.push('availability:error_list_nochildren'); 705 } 706 // Pass to children. 707 for (var i = 0; i < this.children.length; i++) { 708 this.children[i].fillErrors(errors); 709 } 710 }; 711 712 /** 713 * Eye icon for this list (null if none). 714 * 715 * @property eyeIcon 716 * @type M.core_availability.EyeIcon 717 */ 718 M.core_availability.List.prototype.eyeIcon = null; 719 720 /** 721 * True if list is special root level list. 722 * 723 * @property root 724 * @type Boolean 725 */ 726 M.core_availability.List.prototype.root = false; 727 728 /** 729 * Array containing children (Lists or Items). 730 * 731 * @property children 732 * @type M.core_availability.List[]|M.core_availability.Item[] 733 */ 734 M.core_availability.List.prototype.children = null; 735 736 /** 737 * HTML outer node for list. 738 * 739 * @property node 740 * @type Y.Node 741 */ 742 M.core_availability.List.prototype.node = null; 743 744 /** 745 * HTML node for inner div that actually is the displayed list. 746 * 747 * @property node 748 * @type Y.Node 749 */ 750 M.core_availability.List.prototype.inner = null; 751 752 753 /** 754 * Represents a single condition. 755 * 756 * @class M.core_availability.Item 757 * @constructor 758 * @param {Object} json Decoded JSON value 759 * @param {Boolean} root True if this item is a child of the root list. 760 */ 761 M.core_availability.Item = function(json, root) { 762 this.pluginType = json.type; 763 if (M.core_availability.form.plugins[json.type] === undefined) { 764 // Handle undefined plugins. 765 this.plugin = null; 766 this.pluginNode = Y.Node.create('<div class="availability-warning">' + 767 M.str.availability.missingplugin + '</div>'); 768 } else { 769 // Plugin is known. 770 this.plugin = M.core_availability.form.plugins[json.type]; 771 this.pluginNode = this.plugin.getNode(json); 772 773 // Add a class with the plugin Frankenstyle name to make CSS easier in plugin. 774 this.pluginNode.addClass('availability_' + json.type); 775 } 776 777 this.node = Y.Node.create('<div class="availability-item"><h3 class="accesshide"></h3></div>'); 778 779 // Add eye icon if required. This icon is added for root items, but may be 780 // hidden depending on the selected list operator. 781 if (root) { 782 var shown = true; 783 if(json.showc !== undefined) { 784 shown = json.showc; 785 } 786 this.eyeIcon = new M.core_availability.EyeIcon(true, shown); 787 this.node.appendChild(this.eyeIcon.span); 788 } 789 790 // Add plugin controls. 791 this.pluginNode.addClass('availability-plugincontrols'); 792 this.node.appendChild(this.pluginNode); 793 794 // Add delete button for node. 795 var deleteIcon = new M.core_availability.DeleteIcon(this); 796 this.node.appendChild(deleteIcon.span); 797 798 // Add the invalid marker (empty). 799 this.node.appendChild(document.createTextNode(' ')); 800 this.node.appendChild(Y.Node.create('<span class="label label-warning"/>')); 801 }; 802 803 /** 804 * Obtains the value of this condition, which will be serialized into JSON 805 * format and stored in the form. 806 * 807 * @method getValue 808 * @return {Object} JavaScript object containing value of this item 809 */ 810 M.core_availability.Item.prototype.getValue = function() { 811 value = { 'type' : this.pluginType }; 812 if (this.plugin) { 813 this.plugin.fillValue(value, this.pluginNode); 814 } 815 return value; 816 }; 817 818 /** 819 * Checks whether this condition has any errors (incorrect user input). If so, 820 * an error string identifier in the form langfile:langstring should be pushed 821 * into the errors array. 822 * 823 * @method fillErrors 824 * @param {Array} errors Array of errors so far 825 */ 826 M.core_availability.Item.prototype.fillErrors = function(errors) { 827 var before = errors.length; 828 if (this.plugin) { 829 // Pass to plugin. 830 this.plugin.fillErrors(errors, this.pluginNode); 831 } else { 832 // Unknown plugin is an error 833 errors.push('core_availability:item_unknowntype'); 834 } 835 // If any errors were added, add the marker to this item. 836 var errorLabel = this.node.one('> .label-warning'); 837 if (errors.length !== before && !errorLabel.get('firstChild')) { 838 errorLabel.appendChild(document.createTextNode(M.str.availability.invalid)); 839 } else if (errors.length === before && errorLabel.get('firstChild')) { 840 errorLabel.get('firstChild').remove(); 841 } 842 }; 843 844 /** 845 * Renumbers the item. 846 * 847 * @method renumber 848 * @param {String} number Number to use in heading for this item 849 */ 850 M.core_availability.Item.prototype.renumber = function(number) { 851 // Update heading for item. 852 var headingParams = { number: number }; 853 if (this.plugin) { 854 headingParams.type = M.str['availability_' + this.pluginType].title; 855 } else { 856 headingParams.type = '[' + this.pluginType + ']'; 857 } 858 headingParams.number = number + ':'; 859 var heading = M.util.get_string('itemheading', 'availability', headingParams); 860 this.node.one('> h3').set('innerHTML', heading); 861 }; 862 863 /** 864 * Focuses something after a new item is added. 865 * 866 * @method focusAfterAdd 867 */ 868 M.core_availability.Item.prototype.focusAfterAdd = function() { 869 this.plugin.focusAfterAdd(this.pluginNode); 870 }; 871 872 /** 873 * Name of plugin. 874 * 875 * @property pluginType 876 * @type String 877 */ 878 M.core_availability.Item.prototype.pluginType = null; 879 880 /** 881 * Object representing plugin form controls. 882 * 883 * @property plugin 884 * @type Object 885 */ 886 M.core_availability.Item.prototype.plugin = null; 887 888 /** 889 * Eye icon for item. 890 * 891 * @property eyeIcon 892 * @type M.core_availability.EyeIcon 893 */ 894 M.core_availability.Item.prototype.eyeIcon = null; 895 896 /** 897 * HTML node for item. 898 * 899 * @property node 900 * @type Y.Node 901 */ 902 M.core_availability.Item.prototype.node = null; 903 904 /** 905 * Inner part of node that is owned by plugin. 906 * 907 * @property pluginNode 908 * @type Y.Node 909 */ 910 M.core_availability.Item.prototype.pluginNode = null; 911 912 913 /** 914 * Eye icon (to control show/hide of the activity if the user fails a condition). 915 * 916 * There are individual eye icons (show/hide control for a single condition) and 917 * 'all' eye icons (show/hide control that applies to the entire item, whatever 918 * reason it fails for). This is necessary because the individual conditions 919 * don't make sense for OR and AND NOT lists. 920 * 921 * @class M.core_availability.EyeIcon 922 * @constructor 923 * @param {Boolean} individual True if the icon is controlling a single condition 924 * @param {Boolean} shown True if icon is initially in shown state 925 */ 926 M.core_availability.EyeIcon = function(individual, shown) { 927 this.individual = individual; 928 this.span = Y.Node.create('<a class="availability-eye" href="#" role="button">'); 929 var icon = Y.Node.create('<img />'); 930 this.span.appendChild(icon); 931 932 // Set up button text and icon. 933 var suffix = individual ? '_individual' : '_all'; 934 var setHidden = function() { 935 icon.set('src', M.util.image_url('i/show', 'core')); 936 icon.set('alt', M.str.availability['hidden' + suffix]); 937 this.span.set('title', M.str.availability['hidden' + suffix] + ' \u2022 ' + 938 M.str.availability.show_verb); 939 }; 940 var setShown = function() { 941 icon.set('src', M.util.image_url('i/hide', 'core')); 942 icon.set('alt', M.str.availability['shown' + suffix]); 943 this.span.set('title', M.str.availability['shown' + suffix] + ' \u2022 ' + 944 M.str.availability.hide_verb); 945 }; 946 if(shown) { 947 setShown.call(this); 948 } else { 949 setHidden.call(this); 950 } 951 952 // Update when button is clicked. 953 var click = function(e) { 954 e.preventDefault(); 955 if (this.isHidden()) { 956 setShown.call(this); 957 } else { 958 setHidden.call(this); 959 } 960 M.core_availability.form.update(); 961 }; 962 this.span.on('click', click, this); 963 this.span.on('key', click, 'up:32', this); 964 this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); 965 }; 966 967 /** 968 * True if this eye icon is an individual one (see above). 969 * 970 * @property individual 971 * @type Boolean 972 */ 973 M.core_availability.EyeIcon.prototype.individual = false; 974 975 /** 976 * YUI node for the span that contains this icon. 977 * 978 * @property span 979 * @type Y.Node 980 */ 981 M.core_availability.EyeIcon.prototype.span = null; 982 983 /** 984 * Checks the current state of the icon. 985 * 986 * @method isHidden 987 * @return {Boolean} True if this icon is set to 'hidden' 988 */ 989 M.core_availability.EyeIcon.prototype.isHidden = function() { 990 var suffix = this.individual ? '_individual' : '_all'; 991 var compare = M.str.availability['hidden' + suffix]; 992 return this.span.one('img').get('alt') === compare; 993 }; 994 995 996 /** 997 * Delete icon (to delete an Item or List). 998 * 999 * @class M.core_availability.DeleteIcon 1000 * @constructor 1001 * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete 1002 */ 1003 M.core_availability.DeleteIcon = function(toDelete) { 1004 this.span = Y.Node.create('<a class="availability-delete" href="#" title="' + 1005 M.str.moodle['delete'] + '" role="button">'); 1006 var img = Y.Node.create('<img src="' + M.util.image_url('t/delete', 'core') + 1007 '" alt="' + M.str.moodle['delete'] + '" />'); 1008 this.span.appendChild(img); 1009 var click = function(e) { 1010 e.preventDefault(); 1011 M.core_availability.form.rootList.deleteDescendant(toDelete); 1012 M.core_availability.form.rootList.renumber(); 1013 }; 1014 this.span.on('click', click, this); 1015 this.span.on('key', click, 'up:32', this); 1016 this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this); 1017 }; 1018 1019 /** 1020 * YUI node for the span that contains this icon. 1021 * 1022 * @property span 1023 * @type Y.Node 1024 */ 1025 M.core_availability.DeleteIcon.prototype.span = null;
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 |