Merge branch '5.3'
authorMatthias Schmidt <gravatronics@live.com>
Mon, 9 Nov 2020 13:10:31 +0000 (14:10 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Mon, 9 Nov 2020 13:10:31 +0000 (14:10 +0100)
1  2 
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js
wcfsetup/install/files/lib/system/WCFACP.class.php
wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js

index 756a67e6aa77774fcbaf4b2c75f2d2f1c911e5dd,88d39e08ff567131a5024470177cecd51b85f210..6c1fbd0a289a78a9ca8217bda4ac7ec436e8e000
   * @module    WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager
   * @since     5.2
   */
 -define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Traverse', 'Dom/Util', 'ObjectMap'], function(Dictionary, DomChangeListener, EventHandler, List, DomTraverse, DomUtil, ObjectMap) {
 -      "use strict";
 -      
 -      /**
 -       * is `true` if containters are currently checked for their availablility, otherwise `false`
 -       * @type        {boolean}
 -       * @private
 -       */
 -      var _checkingContainers = false;
 -      
 -      /**
 -       * is `true` if containter will be checked again after the current check for their availablility
 -       * has finished, otherwise `false`
 -       * @type        {boolean}
 -       * @private
 -       */
 -      var _checkContainersAgain = true;
 -      
 -      /**
 -       * list of containers hidden due to their own dependencies
 -       * @type        {List}
 -       * @private
 -       */
 -      var _dependencyHiddenNodes = new List();
 -      
 -      /**
 -       * list of fields for which event listeners have been registered
 -       * @type        {Dictionary}
 -       * @private
 -       */
 -      var _fields = new Dictionary();
 -      
 -      /**
 -       * list of registered forms
 -       * @type        {List}
 -       * @private
 -       */
 -      var _forms = new List();
 -      
 -      /**
 -       * list of dependencies grouped by the dependent node they belong to
 -       * @type        {Dictionary}
 -       * @private
 -       */
 -      var _nodeDependencies = new Dictionary();
 -      
 -      /**
 -       * cache of validation-related properties of hidden form fields
 -       * @type        {ObjectMap}
 -       * @private
 -       */
 -      var _validatedFieldProperties = new ObjectMap();
 -      
 -      return {
 -              /**
 -               * Hides the given node because of its own dependencies.
 -               * 
 -               * @param       {HTMLElement}   node    hidden node
 -               * @protected
 -               */
 -              _hide: function(node) {
 -                      elHide(node);
 -                      _dependencyHiddenNodes.add(node);
 -                      
 -                      // also hide tab menu entry
 -                      if (node.classList.contains('tabMenuContent')) {
 -                              elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function(tabLink) {
 -                                      if (elData(tabLink, 'name') === elData(node, 'name')) {
 -                                              elHide(tabLink);
 -                                      }
 -                              });
 -                      }
 -                      
 -                      elBySelAll('[max], [maxlength], [min], [required]', node, function(validatedField) {
 -                              var properties = new Dictionary();
 -                              
 -                              var max = elAttr(validatedField, 'max');
 -                              if (max) {
 -                                      properties.set('max', max);
 -                                      validatedField.removeAttribute('max');
 -                              }
 -                              
 -                              var maxlength = elAttr(validatedField, 'maxlength');
 -                              if (maxlength) {
 -                                      properties.set('maxlength', maxlength);
 -                                      validatedField.removeAttribute('maxlength');
 -                              }
 -                              
 -                              var min = elAttr(validatedField, 'min');
 -                              if (min) {
 -                                      properties.set('min', min);
 -                                      validatedField.removeAttribute('min');
 -                              }
 -                              
 -                              if (validatedField.required) {
 -                                      properties.set('required', true);
 -                                      validatedField.removeAttribute('required');
 -                              }
 -                              
 -                              _validatedFieldProperties.set(validatedField, properties);
 -                      });
 -              },
 -              
 -              /**
 -               * Shows the given node because of its own dependencies.
 -               * 
 -               * @param       {HTMLElement}   node    shown node
 -               * @protected
 -               */
 -              _show: function(node) {
 -                      elShow(node);
 -                      _dependencyHiddenNodes.delete(node);
 -                      
 -                      // also show tab menu entry
 -                      if (node.classList.contains('tabMenuContent')) {
 -                              elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function(tabLink) {
 -                                      if (elData(tabLink, 'name') === elData(node, 'name')) {
 -                                              elShow(tabLink);
 -                                      }
 -                              });
 -                      }
 -                      
 -                      elBySelAll('input, select', node, function(validatedField) {
 -                              // if a container is shown, ignore all fields that
 -                              // have a hidden parent element within the container
 -                              var parentNode = validatedField.parentNode;
 -                              while (parentNode !== node && parentNode.style.getPropertyValue('display') !== 'none') {
 -                                      parentNode = parentNode.parentNode;
 -                              }
 -                              
 -                              if (parentNode === node && _validatedFieldProperties.has(validatedField)) {
 -                                      var properties = _validatedFieldProperties.get(validatedField);
 -                                      
 -                                      if (properties.has('max')) {
 -                                              elAttr(validatedField, 'max', properties.get('max'));
 -                                      }
 -                                      if (properties.has('maxlength')) {
 -                                              elAttr(validatedField, 'maxlength', properties.get('maxlength'));
 -                                      }
 -                                      if (properties.has('min')) {
 -                                              elAttr(validatedField, 'min', properties.get('min'));
 -                                      }
 -                                      if (properties.has('required')) {
 -                                              elAttr(validatedField, 'required', '');
 -                                      }
 -                                      
 -                                      _validatedFieldProperties.delete(validatedField);
 -                              }
 -                      });
 -              },
 -              
 -              /**
 -               * Registers a new form field dependency.
 -               * 
 -               * @param       {WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract}      dependency      new dependency
 -               */
 -              addDependency: function(dependency) {
 -                      var dependentNode = dependency.getDependentNode();
 -                      if (!_nodeDependencies.has(dependentNode.id)) {
 -                              _nodeDependencies.set(dependentNode.id, [dependency]);
 -                      }
 -                      else {
 -                              _nodeDependencies.get(dependentNode.id).push(dependency);
 -                      }
 -                      
 -                      var fields = dependency.getFields();
 -                      for (var i = 0, length = fields.length; i < length; i++) {
 -                              var field = fields[i];
 -                              var id = DomUtil.identify(field);
 -                              
 -                              if (!_fields.has(id)) {
 -                                      _fields.set(id, field);
 -                                      
 -                                      if (field.tagName === 'INPUT' && (field.type === 'checkbox' || field.type === 'radio' || field.type === 'hidden')) {
 -                                              field.addEventListener('change', this.checkDependencies.bind(this));
 -                                      }
 -                                      else {
 -                                              field.addEventListener('input', this.checkDependencies.bind(this));
 -                                      }
 -                              }
 -                      }
 -              },
 -              
 -              /**
 -               * Checks if all dependencies are met.
 -               */
 -              checkDependencies: function() {
 -                      var obsoleteNodeIds = [];
 -                      
 -                      _nodeDependencies.forEach(function(nodeDependencies, nodeId) {
 -                              var dependentNode = elById(nodeId);
 -                              
 -                              // check if dependent node still exists
 -                              if (dependentNode === null) {
 -                                      obsoleteNodeIds.push(nodeId);
 -                                      return;
 -                              }
 -                              
 -                              for (var i = 0, length = nodeDependencies.length; i < length; i++) {
 -                                      // if any dependency is not met, hide the element
 -                                      if (!nodeDependencies[i].checkDependency()) {
 -                                              this._hide(dependentNode);
 -                                              return;
 -                                      }
 -                              }
 -                              
 -                              // all node dependency is met
 -                              this._show(dependentNode);
 -                      }.bind(this));
 -                      
 -                      // delete dependencies for removed elements
 -                      for (var i = 0, length = obsoleteNodeIds.length; i < length; i++) {
 -                              _nodeDependencies.delete(obsoleteNodeIds[i]);
 -                      }
 -                      
 -                      this.checkContainers();
 -              },
 -              
 -              /**
 -               * Adds the given callback to the list of callbacks called when checking containers.
 -               * 
 -               * @param       {function}      callback
 -               */
 -              addContainerCheckCallback: function(callback) {
 -                      if (typeof callback !== 'function') {
 -                              throw new TypeError("Expected a valid callback for parameter 'callback'.");
 -                      }
 -                      
 -                      EventHandler.add('com.woltlab.wcf.form.builder.dependency', 'checkContainers', callback);
 -              },
 -              
 -              /**
 -               * Checks the containers for their availability.
 -               * 
 -               * If this function is called while containers are currently checked, the containers
 -               * will be checked after the current check has been finished completely.
 -               */
 -              checkContainers: function() {
 -                      // check if containers are currently being checked
 -                      if (_checkingContainers === true) {
 -                              // and if that is the case, calling this method indicates, that after the current round,
 -                              // containters should be checked to properly propagate changes in children to their parents
 -                              _checkContainersAgain = true;
 -                              
 -                              return;
 -                      }
 -                      
 -                      // starting to check containers also resets the flag to check containers again after the current check 
 -                      _checkingContainers = true;
 -                      _checkContainersAgain = false;
 -                      
 -                      EventHandler.fire('com.woltlab.wcf.form.builder.dependency', 'checkContainers');
 -                      
 -                      // finish checking containers and check if containters should be checked again
 -                      _checkingContainers = false;
 -                      if (_checkContainersAgain) {
 -                              this.checkContainers();
 -                      }
 -              },
 -              
 -              /**
 -               * Returns `true` if the given node has been hidden because of its own dependencies.
 -               * 
 -               * @param       {HTMLElement}   node    checked node
 -               * @return      {boolean}
 -               */
 -              isHiddenByDependencies: function(node) {
 -                      if (_dependencyHiddenNodes.has(node)) {
 -                              return true;
 -                      }
 -                      
 -                      var returnValue = false;
 -                      _dependencyHiddenNodes.forEach(function(hiddenNode) {
 -                              if (DomUtil.contains(hiddenNode, node)) {
 -                                      returnValue = true;
 -                              }
 -                      });
 -                      
 -                      return returnValue;
 -              },
 -              
 -              /**
 -               * Registers the form with the given id with the dependency manager.
 -               * 
 -               * @param       {string}        formId          id of register form
 -               * @throws      {Error}                         if given form id is invalid or has already been registered
 -               */
 -              register: function(formId) {
 -                      var form = elById(formId);
 -                      
 -                      if (form === null) {
 -                              throw new Error("Unknown element with id '" + formId + "'");
 -                      }
 -                      
 -                      if (_forms.has(form)) {
 -                              throw new Error("Form with id '" + formId + "' has already been registered.");
 -                      }
 -                      
 -                      _forms.add(form);
 -              },
 -              
 -              /**
 -               * Unregisters the form with the given id and all of its dependencies.
 -               * 
 -               * @param       {string}        formId          id of unregistered form
 -               */
 -              unregister: function(formId) {
 -                      var form = elById(formId);
 -                      
 -                      if (form === null) {
 -                              throw new Error("Unknown element with id '" + formId + "'");
 -                      }
 -                      
 -                      if (!_forms.has(form)) {
 -                              throw new Error("Form with id '" + formId + "' has not been registered.");
 -                      }
 -                      
 -                      _forms.delete(form);
 -                      
 -                      _dependencyHiddenNodes.forEach(function(hiddenNode) {
 -                              if (form.contains(hiddenNode)) {
 -                                      _dependencyHiddenNodes.delete(hiddenNode);
 -                              }
 -                      });
 -                      _nodeDependencies.forEach(function(dependencies, nodeId) {
 -                              if (form.contains(elById(nodeId))) {
 -                                      _nodeDependencies.delete(nodeId);
 -                              }
 -                              
 -                              for (var i = 0, length = dependencies.length; i < length; i++) {
 -                                      var fields = dependencies[i].getFields();
 -                                      for (var j = 0, fieldsLength = fields.length; j < fieldsLength; j++) {
 -                                              var field = fields[j];
 -                                              
 -                                              _fields.delete(field.id);
 -                                              
 -                                              _validatedFieldProperties.delete(field);
 -                                      }
 -                              }
 -                      });
 -              }
 -      };
 +define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Traverse', 'Dom/Util', 'ObjectMap'], function (Dictionary, DomChangeListener, EventHandler, List, DomTraverse, DomUtil, ObjectMap) {
 +    "use strict";
 +    /**
 +     * is `true` if containters are currently checked for their availablility, otherwise `false`
 +     * @type  {boolean}
 +     * @private
 +     */
 +    var _checkingContainers = false;
 +    /**
 +     * is `true` if containter will be checked again after the current check for their availablility
 +     * has finished, otherwise `false`
 +     * @type  {boolean}
 +     * @private
 +     */
 +    var _checkContainersAgain = true;
 +    /**
 +     * list of containers hidden due to their own dependencies
 +     * @type  {List}
 +     * @private
 +     */
 +    var _dependencyHiddenNodes = new List();
 +    /**
 +     * list of fields for which event listeners have been registered
 +     * @type  {Dictionary}
 +     * @private
 +     */
 +    var _fields = new Dictionary();
 +    /**
 +     * list of registered forms
 +     * @type  {List}
 +     * @private
 +     */
 +    var _forms = new List();
 +    /**
 +     * list of dependencies grouped by the dependent node they belong to
 +     * @type  {Dictionary}
 +     * @private
 +     */
 +    var _nodeDependencies = new Dictionary();
 +    /**
 +     * cache of validation-related properties of hidden form fields
 +     * @type  {ObjectMap}
 +     * @private
 +     */
 +    var _validatedFieldProperties = new ObjectMap();
 +    return {
 +        /**
 +         * Hides the given node because of its own dependencies.
 +         *
 +         * @param     {HTMLElement}   node    hidden node
 +         * @protected
 +         */
 +        _hide: function (node) {
 +            elHide(node);
 +            _dependencyHiddenNodes.add(node);
 +            // also hide tab menu entry
 +            if (node.classList.contains('tabMenuContent')) {
 +                elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function (tabLink) {
 +                    if (elData(tabLink, 'name') === elData(node, 'name')) {
 +                        elHide(tabLink);
 +                    }
 +                });
 +            }
 +            elBySelAll('[max], [maxlength], [min], [required]', node, function (validatedField) {
 +                var properties = new Dictionary();
 +                var max = elAttr(validatedField, 'max');
 +                if (max) {
 +                    properties.set('max', max);
 +                    validatedField.removeAttribute('max');
 +                }
 +                var maxlength = elAttr(validatedField, 'maxlength');
 +                if (maxlength) {
 +                    properties.set('maxlength', maxlength);
 +                    validatedField.removeAttribute('maxlength');
 +                }
 +                var min = elAttr(validatedField, 'min');
 +                if (min) {
 +                    properties.set('min', min);
 +                    validatedField.removeAttribute('min');
 +                }
 +                if (validatedField.required) {
 +                    properties.set('required', true);
 +                    validatedField.removeAttribute('required');
 +                }
 +                _validatedFieldProperties.set(validatedField, properties);
 +            });
 +        },
 +        /**
 +         * Shows the given node because of its own dependencies.
 +         *
 +         * @param     {HTMLElement}   node    shown node
 +         * @protected
 +         */
 +        _show: function (node) {
 +            elShow(node);
 +            _dependencyHiddenNodes.delete(node);
 +            // also show tab menu entry
 +            if (node.classList.contains('tabMenuContent')) {
 +                elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function (tabLink) {
 +                    if (elData(tabLink, 'name') === elData(node, 'name')) {
 +                        elShow(tabLink);
 +                    }
 +                });
 +            }
 +            elBySelAll('input, select', node, function (validatedField) {
 +                // if a container is shown, ignore all fields that
 +                // have a hidden parent element within the container
 +                var parentNode = validatedField.parentNode;
 +                while (parentNode !== node && parentNode.style.getPropertyValue('display') !== 'none') {
 +                    parentNode = parentNode.parentNode;
 +                }
 +                if (parentNode === node && _validatedFieldProperties.has(validatedField)) {
 +                    var properties = _validatedFieldProperties.get(validatedField);
 +                    if (properties.has('max')) {
 +                        elAttr(validatedField, 'max', properties.get('max'));
 +                    }
 +                    if (properties.has('maxlength')) {
 +                        elAttr(validatedField, 'maxlength', properties.get('maxlength'));
 +                    }
 +                    if (properties.has('min')) {
 +                        elAttr(validatedField, 'min', properties.get('min'));
 +                    }
 +                    if (properties.has('required')) {
 +                        elAttr(validatedField, 'required', '');
 +                    }
 +                    _validatedFieldProperties.delete(validatedField);
 +                }
 +            });
 +        },
 +        /**
 +         * Registers a new form field dependency.
 +         *
 +         * @param     {WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract}      dependency      new dependency
 +         */
 +        addDependency: function (dependency) {
 +            var dependentNode = dependency.getDependentNode();
 +            if (!_nodeDependencies.has(dependentNode.id)) {
 +                _nodeDependencies.set(dependentNode.id, [dependency]);
 +            }
 +            else {
 +                _nodeDependencies.get(dependentNode.id).push(dependency);
 +            }
 +            var fields = dependency.getFields();
 +            for (var i = 0, length = fields.length; i < length; i++) {
 +                var field = fields[i];
 +                var id = DomUtil.identify(field);
 +                if (!_fields.has(id)) {
 +                    _fields.set(id, field);
 +                    if (field.tagName === 'INPUT' && (field.type === 'checkbox' || field.type === 'radio' || field.type === 'hidden')) {
 +                        field.addEventListener('change', this.checkDependencies.bind(this));
 +                    }
 +                    else {
 +                        field.addEventListener('input', this.checkDependencies.bind(this));
 +                    }
 +                }
 +            }
 +        },
 +        /**
 +         * Checks if all dependencies are met.
 +         */
 +        checkDependencies: function () {
-             var obsoleteNodes = [];
++            var obsoleteNodeIds = [];
 +            _nodeDependencies.forEach(function (nodeDependencies, nodeId) {
 +                var dependentNode = elById(nodeId);
 +                // check if dependent node still exists
 +                if (dependentNode === null) {
-                     obsoleteNodes.push(dependentNode);
++                    obsoleteNodeIds.push(nodeId);
 +                    return;
 +                }
 +                for (var i = 0, length = nodeDependencies.length; i < length; i++) {
 +                    // if any dependency is not met, hide the element
 +                    if (!nodeDependencies[i].checkDependency()) {
 +                        this._hide(dependentNode);
 +                        return;
 +                    }
 +                }
 +                // all node dependency is met
 +                this._show(dependentNode);
 +            }.bind(this));
 +            // delete dependencies for removed elements
-             for (var i = 0, length = obsoleteNodes.length; i < length; i++) {
-                 _nodeDependencies.delete(obsoleteNodes[i].id);
++            for (var i = 0, length = obsoleteNodeIds.length; i < length; i++) {
++                _nodeDependencies.delete(obsoleteNodeIds[i]);
 +            }
 +            this.checkContainers();
 +        },
 +        /**
 +         * Adds the given callback to the list of callbacks called when checking containers.
 +         *
 +         * @param     {function}      callback
 +         */
 +        addContainerCheckCallback: function (callback) {
 +            if (typeof callback !== 'function') {
 +                throw new TypeError("Expected a valid callback for parameter 'callback'.");
 +            }
 +            EventHandler.add('com.woltlab.wcf.form.builder.dependency', 'checkContainers', callback);
 +        },
 +        /**
 +         * Checks the containers for their availability.
 +         *
 +         * If this function is called while containers are currently checked, the containers
 +         * will be checked after the current check has been finished completely.
 +         */
 +        checkContainers: function () {
 +            // check if containers are currently being checked
 +            if (_checkingContainers === true) {
 +                // and if that is the case, calling this method indicates, that after the current round,
 +                // containters should be checked to properly propagate changes in children to their parents
 +                _checkContainersAgain = true;
 +                return;
 +            }
 +            // starting to check containers also resets the flag to check containers again after the current check 
 +            _checkingContainers = true;
 +            _checkContainersAgain = false;
 +            EventHandler.fire('com.woltlab.wcf.form.builder.dependency', 'checkContainers');
 +            // finish checking containers and check if containters should be checked again
 +            _checkingContainers = false;
 +            if (_checkContainersAgain) {
 +                this.checkContainers();
 +            }
 +        },
 +        /**
 +         * Returns `true` if the given node has been hidden because of its own dependencies.
 +         *
 +         * @param     {HTMLElement}   node    checked node
 +         * @return    {boolean}
 +         */
 +        isHiddenByDependencies: function (node) {
 +            if (_dependencyHiddenNodes.has(node)) {
 +                return true;
 +            }
 +            var returnValue = false;
 +            _dependencyHiddenNodes.forEach(function (hiddenNode) {
 +                if (DomUtil.contains(hiddenNode, node)) {
 +                    returnValue = true;
 +                }
 +            });
 +            return returnValue;
 +        },
 +        /**
 +         * Registers the form with the given id with the dependency manager.
 +         *
 +         * @param     {string}        formId          id of register form
 +         * @throws    {Error}                         if given form id is invalid or has already been registered
 +         */
 +        register: function (formId) {
 +            var form = elById(formId);
 +            if (form === null) {
 +                throw new Error("Unknown element with id '" + formId + "'");
 +            }
 +            if (_forms.has(form)) {
 +                throw new Error("Form with id '" + formId + "' has already been registered.");
 +            }
 +            _forms.add(form);
 +        },
 +        /**
 +         * Unregisters the form with the given id and all of its dependencies.
 +         *
 +         * @param     {string}        formId          id of unregistered form
 +         */
 +        unregister: function (formId) {
 +            var form = elById(formId);
 +            if (form === null) {
 +                throw new Error("Unknown element with id '" + formId + "'");
 +            }
 +            if (!_forms.has(form)) {
 +                throw new Error("Form with id '" + formId + "' has not been registered.");
 +            }
 +            _forms.delete(form);
 +            _dependencyHiddenNodes.forEach(function (hiddenNode) {
 +                if (form.contains(hiddenNode)) {
 +                    _dependencyHiddenNodes.delete(hiddenNode);
 +                }
 +            });
 +            _nodeDependencies.forEach(function (dependencies, nodeId) {
 +                if (form.contains(elById(nodeId))) {
 +                    _nodeDependencies.delete(nodeId);
 +                }
 +                for (var i = 0, length = dependencies.length; i < length; i++) {
 +                    var fields = dependencies[i].getFields();
 +                    for (var j = 0, fieldsLength = fields.length; j < fieldsLength; j++) {
 +                        var field = fields[j];
 +                        _fields.delete(field.id);
 +                        _validatedFieldProperties.delete(field);
 +                    }
 +                }
 +            });
 +        }
 +    };
  });
