MDL-44992 Conditional availability tab focus bug
[moodle.git] / availability / yui / build / moodle-core_availability-form / moodle-core_availability-form-debug.js
CommitLineData
46d70f05 1YUI.add('moodle-core_availability-form', function (Y, NAME) {
2
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 */
20M.core_availability = M.core_availability || {};
21
22/**
23 * Core static functions for availability settings in editing form.
24 *
25 * @class M.core_availability.form
26 * @static
27 */
28M.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 : {},
36
37 /**
38 * Availability field (textarea).
39 *
40 * @property field
41 * @type Y.Node
42 */
43 field : null,
44
45 /**
46 * Main div that replaces the availability field.
47 *
48 * @property mainDiv
49 * @type Y.Node
50 */
51 mainDiv : null,
52
53 /**
54 * Object that represents the root of the tree.
55 *
56 * @property rootList
57 * @type M.core_availability.List
58 */
59 rootList : null,
60
61 /**
62 * Counter used when creating anything that needs an id.
63 *
64 * @property idCounter
65 * @type Number
66 */
67 idCounter : 0,
68
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 }
82
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');
96
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);
106
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();
111
112 // Mark main area as dynamically updated.
113 this.mainDiv.setAttribute('aria-live', 'polite');
114
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 },
121
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();
132
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 }
139
140 // Set into hidden form field, JS-encoded.
141 this.field.set('value', Y.JSON.stringify(jsValue));
142 }
143};
144
145
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 */
152M.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,
160
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 },
174
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 },
186
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 },
198
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 },
212
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 },
228
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};
240
241
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 */
251M.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 = [];
255
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');
282
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 }
300
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);
309
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 }
314
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);
320
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 }
343
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 }
361
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);
373
374 // Update HTML to hide unnecessary parts.
375 this.updateHtml();
376};
377
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 */
385M.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};
397
398/**
399 * Focuses something after a new list is added.
400 *
401 * @method focusAfterAdd
402 */
403M.core_availability.List.prototype.focusAfterAdd = function() {
404 this.inner.one('button').focus();
405};
406
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 */
422M.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};
430
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 */
437M.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);
450
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};
457
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 */
464M.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 }
482
483 // For root list, control eye icons.
484 if (this.root) {
485 var showEyes = this.isIndividualShowIcons();
486
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 }
496
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 }
504
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};
516
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 */
527M.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 }
559
560 return false;
561};
562
563/**
564 * Shows the 'add restriction' dialogue box.
565 *
566 * @method clickAdd
567 */
568M.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');
575
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);
609
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};
627
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 */
637M.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};
657
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 */
664M.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');
669
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 }
676
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};
690
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 */
699M.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};
709
710/**
711 * Eye icon for this list (null if none).
712 *
713 * @property eyeIcon
714 * @type M.core_availability.EyeIcon
715 */
716M.core_availability.List.prototype.eyeIcon = null;
717
718/**
719 * True if list is special root level list.
720 *
721 * @property root
722 * @type Boolean
723 */
724M.core_availability.List.prototype.root = false;
725
726/**
727 * Array containing children (Lists or Items).
728 *
729 * @property children
730 * @type M.core_availability.List[]|M.core_availability.Item[]
731 */
732M.core_availability.List.prototype.children = null;
733
734/**
735 * HTML outer node for list.
736 *
737 * @property node
738 * @type Y.Node
739 */
740M.core_availability.List.prototype.node = null;
741
742/**
743 * HTML node for inner div that actually is the displayed list.
744 *
745 * @property node
746 * @type Y.Node
747 */
748M.core_availability.List.prototype.inner = null;
749
750
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 */
759M.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);
770
771 // Add a class with the plugin Frankenstyle name to make CSS easier in plugin.
772 this.pluginNode.addClass('availability_' + json.type);
773 }
774
775 this.node = Y.Node.create('<div class="availability-item"><h3 class="accesshide"></h3></div>');
776
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 }
787
788 // Add plugin controls.
789 this.pluginNode.addClass('availability-plugincontrols');
790 this.node.appendChild(this.pluginNode);
791
792 // Add delete button for node.
793 var deleteIcon = new M.core_availability.DeleteIcon(this);
794 this.node.appendChild(deleteIcon.span);
795
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};
800
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 */
808M.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};
815
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 */
824M.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};
841
842/**
843 * Renumbers the item.
844 *
845 * @method renumber
846 * @param {String} number Number to use in heading for this item
847 */
848M.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};
860
861/**
862 * Focuses something after a new item is added.
863 *
864 * @method focusAfterAdd
865 */
866M.core_availability.Item.prototype.focusAfterAdd = function() {
867 this.plugin.focusAfterAdd(this.pluginNode);
868};
869
870/**
871 * Name of plugin.
872 *
873 * @property pluginType
874 * @type String
875 */
876M.core_availability.Item.prototype.pluginType = null;
877
878/**
879 * Object representing plugin form controls.
880 *
881 * @property plugin
882 * @type Object
883 */
884M.core_availability.Item.prototype.plugin = null;
885
886/**
887 * Eye icon for item.
888 *
889 * @property eyeIcon
890 * @type M.core_availability.EyeIcon
891 */
892M.core_availability.Item.prototype.eyeIcon = null;
893
894/**
895 * HTML node for item.
896 *
897 * @property node
898 * @type Y.Node
899 */
900M.core_availability.Item.prototype.node = null;
901
902/**
903 * Inner part of node that is owned by plugin.
904 *
905 * @property pluginNode
906 * @type Y.Node
907 */
908M.core_availability.Item.prototype.pluginNode = null;
909
910
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 */
924M.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"/>');
929
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 }
949
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);
0a8de7f6 961 hideButton.on('key', click, 'up:enter', this);
46d70f05 962};
963
964/**
965 * True if this eye icon is an individual one (see above).
966 *
967 * @property individual
968 * @type Boolean
969 */
970M.core_availability.EyeIcon.prototype.individual = false;
971
972/**
973 * YUI node for the span that contains this icon.
974 *
975 * @property span
976 * @type Y.Node
977 */
978M.core_availability.EyeIcon.prototype.span = null;
979
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 */
986M.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};
991
992
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 */
1000M.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};
1015
1016/**
1017 * YUI node for the span that contains this icon.
1018 *
1019 * @property span
1020 * @type Y.Node
1021 */
1022M.core_availability.DeleteIcon.prototype.span = null;
1023
1024
1025}, '@VERSION@', {"requires": ["base", "node", "event", "panel", "moodle-core-notification-dialogue", "json"]});