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