weekly release 3.2dev
[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
bef9ab0a
TH
9/**
10 * Stores a list of the dependencyManager for each form on the page.
11 */
12M.form.dependencyManagers = {};
13
c7e3e61c
SH
14/**
15 * Initialises a manager for a forms dependencies.
16 * This should happen once per form.
17 */
18M.form.initFormDependencies = function(Y, formid, dependencies) {
19
20 // If the dependencies isn't an array or object we don't want to
21 // know about it
22 if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
23 return false;
24 }
25
26 /**
27 * Fixes an issue with YUI's processing method of form.elements property
28 * in Internet Explorer.
29 * http://yuilibrary.com/projects/yui3/ticket/2528030
30 */
31 Y.Node.ATTRS.elements = {
32 getter: function() {
33 return Y.all(new Y.Array(this._node.elements, 0, true));
34 }
35 };
36
37 // Define the dependency manager if it hasn't already been defined.
38 M.form.dependencyManager = M.form.dependencyManager || (function(){
39 var dependencyManager = function(config) {
40 dependencyManager.superclass.constructor.apply(this, arguments);
d3067516 41 };
c7e3e61c
SH
42 dependencyManager.prototype = {
43 _form : null,
2f8edd06
MP
44 _locks : [],
45 _hides : [],
46 _dirty : [],
47 _nameCollections : null,
48 _fileinputs : null,
c7e3e61c
SH
49 initializer : function(config) {
50 var i = 0, nodeName;
51 this._form = Y.one('#'+formid);
52 for (i in dependencies) {
2f8edd06
MP
53 var elements = this.elementsByName(i);
54 if (elements.size() == 0) {
c7e3e61c
SH
55 continue;
56 }
2f8edd06 57 elements.each(function(node){
c7e3e61c
SH
58 nodeName = node.get('nodeName').toUpperCase();
59 if (nodeName == 'INPUT') {
60 if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
2f8edd06 61 node.on('click', this.updateEventDependencies, this);
c7e3e61c 62 } else {
2f8edd06 63 node.on('blur', this.updateEventDependencies, this);
c7e3e61c 64 }
2f8edd06 65 node.on('change', this.updateEventDependencies, this);
c7e3e61c 66 } else if (nodeName == 'SELECT') {
2f8edd06 67 node.on('change', this.updateEventDependencies, this);
c7e3e61c 68 } else {
2f8edd06
MP
69 node.on('click', this.updateEventDependencies, this);
70 node.on('blur', this.updateEventDependencies, this);
71 node.on('change', this.updateEventDependencies, this);
c7e3e61c
SH
72 }
73 }, this);
74 }
5e60ed9b
SH
75 this._form.get('elements').each(function(input){
76 if (input.getAttribute('type')=='reset') {
77 input.on('click', function(){
78 this._form.reset();
2f8edd06 79 this.updateAllDependencies();
5e60ed9b
SH
80 }, this);
81 }
c7e3e61c
SH
82 }, this);
83
2f8edd06
MP
84 return this.updateAllDependencies();
85 },
86 /**
87 * Initializes the mapping from element name to YUI NodeList
88 */
89 initElementsByName : function() {
90 var names = [];
91 // Collect element names
92 for (var i in dependencies) {
93 names[i] = new Y.NodeList();
94 for (var condition in dependencies[i]) {
95 for (var value in dependencies[i][condition]) {
96 for (var ei in dependencies[i][condition][value]) {
97 names[dependencies[i][condition][value][ei]] = new Y.NodeList();
98 }
99 }
100 }
101 }
102 // Locate elements for each name
103 this._form.get('elements').each(function(node){
104 var name = node.getAttribute('name');
105 if (names[name]) {
106 names[name].push(node);
107 }
108 });
109 this._nameCollections = names;
c7e3e61c
SH
110 },
111 /**
bef9ab0a 112 * Gets all elements in the form by their name and returns
c7e3e61c 113 * a YUI NodeList
2f8edd06
MP
114 *
115 * @param {string} name The form element name.
116 * @return {Y.NodeList}
c7e3e61c
SH
117 */
118 elementsByName : function(name) {
2f8edd06
MP
119 if (!this._nameCollections) {
120 this.initElementsByName();
121 }
c7e3e61c 122 if (!this._nameCollections[name]) {
2f8edd06 123 return new Y.NodeList();
c7e3e61c
SH
124 }
125 return this._nameCollections[name];
126 },
127 /**
128 * Checks the dependencies the form has an makes any changes to the
129 * form that are required.
d3067516 130 *
c7e3e61c
SH
131 * Changes are made by functions title _dependency_{dependencytype}
132 * and more can easily be introduced by defining further functions.
2f8edd06
MP
133 *
134 * @param {EventFacade | null} e The event, if any.
135 * @param {string} name The form element name to check dependencies against.
c7e3e61c 136 */
2f8edd06
MP
137 checkDependencies : function(e, dependon) {
138 var tohide = [],
139 tolock = [],
140 condition, value, lock, hide,
141 checkfunction, result, elements;
142 if (!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 checkfunction = '_dependency_'+condition;
149 if (Y.Lang.isFunction(this[checkfunction])) {
150 result = this[checkfunction].apply(this, [elements, value, e]);
151 } else {
152 result = this._dependency_default(elements, value, e);
153 }
154 lock = result.lock || false;
155 hide = result.hide || false;
156 for (var ei in dependencies[dependon][condition][value]) {
157 var eltolock = dependencies[dependon][condition][value][ei];
158 tohide[eltolock] = tohide[eltolock] || hide;
159 tolock[eltolock] = tolock[eltolock] || lock;
c7e3e61c
SH
160 }
161 }
162 }
163 for (var el in tolock) {
2f8edd06
MP
164 var needsupdate = false;
165 if (tolock[el]) {
166 this._locks[el] = this._locks[el] || [];
167 if (!this._locks[el][dependon]) {
168 this._locks[el][dependon] = true;
169 needsupdate = true;
170 }
171 } else if (this._locks[el] && this._locks[el][dependon]) {
172 delete this._locks[el][dependon];
173 needsupdate = true;
174 }
175 if (tohide[el]) {
176 this._hides[el] = this._hides[el] || [];
177 if (!this._hides[el][dependon]) {
178 this._hides[el][dependon] = true;
179 needsupdate = true;
180 }
181 } else if (this._hides[el] && this._hides[el][dependon]) {
182 delete this._hides[el][dependon];
183 needsupdate = true;
184 }
185 if (needsupdate) {
186 this._dirty[el] = true;
c7e3e61c
SH
187 }
188 }
189 return true;
190 },
191 /**
2f8edd06
MP
192 * Update all dependencies in form
193 */
194 updateAllDependencies : function() {
195 for (var el in dependencies) {
196 this.checkDependencies(null, el);
197 }
198 this.updateForm();
199 },
200 /**
201 * Update dependencies associated with event
202 *
203 * @param {Event} e The event.
204 */
205 updateEventDependencies : function(e) {
206 var el = e.target.getAttribute('name');
207 this.checkDependencies(e, el);
208 this.updateForm();
209 },
210 /**
211 * Flush pending changes to the form
212 */
213 updateForm : function() {
214 for (var el in this._dirty) {
215 if (this._locks[el]) {
216 var locked = !this._isObjectEmpty(this._locks[el]);
217 this._disableElement(el, locked);
218 }
219 if (this._hides[el]) {
220 var hidden = !this._isObjectEmpty(this._hides[el]);
221 this._hideElement(el, hidden);
222 }
223 }
224 this._dirty = [];
225 },
226 /**
227 * Disables or enables all form elements with the given name
228 *
229 * @param {string} name The form element name.
230 * @param {boolean} disabled True to disable, false to enable.
c7e3e61c
SH
231 */
232 _disableElement : function(name, disabled) {
233 var els = this.elementsByName(name);
2f8edd06
MP
234 var filepicker = this.isFilePicker(name);
235 els.each(function(node){
c7e3e61c 236 if (disabled) {
2f8edd06 237 node.setAttribute('disabled', 'disabled');
c7e3e61c 238 } else {
2f8edd06 239 node.removeAttribute('disabled');
c7e3e61c 240 }
4b72f9eb 241
c81f3328 242 // Extra code to disable filepicker or filemanager form elements
2f8edd06
MP
243 if (filepicker) {
244 var fitem = node.ancestor('.fitem');
245 if (fitem) {
246 if (disabled){
247 fitem.addClass('disabled');
248 } else {
249 fitem.removeClass('disabled');
250 }
c81f3328 251 }
4b72f9eb 252 }
c7e3e61c
SH
253 })
254 },
255 /**
2f8edd06
MP
256 * Hides or shows all form elements with the given name.
257 *
258 * @param {string} name The form element name.
259 * @param {boolean} disabled True to hide, false to show.
c7e3e61c
SH
260 */
261 _hideElement : function(name, hidden) {
262 var els = this.elementsByName(name);
2f8edd06
MP
263 els.each(function(node){
264 var e = node.ancestor('.fitem');
c7e3e61c
SH
265 if (e) {
266 e.setStyles({
267 display : (hidden)?'none':''
268 })
269 }
270 });
271 },
2f8edd06
MP
272 /**
273 * Is the form element inside a filepicker or filemanager?
274 *
275 * @param {string} el The form element name.
276 * @return {boolean}
277 */
278 isFilePicker : function(el) {
279 if (!this._fileinputs) {
280 var fileinputs = [];
281 var els = this._form.all('.fitem.fitem_ffilepicker input,.fitem.fitem_ffilemanager input');
282 els.each(function(node){
283 fileinputs[node.getAttribute('name')] = true;
284 });
285 this._fileinputs = fileinputs;
286 }
287 return this._fileinputs[el] || false;
288 },
289 /**
290 * Check if the object is empty
291 *
292 * @param {object} obj
293 * @return {boolean}
294 */
295 _isObjectEmpty : function(obj) {
296 for(var prop in obj) {
297 if(obj.hasOwnProperty(prop))
298 return false;
299 }
300 return true;
301 },
c7e3e61c
SH
302 _dependency_notchecked : function(elements, value) {
303 var lock = false;
304 elements.each(function(){
8f6384a9
TH
305 if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
306 // This is the hidden input that is part of an advcheckbox.
307 return;
308 }
c1422316 309 if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
e2620b9d
SH
310 return;
311 }
312 lock = lock || !Y.Node.getDOMNode(this).checked;
c7e3e61c
SH
313 });
314 return {
315 lock : lock,
316 hide : false
317 }
318 },
319 _dependency_checked : function(elements, value) {
320 var lock = false;
321 elements.each(function(){
8f6384a9
TH
322 if (this.getAttribute('type').toLowerCase()=='hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
323 // This is the hidden input that is part of an advcheckbox.
324 return;
325 }
c1422316 326 if (this.getAttribute('type').toLowerCase()=='radio' && this.get('value') != value) {
e2620b9d
SH
327 return;
328 }
329 lock = lock || Y.Node.getDOMNode(this).checked;
c7e3e61c
SH
330 });
331 return {
332 lock : lock,
333 hide : false
334 }
335 },
336 _dependency_noitemselected : function(elements, value) {
337 var lock = false;
338 elements.each(function(){
339 lock = lock || this.get('selectedIndex') == -1;
340 });
341 return {
342 lock : lock,
343 hide : false
344 }
345 },
346 _dependency_eq : function(elements, value) {
347 var lock = false;
8f6384a9 348 var hidden_val = false;
58f3865f 349 var options, v, selected, values;
c7e3e61c 350 elements.each(function(){
e2620b9d
SH
351 if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
352 return;
8f6384a9
TH
353 } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
354 // This is the hidden input that is part of an advcheckbox.
355 hidden_val = (this.get('value') == value);
356 return;
c1422316 357 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
8f6384a9 358 lock = lock || hidden_val;
c1422316 359 return;
e2620b9d 360 }
63d5c4ac 361 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
58f3865f 362 // Check for filepicker status.
63d5c4ac
RT
363 var elementname = this.getAttribute('name');
364 if (elementname && M.form_filepicker.instances[elementname].fileadded) {
365 lock = false;
366 } else {
367 lock = true;
368 }
58f3865f
FM
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 }
63d5c4ac
RT
393 } else {
394 lock = lock || this.get('value') == value;
395 }
c7e3e61c
SH
396 });
397 return {
398 lock : lock,
399 hide : false
400 }
401 },
6513cc03
AA
402 /**
403 * Lock the given field if the field value is in the given set of values.
404 *
405 * @param elements
406 * @param values
407 * @returns {{lock: boolean, hide: boolean}}
408 * @private
409 */
410 _dependency_in : function(elements, values) {
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 hidden_val = 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' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
421 // This is the hidden input that is part of an advcheckbox.
422 hidden_val = (values.indexOf(this.get('value')) > -1);
423 return;
424 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
425 lock = lock || hidden_val;
426 return;
427 }
428 if (this.getAttribute('class').toLowerCase() == 'filepickerhidden') {
429 // Check for filepicker status.
430 var elementname = this.getAttribute('name');
431 if (elementname && M.form_filepicker.instances[elementname].fileadded) {
432 lock = false;
433 } else {
434 lock = true;
435 }
436 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
437 // Multiple selects can have one or more value assigned.
438 selected = [];
439 options = this.get('options');
440 options.each(function() {
441 if (this.get('selected')) {
442 selected[selected.length] = this.get('value');
443 }
444 });
445 if (selected.length > 0 && selected.length === values.length) {
446 for (var i in selected) {
447 v = selected[i];
448 if (values.indexOf(v) > -1) {
449 lock = true;
450 } else {
451 lock = false;
452 return;
453 }
454 }
455 } else {
456 lock = false;
457 }
458 } else {
459 value = this.get('value');
460 lock = lock || (values.indexOf(value) > -1);
461 }
462 });
463 return {
464 lock : lock,
465 hide : false
466 }
467 },
c7e3e61c
SH
468 _dependency_hide : function(elements, value) {
469 return {
470 lock : false,
471 hide : true
472 }
473 },
e2620b9d 474 _dependency_default : function(elements, value, ev) {
c7e3e61c 475 var lock = false;
8f6384a9 476 var hidden_val = false;
c7e3e61c 477 elements.each(function(){
e2620b9d
SH
478 if (this.getAttribute('type').toLowerCase()=='radio' && !Y.Node.getDOMNode(this).checked) {
479 return;
8f6384a9
TH
480 } else if (this.getAttribute('type').toLowerCase() == 'hidden' && !this.siblings('input[type=checkbox][name="' + this.get('name') + '"]').isEmpty()) {
481 // This is the hidden input that is part of an advcheckbox.
482 hidden_val = (this.get('value') != value);
483 return;
c1422316 484 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
8f6384a9 485 lock = lock || hidden_val;
c1422316 486 return;
e2620b9d 487 }
63d5c4ac
RT
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 }
58f3865f
FM
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 options = this.get('options');
502 options.each(function() {
503 if (this.get('selected')) {
504 selected[selected.length] = this.get('value');
505 }
506 });
507 if (selected.length > 0 && selected.length === values.length) {
508 for (var i in selected) {
509 v = selected[i];
510 if (values.indexOf(v) > -1) {
511 lock = false;
512 } else {
513 lock = true;
514 return;
515 }
516 }
517 } else {
518 lock = true;
519 }
63d5c4ac
RT
520 } else {
521 lock = lock || this.get('value') != value;
522 }
c7e3e61c
SH
523 });
524 return {
525 lock : lock,
526 hide : false
527 }
528 }
d3067516 529 };
c7e3e61c
SH
530 Y.extend(dependencyManager, Y.Base, dependencyManager.prototype, {
531 NAME : 'mform-dependency-manager'
532 });
d3067516 533
c7e3e61c
SH
534 return dependencyManager;
535 })();
536
bef9ab0a
TH
537 M.form.dependencyManagers[formid] = new M.form.dependencyManager();
538 return M.form.dependencyManagers[formid];
539};
540
541/**
542 * Update the state of a form. You need to call this after, for example, changing
543 * the state of some of the form input elements in your own code, in order that
544 * things like the disableIf state of elements can be updated.
545 */
546M.form.updateFormState = function(formid) {
547 if (formid in M.form.dependencyManagers) {
2f8edd06 548 M.form.dependencyManagers[formid].updateAllDependencies();
bef9ab0a
TH
549 }
550};