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