Convert `Form/Builder/Field/Dependency/Manager` to TypeScript
authorMatthias Schmidt <gravatronics@live.com>
Sat, 12 Dec 2020 15:13:06 +0000 (16:13 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Tue, 15 Dec 2020 17:25:43 +0000 (18:25 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts [new file with mode: 0644]

index 6c7bd9d2fdacb8b7ab320bb3badeb726177fb2cd..6e9780f718700214969aaf3163656801f27184d2 100644 (file)
 /**
  * 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
+ * @author  Matthias Schmidt
+ * @copyright 2001-2020 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/Util', 'ObjectMap'], function (Dictionary, DomChangeListener, EventHandler, List, DomUtil, ObjectMap) {
+define(["require", "exports", "tslib", "../../../../Dom/Util", "../../../../Event/Handler", "../../../../Core"], function (require, exports, tslib_1, Util_1, EventHandler, Core) {
     "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 {
+    Util_1 = tslib_1.__importDefault(Util_1);
+    EventHandler = tslib_1.__importStar(EventHandler);
+    Core = tslib_1.__importStar(Core);
+    const _dependencyHiddenNodes = new Set();
+    const _fields = new Map();
+    const _forms = new Set();
+    const _nodeDependencies = new Map();
+    const _validatedFieldProperties = new WeakMap();
+    let _checkingContainers = false;
+    let _checkContainersAgain = true;
+    const Manager = {
         /**
          * Hides the given node because of its own dependencies.
-         *
-         * @param      {HTMLElement}   node    hidden node
-         * @protected
          */
-        _hide: function (node) {
-            elHide(node);
+        _hide(node) {
+            node.style.display = "none";
             _dependencyHiddenNodes.add(node);
             // also hide tab menu entry
-            if (node.classList.contains('tabMenuContent')) {
-                elBySelAll('li', node.parentNode.querySelector('.tabMenu'), function (tabLink) {
-                    if (elData(tabLink, 'name') === elData(node, 'name')) {
-                        elHide(tabLink);
+            if (node.classList.contains("tabMenuContent")) {
+                node
+                    .parentNode.querySelector(".tabMenu")
+                    .querySelectorAll("li")
+                    .forEach((tabLink) => {
+                    if (tabLink.dataset.name === node.dataset.name) {
+                        tabLink.style.display = "none";
                     }
                 });
             }
-            elBySelAll('[max], [maxlength], [min], [required]', node, function (validatedField) {
-                var properties = new Dictionary();
-                var max = elAttr(validatedField, 'max');
+            node.querySelectorAll("[max], [maxlength], [min], [required]").forEach((validatedField) => {
+                const properties = new Map();
+                const max = validatedField.getAttribute("max");
                 if (max) {
-                    properties.set('max', max);
-                    validatedField.removeAttribute('max');
+                    properties.set("max", max);
+                    validatedField.removeAttribute("max");
                 }
-                var maxlength = elAttr(validatedField, 'maxlength');
+                const maxlength = validatedField.getAttribute("maxlength");
                 if (maxlength) {
-                    properties.set('maxlength', maxlength);
-                    validatedField.removeAttribute('maxlength');
+                    properties.set("maxlength", maxlength);
+                    validatedField.removeAttribute("maxlength");
                 }
-                var min = elAttr(validatedField, 'min');
+                const min = validatedField.getAttribute("min");
                 if (min) {
-                    properties.set('min', min);
-                    validatedField.removeAttribute('min');
+                    properties.set("min", min);
+                    validatedField.removeAttribute("min");
                 }
                 if (validatedField.required) {
-                    properties.set('required', true);
-                    validatedField.removeAttribute('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);
+        _show(node) {
+            node.style.display = "block";
             _dependencyHiddenNodes.delete(node);
             // also show tab menu entry
-            if (node.classList.contains('tabMenuContent')) {
-                elBySelAll('li', node.parentNode.querySelector('.tabMenu'), function (tabLink) {
-                    if (elData(tabLink, 'name') === elData(node, 'name')) {
-                        elShow(tabLink);
+            if (node.classList.contains("tabMenuContent")) {
+                node
+                    .parentNode.querySelector(".tabMenu")
+                    .querySelectorAll("li")
+                    .forEach((tabLink) => {
+                    if (tabLink.dataset.name === node.dataset.name) {
+                        tabLink.style.display = "block";
                     }
                 });
             }
-            elBySelAll('input, select', node, function (validatedField) {
+            node.querySelectorAll("input, select").forEach((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') {
+                let 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'));
+                    const properties = _validatedFieldProperties.get(validatedField);
+                    if (properties.has("max")) {
+                        validatedField.setAttribute("max", properties.get("max"));
                     }
-                    if (properties.has('maxlength')) {
-                        elAttr(validatedField, 'maxlength', properties.get('maxlength'));
+                    if (properties.has("maxlength")) {
+                        validatedField.setAttribute("maxlength", properties.get("maxlength"));
                     }
-                    if (properties.has('min')) {
-                        elAttr(validatedField, 'min', properties.get('min'));
+                    if (properties.has("min")) {
+                        validatedField.setAttribute("min", properties.get("min"));
                     }
-                    if (properties.has('required')) {
-                        elAttr(validatedField, 'required', '');
+                    if (properties.has("required")) {
+                        validatedField.setAttribute("required", "");
                     }
                     _validatedFieldProperties.delete(validatedField);
                 }
             });
         },
+        /**
+         * Adds the given callback to the list of callbacks called when checking containers.
+         */
+        addContainerCheckCallback(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);
+        },
         /**
          * Registers a new form field dependency.
-         *
-         * @param      {WoltLabSuite/Core/Form/Builder/Field/Dependency/Abstract}      dependency      new dependency
          */
-        addDependency: function (dependency) {
-            var dependentNode = dependency.getDependentNode();
+        addDependency(dependency) {
+            const 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);
+            dependency.getFields().forEach((field) => {
+                const id = Util_1.default.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));
+                    if (field.tagName === "INPUT" &&
+                        (field.type === "checkbox" ||
+                            field.type === "radio" ||
+                            field.type === "hidden")) {
+                        field.addEventListener("change", () => this.checkDependencies());
                     }
                     else {
-                        field.addEventListener('input', this.checkDependencies.bind(this));
+                        field.addEventListener("input", () => this.checkDependencies());
                     }
                 }
-            }
-        },
-        /**
-         * 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.
@@ -209,7 +145,7 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Util',
          * 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 () {
+        checkContainers() {
             // 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,
@@ -217,29 +153,51 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Util',
                 _checkContainersAgain = true;
                 return;
             }
-            // starting to check containers also resets the flag to check containers again after the current check 
+            // 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');
+            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();
             }
         },
+        /**
+         * Checks if all dependencies are met.
+         */
+        checkDependencies() {
+            const obsoleteNodeIds = [];
+            _nodeDependencies.forEach((nodeDependencies, nodeId) => {
+                const dependentNode = document.getElementById(nodeId);
+                if (dependentNode === null) {
+                    obsoleteNodeIds.push(nodeId);
+                    return;
+                }
+                let dependenciesMet = true;
+                nodeDependencies.forEach((dependency) => {
+                    if (!dependency.checkDependency()) {
+                        dependentNode.style.display = "none";
+                        dependenciesMet = false;
+                    }
+                });
+                if (dependenciesMet) {
+                    this._show(dependentNode);
+                }
+            });
+            obsoleteNodeIds.forEach((id) => _nodeDependencies.delete(id));
+            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) {
+        isHiddenByDependencies(node) {
             if (_dependencyHiddenNodes.has(node)) {
                 return true;
             }
-            var returnValue = false;
-            _dependencyHiddenNodes.forEach(function (hiddenNode) {
-                if (DomUtil.contains(hiddenNode, node)) {
+            let returnValue = false;
+            _dependencyHiddenNodes.forEach((hiddenNode) => {
+                if (node.contains(hiddenNode)) {
                     returnValue = true;
                 }
             });
@@ -247,12 +205,9 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Util',
         },
         /**
          * 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);
+        register(formId) {
+            const form = document.getElementById(formId);
             if (form === null) {
                 throw new Error("Unknown element with id '" + formId + "'");
             }
@@ -263,11 +218,9 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Util',
         },
         /**
          * 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);
+        unregister(formId) {
+            const form = document.getElementById(formId);
             if (form === null) {
                 throw new Error("Unknown element with id '" + formId + "'");
             }
@@ -275,24 +228,24 @@ define(['Dictionary', 'Dom/ChangeListener', 'EventHandler', 'List', 'Dom/Util',
                 throw new Error("Form with id '" + formId + "' has not been registered.");
             }
             _forms.delete(form);
-            _dependencyHiddenNodes.forEach(function (hiddenNode) {
+            _dependencyHiddenNodes.forEach((hiddenNode) => {
                 if (form.contains(hiddenNode)) {
                     _dependencyHiddenNodes.delete(hiddenNode);
                 }
             });
-            _nodeDependencies.forEach(function (dependencies, nodeId) {
-                if (form.contains(elById(nodeId))) {
+            _nodeDependencies.forEach((dependencies, nodeId) => {
+                if (form.contains(document.getElementById(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];
+                dependencies.forEach((dependency) => {
+                    dependency.getFields().forEach((field) => {
                         _fields.delete(field.id);
                         _validatedFieldProperties.delete(field);
-                    }
-                }
+                    });
+                });
             });
-        }
+        },
     };
+    Core.enableLegacyInheritance(Manager);
+    return Manager;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js
deleted file mode 100644 (file)
index d713724..0000000
+++ /dev/null
@@ -1,352 +0,0 @@
-/**
- * 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/Util', 'ObjectMap'], function(Dictionary, DomChangeListener, EventHandler, List, 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', node.parentNode.querySelector('.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', node.parentNode.querySelector('.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);
-                                       }
-                               }
-                       });
-               }
-       };
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts
new file mode 100644 (file)
index 0000000..1624b8b
--- /dev/null
@@ -0,0 +1,305 @@
+/**
+ * Manages form field dependencies.
+ *
+ * @author  Matthias Schmidt
+ * @copyright 2001-2020 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
+ */
+
+import DomUtil from "../../../../Dom/Util";
+import * as EventHandler from "../../../../Event/Handler";
+import FormBuilderFormFieldDependency from "./Abstract";
+import * as DomTraverse from "../../../../Dom/Traverse";
+import * as Core from "../../../../Core";
+
+type PropertiesMap = Map<string, string>;
+
+const _dependencyHiddenNodes = new Set<HTMLElement>();
+const _fields = new Map<string, HTMLElement>();
+const _forms = new Set<HTMLElement>();
+const _nodeDependencies = new Map<string, FormBuilderFormFieldDependency[]>();
+const _validatedFieldProperties = new WeakMap<HTMLElement, PropertiesMap>();
+
+let _checkingContainers = false;
+let _checkContainersAgain = true;
+
+type Callback = (...args: any[]) => void;
+
+const Manager = {
+  /**
+   * Hides the given node because of its own dependencies.
+   */
+  _hide(node: HTMLElement): void {
+    node.style.display = "none";
+    _dependencyHiddenNodes.add(node);
+
+    // also hide tab menu entry
+    if (node.classList.contains("tabMenuContent")) {
+      node
+        .parentNode!.querySelector(".tabMenu")!
+        .querySelectorAll("li")
+        .forEach((tabLink) => {
+          if (tabLink.dataset.name === node.dataset.name) {
+            tabLink.style.display = "none";
+          }
+        });
+    }
+
+    node.querySelectorAll("[max], [maxlength], [min], [required]").forEach((validatedField: HTMLInputElement) => {
+      const properties = new Map<string, string>();
+
+      const max = validatedField.getAttribute("max");
+      if (max) {
+        properties.set("max", max);
+        validatedField.removeAttribute("max");
+      }
+
+      const maxlength = validatedField.getAttribute("maxlength");
+      if (maxlength) {
+        properties.set("maxlength", maxlength);
+        validatedField.removeAttribute("maxlength");
+      }
+
+      const min = validatedField.getAttribute("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.
+   */
+  _show(node: HTMLElement): void {
+    node.style.display = "block";
+    _dependencyHiddenNodes.delete(node);
+
+    // also show tab menu entry
+    if (node.classList.contains("tabMenuContent")) {
+      node
+        .parentNode!.querySelector(".tabMenu")!
+        .querySelectorAll("li")
+        .forEach((tabLink) => {
+          if (tabLink.dataset.name === node.dataset.name) {
+            tabLink.style.display = "block";
+          }
+        });
+    }
+
+    node.querySelectorAll("input, select").forEach((validatedField: HTMLInputElement | HTMLSelectElement) => {
+      // if a container is shown, ignore all fields that
+      // have a hidden parent element within the container
+      let parentNode = validatedField.parentNode! as HTMLElement;
+      while (parentNode !== node && parentNode.style.getPropertyValue("display") !== "none") {
+        parentNode = parentNode.parentNode! as HTMLElement;
+      }
+
+      if (parentNode === node && _validatedFieldProperties.has(validatedField)) {
+        const properties = _validatedFieldProperties.get(validatedField)!;
+
+        if (properties.has("max")) {
+          validatedField.setAttribute("max", properties.get("max")!);
+        }
+        if (properties.has("maxlength")) {
+          validatedField.setAttribute("maxlength", properties.get("maxlength")!);
+        }
+        if (properties.has("min")) {
+          validatedField.setAttribute("min", properties.get("min")!);
+        }
+        if (properties.has("required")) {
+          validatedField.setAttribute("required", "");
+        }
+
+        _validatedFieldProperties.delete(validatedField);
+      }
+    });
+  },
+
+  /**
+   * Adds the given callback to the list of callbacks called when checking containers.
+   */
+  addContainerCheckCallback(callback: Callback): void {
+    if (typeof callback !== "function") {
+      throw new TypeError("Expected a valid callback for parameter 'callback'.");
+    }
+
+    EventHandler.add("com.woltlab.wcf.form.builder.dependency", "checkContainers", callback);
+  },
+
+  /**
+   * Registers a new form field dependency.
+   */
+  addDependency(dependency: FormBuilderFormFieldDependency): void {
+    const dependentNode = dependency.getDependentNode();
+    if (!_nodeDependencies.has(dependentNode.id)) {
+      _nodeDependencies.set(dependentNode.id, [dependency]);
+    } else {
+      _nodeDependencies.get(dependentNode.id)!.push(dependency);
+    }
+
+    dependency.getFields().forEach((field) => {
+      const id = DomUtil.identify(field);
+
+      if (!_fields.has(id)) {
+        _fields.set(id, field);
+
+        if (
+          field.tagName === "INPUT" &&
+          ((field as HTMLInputElement).type === "checkbox" ||
+            (field as HTMLInputElement).type === "radio" ||
+            (field as HTMLInputElement).type === "hidden")
+        ) {
+          field.addEventListener("change", () => this.checkDependencies());
+        } else {
+          field.addEventListener("input", () => this.checkDependencies());
+        }
+      }
+    });
+  },
+
+  /**
+   * 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(): void {
+    // 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();
+    }
+  },
+
+  /**
+   * Checks if all dependencies are met.
+   */
+  checkDependencies(): void {
+    const obsoleteNodeIds: string[] = [];
+
+    _nodeDependencies.forEach((nodeDependencies, nodeId) => {
+      const dependentNode = document.getElementById(nodeId);
+      if (dependentNode === null) {
+        obsoleteNodeIds.push(nodeId);
+
+        return;
+      }
+
+      let dependenciesMet = true;
+      nodeDependencies.forEach((dependency) => {
+        if (!dependency.checkDependency()) {
+          dependentNode.style.display = "none";
+          dependenciesMet = false;
+        }
+      });
+
+      if (dependenciesMet) {
+        this._show(dependentNode);
+      }
+    });
+
+    obsoleteNodeIds.forEach((id) => _nodeDependencies.delete(id));
+
+    this.checkContainers();
+  },
+
+  /**
+   * Returns `true` if the given node has been hidden because of its own dependencies.
+   */
+  isHiddenByDependencies(node: HTMLElement): boolean {
+    if (_dependencyHiddenNodes.has(node)) {
+      return true;
+    }
+
+    let returnValue = false;
+    _dependencyHiddenNodes.forEach((hiddenNode) => {
+      if (node.contains(hiddenNode)) {
+        returnValue = true;
+      }
+    });
+
+    return returnValue;
+  },
+
+  /**
+   * Registers the form with the given id with the dependency manager.
+   */
+  register(formId: string): void {
+    const form = document.getElementById(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.
+   */
+  unregister(formId: string): void {
+    const form = document.getElementById(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((hiddenNode) => {
+      if (form.contains(hiddenNode)) {
+        _dependencyHiddenNodes.delete(hiddenNode);
+      }
+    });
+    _nodeDependencies.forEach((dependencies, nodeId) => {
+      if (form.contains(document.getElementById(nodeId))) {
+        _nodeDependencies.delete(nodeId);
+      }
+
+      dependencies.forEach((dependency) => {
+        dependency.getFields().forEach((field) => {
+          _fields.delete(field.id);
+
+          _validatedFieldProperties.delete(field);
+        });
+      });
+    });
+  },
+};
+
+Core.enableLegacyInheritance(Manager);
+
+export = Manager;