MDL-45449 Availabillity settings - new visible/hidden control (eye icon) is hard...
[moodle.git] / availability / yui / src / form / js / form.js
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 || {};
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 : {},
35     /**
36      * Availability field (textarea).
37      *
38      * @property field
39      * @type Y.Node
40      */
41     field : null,
43     /**
44      * Main div that replaces the availability field.
45      *
46      * @property mainDiv
47      * @type Y.Node
48      */
49     mainDiv : null,
51     /**
52      * Object that represents the root of the tree.
53      *
54      * @property rootList
55      * @type M.core_availability.List
56      */
57     rootList : null,
59     /**
60      * Counter used when creating anything that needs an id.
61      *
62      * @property idCounter
63      * @type Number
64      */
65     idCounter : 0,
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         }
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');
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);
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();
114         // Mark main area as dynamically updated.
115         this.mainDiv.setAttribute('aria-live', 'polite');
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     },
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();
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         }
142         // Set into hidden form field, JS-encoded.
143         this.field.set('value', Y.JSON.stringify(jsValue));
144     }
145 };
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,
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     },
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     },
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     },
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     },
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     },
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 };
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 = [];
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');
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     }
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);
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     }
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);
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         }
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     }
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);
376     // Update HTML to hide unnecessary parts.
377     this.updateHtml();
378 };
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 };
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 };
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 };
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);
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 };
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     }
485     // For root list, control eye icons.
486     if (this.root) {
487         var showEyes = this.isIndividualShowIcons();
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         }
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     }
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 };
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     }
562     return false;
563 };
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');
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);
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 };
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 };
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');
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     }
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 };
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 };
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;
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;
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;
736 /**
737  * HTML outer node for list.
738  *
739  * @property node
740  * @type Y.Node
741  */
742 M.core_availability.List.prototype.node = null;
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;
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);
773         // Add a class with the plugin Frankenstyle name to make CSS easier in plugin.
774         this.pluginNode.addClass('availability_' + json.type);
775     }
777     this.node = Y.Node.create('<div class="availability-item"><h3 class="accesshide"></h3></div>');
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     }
790     // Add plugin controls.
791     this.pluginNode.addClass('availability-plugincontrols');
792     this.node.appendChild(this.pluginNode);
794     // Add delete button for node.
795     var deleteIcon = new M.core_availability.DeleteIcon(this);
796     this.node.appendChild(deleteIcon.span);
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 };
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 };
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 };
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 };
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 };
872 /**
873  * Name of plugin.
874  *
875  * @property pluginType
876  * @type String
877  */
878 M.core_availability.Item.prototype.pluginType = null;
880 /**
881  * Object representing plugin form controls.
882  *
883  * @property plugin
884  * @type Object
885  */
886 M.core_availability.Item.prototype.plugin = null;
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;
896 /**
897  * HTML node for item.
898  *
899  * @property node
900  * @type Y.Node
901  */
902 M.core_availability.Item.prototype.node = null;
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;
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 iconBase = M.cfg.wwwroot + '/theme/image.php/' + M.cfg.theme + '/core/' + M.cfg.themerev;
930     var icon = Y.Node.create('<img />');
931     this.span.appendChild(icon);
933     // Set up button text and icon.
934     var suffix = individual ? '_individual' : '_all';
935     var setHidden = function() {
936         icon.set('src', iconBase + '/i/show');
937         icon.set('alt', M.str.availability['hidden' + suffix]);
938         this.span.set('title', M.str.availability['hidden' + suffix] + ' \u2022 ' +
939                 M.str.availability.show_verb);
940     };
941     var setShown = function() {
942         icon.set('src', iconBase + '/i/hide');
943         icon.set('alt', M.str.availability['shown' + suffix]);
944         this.span.set('title', M.str.availability['shown' + suffix] + ' \u2022 ' +
945                 M.str.availability.hide_verb);
946     };
947     if(shown) {
948         setShown.call(this);
949     } else {
950         setHidden.call(this);
951     }
953     // Update when button is clicked.
954     var click = function(e) {
955         e.preventDefault();
956         if (this.isHidden()) {
957             setShown.call(this);
958         } else {
959             setHidden.call(this);
960         }
961         M.core_availability.form.update();
962     };
963     this.span.on('click', click, this);
964     this.span.on('key', click, 'up:32', this);
965     this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this);
966 };
968 /**
969  * True if this eye icon is an individual one (see above).
970  *
971  * @property individual
972  * @type Boolean
973  */
974 M.core_availability.EyeIcon.prototype.individual = false;
976 /**
977  * YUI node for the span that contains this icon.
978  *
979  * @property span
980  * @type Y.Node
981  */
982 M.core_availability.EyeIcon.prototype.span = null;
984 /**
985  * Checks the current state of the icon.
986  *
987  * @method isHidden
988  * @return {Boolean} True if this icon is set to 'hidden'
989  */
990 M.core_availability.EyeIcon.prototype.isHidden = function() {
991     var suffix = this.individual ? '_individual' : '_all';
992     var compare = M.str.availability['hidden' + suffix];
993     return this.span.one('img').get('alt') === compare;
994 };
997 /**
998  * Delete icon (to delete an Item or List).
999  *
1000  * @class M.core_availability.DeleteIcon
1001  * @constructor
1002  * @param {M.core_availability.Item|M.core_availability.List} toDelete Thing to delete
1003  */
1004 M.core_availability.DeleteIcon = function(toDelete) {
1005     this.span = Y.Node.create('<a class="availability-delete" href="#" title="' +
1006             M.str.moodle['delete'] + '" role="button">');
1007     var img = Y.Node.create('<img src="' +
1008             M.cfg.wwwroot + '/theme/image.php/' + M.cfg.theme + '/core/' + M.cfg.themerev +
1009             '/t/delete" alt="' + M.str.moodle['delete'] + '" />');
1010     this.span.appendChild(img);
1011     var click = function(e) {
1012         e.preventDefault();
1013         M.core_availability.form.rootList.deleteDescendant(toDelete);
1014         M.core_availability.form.rootList.renumber();
1015     };
1016     this.span.on('click', click, this);
1017     this.span.on('key', click, 'up:32', this);
1018     this.span.on('key', function(e) { e.preventDefault(); }, 'down:32', this);
1019 };
1021 /**
1022  * YUI node for the span that contains this icon.
1023  *
1024  * @property span
1025  * @type Y.Node
1026  */
1027 M.core_availability.DeleteIcon.prototype.span = null;