From 2f8edd067fc2e1e442844d7c81319dda5f711e35 Mon Sep 17 00:00:00 2001 From: Matt Petro Date: Sat, 1 Dec 2012 00:55:44 -0500 Subject: [PATCH] MDL-35674 formslib: Optimized disabledIf javascript for large forms --- lib/form/form.js | 258 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 186 insertions(+), 72 deletions(-) diff --git a/lib/form/form.js b/lib/form/form.js index 69d803fb771..2d754fc5819 100644 --- a/lib/form/form.js +++ b/lib/form/form.js @@ -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(); } };