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