MDL-35674 formslib: Optimized disabledIf javascript for large forms
authorMatt Petro <petro@engr.wisc.edu>
Sat, 1 Dec 2012 05:55:44 +0000 (00:55 -0500)
committerAndrew Nicols <andrew@nicols.co.uk>
Sun, 21 Jul 2013 23:08:18 +0000 (00:08 +0100)
lib/form/form.js

index 69d803f..2d754fc 100644 (file)
@@ -41,31 +41,34 @@ M.form.initFormDependencies = function(Y, formid, dependencies) {
         };
         dependencyManager.prototype = {
             _form : null,
-            _depElements : [],
-            _nameCollections : [],
+            _locks : [],
+            _hides : [],
+            _dirty : [],
+            _nameCollections : null,
+            _fileinputs : null,
             initializer : function(config) {
                 var i = 0, nodeName;
                 this._form = Y.one('#'+formid);
                 for (i in dependencies) {
-                    this._depElements[i] = this.elementsByName(i);
-                    if (this._depElements[i].size() == 0) {
+                    var elements = this.elementsByName(i);
+                    if (elements.size() == 0) {
                         continue;
                     }
-                    this._depElements[i].each(function(node){
+                    elements.each(function(node){
                         nodeName = node.get('nodeName').toUpperCase();
                         if (nodeName == 'INPUT') {
                             if (node.getAttribute('type').match(/^(button|submit|radio|checkbox)$/)) {
-                                node.on('click', this.checkDependencies, this);
+                                node.on('click', this.updateEventDependencies, this);
                             } else {
-                                node.on('blur', this.checkDependencies, this);
+                                node.on('blur', this.updateEventDependencies, this);
                             }
-                            node.on('change', this.checkDependencies, this);
+                            node.on('change', this.updateEventDependencies, this);
                         } else if (nodeName == 'SELECT') {
-                            node.on('change', this.checkDependencies, this);
+                            node.on('change', this.updateEventDependencies, this);
                         } else {
-                            node.on('click', this.checkDependencies, this);
-                            node.on('blur', this.checkDependencies, this);
-                            node.on('change', this.checkDependencies, this);
+                            node.on('click', this.updateEventDependencies, this);
+                            node.on('blur', this.updateEventDependencies, this);
+                            node.on('change', this.updateEventDependencies, this);
                         }
                     }, this);
                 }
@@ -73,27 +76,51 @@ M.form.initFormDependencies = function(Y, formid, dependencies) {
                     if (input.getAttribute('type')=='reset') {
                         input.on('click', function(){
                             this._form.reset();
-                            this.checkDependencies();
+                            this.updateAllDependencies();
                         }, this);
                     }
                 }, this);
 
-                return this.checkDependencies(null);
+                return this.updateAllDependencies();
+            },
+            /**
+             * Initializes the mapping from element name to YUI NodeList
+             */
+            initElementsByName : function() {
+                var names = [];
+                // Collect element names
+                for (var i in dependencies) {
+                    names[i] = new Y.NodeList();
+                    for (var condition in dependencies[i]) {
+                        for (var value in dependencies[i][condition]) {
+                            for (var ei in dependencies[i][condition][value]) {
+                                names[dependencies[i][condition][value][ei]] = new Y.NodeList();
+                            }
+                        }
+                    }
+                }
+                // Locate elements for each name
+                this._form.get('elements').each(function(node){
+                    var name = node.getAttribute('name');
+                    if (names[name]) {
+                        names[name].push(node);
+                    }
+                });
+                this._nameCollections = names;
             },
             /**
              * Gets all elements in the form by their name and returns
              * a YUI NodeList
-             * @return Y.NodeList
+             *
+             * @param {string} name The form element name.
+             * @return {Y.NodeList}
              */
             elementsByName : function(name) {
+                if (!this._nameCollections) {
+                    this.initElementsByName();
+                }
                 if (!this._nameCollections[name]) {
-                    var elements = [];
-                    this._form.get('elements').each(function(){
-                        if (this.getAttribute('name') == name) {
-                            elements.push(this);
-                        }
-                    });
-                    this._nameCollections[name] = new Y.NodeList(elements);
+                    return new Y.NodeList();
                 }
                 return this._nameCollections[name];
             },
@@ -103,81 +130,138 @@ M.form.initFormDependencies = function(Y, formid, dependencies) {
              *
              * Changes are made by functions title _dependency_{dependencytype}
              * and more can easily be introduced by defining further functions.
+             *
+             * @param {EventFacade | null} e The event, if any.
+             * @param {string} name The form element name to check dependencies against.
              */
-            checkDependencies : function(e) {
-                var tolock = [],
-                    tohide = [],
-                    dependon, condition, value,
-                    lock, hide, checkfunction, result;
-                for (dependon in dependencies) {
-                    if (this._depElements[dependon].size() == 0) {
-                        continue;
-                    }
-                    for (condition in dependencies[dependon]) {
-                        for (value in dependencies[dependon][condition]) {
-                            lock = false;
-                            hide = false;
-                            checkfunction = '_dependency_'+condition;
-                            if (Y.Lang.isFunction(this[checkfunction])) {
-                                result = this[checkfunction].apply(this, [this._depElements[dependon], value, e]);
-                            } else {
-                                result = this._dependency_default(this._depElements[dependon], value, e);
-                            }
-                            lock = result.lock || false;
-                            hide = result.hide || false;
-                            for (var ei in dependencies[dependon][condition][value]) {
-                                var eltolock = dependencies[dependon][condition][value][ei];
-                                if (hide) {
-                                    tohide[eltolock] = true;
-                                }
-                                if (tolock[eltolock] != null) {
-                                    tolock[eltolock] = lock || tolock[eltolock];
-                                } else {
-                                    tolock[eltolock] = lock;
-                                }
-                            }
+            checkDependencies : function(e, dependon) {
+                var tohide = [],
+                    tolock = [],
+                    condition, value, lock, hide,
+                    checkfunction, result, elements;
+                if (!dependencies[dependon]) {
+                    return true;
+                }
+                elements = this.elementsByName(dependon);
+                for (condition in dependencies[dependon]) {
+                    for (value in dependencies[dependon][condition]) {
+                        checkfunction = '_dependency_'+condition;
+                        if (Y.Lang.isFunction(this[checkfunction])) {
+                            result = this[checkfunction].apply(this, [elements, value, e]);
+                        } else {
+                            result = this._dependency_default(elements, value, e);
+                        }
+                        lock = result.lock || false;
+                        hide = result.hide || false;
+                        for (var ei in dependencies[dependon][condition][value]) {
+                            var eltolock = dependencies[dependon][condition][value][ei];
+                            tohide[eltolock] = tohide[eltolock] || hide;
+                            tolock[eltolock] = tolock[eltolock] || lock;
                         }
                     }
                 }
                 for (var el in tolock) {
-                    this._disableElement(el, tolock[el]);
-                    if (tohide.propertyIsEnumerable(el)) {
-                        this._hideElement(el, tohide[el]);
+                    var needsupdate = false;
+                    if (tolock[el]) {
+                        this._locks[el] = this._locks[el] || [];
+                        if (!this._locks[el][dependon]) {
+                            this._locks[el][dependon] = true;
+                            needsupdate = true;
+                        }
+                    } else if (this._locks[el] && this._locks[el][dependon]) {
+                        delete this._locks[el][dependon];
+                        needsupdate = true;
+                    }
+                    if (tohide[el]) {
+                        this._hides[el] = this._hides[el] || [];
+                        if (!this._hides[el][dependon]) {
+                            this._hides[el][dependon] = true;
+                            needsupdate = true;
+                        }
+                    } else if (this._hides[el] && this._hides[el][dependon]) {
+                        delete this._hides[el][dependon];
+                        needsupdate = true;
+                    }
+                    if (needsupdate) {
+                        this._dirty[el] = true;
                     }
                 }
                 return true;
             },
             /**
-             * Disabled all form elements with the given name
+             * Update all dependencies in form
+             */
+            updateAllDependencies : function() {
+                for (var el in dependencies) {
+                    this.checkDependencies(null, el);
+                }
+                this.updateForm();
+            },
+            /**
+             * Update dependencies associated with event
+             *
+             * @param {Event} e The event.
+             */
+            updateEventDependencies : function(e) {
+                var el = e.target.getAttribute('name');
+                this.checkDependencies(e, el);
+                this.updateForm();
+            },
+            /**
+             * Flush pending changes to the form
+             */
+            updateForm : function() {
+                for (var el in this._dirty) {
+                    if (this._locks[el]) {
+                        var locked = !this._isObjectEmpty(this._locks[el]);
+                        this._disableElement(el, locked);
+                    }
+                    if (this._hides[el]) {
+                        var hidden = !this._isObjectEmpty(this._hides[el]);
+                        this._hideElement(el, hidden);
+                    }
+                }
+                this._dirty = [];
+            },
+            /**
+             * Disables or enables all form elements with the given name
+             *
+             * @param {string} name The form element name.
+             * @param {boolean} disabled True to disable, false to enable.
              */
             _disableElement : function(name, disabled) {
                 var els = this.elementsByName(name);
-                var form = this;
-                els.each(function(){
+                var filepicker = this.isFilePicker(name);
+                els.each(function(node){
                     if (disabled) {
-                        this.setAttribute('disabled', 'disabled');
+                        node.setAttribute('disabled', 'disabled');
                     } else {
-                        this.removeAttribute('disabled');
+                        node.removeAttribute('disabled');
                     }
 
                     // Extra code to disable filepicker or filemanager form elements
-                    var fitem = this.ancestor('.fitem');
-                    if (fitem && (fitem.hasClass('fitem_ffilemanager') || fitem.hasClass('fitem_ffilepicker'))) {
-                        if (disabled){
-                            fitem.addClass('disabled');
-                        } else {
-                            fitem.removeClass('disabled');
+                    if (filepicker) {
+                        var fitem = node.ancestor('.fitem');
+                        if (fitem) {
+                            if (disabled){
+                                fitem.addClass('disabled');
+                            } else {
+                                fitem.removeClass('disabled');
+                            }
                         }
                     }
                 })
             },
             /**
-             * Hides all elements with the given name.
+             * Hides or shows all form elements with the given name.
+             *
+             * @param {string} name The form element name.
+             * @param {boolean} disabled True to hide, false to show.
              */
             _hideElement : function(name, hidden) {
                 var els = this.elementsByName(name);
-                els.each(function(){
-                    var e = els.ancestor('.fitem');
+                els.each(function(node){
+                    var e = node.ancestor('.fitem');
                     if (e) {
                         e.setStyles({
                             display : (hidden)?'none':''
@@ -185,6 +269,36 @@ M.form.initFormDependencies = function(Y, formid, dependencies) {
                     }
                 });
             },
+            /**
+             * Is the form element inside a filepicker or filemanager?
+             *
+             * @param {string} el The form element name.
+             * @return {boolean}
+             */
+            isFilePicker : function(el) {
+                if (!this._fileinputs) {
+                    var fileinputs = [];
+                    var els = this._form.all('.fitem.fitem_ffilepicker input,.fitem.fitem_ffilemanager input');
+                    els.each(function(node){
+                        fileinputs[node.getAttribute('name')] = true;
+                    });
+                    this._fileinputs = fileinputs;
+                }
+                return this._fileinputs[el] || false;
+            },
+            /**
+             * Check if the object is empty
+             *
+             * @param {object} obj
+             * @return {boolean}
+             */
+            _isObjectEmpty : function(obj) {
+                for(var prop in obj) {
+                    if(obj.hasOwnProperty(prop))
+                        return false;
+                }
+                return true;
+            },
             _dependency_notchecked : function(elements, value) {
                 var lock = false;
                 elements.each(function(){
@@ -365,6 +479,6 @@ M.form.initFormDependencies = function(Y, formid, dependencies) {
  */
 M.form.updateFormState = function(formid) {
     if (formid in M.form.dependencyManagers) {
-        M.form.dependencyManagers[formid].checkDependencies(null);
+        M.form.dependencyManagers[formid].updateAllDependencies();
     }
 };