Merge branch 'MDL-54778-master' of git://github.com/andrewnicols/moodle
[moodle.git] / lib / form / form.js
CommitLineData
c7e3e61c
SH
1/**
2 * This file contains JS functionality required by mforms and is included automatically
3 * when required.
4 */
5
6// Namespace for the form bits and bobs
7M.form = M.form || {};
8
badbaa64
AN
9if (typeof M.form.dependencyManager === 'undefined') {
10 var dependencyManager = function() {
11 dependencyManager.superclass.constructor.apply(this, arguments);
c7e3e61c 12 };
badbaa64
AN
13 Y.extend(dependencyManager, Y.Base, {
14 _locks: null,
15 _hides: null,
16 _dirty: null,
17 _nameCollections: null,
18 _fileinputs: null,
c7e3e61c 19
badbaa64
AN
20 initializer: function() {
21 // Setup initial values for complex properties.
22 this._locks = {};
23 this._hides = {};
24 this._dirty = {};
25
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)$/)) {
2f8edd06 33 node.on('click', this.updateEventDependencies, this);
badbaa64 34 } else {
2f8edd06 35 node.on('blur', this.updateEventDependencies, this);
c7e3e61c 36 }
badbaa64
AN
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);
5e60ed9b 44 }
c7e3e61c 45 }, this);
badbaa64 46 }, this);
c7e3e61c 47
badbaa64
AN
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);
57
58 this.updateAllDependencies();
59 },
60
61 /**
62 * Initializes the mapping from element name to YUI NodeList
63 */
64 initElementsByName: function() {
65 var names = {};
66
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();
2f8edd06
MP
74 }
75 }
76 }
badbaa64
AN
77 });
78
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);
2f8edd06 84 }
badbaa64
AN
85 });
86 this._nameCollections = names;
87 },
88
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 },
105
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;
2f8edd06 140 } else {
badbaa64 141 tohide[eltolock] = hide;
2f8edd06 142 }
badbaa64
AN
143
144 if (({}).hasOwnProperty.call(tolock, eltolock)) {
2f8edd06 145 tolock[eltolock] = tolock[eltolock] || lock;
badbaa64
AN
146 } else {
147 tolock[eltolock] = lock;
c7e3e61c
SH
148 }
149 }
150 }
badbaa64
AN
151 }
152
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;
2f8edd06
MP
161 needsupdate = true;
162 }
badbaa64
AN
163 } else if (({}).hasOwnProperty.call(this._locks[el], dependon) && this._locks[el][dependon]) {
164 delete this._locks[el][dependon];
165 needsupdate = true;
166 }
167
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;
2f8edd06
MP
174 needsupdate = true;
175 }
badbaa64
AN
176 } else if (({}).hasOwnProperty.call(this._hides[el], dependon) && this._hides[el][dependon]) {
177 delete this._hides[el][dependon];
178 needsupdate = true;
c7e3e61c 179 }
badbaa64
AN
180
181 if (needsupdate) {
182 this._dirty[el] = true;
2f8edd06 183 }
badbaa64
AN
184 }
185
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);
195
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]));
2f8edd06 216 }
badbaa64
AN
217 if (({}).hasOwnProperty.call(this._hides, el)) {
218 this._hideElement(el, !Y.Object.isEmpty(this._hides[el]));
219 }
220 }
4b72f9eb 221
badbaa64
AN
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 }
239
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');
c81f3328 248 }
4b72f9eb 249 }
badbaa64
AN
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':''
2f8edd06 266 });
2f8edd06 267 }
badbaa64
AN
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 }
285
286 if (({}).hasOwnProperty.call(this._fileinputs, el)) {
2f8edd06 287 return this._fileinputs[el] || false;
badbaa64
AN
288 }
289
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;
2f8edd06 298 }
badbaa64
AN
299 if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
300 return;
c7e3e61c 301 }
badbaa64
AN
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;
c7e3e61c 315 }
badbaa64
AN
316 if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
317 return;
c7e3e61c 318 }
badbaa64
AN
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;
e2620b9d 358 }
badbaa64
AN
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');
63d5c4ac 368 }
badbaa64
AN
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;
58f3865f 378 }
58f3865f 379 }
63d5c4ac 380 } else {
badbaa64 381 lock = false;
63d5c4ac 382 }
badbaa64
AN
383 } else {
384 lock = lock || this.get('value') == value;
c7e3e61c 385 }
badbaa64
AN
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;
6513cc03 425 }
badbaa64
AN
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');
6513cc03 433 }
badbaa64
AN
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;
6513cc03 443 }
6513cc03
AA
444 }
445 } else {
badbaa64 446 lock = false;
6513cc03 447 }
badbaa64
AN
448 } else {
449 value = this.get('value');
450 lock = lock || (values.indexOf(value) > -1);
6513cc03 451 }
badbaa64
AN
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;
c7e3e61c 480 }
badbaa64
AN
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;
e2620b9d 488 }
badbaa64
AN
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');
63d5c4ac 496 }
badbaa64
AN
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;
58f3865f 505 }
58f3865f 506 }
63d5c4ac 507 } else {
badbaa64 508 lock = true;
63d5c4ac 509 }
badbaa64
AN
510 } else {
511 lock = lock || this.get('value') != value;
c7e3e61c 512 }
badbaa64
AN
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 },
528
529 dependencies: {
530 value: {}
c7e3e61c 531 }
badbaa64
AN
532 }
533 });
534
535 M.form.dependencyManager = dependencyManager;
536}
d3067516 537
badbaa64
AN
538/**
539 * Stores a list of the dependencyManager for each form on the page.
540 */
541M.form.dependencyManagers = {};
542
543/**
544 * Initialises a manager for a forms dependencies.
545 * This should happen once per form.
546 */
547M.form.initFormDependencies = function(Y, formid, dependencies) {
548
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 }
554
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 };
c7e3e61c 565
badbaa64 566 M.form.dependencyManagers[formid] = new M.form.dependencyManager({form: formid, dependencies: dependencies});
bef9ab0a
TH
567 return M.form.dependencyManagers[formid];
568};
569
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 */
575M.form.updateFormState = function(formid) {
576 if (formid in M.form.dependencyManagers) {
2f8edd06 577 M.form.dependencyManagers[formid].updateAllDependencies();
bef9ab0a
TH
578 }
579};