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