MDL-53848 form: hide entire group if hideIf test is applied to the group
[moodle.git] / lib / form / form.js
1 /**
2  * This file contains JS functionality required by mforms and is included automatically
3  * when required.
4  */
6 // Namespace for the form bits and bobs
7 M.form = M.form || {};
9 if (typeof M.form.dependencyManager === 'undefined') {
10     var dependencyManager = function() {
11         dependencyManager.superclass.constructor.apply(this, arguments);
12     };
13     Y.extend(dependencyManager, Y.Base, {
14         _locks: null,
15         _hides: null,
16         _dirty: null,
17         _nameCollections: null,
18         _fileinputs: null,
20         initializer: function() {
21             // Setup initial values for complex properties.
22             this._locks = {};
23             this._hides = {};
24             this._dirty = {};
26             // Setup event handlers.
27             Y.Object.each(this.get('dependencies'), function(value, i) {
28                 var elements = this.elementsByName(i);
29                 elements.each(function(node) {
30                     var nodeName = node.get('nodeName').toUpperCase();
31                     if (nodeName == 'INPUT') {
32                         if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
33                             node.on('click', this.updateEventDependencies, this);
34                         } else {
35                             node.on('blur', this.updateEventDependencies, this);
36                         }
37                         node.on('change', this.updateEventDependencies, this);
38                     } else if (nodeName == 'SELECT') {
39                         node.on('change', this.updateEventDependencies, this);
40                     } else {
41                         node.on('click', this.updateEventDependencies, this);
42                         node.on('blur', this.updateEventDependencies, this);
43                         node.on('change', this.updateEventDependencies, this);
44                     }
45                 }, this);
46             }, this);
48             // Handle the reset button.
49             this.get('form').get('elements').each(function(input) {
50                 if (input.getAttribute('type') == 'reset') {
51                     input.on('click', function() {
52                         this.get('form').reset();
53                         this.updateAllDependencies();
54                     }, this);
55                 }
56             }, this);
58             this.updateAllDependencies();
59         },
61         /**
62          * Initializes the mapping from element name to YUI NodeList
63          */
64         initElementsByName: function() {
65             var names = {};
67             // Collect element names.
68             Y.Object.each(this.get('dependencies'), function(conditions, i) {
69                 names[i] = new Y.NodeList();
70                 for (var condition in conditions) {
71                     for (var value in conditions[condition]) {
72                         for (var hide in conditions[condition][value]) {
73                             for (var ei in conditions[condition][value][hide]) {
74                                 names[conditions[condition][value][hide][ei]] = new Y.NodeList();
75                             }
76                         }
77                     }
78                 }
79             });
81             // Locate elements for each name.
82             this.get('form').get('elements').each(function(node) {
83                 var name = node.getAttribute('name');
84                 if (({}).hasOwnProperty.call(names, name)) {
85                     names[name].push(node);
86                 }
87             });
88             // Locate any groups with the given name.
89             this.get('form').all('.fitem').each(function(node) {
90                 var name = node.getData('groupname');
91                 if (name && ({}).hasOwnProperty.call(names, name)) {
92                     names[name].push(node);
93                 }
94             });
95             this._nameCollections = names;
96         },
98         /**
99          * Gets all elements in the form by their name and returns
100          * a YUI NodeList
101          *
102          * @param {String} name The form element name.
103          * @return {Y.NodeList}
104          */
105         elementsByName: function(name) {
106             if (!this._nameCollections) {
107                 this.initElementsByName();
108             }
109             if (!({}).hasOwnProperty.call(this._nameCollections, name)) {
110                 return new Y.NodeList();
111             }
112             return this._nameCollections[name];
113         },
115         /**
116          * Checks the dependencies the form has an makes any changes to the
117          * form that are required.
118          *
119          * Changes are made by functions title _dependency{Dependencytype}
120          * and more can easily be introduced by defining further functions.
121          *
122          * @param {EventFacade | null} e The event, if any.
123          * @param {String} dependon The form element name to check dependencies against.
124          * @return {Boolean}
125          */
126         checkDependencies: function(e, dependon) {
127             var dependencies = this.get('dependencies'),
128                 tohide = {},
129                 tolock = {},
130                 condition, value, isHide, lock, hide,
131                 checkfunction, result, elements;
132             if (!({}).hasOwnProperty.call(dependencies, dependon)) {
133                 return true;
134             }
135             elements = this.elementsByName(dependon);
136             for (condition in dependencies[dependon]) {
137                 for (value in dependencies[dependon][condition]) {
138                     for (isHide in dependencies[dependon][condition][value]) {
139                         checkfunction = '_dependency' + condition[0].toUpperCase() + condition.slice(1);
140                         if (Y.Lang.isFunction(this[checkfunction])) {
141                             result = this[checkfunction].apply(this, [elements, value, !!isHide, e]);
142                         } else {
143                             result = this._dependencyDefault(elements, value, !!isHide, e);
144                         }
145                         lock = result.lock || false;
146                         hide = result.hide || false;
147                         for (var ei in dependencies[dependon][condition][value][isHide]) {
148                             var eltolock = dependencies[dependon][condition][value][isHide][ei];
149                             if (({}).hasOwnProperty.call(tohide, eltolock)) {
150                                 tohide[eltolock] = tohide[eltolock] || hide;
151                             } else {
152                                 tohide[eltolock] = hide;
153                             }
155                             if (({}).hasOwnProperty.call(tolock, eltolock)) {
156                                 tolock[eltolock] = tolock[eltolock] || lock;
157                             } else {
158                                 tolock[eltolock] = lock;
159                             }
160                         }
161                     }
162                 }
163             }
165             for (var el in tolock) {
166                 var needsupdate = false;
167                 if (!({}).hasOwnProperty.call(this._locks, el)) {
168                     this._locks[el] = {};
169                 }
170                 if (({}).hasOwnProperty.call(tolock, el) && tolock[el]) {
171                     if (!({}).hasOwnProperty.call(this._locks[el], dependon) || this._locks[el][dependon]) {
172                         this._locks[el][dependon] = true;
173                         needsupdate = true;
174                     }
175                 } else if (({}).hasOwnProperty.call(this._locks[el], dependon) && this._locks[el][dependon]) {
176                     delete this._locks[el][dependon];
177                     needsupdate = true;
178                 }
180                 if (!({}).hasOwnProperty.call(this._hides, el)) {
181                     this._hides[el] = {};
182                 }
183                 if (({}).hasOwnProperty.call(tohide, el) && tohide[el]) {
184                     if (!({}).hasOwnProperty.call(this._hides[el], dependon) || this._hides[el][dependon]) {
185                         this._hides[el][dependon] = true;
186                         needsupdate = true;
187                     }
188                 } else if (({}).hasOwnProperty.call(this._hides[el], dependon) && this._hides[el][dependon]) {
189                     delete this._hides[el][dependon];
190                     needsupdate = true;
191                 }
193                 if (needsupdate) {
194                     this._dirty[el] = true;
195                 }
196             }
198             return true;
199         },
200         /**
201          * Update all dependencies in form
202          */
203         updateAllDependencies: function() {
204             Y.Object.each(this.get('dependencies'), function(value, name) {
205                 this.checkDependencies(null, name);
206             }, this);
208             this.updateForm();
209         },
210         /**
211          * Update dependencies associated with event
212          *
213          * @param {Event} e The event.
214          */
215         updateEventDependencies: function(e) {
216             var el = e.target.getAttribute('name');
217             this.checkDependencies(e, el);
218             this.updateForm();
219         },
220         /**
221          * Flush pending changes to the form
222          */
223         updateForm: function() {
224             var el;
225             for (el in this._dirty) {
226                 if (({}).hasOwnProperty.call(this._locks, el)) {
227                     this._disableElement(el, !Y.Object.isEmpty(this._locks[el]));
228                 }
229                 if (({}).hasOwnProperty.call(this._hides, el)) {
230                     this._hideElement(el, !Y.Object.isEmpty(this._hides[el]));
231                 }
232             }
234             this._dirty = {};
235         },
236         /**
237          * Disables or enables all form elements with the given name
238          *
239          * @param {String} name The form element name.
240          * @param {Boolean} disabled True to disable, false to enable.
241          */
242         _disableElement: function(name, disabled) {
243             var els = this.elementsByName(name);
244             var filepicker = this.isFilePicker(name);
245             els.each(function(node) {
246                 if (disabled) {
247                     node.setAttribute('disabled', 'disabled');
248                 } else {
249                     node.removeAttribute('disabled');
250                 }
252                 // Extra code to disable filepicker or filemanager form elements
253                 if (filepicker) {
254                     var fitem = node.ancestor('.fitem');
255                     if (fitem) {
256                         if (disabled) {
257                             fitem.addClass('disabled');
258                         } else {
259                             fitem.removeClass('disabled');
260                         }
261                     }
262                 }
263             });
264         },
265         /**
266          * Hides or shows all form elements with the given name.
267          *
268          * @param {String} name The form element name.
269          * @param {Boolean} hidden True to hide, false to show.
270          */
271         _hideElement: function(name, hidden) {
272             var els = this.elementsByName(name);
273             els.each(function(node) {
274                 var e = node.ancestor('.fitem', true);
275                 if (e) {
276                     (hidden) ? e.setAttribute('hidden', 'hidden') : e.removeAttribute('hidden');
277                     e.setStyles({
278                         display: (hidden) ? 'none' : ''
279                     });
280                 }
281             });
282         },
283         /**
284          * Is the form element inside a filepicker or filemanager?
285          *
286          * @param {String} el The form element name.
287          * @return {Boolean}
288          */
289         isFilePicker: function(el) {
290             if (!this._fileinputs) {
291                 var fileinputs = {};
292                 var selector = '.fitem [data-fieldtype="filepicker"] input,.fitem [data-fieldtype="filemanager"] input';
293                 var els = this.get('form').all(selector);
294                 els.each(function(node) {
295                     fileinputs[node.getAttribute('name')] = true;
296                 });
297                 this._fileinputs = fileinputs;
298             }
300             if (({}).hasOwnProperty.call(this._fileinputs, el)) {
301                 return this._fileinputs[el] || false;
302             }
304             return false;
305         },
306         _dependencyNotchecked: function(elements, value, isHide) {
307             var lock = false;
308             elements.each(function() {
309                 if (this.getAttribute('type').toLowerCase() == 'hidden' &&
310                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
311                     // This is the hidden input that is part of an advcheckbox.
312                     return;
313                 }
314                 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
315                     return;
316                 }
317                 lock = lock || !Y.Node.getDOMNode(this).checked;
318             });
319             return {
320                 lock: lock,
321                 hide: isHide ? lock : false
322             };
323         },
324         _dependencyChecked: function(elements, value, isHide) {
325             var lock = false;
326             elements.each(function() {
327                 if (this.getAttribute('type').toLowerCase() == 'hidden' &&
328                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
329                     // This is the hidden input that is part of an advcheckbox.
330                     return;
331                 }
332                 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
333                     return;
334                 }
335                 lock = lock || Y.Node.getDOMNode(this).checked;
336             });
337             return {
338                 lock: lock,
339                 hide: isHide ? lock : false
340             };
341         },
342         _dependencyNoitemselected: function(elements, value, isHide) {
343             var lock = false;
344             elements.each(function() {
345                 lock = lock || this.get('selectedIndex') == -1;
346             });
347             return {
348                 lock: lock,
349                 hide: isHide ? lock : false
350             };
351         },
352         _dependencyEq: function(elements, value, isHide) {
353             var lock = false;
354             var hiddenVal = false;
355             var options, v, selected, values;
356             elements.each(function() {
357                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
358                     return;
359                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
360                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
361                     // This is the hidden input that is part of an advcheckbox.
362                     hiddenVal = (this.get('value') == value);
363                     return;
364                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
365                     lock = lock || hiddenVal;
366                     return;
367                 }
368                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
369                     // Check for filepicker status.
370                     var elementname = this.getAttribute('name');
371                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
372                         lock = false;
373                     } else {
374                         lock = true;
375                     }
376                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
377                     // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
378                     // when multiple values have to be selected at the same time.
379                     values = value.split('|');
380                     selected = [];
381                     options = this.get('options');
382                     options.each(function() {
383                         if (this.get('selected')) {
384                             selected[selected.length] = this.get('value');
385                         }
386                     });
387                     if (selected.length > 0 && selected.length === values.length) {
388                         for (var i in selected) {
389                             v = selected[i];
390                             if (values.indexOf(v) > -1) {
391                                 lock = true;
392                             } else {
393                                 lock = false;
394                                 return;
395                             }
396                         }
397                     } else {
398                         lock = false;
399                     }
400                 } else {
401                     lock = lock || this.get('value') == value;
402                 }
403             });
404             return {
405                 lock: lock,
406                 hide: isHide ? lock : false
407             };
408         },
409         /**
410          * Lock the given field if the field value is in the given set of values.
411          *
412          * @param {Array} elements
413          * @param {String} values Single value or pipe (|) separated values when multiple
414          * @returns {{lock: boolean, hide: boolean}}
415          * @private
416          */
417         _dependencyIn: function(elements, values, isHide) {
418             // A pipe (|) is used as a value separator
419             // when multiple values have to be passed on at the same time.
420             values = values.split('|');
421             var lock = false;
422             var hiddenVal = false;
423             var options, v, selected, value;
424             elements.each(function() {
425                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
426                     return;
427                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
428                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
429                     // This is the hidden input that is part of an advcheckbox.
430                     hiddenVal = (values.indexOf(this.get('value')) > -1);
431                     return;
432                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
433                     lock = lock || hiddenVal;
434                     return;
435                 }
436                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
437                     // Check for filepicker status.
438                     var elementname = this.getAttribute('name');
439                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
440                         lock = false;
441                     } else {
442                         lock = true;
443                     }
444                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
445                     // Multiple selects can have one or more value assigned.
446                     selected = [];
447                     options = this.get('options');
448                     options.each(function() {
449                         if (this.get('selected')) {
450                             selected[selected.length] = this.get('value');
451                         }
452                     });
453                     if (selected.length > 0 && selected.length === values.length) {
454                         for (var i in selected) {
455                             v = selected[i];
456                             if (values.indexOf(v) > -1) {
457                                 lock = true;
458                             } else {
459                                 lock = false;
460                                 return;
461                             }
462                         }
463                     } else {
464                         lock = false;
465                     }
466                 } else {
467                     value = this.get('value');
468                     lock = lock || (values.indexOf(value) > -1);
469                 }
470             });
471             return {
472                 lock: lock,
473                 hide: isHide ? lock : false
474             };
475         },
476         _dependencyHide: function(elements, value) {
477             return {
478                 lock: false,
479                 hide: true
480             };
481         },
482         _dependencyDefault: function(elements, value, isHide) {
483             var lock = false,
484                 hiddenVal = false,
485                 values
486                 ;
487             elements.each(function() {
488                 var selected;
489                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
490                     return;
491                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
492                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
493                     // This is the hidden input that is part of an advcheckbox.
494                     hiddenVal = (this.get('value') != value);
495                     return;
496                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
497                     lock = lock || hiddenVal;
498                     return;
499                 }
500                 // Check for filepicker status.
501                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
502                     var elementname = this.getAttribute('name');
503                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
504                         lock = true;
505                     } else {
506                         lock = false;
507                     }
508                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
509                     // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
510                     // when multiple values have to be selected at the same time.
511                     values = value.split('|');
512                     selected = [];
513                     this.get('options').each(function() {
514                         if (this.get('selected')) {
515                             selected[selected.length] = this.get('value');
516                         }
517                     });
518                     if (selected.length > 0 && selected.length === values.length) {
519                         for (var i in selected) {
520                             if (values.indexOf(selected[i]) > -1) {
521                                 lock = false;
522                             } else {
523                                 lock = true;
524                                 return;
525                             }
526                         }
527                     } else {
528                         lock = true;
529                     }
530                 } else {
531                     lock = lock || this.get('value') != value;
532                 }
533             });
534             return {
535                 lock: lock,
536                 hide: isHide ? lock : false
537             };
538         }
539     }, {
540         NAME: 'mform-dependency-manager',
541         ATTRS: {
542             form: {
543                 setter: function(value) {
544                     return Y.one('#' + value);
545                 },
546                 value: null
547             },
549             dependencies: {
550                 value: {}
551             }
552         }
553     });
555     M.form.dependencyManager = dependencyManager;
558 /**
559  * Stores a list of the dependencyManager for each form on the page.
560  */
561 M.form.dependencyManagers = {};
563 /**
564  * Initialises a manager for a forms dependencies.
565  * This should happen once per form.
566  *
567  * @param {YUI} Y YUI3 instance
568  * @param {String} formid ID of the form
569  * @param {Array} dependencies array
570  * @return {M.form.dependencyManager}
571  */
572 M.form.initFormDependencies = function(Y, formid, dependencies) {
574     // If the dependencies isn't an array or object we don't want to
575     // know about it
576     if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
577         return false;
578     }
580     /**
581      * Fixes an issue with YUI's processing method of form.elements property
582      * in Internet Explorer.
583      *     http://yuilibrary.com/projects/yui3/ticket/2528030
584      */
585     Y.Node.ATTRS.elements = {
586         getter: function() {
587             return Y.all(new Y.Array(this._node.elements, 0, true));
588         }
589     };
591     M.form.dependencyManagers[formid] = new M.form.dependencyManager({form: formid, dependencies: dependencies});
592     return M.form.dependencyManagers[formid];
593 };
595 /**
596  * Update the state of a form. You need to call this after, for example, changing
597  * the state of some of the form input elements in your own code, in order that
598  * things like the disableIf state of elements can be updated.
599  *
600  * @param {String} formid ID of the form
601  */
602 M.form.updateFormState = function(formid) {
603     if (formid in M.form.dependencyManagers) {
604         M.form.dependencyManagers[formid].updateAllDependencies();
605     }
606 };