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