Convert `Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList` to TypeScript
authorMatthias Schmidt <gravatronics@live.com>
Tue, 12 Jan 2021 09:57:15 +0000 (10:57 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Tue, 12 Jan 2021 09:57:15 +0000 (10:57 +0100)
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.ts [new file with mode: 0644]

index 94403ad4ff6826bbebae80a7c49e63a164f77db8..05a71361362058be1ab3ad4ae5f0063bbc0870c4 100644 (file)
 /**
- * Abstract implementation of the JavaScript component of a form field handling
- * a list of packages.
+ * Abstract implementation of the JavaScript component of a form field handling a list of packages.
  *
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
- * @since      5.2
+ * @author  Matthias Schmidt
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
+ * @since 5.2
  */
-define(['Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language'], function (DomChangeListener, DomTraverse, DomUtil, EventKey, Language) {
+define(["require", "exports", "tslib", "../../../../../../Core", "../../../../../../Language", "../../../../../../Dom/Traverse", "../../../../../../Dom/Change/Listener", "../../../../../../Dom/Util"], function (require, exports, tslib_1, Core, Language, DomTraverse, Listener_1, Util_1) {
     "use strict";
-    /**
-     * @constructor
-     */
-    function AbstractPackageList(formFieldId, existingPackages) {
-        this.init(formFieldId, existingPackages);
-    }
-    ;
-    AbstractPackageList.prototype = {
-        /**
-         * Initializes the package list handler.
-         *
-         * @param      {string}        formFieldId             id of the associated form field
-         * @param      {object[]}      existingPackages        data of existing packages
-         */
-        init: function (formFieldId, existingPackages) {
-            this._formFieldId = formFieldId;
-            this._packageList = elById(this._formFieldId + '_packageList');
-            if (this._packageList === null) {
-                throw new Error("Cannot find package list for packages field with id '" + this._formFieldId + "'.");
-            }
-            this._packageIdentifier = elById(this._formFieldId + '_packageIdentifier');
-            if (this._packageIdentifier === null) {
-                throw new Error("Cannot find package identifier form field for packages field with id '" + this._formFieldId + "'.");
-            }
-            this._packageIdentifier.addEventListener('keypress', this._keyPress.bind(this));
-            this._addButton = elById(this._formFieldId + '_addButton');
-            if (this._addButton === null) {
-                throw new Error("Cannot find add button for packages field with id '" + this._formFieldId + "'.");
-            }
-            this._addButton.addEventListener('click', this._addPackage.bind(this));
-            this._form = this._packageList.closest('form');
-            if (this._form === null) {
-                throw new Error("Cannot find form element for packages field with id '" + this._formFieldId + "'.");
-            }
-            this._form.addEventListener('submit', this._submit.bind(this));
-            existingPackages.forEach(this._addPackageByData.bind(this));
-        },
+    Core = tslib_1.__importStar(Core);
+    Language = tslib_1.__importStar(Language);
+    DomTraverse = tslib_1.__importStar(DomTraverse);
+    Listener_1 = tslib_1.__importDefault(Listener_1);
+    Util_1 = tslib_1.__importDefault(Util_1);
+    class AbstractPackageList {
+        constructor(formFieldId, existingPackages) {
+            this.formFieldId = formFieldId;
+            this.packageList = document.getElementById(`${this.formFieldId}_packageList`);
+            if (this.packageList === null) {
+                throw new Error(`Cannot find package list for packages field with id '${this.formFieldId}'.`);
+            }
+            this.packageIdentifier = document.getElementById(`${this.formFieldId}_packageIdentifier`);
+            if (this.packageIdentifier === null) {
+                throw new Error(`Cannot find package identifier form field for packages field with id '${this.formFieldId}'.`);
+            }
+            this.packageIdentifier.addEventListener("keypress", (ev) => this.keyPress(ev));
+            this.addButton = document.getElementById(`${this.formFieldId}_addButton`);
+            if (this.addButton === null) {
+                throw new Error(`Cannot find add button for packages field with id '${this.formFieldId}'.`);
+            }
+            this.addButton.addEventListener("click", (ev) => this.addPackage(ev));
+            this.form = this.packageList.closest("form");
+            if (this.form === null) {
+                throw new Error(`Cannot find form element for packages field with id '${this.formFieldId}'.`);
+            }
+            this.form.addEventListener("submit", () => this.submit());
+            existingPackages.forEach((data) => this.addPackageByData(data));
+        }
         /**
-         * Adds a package to the package list as a consequence of the given
-         * event. If the package data is invalid, an error message is shown
-         * and no package is added.
+         * Adds a package to the package list as a consequence of the given event.
          *
-         * @param      {Event}         event   event that triggered trying to add the package
+         * If the package data is invalid, an error message is shown and no package is added.
          */
-        _addPackage: function (event) {
+        addPackage(event) {
             event.preventDefault();
             event.stopPropagation();
             // validate data
-            if (!this._validateInput()) {
+            if (!this.validateInput()) {
                 return;
             }
-            this._addPackageByData(this._getInputData());
+            this.addPackageByData(this.getInputData());
             // empty fields
-            this._emptyInput();
-            this._packageIdentifier.focus();
-        },
+            this.emptyInput();
+            this.packageIdentifier.focus();
+        }
         /**
          * Adds a package to the package list using the given package data.
-         *
-         * @param      {object}        packageData
          */
-        _addPackageByData: function (packageData) {
+        addPackageByData(packageData) {
             // add package to list
-            var listItem = elCreate('li');
-            this._populateListItem(listItem, packageData);
+            const listItem = document.createElement("li");
+            this.populateListItem(listItem, packageData);
             // add delete button
-            var deleteButton = elCreate('span');
-            deleteButton.className = 'icon icon16 fa-times pointer jsTooltip';
-            elAttr(deleteButton, 'title', Language.get('wcf.global.button.delete'));
-            deleteButton.addEventListener('click', this._removePackage.bind(this));
-            DomUtil.prepend(deleteButton, listItem);
-            this._packageList.appendChild(listItem);
-            DomChangeListener.trigger();
-        },
+            const deleteButton = document.createElement("span");
+            deleteButton.className = "icon icon16 fa-times pointer jsTooltip";
+            deleteButton.title = Language.get("wcf.global.button.delete");
+            deleteButton.addEventListener("click", (ev) => this.removePackage(ev));
+            listItem.insertAdjacentElement("afterbegin", deleteButton);
+            this.packageList.appendChild(listItem);
+            Listener_1.default.trigger();
+        }
         /**
          * Creates the hidden fields when the form is submitted.
-         *
-         * @param      {HTMLElement}   listElement     package list element from the package list
-         * @param      {int}           index           package index
          */
-        _createSubmitFields: function (listElement, index) {
-            var packageIdentifier = elCreate('input');
-            elAttr(packageIdentifier, 'type', 'hidden');
-            elAttr(packageIdentifier, 'name', this._formFieldId + '[' + index + '][packageIdentifier]');
-            packageIdentifier.value = elData(listElement, 'package-identifier');
-            this._form.appendChild(packageIdentifier);
-        },
+        createSubmitFields(listElement, index) {
+            const packageIdentifier = document.createElement("input");
+            packageIdentifier.type = "hidden";
+            packageIdentifier.name = `${this.formFieldId}[${index}][packageIdentifier]`;
+            packageIdentifier.value = listElement.dataset.packageIdentifier;
+            this.form.appendChild(packageIdentifier);
+        }
         /**
          * Empties the input fields.
          */
-        _emptyInput: function () {
-            this._packageIdentifier.value = '';
-        },
-        /**
-         * Returns the error element for the given form field element.
-         * If `createIfNonExistent` is not given or `false`, `null` is returned
-         * if there is no error element, otherwise an empty error element
-         * is created and returned.
-         *
-         * @param      {?boolean}      createIfNonExistent
-         * @return     {?HTMLElement}
-         */
-        _getErrorElement: function (element, createIfNoNExistent) {
-            var error = DomTraverse.nextByClass(element, 'innerError');
-            if (error === null && createIfNoNExistent) {
-                error = elCreate('small');
-                error.className = 'innerError';
-                DomUtil.insertAfter(error, element);
-            }
-            return error;
-        },
+        emptyInput() {
+            this.packageIdentifier.value = "";
+        }
         /**
          * Returns the current data of the input fields to add a new package.
-         *
-         * @return     {object}
          */
-        _getInputData: function () {
+        getInputData() {
             return {
-                packageIdentifier: this._packageIdentifier.value
+                packageIdentifier: this.packageIdentifier.value,
             };
-        },
-        /**
-         * Returns the error element for the package identifier form field.
-         * If `createIfNonExistent` is not given or `false`, `null` is returned
-         * if there is no error element, otherwise an empty error element
-         * is created and returned.
-         *
-         * @param      {?boolean}      createIfNonExistent
-         * @return     {?HTMLElement}
-         */
-        _getPackageIdentifierErrorElement: function (createIfNonExistent) {
-            return this._getErrorElement(this._packageIdentifier, createIfNonExistent);
-        },
+        }
         /**
-         * Adds a package to the package list after pressing ENTER in a
-         * text field.
-         *
-         * @param      {Event}         event
+         * Adds a package to the package list after pressing ENTER in a text field.
          */
-        _keyPress: function (event) {
-            if (EventKey.Enter(event)) {
-                this._addPackage(event);
+        keyPress(event) {
+            if (event.key === "Enter") {
+                this.addPackage(event);
             }
-        },
+        }
         /**
          * Adds all necessary package-relavant data to the given list item.
-         *
-         * @param      {HTMLElement}   listItem        package list element holding package data
-         * @param      {object}        packageData     package data
          */
-        _populateListItem: function (listItem, packageData) {
-            elData(listItem, 'package-identifier', packageData.packageIdentifier);
-        },
+        populateListItem(listItem, packageData) {
+            listItem.dataset.packageIdentifier = packageData.packageIdentifier;
+        }
         /**
          * Removes a package by clicking on its delete button.
-         *
-         * @param      {Event}         event           delete button click event
          */
-        _removePackage: function (event) {
-            elRemove(event.currentTarget.closest('li'));
+        removePackage(event) {
+            event.currentTarget.closest("li").remove();
             // remove field errors if the last package has been deleted
-            if (!this._packageList.childElementCount &&
-                this._packageList.nextElementSibling.tagName === 'SMALL' &&
-                this._packageList.nextElementSibling.classList.contains('innerError')) {
-                elRemove(this._packageList.nextElementSibling);
-            }
-        },
+            Util_1.default.innerError(this.packageList, "");
+        }
         /**
-         * Adds all necessary (hidden) form fields to the form when
-         * submitting the form.
+         * Adds all necessary (hidden) form fields to the form when submitting the form.
          */
-        _submit: function () {
-            DomTraverse.childrenByTag(this._packageList, 'LI').forEach(this._createSubmitFields.bind(this));
-        },
+        submit() {
+            DomTraverse.childrenByTag(this.packageList, "LI").forEach((listItem, index) => this.createSubmitFields(listItem, index));
+        }
         /**
-         * Returns `true` if the currently entered package data is valid.
-         * Otherwise `false` is returned and relevant error messages are
-         * shown.
-         *
-         * @return     {boolean}
+         * Returns `true` if the currently entered package data is valid. Otherwise `false` is returned and relevant error
+         * messages are shown.
          */
-        _validateInput: function () {
-            return this._validatePackageIdentifier();
-        },
+        validateInput() {
+            return this.validatePackageIdentifier();
+        }
         /**
-         * Returns `true` if the currently entered package identifier is
-         * valid. Otherwise `false` is returned and an error message is
-         * shown.
-         *
-         * @return     {boolean}
+         * Returns `true` if the currently entered package identifier is valid. Otherwise `false` is returned and an error
+         * message is shown.
          */
-        _validatePackageIdentifier: function () {
-            var packageIdentifier = this._packageIdentifier.value;
-            if (packageIdentifier === '') {
-                this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.global.form.error.empty');
+        validatePackageIdentifier() {
+            const packageIdentifier = this.packageIdentifier.value;
+            if (packageIdentifier === "") {
+                Util_1.default.innerError(this.packageIdentifier, Language.get("wcf.global.form.error.empty"));
                 return false;
             }
             if (packageIdentifier.length < 3) {
-                this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.minimumLength');
+                Util_1.default.innerError(this.packageIdentifier, Language.get("wcf.acp.devtools.project.packageIdentifier.error.minimumLength"));
                 return false;
             }
             else if (packageIdentifier.length > 191) {
-                this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.maximumLength');
+                Util_1.default.innerError(this.packageIdentifier, Language.get("wcf.acp.devtools.project.packageIdentifier.error.maximumLength"));
                 return false;
             }
-            // see `wcf\data\package\Package::isValidPackageName()`
-            if (!packageIdentifier.match(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/)) {
-                this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.format');
+            if (!AbstractPackageList.packageIdentifierRegExp.test(packageIdentifier)) {
+                Util_1.default.innerError(this.packageIdentifier, Language.get("wcf.acp.devtools.project.packageIdentifier.error.format"));
                 return false;
             }
             // check if package has already been added
-            var duplicate = false;
-            DomTraverse.childrenByTag(this._packageList, 'LI').forEach(function (listItem, index) {
-                if (elData(listItem, 'package-identifier') === packageIdentifier) {
-                    duplicate = true;
-                }
-            });
+            const duplicate = DomTraverse.childrenByTag(this.packageList, "LI").some((listItem) => listItem.dataset.packageIdentifier === packageIdentifier);
             if (duplicate) {
-                this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.duplicate');
+                Util_1.default.innerError(this.packageIdentifier, Language.get("wcf.acp.devtools.project.packageIdentifier.error.duplicate"));
                 return false;
             }
             // remove outdated errors
-            var error = this._getPackageIdentifierErrorElement();
-            if (error !== null) {
-                elRemove(error);
-            }
+            Util_1.default.innerError(this.packageIdentifier, "");
             return true;
-        },
+        }
         /**
-         * Returns `true` if the given version is valid. Otherwise `false`
-         * is returned and an error message is shown.
-         *
-         * @param      {string}        version                 validated version
-         * @param      {function}      versionErrorGetter      returns the version error element
-         * @return     {boolean}
+         * Returns `true` if the given version is valid. Otherwise `false` is returned and an error message is shown.
          */
-        _validateVersion: function (version, versionErrorGetter) {
+        validateVersion(versionElement) {
+            const version = versionElement.value;
             // see `wcf\data\package\Package::isValidVersion()`
             // the version is no a required attribute
-            if (version !== '') {
+            if (version !== "") {
                 if (version.length > 255) {
-                    versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.maximumLength');
+                    Util_1.default.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.maximumLength"));
                     return false;
                 }
-                // see `wcf\data\package\Package::isValidVersion()`
-                if (!version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$/i)) {
-                    versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
+                if (!AbstractPackageList.versionRegExp.test(version)) {
+                    Util_1.default.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.format"));
                     return false;
                 }
             }
             // remove outdated errors
-            var error = versionErrorGetter();
-            if (error !== null) {
-                elRemove(error);
-            }
+            Util_1.default.innerError(versionElement, "");
             return true;
         }
-    };
+    }
+    // see `wcf\data\package\Package::isValidPackageName()`
+    AbstractPackageList.packageIdentifierRegExp = new RegExp(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/);
+    // see `wcf\data\package\Package::isValidVersion()`
+    AbstractPackageList.versionRegExp = new RegExp(/^([0-9]+).([0-9]+)\.([0-9]+)( (a|alpha|b|beta|d|dev|rc|pl) ([0-9]+))?$/i);
+    Core.enableLegacyInheritance(AbstractPackageList);
     return AbstractPackageList;
 });
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js
deleted file mode 100644 (file)
index a6ac35a..0000000
+++ /dev/null
@@ -1,322 +0,0 @@
-/**
- * Abstract implementation of the JavaScript component of a form field handling
- * a list of packages.
- * 
- * @author     Matthias Schmidt
- * @copyright  2001-2019 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module     WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
- * @since      5.2
- */
-define(['Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language'], function(DomChangeListener, DomTraverse, DomUtil, EventKey, Language) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function AbstractPackageList(formFieldId, existingPackages) {
-               this.init(formFieldId, existingPackages);
-       };
-       AbstractPackageList.prototype = {
-               /**
-                * Initializes the package list handler.
-                * 
-                * @param       {string}        formFieldId             id of the associated form field
-                * @param       {object[]}      existingPackages        data of existing packages
-                */
-               init: function(formFieldId, existingPackages) {
-                       this._formFieldId = formFieldId;
-                       
-                       this._packageList = elById(this._formFieldId + '_packageList');
-                       if (this._packageList === null) {
-                               throw new Error("Cannot find package list for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       
-                       this._packageIdentifier = elById(this._formFieldId + '_packageIdentifier');
-                       if (this._packageIdentifier === null) {
-                               throw new Error("Cannot find package identifier form field for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       this._packageIdentifier.addEventListener('keypress', this._keyPress.bind(this));
-                       
-                       this._addButton = elById(this._formFieldId + '_addButton');
-                       if (this._addButton === null) {
-                               throw new Error("Cannot find add button for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       this._addButton.addEventListener('click', this._addPackage.bind(this));
-                       
-                       this._form = this._packageList.closest('form');
-                       if (this._form === null) {
-                               throw new Error("Cannot find form element for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       this._form.addEventListener('submit', this._submit.bind(this));
-                       
-                       existingPackages.forEach(this._addPackageByData.bind(this));
-               },
-               
-               /**
-                * Adds a package to the package list as a consequence of the given
-                * event. If the package data is invalid, an error message is shown
-                * and no package is added.
-                * 
-                * @param       {Event}         event   event that triggered trying to add the package
-                */
-               _addPackage: function(event) {
-                       event.preventDefault();
-                       event.stopPropagation();
-                       
-                       // validate data
-                       if (!this._validateInput()) {
-                               return;
-                       }
-                       
-                       this._addPackageByData(this._getInputData());
-                       
-                       // empty fields
-                       this._emptyInput();
-                       
-                       this._packageIdentifier.focus();
-               },
-               
-               /**
-                * Adds a package to the package list using the given package data.
-                * 
-                * @param       {object}        packageData
-                */
-               _addPackageByData: function(packageData) {
-                       // add package to list
-                       var listItem = elCreate('li');
-                       this._populateListItem(listItem, packageData);
-                       
-                       // add delete button
-                       var deleteButton = elCreate('span');
-                       deleteButton.className = 'icon icon16 fa-times pointer jsTooltip';
-                       elAttr(deleteButton, 'title', Language.get('wcf.global.button.delete'));
-                       deleteButton.addEventListener('click', this._removePackage.bind(this));
-                       DomUtil.prepend(deleteButton, listItem);
-                       
-                       this._packageList.appendChild(listItem);
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Creates the hidden fields when the form is submitted.
-                * 
-                * @param       {HTMLElement}   listElement     package list element from the package list
-                * @param       {int}           index           package index
-                */
-               _createSubmitFields: function(listElement, index) {
-                       var packageIdentifier = elCreate('input');
-                       elAttr(packageIdentifier, 'type', 'hidden');
-                       elAttr(packageIdentifier, 'name', this._formFieldId + '[' + index + '][packageIdentifier]')
-                       packageIdentifier.value = elData(listElement, 'package-identifier');
-                       this._form.appendChild(packageIdentifier);
-               },
-               
-               /**
-                * Empties the input fields.
-                */
-               _emptyInput: function() {
-                       this._packageIdentifier.value = '';
-               },
-               
-               /**
-                * Returns the error element for the given form field element.
-                * If `createIfNonExistent` is not given or `false`, `null` is returned
-                * if there is no error element, otherwise an empty error element
-                * is created and returned.
-                * 
-                * @param       {?boolean}      createIfNonExistent
-                * @return      {?HTMLElement}
-                */
-               _getErrorElement: function(element, createIfNoNExistent) {
-                       var error = DomTraverse.nextByClass(element, 'innerError');
-                       
-                       if (error === null && createIfNoNExistent) {
-                               error = elCreate('small');
-                               error.className = 'innerError';
-                               
-                               DomUtil.insertAfter(error, element);
-                       }
-                       
-                       return error;
-               },
-               
-               /**
-                * Returns the current data of the input fields to add a new package. 
-                * 
-                * @return      {object}
-                */
-               _getInputData: function() {
-                       return {
-                               packageIdentifier: this._packageIdentifier.value
-                       };
-               },
-               
-               /**
-                * Returns the error element for the package identifier form field.
-                * If `createIfNonExistent` is not given or `false`, `null` is returned
-                * if there is no error element, otherwise an empty error element
-                * is created and returned.
-                * 
-                * @param       {?boolean}      createIfNonExistent
-                * @return      {?HTMLElement}
-                */
-               _getPackageIdentifierErrorElement: function(createIfNonExistent) {
-                       return this._getErrorElement(this._packageIdentifier, createIfNonExistent);
-               },
-               
-               /**
-                * Adds a package to the package list after pressing ENTER in a
-                * text field.
-                * 
-                * @param       {Event}         event
-                */
-               _keyPress: function(event) {
-                       if (EventKey.Enter(event)) {
-                               this._addPackage(event);
-                       }
-               },
-               
-               /**
-                * Adds all necessary package-relavant data to the given list item.
-                * 
-                * @param       {HTMLElement}   listItem        package list element holding package data
-                * @param       {object}        packageData     package data
-                */
-               _populateListItem: function(listItem, packageData) {
-                       elData(listItem, 'package-identifier', packageData.packageIdentifier);
-               },
-               
-               /**
-                * Removes a package by clicking on its delete button.
-                * 
-                * @param       {Event}         event           delete button click event
-                */
-               _removePackage: function(event) {
-                       elRemove(event.currentTarget.closest('li'));
-                       
-                       // remove field errors if the last package has been deleted
-                       if (
-                               !this._packageList.childElementCount &&
-                               this._packageList.nextElementSibling.tagName === 'SMALL' &&
-                               this._packageList.nextElementSibling.classList.contains('innerError')
-                       ) {
-                               elRemove(this._packageList.nextElementSibling);
-                       }
-               },
-               
-               /**
-                * Adds all necessary (hidden) form fields to the form when
-                * submitting the form.
-                */
-               _submit: function() {
-                       DomTraverse.childrenByTag(this._packageList, 'LI').forEach(this._createSubmitFields.bind(this));
-               },
-               
-               /**
-                * Returns `true` if the currently entered package data is valid.
-                * Otherwise `false` is returned and relevant error messages are
-                * shown.
-                * 
-                * @return      {boolean}
-                */
-               _validateInput: function() {
-                       return this._validatePackageIdentifier();
-               },
-               
-               /**
-                * Returns `true` if the currently entered package identifier is
-                * valid. Otherwise `false` is returned and an error message is
-                * shown.
-                * 
-                * @return      {boolean}
-                */
-               _validatePackageIdentifier: function() {
-                       var packageIdentifier = this._packageIdentifier.value;
-                       
-                       if (packageIdentifier === '') {
-                               this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.global.form.error.empty');
-                               
-                               return false;
-                       }
-                       
-                       if (packageIdentifier.length < 3) {
-                               this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.minimumLength');
-                               
-                               return false;
-                       }
-                       else if (packageIdentifier.length > 191) {
-                               this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.maximumLength');
-                               
-                               return false;
-                       }
-                       
-                       // see `wcf\data\package\Package::isValidPackageName()`
-                       if (!packageIdentifier.match(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/)) {
-                               this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.format');
-                               
-                               return false;
-                       }
-                       
-                       // check if package has already been added
-                       var duplicate = false;
-                       DomTraverse.childrenByTag(this._packageList, 'LI').forEach(function(listItem, index) {
-                               if (elData(listItem, 'package-identifier') === packageIdentifier) {
-                                       duplicate = true;
-                               }
-                       });
-                       
-                       if (duplicate) {
-                               this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.duplicate');
-                               
-                               return false;
-                       }
-                       
-                       // remove outdated errors
-                       var error = this._getPackageIdentifierErrorElement();
-                       if (error !== null) {
-                               elRemove(error);
-                       }
-                       
-                       return true;
-               },
-               
-               /**
-                * Returns `true` if the given version is valid. Otherwise `false`
-                * is returned and an error message is shown.
-                * 
-                * @param       {string}        version                 validated version
-                * @param       {function}      versionErrorGetter      returns the version error element
-                * @return      {boolean}
-                */
-               _validateVersion: function(version, versionErrorGetter) {
-                       // see `wcf\data\package\Package::isValidVersion()`
-                       // the version is no a required attribute
-                       if (version !== '') {
-                               if (version.length > 255) {
-                                       versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.maximumLength');
-                                       
-                                       return false;
-                               }
-                               
-                               // see `wcf\data\package\Package::isValidVersion()`
-                               if (!version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$/i)) {
-                                       versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
-                                       
-                                       return false;
-                               }
-                       }
-                       
-                       // remove outdated errors
-                       var error = versionErrorGetter();
-                       if (error !== null) {
-                               elRemove(error);
-                       }
-                       
-                       return true;
-               }
-       };
-       
-       return AbstractPackageList;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.ts
new file mode 100644 (file)
index 0000000..955a36a
--- /dev/null
@@ -0,0 +1,263 @@
+/**
+ * Abstract implementation of the JavaScript component of a form field handling a list of packages.
+ *
+ * @author  Matthias Schmidt
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module  WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
+ * @since 5.2
+ */
+
+import * as Core from "../../../../../../Core";
+import * as Language from "../../../../../../Language";
+import * as DomTraverse from "../../../../../../Dom/Traverse";
+import DomChangeListener from "../../../../../../Dom/Change/Listener";
+import DomUtil from "../../../../../../Dom/Util";
+import { PackageData } from "./Data";
+
+abstract class AbstractPackageList<TPackageData extends PackageData = PackageData> {
+  protected readonly addButton: HTMLAnchorElement;
+  protected readonly form: HTMLFormElement;
+  protected readonly formFieldId: string;
+  protected readonly packageList: HTMLOListElement;
+  protected readonly packageIdentifier: HTMLInputElement;
+
+  // see `wcf\data\package\Package::isValidPackageName()`
+  protected static packageIdentifierRegExp = new RegExp(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/);
+
+  // see `wcf\data\package\Package::isValidVersion()`
+  protected static versionRegExp = new RegExp(
+    /^([0-9]+).([0-9]+)\.([0-9]+)( (a|alpha|b|beta|d|dev|rc|pl) ([0-9]+))?$/i,
+  );
+
+  constructor(formFieldId: string, existingPackages: TPackageData[]) {
+    this.formFieldId = formFieldId;
+
+    this.packageList = document.getElementById(`${this.formFieldId}_packageList`) as HTMLOListElement;
+    if (this.packageList === null) {
+      throw new Error(`Cannot find package list for packages field with id '${this.formFieldId}'.`);
+    }
+
+    this.packageIdentifier = document.getElementById(`${this.formFieldId}_packageIdentifier`) as HTMLInputElement;
+    if (this.packageIdentifier === null) {
+      throw new Error(`Cannot find package identifier form field for packages field with id '${this.formFieldId}'.`);
+    }
+    this.packageIdentifier.addEventListener("keypress", (ev) => this.keyPress(ev));
+
+    this.addButton = document.getElementById(`${this.formFieldId}_addButton`) as HTMLAnchorElement;
+    if (this.addButton === null) {
+      throw new Error(`Cannot find add button for packages field with id '${this.formFieldId}'.`);
+    }
+    this.addButton.addEventListener("click", (ev) => this.addPackage(ev));
+
+    this.form = this.packageList.closest("form") as HTMLFormElement;
+    if (this.form === null) {
+      throw new Error(`Cannot find form element for packages field with id '${this.formFieldId}'.`);
+    }
+    this.form.addEventListener("submit", () => this.submit());
+
+    existingPackages.forEach((data) => this.addPackageByData(data));
+  }
+
+  /**
+   * Adds a package to the package list as a consequence of the given event.
+   *
+   * If the package data is invalid, an error message is shown and no package is added.
+   */
+  protected addPackage(event: Event): void {
+    event.preventDefault();
+    event.stopPropagation();
+
+    // validate data
+    if (!this.validateInput()) {
+      return;
+    }
+
+    this.addPackageByData(this.getInputData());
+
+    // empty fields
+    this.emptyInput();
+
+    this.packageIdentifier.focus();
+  }
+
+  /**
+   * Adds a package to the package list using the given package data.
+   */
+  protected addPackageByData(packageData: TPackageData): void {
+    // add package to list
+    const listItem = document.createElement("li");
+    this.populateListItem(listItem, packageData);
+
+    // add delete button
+    const deleteButton = document.createElement("span");
+    deleteButton.className = "icon icon16 fa-times pointer jsTooltip";
+    deleteButton.title = Language.get("wcf.global.button.delete");
+    deleteButton.addEventListener("click", (ev) => this.removePackage(ev));
+    listItem.insertAdjacentElement("afterbegin", deleteButton);
+
+    this.packageList.appendChild(listItem);
+
+    DomChangeListener.trigger();
+  }
+
+  /**
+   * Creates the hidden fields when the form is submitted.
+   */
+  protected createSubmitFields(listElement: HTMLLIElement, index: number): void {
+    const packageIdentifier = document.createElement("input");
+    packageIdentifier.type = "hidden";
+    packageIdentifier.name = `${this.formFieldId}[${index}][packageIdentifier]`;
+    packageIdentifier.value = listElement.dataset.packageIdentifier!;
+    this.form.appendChild(packageIdentifier);
+  }
+
+  /**
+   * Empties the input fields.
+   */
+  protected emptyInput(): void {
+    this.packageIdentifier.value = "";
+  }
+
+  /**
+   * Returns the current data of the input fields to add a new package.
+   */
+  protected getInputData(): TPackageData {
+    return {
+      packageIdentifier: this.packageIdentifier.value,
+    } as TPackageData;
+  }
+
+  /**
+   * Adds a package to the package list after pressing ENTER in a text field.
+   */
+  protected keyPress(event: KeyboardEvent): void {
+    if (event.key === "Enter") {
+      this.addPackage(event);
+    }
+  }
+
+  /**
+   * Adds all necessary package-relavant data to the given list item.
+   */
+  protected populateListItem(listItem: HTMLLIElement, packageData: TPackageData): void {
+    listItem.dataset.packageIdentifier = packageData.packageIdentifier;
+  }
+
+  /**
+   * Removes a package by clicking on its delete button.
+   */
+  protected removePackage(event: Event): void {
+    (event.currentTarget as HTMLElement).closest("li")!.remove();
+
+    // remove field errors if the last package has been deleted
+    DomUtil.innerError(this.packageList, "");
+  }
+
+  /**
+   * Adds all necessary (hidden) form fields to the form when submitting the form.
+   */
+  protected submit(): void {
+    DomTraverse.childrenByTag(this.packageList, "LI").forEach((listItem, index) =>
+      this.createSubmitFields(listItem, index),
+    );
+  }
+
+  /**
+   * Returns `true` if the currently entered package data is valid. Otherwise `false` is returned and relevant error
+   * messages are shown.
+   */
+  protected validateInput(): boolean {
+    return this.validatePackageIdentifier();
+  }
+
+  /**
+   * Returns `true` if the currently entered package identifier is valid. Otherwise `false` is returned and an error
+   * message is shown.
+   */
+  protected validatePackageIdentifier(): boolean {
+    const packageIdentifier = this.packageIdentifier.value;
+
+    if (packageIdentifier === "") {
+      DomUtil.innerError(this.packageIdentifier, Language.get("wcf.global.form.error.empty"));
+
+      return false;
+    }
+
+    if (packageIdentifier.length < 3) {
+      DomUtil.innerError(
+        this.packageIdentifier,
+        Language.get("wcf.acp.devtools.project.packageIdentifier.error.minimumLength"),
+      );
+
+      return false;
+    } else if (packageIdentifier.length > 191) {
+      DomUtil.innerError(
+        this.packageIdentifier,
+        Language.get("wcf.acp.devtools.project.packageIdentifier.error.maximumLength"),
+      );
+
+      return false;
+    }
+
+    if (!AbstractPackageList.packageIdentifierRegExp.test(packageIdentifier)) {
+      DomUtil.innerError(
+        this.packageIdentifier,
+        Language.get("wcf.acp.devtools.project.packageIdentifier.error.format"),
+      );
+
+      return false;
+    }
+
+    // check if package has already been added
+    const duplicate = DomTraverse.childrenByTag(this.packageList, "LI").some(
+      (listItem) => listItem.dataset.packageIdentifier === packageIdentifier,
+    );
+
+    if (duplicate) {
+      DomUtil.innerError(
+        this.packageIdentifier,
+        Language.get("wcf.acp.devtools.project.packageIdentifier.error.duplicate"),
+      );
+
+      return false;
+    }
+
+    // remove outdated errors
+    DomUtil.innerError(this.packageIdentifier, "");
+
+    return true;
+  }
+
+  /**
+   * Returns `true` if the given version is valid. Otherwise `false` is returned and an error message is shown.
+   */
+  protected validateVersion(versionElement: HTMLInputElement): boolean {
+    const version = versionElement.value;
+
+    // see `wcf\data\package\Package::isValidVersion()`
+    // the version is no a required attribute
+    if (version !== "") {
+      if (version.length > 255) {
+        DomUtil.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.maximumLength"));
+
+        return false;
+      }
+
+      if (!AbstractPackageList.versionRegExp.test(version)) {
+        DomUtil.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.format"));
+
+        return false;
+      }
+    }
+
+    // remove outdated errors
+    DomUtil.innerError(versionElement, "");
+
+    return true;
+  }
+}
+
+Core.enableLegacyInheritance(AbstractPackageList);
+
+export = AbstractPackageList;