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