MDL-59811 enrol: Move get_user_enrolment_actions logic to parent class
[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 === "1"), e]);
142                         } else {
143                             result = this._dependencyDefault(elements, value, (isHide === "1"), 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                     if (hidden) {
277                         e.setAttribute('hidden', 'hidden');
278                     } else {
279                         e.removeAttribute('hidden');
280                     }
281                     e.setStyles({
282                         display: (hidden) ? 'none' : ''
283                     });
284                 }
285             });
286         },
287         /**
288          * Is the form element inside a filepicker or filemanager?
289          *
290          * @param {String} el The form element name.
291          * @return {Boolean}
292          */
293         isFilePicker: function(el) {
294             if (!this._fileinputs) {
295                 var fileinputs = {};
296                 var selector = '.fitem [data-fieldtype="filepicker"] input,.fitem [data-fieldtype="filemanager"] input';
297                 var els = this.get('form').all(selector);
298                 els.each(function(node) {
299                     fileinputs[node.getAttribute('name')] = true;
300                 });
301                 this._fileinputs = fileinputs;
302             }
304             if (({}).hasOwnProperty.call(this._fileinputs, el)) {
305                 return this._fileinputs[el] || false;
306             }
308             return false;
309         },
310         _dependencyNotchecked: function(elements, value, isHide) {
311             var lock = false;
312             elements.each(function() {
313                 if (this.getAttribute('type').toLowerCase() == 'hidden' &&
314                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
315                     // This is the hidden input that is part of an advcheckbox.
316                     return;
317                 }
318                 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
319                     return;
320                 }
321                 lock = lock || !Y.Node.getDOMNode(this).checked;
322             });
323             return {
324                 lock: lock,
325                 hide: isHide ? lock : false
326             };
327         },
328         _dependencyChecked: function(elements, value, isHide) {
329             var lock = false;
330             elements.each(function() {
331                 if (this.getAttribute('type').toLowerCase() == 'hidden' &&
332                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
333                     // This is the hidden input that is part of an advcheckbox.
334                     return;
335                 }
336                 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
337                     return;
338                 }
339                 lock = lock || Y.Node.getDOMNode(this).checked;
340             });
341             return {
342                 lock: lock,
343                 hide: isHide ? lock : false
344             };
345         },
346         _dependencyNoitemselected: function(elements, value, isHide) {
347             var lock = false;
348             elements.each(function() {
349                 lock = lock || this.get('selectedIndex') == -1;
350             });
351             return {
352                 lock: lock,
353                 hide: isHide ? lock : false
354             };
355         },
356         _dependencyEq: function(elements, value, isHide) {
357             var lock = false;
358             var hiddenVal = false;
359             var options, v, selected, values;
360             elements.each(function() {
361                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
362                     return;
363                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
364                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
365                     // This is the hidden input that is part of an advcheckbox.
366                     hiddenVal = (this.get('value') == value);
367                     return;
368                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
369                     lock = lock || hiddenVal;
370                     return;
371                 }
372                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
373                     // Check for filepicker status.
374                     var elementname = this.getAttribute('name');
375                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
376                         lock = false;
377                     } else {
378                         lock = true;
379                     }
380                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
381                     // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
382                     // when multiple values have to be selected at the same time.
383                     values = value.split('|');
384                     selected = [];
385                     options = this.get('options');
386                     options.each(function() {
387                         if (this.get('selected')) {
388                             selected[selected.length] = this.get('value');
389                         }
390                     });
391                     if (selected.length > 0 && selected.length === values.length) {
392                         for (var i in selected) {
393                             v = selected[i];
394                             if (values.indexOf(v) > -1) {
395                                 lock = true;
396                             } else {
397                                 lock = false;
398                                 return;
399                             }
400                         }
401                     } else {
402                         lock = false;
403                     }
404                 } else {
405                     lock = lock || this.get('value') == value;
406                 }
407             });
408             return {
409                 lock: lock,
410                 hide: isHide ? lock : false
411             };
412         },
413         /**
414          * Lock the given field if the field value is in the given set of values.
415          *
416          * @param {Array} elements
417          * @param {String} values Single value or pipe (|) separated values when multiple
418          * @returns {{lock: boolean, hide: boolean}}
419          * @private
420          */
421         _dependencyIn: function(elements, values, isHide) {
422             // A pipe (|) is used as a value separator
423             // when multiple values have to be passed on at the same time.
424             values = values.split('|');
425             var lock = false;
426             var hiddenVal = false;
427             var options, v, selected, value;
428             elements.each(function() {
429                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
430                     return;
431                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
432                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
433                     // This is the hidden input that is part of an advcheckbox.
434                     hiddenVal = (values.indexOf(this.get('value')) > -1);
435                     return;
436                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
437                     lock = lock || hiddenVal;
438                     return;
439                 }
440                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
441                     // Check for filepicker status.
442                     var elementname = this.getAttribute('name');
443                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
444                         lock = false;
445                     } else {
446                         lock = true;
447                     }
448                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
449                     // Multiple selects can have one or more value assigned.
450                     selected = [];
451                     options = this.get('options');
452                     options.each(function() {
453                         if (this.get('selected')) {
454                             selected[selected.length] = this.get('value');
455                         }
456                     });
457                     if (selected.length > 0 && selected.length === values.length) {
458                         for (var i in selected) {
459                             v = selected[i];
460                             if (values.indexOf(v) > -1) {
461                                 lock = true;
462                             } else {
463                                 lock = false;
464                                 return;
465                             }
466                         }
467                     } else {
468                         lock = false;
469                     }
470                 } else {
471                     value = this.get('value');
472                     lock = lock || (values.indexOf(value) > -1);
473                 }
474             });
475             return {
476                 lock: lock,
477                 hide: isHide ? lock : false
478             };
479         },
480         _dependencyHide: function(elements, value) {
481             return {
482                 lock: false,
483                 hide: true
484             };
485         },
486         _dependencyDefault: function(elements, value, isHide) {
487             var lock = false,
488                 hiddenVal = false,
489                 values
490                 ;
491             elements.each(function() {
492                 var selected;
493                 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
494                     return;
495                 } else if (this.getAttribute('type').toLowerCase() == 'hidden' &&
496                         !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
497                     // This is the hidden input that is part of an advcheckbox.
498                     hiddenVal = (this.get('value') != value);
499                     return;
500                 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
501                     lock = lock || hiddenVal;
502                     return;
503                 }
504                 // Check for filepicker status.
505                 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
506                     var elementname = this.getAttribute('name');
507                     if (elementname && M.form_filepicker.instances[elementname].fileadded) {
508                         lock = true;
509                     } else {
510                         lock = false;
511                     }
512                 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
513                     // Multiple selects can have one or more value assigned. A pipe (|) is used as a value separator
514                     // when multiple values have to be selected at the same time.
515                     values = value.split('|');
516                     selected = [];
517                     this.get('options').each(function() {
518                         if (this.get('selected')) {
519                             selected[selected.length] = this.get('value');
520                         }
521                     });
522                     if (selected.length > 0 && selected.length === values.length) {
523                         for (var i in selected) {
524                             if (values.indexOf(selected[i]) > -1) {
525                                 lock = false;
526                             } else {
527                                 lock = true;
528                                 return;
529                             }
530                         }
531                     } else {
532                         lock = true;
533                     }
534                 } else {
535                     lock = lock || this.get('value') != value;
536                 }
537             });
538             return {
539                 lock: lock,
540                 hide: isHide ? lock : false
541             };
542         }
543     }, {
544         NAME: 'mform-dependency-manager',
545         ATTRS: {
546             form: {
547                 setter: function(value) {
548                     return Y.one('#' + value);
549                 },
550                 value: null
551             },
553             dependencies: {
554                 value: {}
555             }
556         }
557     });
559     M.form.dependencyManager = dependencyManager;
562 /**
563  * Stores a list of the dependencyManager for each form on the page.
564  */
565 M.form.dependencyManagers = {};
567 /**
568  * Initialises a manager for a forms dependencies.
569  * This should happen once per form.
570  *
571  * @param {YUI} Y YUI3 instance
572  * @param {String} formid ID of the form
573  * @param {Array} dependencies array
574  * @return {M.form.dependencyManager}
575  */
576 M.form.initFormDependencies = function(Y, formid, dependencies) {
578     // If the dependencies isn't an array or object we don't want to
579     // know about it
580     if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
581         return false;
582     }
584     /**
585      * Fixes an issue with YUI's processing method of form.elements property
586      * in Internet Explorer.
587      *     http://yuilibrary.com/projects/yui3/ticket/2528030
588      */
589     Y.Node.ATTRS.elements = {
590         getter: function() {
591             return Y.all(new Y.Array(this._node.elements, 0, true));
592         }
593     };
595     M.form.dependencyManagers[formid] = new M.form.dependencyManager({form: formid, dependencies: dependencies});
596     return M.form.dependencyManagers[formid];
597 };
599 /**
600  * Update the state of a form. You need to call this after, for example, changing
601  * the state of some of the form input elements in your own code, in order that
602  * things like the disableIf state of elements can be updated.
603  *
604  * @param {String} formid ID of the form
605  */
606 M.form.updateFormState = function(formid) {
607     if (formid in M.form.dependencyManagers) {
608         M.form.dependencyManagers[formid].updateAllDependencies();
609     }
610 };