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