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