2 * This file contains JS functionality required by mforms and is included automatically
6 // Namespace for the form bits and bobs
9 if (typeof M.form.dependencyManager === 'undefined') {
10 var dependencyManager = function() {
11 dependencyManager.superclass.constructor.apply(this, arguments);
13 Y.extend(dependencyManager, Y.Base, {
17 _nameCollections: null,
20 initializer: function() {
21 // Setup initial values for complex properties.
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);
35 node.on('blur', this.updateEventDependencies, this);
37 node.on('change', this.updateEventDependencies, this);
38 } else if (nodeName == 'SELECT') {
39 node.on('change', this.updateEventDependencies, this);
41 node.on('click', this.updateEventDependencies, this);
42 node.on('blur', this.updateEventDependencies, this);
43 node.on('change', this.updateEventDependencies, 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();
58 this.updateAllDependencies();
62 * Initializes the mapping from element name to YUI NodeList
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();
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);
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);
99 this._nameCollections = {names: names, allnames: allnames};
103 * Gets all elements in the form by their name and returns
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}
110 elementsByName: function(name, includeGroups) {
111 if (includeGroups === undefined) {
112 includeGroups = false;
114 var collection = (includeGroups ? 'allnames' : 'names');
116 if (!this._nameCollections) {
117 this.initElementsByName();
119 if (!({}).hasOwnProperty.call(this._nameCollections[collection], name)) {
120 return new Y.NodeList();
122 return this._nameCollections[collection][name];
126 * Checks the dependencies the form has an makes any changes to the
127 * form that are required.
129 * Changes are made by functions title _dependency{Dependencytype}
130 * and more can easily be introduced by defining further functions.
132 * @param {EventFacade | null} e The event, if any.
133 * @param {String} dependon The form element name to check dependencies against.
136 checkDependencies: function(e, dependon) {
137 var dependencies = this.get('dependencies'),
140 condition, value, isHide, lock, hide,
141 checkfunction, result, elements;
142 if (!({}).hasOwnProperty.call(dependencies, dependon)) {
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]);
153 result = this._dependencyDefault(elements, value, (isHide === "1"), e);
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;
162 tohide[eltolock] = hide;
165 if (({}).hasOwnProperty.call(tolock, eltolock)) {
166 tolock[eltolock] = tolock[eltolock] || lock;
168 tolock[eltolock] = lock;
175 for (var el in tolock) {
176 var needsupdate = false;
177 if (!({}).hasOwnProperty.call(this._locks, el)) {
178 this._locks[el] = {};
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;
185 } else if (({}).hasOwnProperty.call(this._locks[el], dependon) && this._locks[el][dependon]) {
186 delete this._locks[el][dependon];
190 if (!({}).hasOwnProperty.call(this._hides, el)) {
191 this._hides[el] = {};
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;
198 } else if (({}).hasOwnProperty.call(this._hides[el], dependon) && this._hides[el][dependon]) {
199 delete this._hides[el][dependon];
204 this._dirty[el] = true;
211 * Update all dependencies in form
213 updateAllDependencies: function() {
214 Y.Object.each(this.get('dependencies'), function(value, name) {
215 this.checkDependencies(null, name);
221 * Update dependencies associated with event
223 * @param {Event} e The event.
225 updateEventDependencies: function(e) {
226 var el = e.target.getAttribute('name');
227 this.checkDependencies(e, el);
231 * Flush pending changes to the form
233 updateForm: function() {
235 for (el in this._dirty) {
236 if (({}).hasOwnProperty.call(this._locks, el)) {
237 this._disableElement(el, !Y.Object.isEmpty(this._locks[el]));
239 if (({}).hasOwnProperty.call(this._hides, el)) {
240 this._hideElement(el, !Y.Object.isEmpty(this._hides[el]));
247 * Disables or enables all form elements with the given name
249 * @param {String} name The form element name.
250 * @param {Boolean} disabled True to disable, false to enable.
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) {
259 node.setAttribute('disabled', 'disabled');
261 node.removeAttribute('disabled');
264 // Extra code to disable filepicker or filemanager form elements
266 var fitem = node.ancestor('.fitem');
269 fitem.addClass('disabled');
271 fitem.removeClass('disabled');
276 editors.each(function(editor) {
278 editor.setAttribute('readonly', 'readonly');
280 editor.removeAttribute('readonly', 'readonly');
282 editor.getDOMNode().dispatchEvent(new Event('form:editorUpdated'));
286 * Hides or shows all form elements with the given name.
288 * @param {String} name The form element name.
289 * @param {Boolean} hidden True to hide, false to show.
291 _hideElement: function(name, hidden) {
292 var els = this.elementsByName(name, true);
293 els.each(function(node) {
294 var e = node.ancestor('.fitem', true);
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.
306 e.setAttribute('hidden', 'hidden');
308 e.removeAttribute('hidden');
311 display: (hidden) ? 'none' : ''
314 // Hide/unhide the label as well.
317 label = Y.all('label[for="' + id + '"]');
320 label.setAttribute('hidden', 'hidden');
322 label.removeAttribute('hidden');
325 display: (hidden) ? 'none' : ''
333 * Is the form element inside a filepicker or filemanager?
335 * @param {String} el The form element name.
338 isFilePicker: function(el) {
339 if (!this._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;
346 this._fileinputs = fileinputs;
349 if (({}).hasOwnProperty.call(this._fileinputs, el)) {
350 return this._fileinputs[el] || false;
355 _dependencyNotchecked: function(elements, value, isHide) {
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.
363 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
366 lock = lock || !Y.Node.getDOMNode(this).checked;
370 hide: isHide ? lock : false
373 _dependencyChecked: function(elements, value, isHide) {
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.
381 if (this.getAttribute('type').toLowerCase() == 'radio' && this.get('value') != value) {
384 lock = lock || Y.Node.getDOMNode(this).checked;
388 hide: isHide ? lock : false
391 _dependencyNoitemselected: function(elements, value, isHide) {
393 elements.each(function() {
394 lock = lock || this.get('selectedIndex') == -1;
398 hide: isHide ? lock : false
401 _dependencyEq: function(elements, value, isHide) {
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) {
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);
413 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
414 lock = lock || hiddenVal;
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) {
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('|');
430 options = this.get('options');
431 options.each(function() {
432 if (this.get('selected')) {
433 selected[selected.length] = this.get('value');
436 if (selected.length > 0 && selected.length === values.length) {
437 for (var i in selected) {
439 if (values.indexOf(v) > -1) {
450 lock = lock || this.get('value') == value;
455 hide: isHide ? lock : false
459 * Lock the given field if the field value is in the given set of values.
461 * @param {Array} elements
462 * @param {String} values Single value or pipe (|) separated values when multiple
463 * @returns {{lock: boolean, hide: boolean}}
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('|');
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) {
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);
481 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
482 lock = lock || hiddenVal;
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) {
493 } else if (this.get('nodeName').toUpperCase() === 'SELECT' && this.get('multiple') === true) {
494 // Multiple selects can have one or more value assigned.
496 options = this.get('options');
497 options.each(function() {
498 if (this.get('selected')) {
499 selected[selected.length] = this.get('value');
502 if (selected.length > 0 && selected.length === values.length) {
503 for (var i in selected) {
505 if (values.indexOf(v) > -1) {
516 value = this.get('value');
517 lock = lock || (values.indexOf(value) > -1);
522 hide: isHide ? lock : false
525 _dependencyHide: function(elements, value) {
531 _dependencyDefault: function(elements, value, isHide) {
536 elements.each(function() {
538 if (this.getAttribute('type').toLowerCase() == 'radio' && !Y.Node.getDOMNode(this).checked) {
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);
545 } else if (this.getAttribute('type').toLowerCase() == 'checkbox' && !Y.Node.getDOMNode(this).checked) {
546 lock = lock || hiddenVal;
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) {
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('|');
562 this.get('options').each(function() {
563 if (this.get('selected')) {
564 selected[selected.length] = this.get('value');
567 if (selected.length > 0 && selected.length === values.length) {
568 for (var i in selected) {
569 if (values.indexOf(selected[i]) > -1) {
580 lock = lock || this.get('value') != value;
585 hide: isHide ? lock : false
589 NAME: 'mform-dependency-manager',
592 setter: function(value) {
593 return Y.one('#' + value);
604 M.form.dependencyManager = dependencyManager;
608 * Stores a list of the dependencyManager for each form on the page.
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}
621 M.form.initFormDependencies = function(Y, formid, dependencies) {
623 // If the dependencies isn't an array or object we don't want to
625 if (!Y.Lang.isArray(dependencies) && !Y.Lang.isObject(dependencies)) {
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
634 Y.Node.ATTRS.elements = {
636 return Y.all(new Y.Array(this._node.elements, 0, true));
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
651 M.form.updateFormState = function(formid) {
652 if (formid in M.form.dependencyManagers) {
653 M.form.dependencyManagers[formid].updateAllDependencies();