index 8c4e86814cddcfc384d6d70b743e697b16f95086,0000000000000000000000000000000000000000..88d39e08ff567131a5024470177cecd51b85f210
mode 100644,000000..100644
--- /dev/null
@@@ -1,352 -1,0 +1,352 @@@
-                       var obsoleteNodes = [];
 +/**
 + * Manages form field dependencies.
 + * 
 + * @author    Matthias Schmidt
 + * @copyright 2001-2019 WoltLab GmbH
 + * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @module    WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager
 + * @since     5.2
 + */
 +define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Traverse', 'Dom/Util', 'ObjectMap'], function(Dictionary, DomChangeListener, EventHandler, List, DomTraverse, DomUtil, ObjectMap) {
 +      "use strict";
 +      
 +      /**
 +       * is `true` if containters are currently checked for their availablility, otherwise `false`
 +       * @type        {boolean}
 +       * @private
 +       */
 +      var _checkingContainers = false;
 +      
 +      /**
 +       * is `true` if containter will be checked again after the current check for their availablility
 +       * has finished, otherwise `false`
 +       * @type        {boolean}
 +       * @private
 +       */
 +      var _checkContainersAgain = true;
 +      
 +      /**
 +       * list of containers hidden due to their own dependencies
 +       * @type        {List}
 +       * @private
 +       */
 +      var _dependencyHiddenNodes = new List();
 +      
 +      /**
 +       * list of fields for which event listeners have been registered
 +       * @type        {Dictionary}
 +       * @private
 +       */
 +      var _fields = new Dictionary();
 +      
 +      /**
 +       * list of registered forms
 +       * @type        {List}
 +       * @private
 +       */
 +      var _forms = new List();
 +      
 +      /**
 +       * list of dependencies grouped by the dependent node they belong to
 +       * @type        {Dictionary}
 +       * @private
 +       */
 +      var _nodeDependencies = new Dictionary();
 +      
 +      /**
 +       * cache of validation-related properties of hidden form fields
 +       * @type        {ObjectMap}
 +       * @private
 +       */
 +      var _validatedFieldProperties = new ObjectMap();
 +      
 +      return {
 +              /**
 +               * Hides the given node because of its own dependencies.
 +               * 
 +               * @param       {HTMLElement}   node    hidden node
 +               * @protected
 +               */
 +              _hide: function(node) {
 +                      elHide(node);
 +                      _dependencyHiddenNodes.add(node);
 +                      
 +                      // also hide tab menu entry
 +                      if (node.classList.contains('tabMenuContent')) {
 +                              elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function(tabLink) {
 +                                      if (elData(tabLink, 'name') === elData(node, 'name')) {
 +                                              elHide(tabLink);
 +                                      }
 +                              });
 +                      }
 +                      
 +                      elBySelAll('[max], [maxlength], [min], [required]', node, function(validatedField) {
 +                              var properties = new Dictionary();
 +                              
 +                              var max = elAttr(validatedField, 'max');
 +                              if (max) {
 +                                      properties.set('max', max);
 +                                      validatedField.removeAttribute('max');
 +                              }
 +                              
 +                              var maxlength = elAttr(validatedField, 'maxlength');
 +                              if (maxlength) {
 +                                      properties.set('maxlength', maxlength);
 +                                      validatedField.removeAttribute('maxlength');
 +                              }
 +                              
 +                              var min = elAttr(validatedField, 'min');
 +                              if (min) {
 +                                      properties.set('min', min);
 +                                      validatedField.removeAttribute('min');
 +                              }
 +                              
 +                              if (validatedField.required) {
 +                                      properties.set('required', true);
 +                                      validatedField.removeAttribute('required');
 +                              }
 +                              
 +                              _validatedFieldProperties.set(validatedField, properties);
 +                      });
 +              },
 +              
 +              /**
 +               * Shows the given node because of its own dependencies.
 +               * 
 +               * @param       {HTMLElement}   node    shown node
 +               * @protected
 +               */
 +              _show: function(node) {
 +                      elShow(node);
 +                      _dependencyHiddenNodes.delete(node);
 +                      
 +                      // also show tab menu entry
 +                      if (node.classList.contains('tabMenuContent')) {
 +                              elBySelAll('li', DomTraverse.prevByClass(node, 'tabMenu'), function(tabLink) {
 +                                      if (elData(tabLink, 'name') === elData(node, 'name')) {
 +                                              elShow(tabLink);
 +                                      }
 +                              });
 +                      }
 +                      
 +                      elBySelAll('input, select', node, function(validatedField) {
 +                              // if a container is shown, ignore all fields that
 +                              // have a hidden parent element within the container
 +                              var parentNode = validatedField.parentNode;
 +                              while (parentNode !== node && parentNode.style.getPropertyValue('display') !== 'none') {
 +                                      parentNode = parentNode.parentNode;
 +                              }
 +                              
 +                              if (parentNode === node && _validatedFieldProperties.has(validatedField)) {
 +                                      var properties = _validatedFieldProperties.get(validatedField);
 +                                      
 +                                      if (properties.has('max')) {
 +                                              elAttr(validatedField, 'max', properties.get('max'));
 +                                      }
 +                                      if (properties.has('maxlength')) {
 +                                              elAttr(validatedField, 'maxlength', properties.get('maxlength'));
 +                                      }
 +                                      if (properties.has('min')) {
 +                                              elAttr(validatedField, 'min', properties.get('min'));
 +                                      }
 +                                      if (properties.has('required')) {
 +                                              elAttr(validatedField, 'required', '');
 +                                      }
 +                                      
 +                                      _validatedFieldProperties.delete(validatedField);
 +                              }
 +                      });
 +              },
 +              
 +              /**
 +               * Registers a new form field dependency.
 +               * 
 +               * @param       {WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract}      dependency      new dependency
 +               */
 +              addDependency: function(dependency) {
 +                      var dependentNode = dependency.getDependentNode();
 +                      if (!_nodeDependencies.has(dependentNode.id)) {
 +                              _nodeDependencies.set(dependentNode.id, [dependency]);
 +                      }
 +                      else {
 +                              _nodeDependencies.get(dependentNode.id).push(dependency);
 +                      }
 +                      
 +                      var fields = dependency.getFields();
 +                      for (var i = 0, length = fields.length; i < length; i++) {
 +                              var field = fields[i];
 +                              var id = DomUtil.identify(field);
 +                              
 +                              if (!_fields.has(id)) {
 +                                      _fields.set(id, field);
 +                                      
 +                                      if (field.tagName === 'INPUT' && (field.type === 'checkbox' || field.type === 'radio' || field.type === 'hidden')) {
 +                                              field.addEventListener('change', this.checkDependencies.bind(this));
 +                                      }
 +                                      else {
 +                                              field.addEventListener('input', this.checkDependencies.bind(this));
 +                                      }
 +                              }
 +                      }
 +              },
 +              
 +              /**
 +               * Checks if all dependencies are met.
 +               */
 +              checkDependencies: function() {
-                                       obsoleteNodes.push(dependentNode);
++                      var obsoleteNodeIds = [];
 +                      
 +                      _nodeDependencies.forEach(function(nodeDependencies, nodeId) {
 +                              var dependentNode = elById(nodeId);
 +                              
 +                              // check if dependent node still exists
 +                              if (dependentNode === null) {
-                       for (var i = 0, length = obsoleteNodes.length; i < length; i++) {
-                               _nodeDependencies.delete(obsoleteNodes[i].id);
++                                      obsoleteNodeIds.push(nodeId);
 +                                      return;
 +                              }
 +                              
 +                              for (var i = 0, length = nodeDependencies.length; i < length; i++) {
 +                                      // if any dependency is not met, hide the element
 +                                      if (!nodeDependencies[i].checkDependency()) {
 +                                              this._hide(dependentNode);
 +                                              return;
 +                                      }
 +                              }
 +                              
 +                              // all node dependency is met
 +                              this._show(dependentNode);
 +                      }.bind(this));
 +                      
 +                      // delete dependencies for removed elements
++                      for (var i = 0, length = obsoleteNodeIds.length; i < length; i++) {
++                              _nodeDependencies.delete(obsoleteNodeIds[i]);
 +                      }
 +                      
 +                      this.checkContainers();
 +              },
 +              
 +              /**
 +               * Adds the given callback to the list of callbacks called when checking containers.
 +               * 
 +               * @param       {function}      callback
 +               */
 +              addContainerCheckCallback: function(callback) {
 +                      if (typeof callback !== 'function') {
 +                              throw new TypeError("Expected a valid callback for parameter 'callback'.");
 +                      }
 +                      
 +                      EventHandler.add('com.woltlab.wcf.form.builder.dependency', 'checkContainers', callback);
 +              },
 +              
 +              /**
 +               * Checks the containers for their availability.
 +               * 
 +               * If this function is called while containers are currently checked, the containers
 +               * will be checked after the current check has been finished completely.
 +               */
 +              checkContainers: function() {
 +                      // check if containers are currently being checked
 +                      if (_checkingContainers === true) {
 +                              // and if that is the case, calling this method indicates, that after the current round,
 +                              // containters should be checked to properly propagate changes in children to their parents
 +                              _checkContainersAgain = true;
 +                              
 +                              return;
 +                      }
 +                      
 +                      // starting to check containers also resets the flag to check containers again after the current check 
 +                      _checkingContainers = true;
 +                      _checkContainersAgain = false;
 +                      
 +                      EventHandler.fire('com.woltlab.wcf.form.builder.dependency', 'checkContainers');
 +                      
 +                      // finish checking containers and check if containters should be checked again
 +                      _checkingContainers = false;
 +                      if (_checkContainersAgain) {
 +                              this.checkContainers();
 +                      }
 +              },
 +              
 +              /**
 +               * Returns `true` if the given node has been hidden because of its own dependencies.
 +               * 
 +               * @param       {HTMLElement}   node    checked node
 +               * @return      {boolean}
 +               */
 +              isHiddenByDependencies: function(node) {
 +                      if (_dependencyHiddenNodes.has(node)) {
 +                              return true;
 +                      }
 +                      
 +                      var returnValue = false;
 +                      _dependencyHiddenNodes.forEach(function(hiddenNode) {
 +                              if (DomUtil.contains(hiddenNode, node)) {
 +                                      returnValue = true;
 +                              }
 +                      });
 +                      
 +                      return returnValue;
 +              },
 +              
 +              /**
 +               * Registers the form with the given id with the dependency manager.
 +               * 
 +               * @param       {string}        formId          id of register form
 +               * @throws      {Error}                         if given form id is invalid or has already been registered
 +               */
 +              register: function(formId) {
 +                      var form = elById(formId);
 +                      
 +                      if (form === null) {
 +                              throw new Error("Unknown element with id '" + formId + "'");
 +                      }
 +                      
 +                      if (_forms.has(form)) {
 +                              throw new Error("Form with id '" + formId + "' has already been registered.");
 +                      }
 +                      
 +                      _forms.add(form);
 +              },
 +              
 +              /**
 +               * Unregisters the form with the given id and all of its dependencies.
 +               * 
 +               * @param       {string}        formId          id of unregistered form
 +               */
 +              unregister: function(formId) {
 +                      var form = elById(formId);
 +                      
 +                      if (form === null) {
 +                              throw new Error("Unknown element with id '" + formId + "'");
 +                      }
 +                      
 +                      if (!_forms.has(form)) {
 +                              throw new Error("Form with id '" + formId + "' has not been registered.");
 +                      }
 +                      
 +                      _forms.delete(form);
 +                      
 +                      _dependencyHiddenNodes.forEach(function(hiddenNode) {
 +                              if (form.contains(hiddenNode)) {
 +                                      _dependencyHiddenNodes.delete(hiddenNode);
 +                              }
 +                      });
 +                      _nodeDependencies.forEach(function(dependencies, nodeId) {
 +                              if (form.contains(elById(nodeId))) {
 +                                      _nodeDependencies.delete(nodeId);
 +                              }
 +                              
 +                              for (var i = 0, length = dependencies.length; i < length; i++) {
 +                                      var fields = dependencies[i].getFields();
 +                                      for (var j = 0, fieldsLength = fields.length; j < fieldsLength; j++) {
 +                                              var field = fields[j];
 +                                              
 +                                              _fields.delete(field.id);
 +                                              
 +                                              _validatedFieldProperties.delete(field);
 +                                      }
 +                              }
 +                      });
 +              }
 +      };
 +});