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