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