From 7868948e981a41f277f2e37f7f53a62ad99789af Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sat, 12 Dec 2020 16:13:06 +0100 Subject: [PATCH] Convert `Form/Builder/Field/Dependency/Manager` to TypeScript --- .../Form/Builder/Field/Dependency/Manager.js | 299 +++++++-------- .../Form/Builder/Field/Dependency/Manager.js | 352 ------------------ .../Form/Builder/Field/Dependency/Manager.ts | 305 +++++++++++++++ 3 files changed, 431 insertions(+), 525 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js index 6c7bd9d2fd..6e9780f718 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js @@ -1,207 +1,143 @@ /** * Manages form field dependencies. * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @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 + * @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 index d7137249a2..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.js +++ /dev/null @@ -1,352 +0,0 @@ -/** - * Manages form field dependencies. - * - * @author Matthias Schmidt - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @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 index 0000000000..1624b8bf7c --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Form/Builder/Field/Dependency/Manager.ts @@ -0,0 +1,305 @@ +/** + * Manages form field dependencies. + * + * @author Matthias Schmidt + * @copyright 2001-2020 WoltLab GmbH + * @license GNU Lesser General Public License + * @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; + +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; + +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(); + + 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; -- 2.20.1