Move devtools form field JS files into `Acp` namespace
authorMatthias Schmidt <gravatronics@live.com>
Fri, 5 Jul 2019 17:06:26 +0000 (19:06 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Fri, 5 Jul 2019 17:06:26 +0000 (19:06 +0200)
See  #2772

14 files changed:
wcfsetup/install/files/acp/templates/__devtoolsProjectExcludedPackagesFormField.tpl
wcfsetup/install/files/acp/templates/__devtoolsProjectInstructionsFormField.tpl
wcfsetup/install/files/acp/templates/__devtoolsProjectOptionalPackagesFormField.tpl
wcfsetup/install/files/acp/templates/__devtoolsProjectRequiredPackagesFormField.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/ExcludedPackages.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/OptionalPackages.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/RequiredPackages.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList.js [deleted file]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/ExcludedPackages.js [deleted file]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/Instructions.js [deleted file]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/OptionalPackages.js [deleted file]
wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/RequiredPackages.js [deleted file]

index 4363db66b523296dd8c58d6cc7c9cac7b4ee4fde..cbc3d856b237f25004b972ae42d2f3a8067d339c 100644 (file)
@@ -26,7 +26,7 @@
 </div>
 
 <script data-relocate="true">
-       require(['Language', 'WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/ExcludedPackages'], function(Language, ExcludedPackagesFormField) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/ExcludedPackages'], function(Language, ExcludedPackagesFormField) {
                Language.addObject({
                        'wcf.acp.devtools.project.packageIdentifier.error.duplicate': '{lang}wcf.acp.devtools.project.packageIdentifier.error.duplicate{/lang}',
                        'wcf.acp.devtools.project.packageIdentifier.error.format': '{lang}wcf.acp.devtools.project.packageIdentifier.error.format{/lang}',
index 6d9ccd378153878fde73b1437b55c5389dddb8db..c3edaf217ea6e8c5224ea658ae1c4cb77728adb6 100644 (file)
 <script data-relocate="true">
        require([
                'Language',
-               'WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/Instructions',
+               'WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions',
                'WoltLabSuite/Core/Template'
        ], function(
                Language,
index a231645a4bb7751ea930ed3adfb171ffaff2b2e6..38eb82cbe62ad375db07b5a2114a2d2a0f691538 100644 (file)
@@ -20,7 +20,7 @@
 </div>
 
 <script data-relocate="true">
-       require(['Language', 'WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/OptionalPackages'], function(Language, OptionalPackagesFormField) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/OptionalPackages'], function(Language, OptionalPackagesFormField) {
                Language.addObject({
                        'wcf.acp.devtools.project.packageIdentifier.error.duplicate': '{lang}wcf.acp.devtools.project.packageIdentifier.error.duplicate{/lang}',
                        'wcf.acp.devtools.project.packageIdentifier.error.format': '{lang}wcf.acp.devtools.project.packageIdentifier.error.format{/lang}',
index b9ccb89fa2618838f9cb86fc87d2afc47d1b890f..463248163022aaa4022c8360d4e95821388dc2df 100644 (file)
@@ -34,7 +34,7 @@
 </div>
 
 <script data-relocate="true">
-       require(['Language', 'WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/RequiredPackages'], function(Language, RequiredPackagesFormField) {
+       require(['Language', 'WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/RequiredPackages'], function(Language, RequiredPackagesFormField) {
                Language.addObject({
                        'wcf.acp.devtools.project.packageIdentifier.error.duplicate': '{lang}wcf.acp.devtools.project.packageIdentifier.error.duplicate{/lang}',
                        'wcf.acp.devtools.project.packageIdentifier.error.format': '{lang}wcf.acp.devtools.project.packageIdentifier.error.format{/lang}',
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList.js
new file mode 100644 (file)
index 0000000..a6ac35a
--- /dev/null
@@ -0,0 +1,322 @@
+/**
+ * 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/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/ExcludedPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/ExcludedPackages.js
new file mode 100644 (file)
index 0000000..cd03ed3
--- /dev/null
@@ -0,0 +1,103 @@
+/**
+ * Manages the packages entered in a devtools project excluded package form field.
+ *
+ * @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/ExcludedPackages
+ * @see        module:WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
+ * @since      5.2
+ */
+define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function ExcludedPackages(formFieldId, existingPackages) {
+               this.init(formFieldId, existingPackages);
+       };
+       Core.inherit(ExcludedPackages, AbstractPackageList, {
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#init
+                */
+               init: function(formFieldId, existingPackages) {
+                       ExcludedPackages._super.prototype.init.call(this, formFieldId, existingPackages);
+                       
+                       this._version = elById(this._formFieldId + '_version');
+                       if (this._version === null) {
+                               throw new Error("Cannot find version form field for packages field with id '" + this._formFieldId + "'.");
+                       }
+                       this._version.addEventListener('keypress', this._keyPress.bind(this));
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_createSubmitFields
+                */
+               _createSubmitFields: function(listElement, index) {
+                       ExcludedPackages._super.prototype._createSubmitFields.call(this, listElement, index);
+                       
+                       var version = elCreate('input');
+                       elAttr(version, 'type', 'hidden');
+                       elAttr(version, 'name', this._formFieldId + '[' + index + '][version]')
+                       version.value = elData(listElement, 'version');
+                       this._form.appendChild(version);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_emptyInput
+                */
+               _emptyInput: function() {
+                       ExcludedPackages._super.prototype._emptyInput.call(this);
+                       
+                       this._version.value = '';
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_getInputData
+                */
+               _getInputData: function() {
+                       return Core.extend(ExcludedPackages._super.prototype._getInputData.call(this), {
+                               version: this._version.value
+                       });
+               },
+               
+               /**
+                * Returns the error element for the version 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}
+                */
+               _getVersionErrorElement: function(createIfNonExistent) {
+                       return this._getErrorElement(this._version, createIfNonExistent);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
+                */
+               _populateListItem: function(listItem, packageData) {
+                       ExcludedPackages._super.prototype._populateListItem.call(this, listItem, packageData);
+                       
+                       elData(listItem, 'version', packageData.version);
+                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.excludedPackage.excludedPackage', {
+                               packageIdentifier: packageData.packageIdentifier,
+                               version: packageData.version
+                       });
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_validateInput
+                */
+               _validateInput: function() {
+                       return ExcludedPackages._super.prototype._validateInput.call(this) && this._validateVersion(
+                               this._version.value,
+                               this._getVersionErrorElement.bind(this)
+                       );
+               }
+       });
+       
+       return ExcludedPackages;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js
new file mode 100644 (file)
index 0000000..3e73c41
--- /dev/null
@@ -0,0 +1,775 @@
+/**
+ * Manages the instructions entered in a devtools project instructions form field. 
+ * 
+ * @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/Instructions
+ * @since      5.2
+ */
+define([
+       'Dom/ChangeListener',
+       'Dom/Traverse',
+       'Dom/Util',
+       'EventKey',
+       'Language',
+       'Ui/Confirmation',
+       'Ui/Dialog',
+       'WoltLabSuite/Core/Ui/Sortable/List'
+], function(
+       DomChangeListener,
+       DomTraverse,
+       DomUtil,
+       EventKey,
+       Language,
+       UiConfirmation,
+       UiDialog,
+       UiSortableList
+) {
+       "use strict";
+       
+       var _applicationPips = ['acpTemplate', 'file', 'script', 'template'];
+       
+       /**
+        * @constructor
+        */
+       function Instructions(
+               formFieldId,
+               instructionsTemplate,
+               instructionsEditDialogTemplate,
+               instructionEditDialogTemplate,
+               pipDefaultFilenames,
+               existingInstructions
+       ) {
+               this.init(
+                       formFieldId,
+                       instructionsTemplate,
+                       instructionsEditDialogTemplate,
+                       instructionEditDialogTemplate,
+                       pipDefaultFilenames,
+                       existingInstructions || []
+               );
+       };
+       Instructions.prototype = {
+               /**
+                * Initializes the instructions handler.
+                * 
+                * @param       {string}        formFieldId                     id of the associated form field
+                * @param       {Template}      instructionsTemplate            template used for a new set of instructions
+                * @param       {Template}      instructionsEditDialogTemplate  template used for instructions edit dialogs
+                * @param       {Template}      instructionEditDialogTemplate   template used for instruction edit dialogs
+                * @param       {object}        pipDefaultFilenames             maps pip names to their default filenames
+                * @param       {object[]}      existingInstructions            data of existing instructions
+                */
+               init: function(
+                       formFieldId,
+                       instructionsTemplate,
+                       instructionsEditDialogTemplate,
+                       instructionEditDialogTemplate,
+                       pipDefaultFilenames,
+                       existingInstructions
+               ) {
+                       this._formFieldId = formFieldId;
+                       this._instructionsTemplate = instructionsTemplate;
+                       this._instructionsEditDialogTemplate = instructionsEditDialogTemplate;
+                       this._instructionEditDialogTemplate = instructionEditDialogTemplate;
+                       this._instructionsCounter = 0;
+                       this._pipDefaultFilenames = pipDefaultFilenames;
+                       this._instructionCounter = 0;
+                       
+                       this._instructionsList = elById(this._formFieldId + '_instructionsList');
+                       if (this._instructionsList === null) {
+                               throw new Error("Cannot find package list for packages field with id '" + this._formFieldId + "'.");
+                       }
+                       
+                       this._instructionsType = elById(this._formFieldId + '_instructionsType');
+                       if (this._instructionsType === null) {
+                               throw new Error("Cannot find instruction type form field for instructions field with id '" + this._formFieldId + "'.");
+                       }
+                       this._instructionsType.addEventListener('change', this._toggleFromVersionFormField.bind(this));
+                       
+                       this._fromVersion = elById(this._formFieldId + '_fromVersion');
+                       if (this._fromVersion === null) {
+                               throw new Error("Cannot find from version form field for instructions field with id '" + this._formFieldId + "'.");
+                       }
+                       this._fromVersion.addEventListener('keypress', this._instructionsKeyPress.bind(this));
+                       
+                       this._addButton = elById(this._formFieldId + '_addButton');
+                       if (this._addButton === null) {
+                               throw new Error("Cannot find add button for instructions field with id '" + this._formFieldId + "'.");
+                       }
+                       this._addButton.addEventListener('click', this._addInstructions.bind(this));
+                       
+                       this._form = this._instructionsList.closest('form');
+                       if (this._form === null) {
+                               throw new Error("Cannot find form element for instructions field with id '" + this._formFieldId + "'.");
+                       }
+                       this._form.addEventListener('submit', this._submit.bind(this));
+                       
+                       var hasInstallInstructions = false;
+                       
+                       for (var index in existingInstructions) {
+                               var instructions = existingInstructions[index];
+                               
+                               if (instructions.type === 'install') {
+                                       hasInstallInstructions = true;
+                                       break;
+                               }
+                       }
+                       
+                       // ensure that there are always installation instructions
+                       if (!hasInstallInstructions) {
+                               this._addInstructionsByData({
+                                       fromVersion: '',
+                                       type: 'install'
+                               });
+                       }
+                       
+                       existingInstructions.forEach(this._addInstructionsByData.bind(this));
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Adds an instruction to a set of instructions as a consequence
+                * of the given event. If the instruction data is invalid, an
+                * error message is shown and no instruction is added.
+                * 
+                * @param       {Event}         event   event that triggered trying to add the instruction
+                */
+               _addInstruction: function(event) {
+                       event.preventDefault();
+                       event.stopPropagation();
+                       
+                       var instructionsId = elData(event.currentTarget.closest('li.section'), 'instructions-id');
+                       
+                       // note: data will be validated/filtered by the server
+                       
+                       var pipField = elById(this._formFieldId + '_instructions' + instructionsId + '_pip');
+                       
+                       // ignore pressing button if no PIP has been selected
+                       if (!pipField.value) {
+                               return;
+                       }
+                       
+                       var valueField = elById(this._formFieldId + '_instructions' + instructionsId + '_value');
+                       var runStandaloneField = elById(this._formFieldId + '_instructions' + instructionsId + '_runStandalone');
+                       var applicationField = elById(this._formFieldId + '_instructions' + instructionsId + '_application');
+                       
+                       this._addInstructionByData(instructionsId, {
+                               application: _applicationPips.indexOf(pipField.value) !== -1 ? applicationField.value : '',
+                               pip: pipField.value,
+                               runStandalone: ~~runStandaloneField.checked,
+                               value: valueField.value
+                       });
+                       
+                       // empty fields
+                       pipField.value = '';
+                       valueField.value = '';
+                       runStandaloneField.checked = false;
+                       applicationField.value = '';
+                       elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription').innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
+                       this._toggleApplicationFormField(instructionsId);
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Adds an instruction to the set of instructions with the given id.
+                * 
+                * @param       {int}           instructionsId
+                * @param       {object}        instructionData
+                */
+               _addInstructionByData: function(instructionsId, instructionData) {
+                       var instructionId = ++this._instructionCounter;
+                       
+                       var instructionList = elById(this._formFieldId + '_instructions' + instructionsId + '_instructionList');
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'sortableNode';
+                       listItem.id = this._formFieldId + '_instruction' + instructionId;
+                       elData(listItem, 'instruction-id', instructionId);
+                       elData(listItem, 'application', instructionData.application);
+                       elData(listItem, 'pip', instructionData.pip);
+                       elData(listItem, 'runStandalone', instructionData.runStandalone);
+                       elData(listItem, 'value', instructionData.value);
+                       
+                       var content = '' +
+                               '<div class="sortableNodeLabel">' +
+                               '       <div class="jsDevtoolsProjectInstruction">' +
+                               '               ' + Language.get('wcf.acp.devtools.project.instruction.instruction', instructionData);
+                       
+                       if (instructionData.errors) {
+                               for (var index in instructionData.errors) {
+                                       content += '<small class="innerError">' + instructionData.errors[index] + '</small>';
+                               }
+                       }
+                       
+                       content += '' +
+                                       '       </div>' +
+                               '       <span class="statusDisplay sortableButtonContainer">' +
+                               '               <span class="icon icon16 fa-pencil pointer jsTooltip" id="' + this._formFieldId + '_instruction' + instructionId + '_editButton" title="' + Language.get('wcf.global.button.edit') + '"></span>' +
+                               '               <span class="icon icon16 fa-times pointer jsTooltip" id="' + this._formFieldId + '_instruction' + instructionId + '_deleteButton" title="' + Language.get('wcf.global.button.delete') + '"></span>' +
+                               '       </span>' +
+                               '</div>';
+                       
+                       listItem.innerHTML = content;
+                       
+                       instructionList.appendChild(listItem);
+                       
+                       elById(this._formFieldId + '_instruction' + instructionId + '_deleteButton').addEventListener('click', this._removeInstruction.bind(this));
+                       elById(this._formFieldId + '_instruction' + instructionId + '_editButton').addEventListener('click', this._editInstruction.bind(this));
+               },
+               
+               /**
+                * Adds a set of instructions as a consequenc of the given event.
+                * If the instructions data is invalid, an error message is shown
+                * and no instruction set is added.
+                * 
+                * @param       {Event}         event   event that triggered trying to add the instructions
+                */
+               _addInstructions: function(event) {
+                       event.preventDefault();
+                       event.stopPropagation();
+                       
+                       // validate data
+                       if (!this._validateInstructionsType() || (this._instructionsType.value === 'update' && !this._validateFromVersion(this._fromVersion))) {
+                               return;
+                       }
+                       
+                       this._addInstructionsByData({
+                               fromVersion: this._instructionsType.value === 'update' ? this._fromVersion.value : '',
+                               type: this._instructionsType.value
+                       });
+                       
+                       // empty fields
+                       this._instructionsType.value = '';
+                       this._fromVersion.value = '';
+                       
+                       this._toggleFromVersionFormField();
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * Adds a set of instructions.
+                * 
+                * @param       {object}        instructionData
+                */
+               _addInstructionsByData: function(instructionsData) {
+                       var instructionsId = ++this._instructionsCounter;
+                       
+                       var listItem = elCreate('li');
+                       listItem.className = 'section';
+                       listItem.innerHTML = this._instructionsTemplate.fetch({
+                               instructionsId: instructionsId,
+                               sectionTitle: Language.get('wcf.acp.devtools.project.instructions.type.' + instructionsData.type + '.title', {
+                                       fromVersion: instructionsData.fromVersion
+                               }),
+                               type: instructionsData.type
+                       });
+                       
+                       listItem.id = this._formFieldId + '_instructions' + instructionsId;
+                       elData(listItem, 'instructions-id', instructionsId);
+                       elData(listItem, 'type', instructionsData.type);
+                       elData(listItem, 'fromVersion', instructionsData.fromVersion);
+                       
+                       elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription')
+                       
+                       this._instructionsList.appendChild(listItem);
+                       
+                       var instructionListContainer = elById(this._formFieldId + '_instructions' + instructionsId + '_instructionListContainer');
+                       if (Array.isArray(instructionsData.errors)) {
+                               instructionsData.errors.forEach(function(errorMessage) {
+                                       var small = elCreate('small');
+                                       small.className = 'innerError';
+                                       small.innerHTML = errorMessage;
+
+                                       instructionListContainer.parentNode.insertBefore(small, instructionListContainer);
+                               });
+                       }
+                       
+                       new UiSortableList({
+                               containerId: instructionListContainer.id,
+                               isSimpleSorting: true,
+                               options: {
+                                       toleranceElement: '> div'
+                               }
+                       });
+                       
+                       var deleteButton = elById(this._formFieldId + '_instructions' + instructionsId + '_deleteButton');
+                       if (instructionsData.type === 'update') {
+                               elById(this._formFieldId + '_instructions' + instructionsId + '_deleteButton').addEventListener('click', this._removeInstructions.bind(this));
+                               elById(this._formFieldId + '_instructions' + instructionsId + '_editButton').addEventListener('click', this._editInstructions.bind(this));
+                       }
+                       
+                       elById(this._formFieldId + '_instructions' + instructionsId + '_pip').addEventListener('change', this._changeInstructionPip.bind(this));
+                       elById(this._formFieldId + '_instructions' + instructionsId + '_value').addEventListener('keypress', this._instructionKeyPress.bind(this));
+                       elById(this._formFieldId + '_instructions' + instructionsId + '_addButton').addEventListener('click', this._addInstruction.bind(this));
+                       
+                       if (instructionsData.instructions) {
+                               for (var index in instructionsData.instructions) {
+                                       this._addInstructionByData(instructionsId, instructionsData.instructions[index]);
+                               }
+                       }
+               },
+               
+               /**
+                * Is called if the selected package installation plugin of an
+                * instruction is changed.
+                * 
+                * @param       {Event}         event           change event
+                */
+               _changeInstructionPip: function(event) {
+                       var pip = event.currentTarget.value;
+                       var instructionsId = elData(event.currentTarget.closest('li.section'), 'instructions-id');
+                       var description = elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription');
+                       
+                       // update value description
+                       if (this._pipDefaultFilenames[pip] !== '') {
+                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description.defaultFilename', {
+                                       defaultFilename: this._pipDefaultFilenames[pip]
+                               });
+                       }
+                       else {
+                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
+                       }
+                       
+                       var valueDlClassList = elById(this._formFieldId + '_instructions' + instructionsId + '_value').closest('dl').classList;
+                       var applicationDl = elById(this._formFieldId + '_instructions' + instructionsId + '_application').closest('dl');
+                       
+                       // toggle application selector
+                       this._toggleApplicationFormField(instructionsId);
+               },
+               
+               /**
+                * Opens a dialog to edit an existing instruction.
+                * 
+                * @param       {Event}         event   edit button click event
+                */
+               _editInstruction: function(event) {
+                       var listItem = event.currentTarget.closest('li');
+                       
+                       var instructionId = elData(listItem, 'instruction-id');
+                       var application = elData(listItem, 'application');
+                       var pip = elData(listItem, 'pip');
+                       var runStandalone = elDataBool(listItem, 'runStandalone');
+                       var value = elData(listItem, 'value');
+                       
+                       var dialogContent = this._instructionEditDialogTemplate.fetch({
+                               runStandalone: runStandalone,
+                               value: value
+                       });
+                       
+                       var dialogId = 'instructionEditDialog' + instructionId;
+                       if (!UiDialog.getDialog(dialogId)) {
+                               UiDialog.openStatic(dialogId, dialogContent, {
+                                       onSetup: function(content) {
+                                               var applicationSelect = elBySel('select[name=application]', content);
+                                               var pipSelect = elBySel('select[name=pip]', content);
+                                               var runStandaloneInput = elBySel('input[name=runStandalone]', content);
+                                               var valueInput = elBySel('input[name=value]', content);
+                                               
+                                               // set values of `select` elements
+                                               applicationSelect.value = application;
+                                               pipSelect.value = pip;
+                                               
+                                               var submit = function() {
+                                                       var listItem = elById(this._formFieldId + '_instruction' + instructionId);
+                                                       elData(listItem, 'application', _applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : '');
+                                                       elData(listItem, 'pip', pipSelect.value);
+                                                       elData(listItem, 'runStandalone', ~~runStandaloneInput.checked);
+                                                       elData(listItem, 'value', valueInput.value);
+                                                       
+                                                       // note: data will be validated/filtered by the server
+                                                       
+                                                       elByClass('jsDevtoolsProjectInstruction', listItem)[0].innerHTML = Language.get('wcf.acp.devtools.project.instruction.instruction', {
+                                                               application: elData(listItem, 'application'),
+                                                               pip: elData(listItem, 'pip'),
+                                                               runStandalone: elDataBool(listItem, 'runStandalone'),
+                                                               value: elData(listItem, 'value'),
+                                                       });
+                                                       
+                                                       DomChangeListener.trigger();
+                                                       
+                                                       UiDialog.close(dialogId);
+                                               }.bind(this);
+                                               
+                                               valueInput.addEventListener('keypress', function(event) {
+                                                       if (EventKey.Enter(event)) {
+                                                               submit();
+                                                       }
+                                               });
+                                               
+                                               elBySel('button[data-type=submit]', content).addEventListener('click', submit);
+                                               
+                                               var pipChange = function() {
+                                                       var pip = pipSelect.value;
+                                                       
+                                                       if (_applicationPips.indexOf(pip) !== -1) {
+                                                               elShow(applicationSelect.closest('dl'));
+                                                       }
+                                                       else {
+                                                               elHide(applicationSelect.closest('dl'));
+                                                       }
+                                                       
+                                                       var description = DomTraverse.nextByTag(valueInput, 'SMALL');
+                                                       if (this._pipDefaultFilenames[pip] !== '') {
+                                                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description.defaultFilename', {
+                                                                       defaultFilename: this._pipDefaultFilenames[pip]
+                                                               });
+                                                       }
+                                                       else {
+                                                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
+                                                       }
+                                               }.bind(this);
+                                               
+                                               pipSelect.addEventListener('change', pipChange);
+                                               pipChange();
+                                       }.bind(this),
+                                       title: Language.get('wcf.acp.devtools.project.instruction.edit')
+                               });
+                       }
+                       else {
+                               UiDialog.openStatic(dialogId);
+                       }
+               },
+               
+               /**
+                * Opens a dialog to edit an existing set of instructions.
+                * 
+                * @param       {Event}         event   edit button click event
+                */
+               _editInstructions: function(event) {
+                       var listItem = event.currentTarget.closest('li');
+                       
+                       var instructionsId = elData(listItem, 'instructions-id');
+                       var fromVersion = elData(listItem, 'fromVersion');
+                       
+                       var dialogContent = this._instructionsEditDialogTemplate.fetch({
+                               fromVersion: fromVersion
+                       });
+                       
+                       var dialogId = 'instructionsEditDialog' + instructionsId;
+                       if (!UiDialog.getDialog(dialogId)) {
+                               UiDialog.openStatic(dialogId, dialogContent, {
+                                       onSetup: function (content) {
+                                               var fromVersion = elBySel('input[name=fromVersion]', content);
+                                               
+                                               var submit = function () {
+                                                       if (!this._validateFromVersion(fromVersion)) {
+                                                               return;
+                                                       }
+                                                       
+                                                       var instructions = elById(this._formFieldId + '_instructions' + instructionsId);
+                                                       elData(instructions, 'fromVersion', fromVersion.value);
+                                                       
+                                                       elByClass('jsInstructionsTitle', instructions)[0].textContent = Language.get('wcf.acp.devtools.project.instructions.type.update.title', {
+                                                               fromVersion: fromVersion.value
+                                                       });
+                                                       
+                                                       DomChangeListener.trigger();
+                                                       
+                                                       UiDialog.close(dialogId);
+                                               }.bind(this);
+                                               
+                                               fromVersion.addEventListener('keypress', function (event) {
+                                                       if (EventKey.Enter(event)) {
+                                                               submit();
+                                                       }
+                                               });
+                                               
+                                               elBySel('button[data-type=submit]', content).addEventListener('click', submit);
+                                       }.bind(this),
+                                       title: Language.get('wcf.acp.devtools.project.instructions.edit')
+                               });
+                       }
+                       else {
+                               UiDialog.openStatic(dialogId);
+                       }
+               },
+               
+               /**
+                * 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 error element for the from version 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}
+                */
+               _getFromVersionErrorElement: function(inputField, createIfNonExistent) {
+                       return this._getErrorElement(inputField, createIfNonExistent);
+               },
+               
+               /**
+                * Returns the error element for the instruction type 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}
+                */
+               _getInstructionsTypeErrorElement: function(createIfNonExistent) {
+                       return this._getErrorElement(this._instructionsType, createIfNonExistent);
+               },
+               
+               /**
+                * Adds an instruction after pressing ENTER in a relevant text
+                * field.
+                * 
+                * @param       {Event}         event
+                */
+               _instructionKeyPress: function(event) {
+                       if (EventKey.Enter(event)) {
+                               this._addInstruction(event);
+                       }
+               },
+               
+               /**
+                * Adds a set of instruction after pressing ENTER in a relevant
+                * text field.
+                * 
+                * @param       {Event}         event
+                */
+               _instructionsKeyPress: function(event) {
+                       if (EventKey.Enter(event)) {
+                               this._addInstructions(event);
+                       }
+               },
+               
+               /**
+                * Removes an instruction by clicking on its delete button.
+                * 
+                * @param       {Event}         event           delete button click event
+                */
+               _removeInstruction: function(event) {
+                       var instruction = event.currentTarget.closest('li');
+                       
+                       UiConfirmation.show({
+                               confirm: function() {
+                                       elRemove(instruction);
+                               },
+                               message: Language.get('wcf.acp.devtools.project.instruction.delete.confirmMessages')
+                       });
+               },
+               
+               /**
+                * Removes a set of instructions by clicking on its delete button.
+                * 
+                * @param       {Event}         event           delete button click event
+                */
+               _removeInstructions: function(event) {
+                       var instructions = event.currentTarget.closest('li');
+                       
+                       UiConfirmation.show({
+                               confirm: function() {
+                                       elRemove(instructions);
+                               },
+                               message: Language.get('wcf.acp.devtools.project.instructions.delete.confirmMessages')
+                       });
+               },
+               
+               /**
+                * Adds all necessary (hidden) form fields to the form when
+                * submitting the form.
+                */
+               _submit: function(event) {
+                       DomTraverse.childrenByTag(this._instructionsList, 'LI').forEach(function(instructions, instructionsIndex) {
+                               var namePrefix = this._formFieldId + '[' + instructionsIndex + ']';
+                               
+                               var instructionsType = elCreate('input');
+                               elAttr(instructionsType, 'type', 'hidden');
+                               elAttr(instructionsType, 'name', namePrefix + '[type]')
+                               instructionsType.value = elData(instructions, 'type');
+                               this._form.appendChild(instructionsType);
+                               
+                               if (instructionsType.value === 'update') {
+                                       var fromVersion = elCreate('input');
+                                       elAttr(fromVersion, 'type', 'hidden');
+                                       elAttr(fromVersion, 'name', this._formFieldId + '[' + instructionsIndex + '][fromVersion]')
+                                       fromVersion.value = elData(instructions, 'fromVersion');
+                                       this._form.appendChild(fromVersion);
+                               }
+                               
+                               DomTraverse.childrenByTag(elById(instructions.id + '_instructionList'), 'LI').forEach(function(instruction, instructionIndex) {
+                                       var namePrefix = this._formFieldId + '[' + instructionsIndex + '][instructions][' + instructionIndex + ']';
+                                       
+                                       ['pip', 'value', 'runStandalone'].forEach((function(property) {
+                                               var element = elCreate('input');
+                                               elAttr(element, 'type', 'hidden');
+                                               elAttr(element, 'name', namePrefix + '[' + property + ']');
+                                               element.value = elData(instruction, property);
+                                               this._form.appendChild(element);
+                                       }).bind(this));
+                                       
+                                       if (_applicationPips.indexOf(elData(instruction, 'pip')) !== -1) {
+                                               var application = elCreate('input');
+                                               elAttr(application, 'type', 'hidden');
+                                               elAttr(application, 'name', namePrefix + '[application]');
+                                               application.value = elData(instruction, 'application');
+                                               this._form.appendChild(application);
+                                       }
+                               }.bind(this));
+                       }.bind(this));
+               },
+               
+               /**
+                * Toggles the visibility of the application form field based on
+                * the selected pip for the instructions with the given id.
+                *
+                * @param       {int}   instructionsId          id of the relevant instruction set
+                */
+               _toggleApplicationFormField: function(instructionsId) {
+                       var pip = elById(this._formFieldId + '_instructions' + instructionsId + '_pip').value;
+                       
+                       var valueDlClassList = elById(this._formFieldId + '_instructions' + instructionsId + '_value').closest('dl').classList;
+                       var applicationDl = elById(this._formFieldId + '_instructions' + instructionsId + '_application').closest('dl');
+                       
+                       if (_applicationPips.indexOf(pip) !== -1) {
+                               valueDlClassList.remove('col-md-9');
+                               valueDlClassList.add('col-md-7');
+                               elShow(applicationDl);
+                       }
+                       else {
+                               valueDlClassList.remove('col-md-7');
+                               valueDlClassList.add('col-md-9');
+                               elHide(applicationDl);
+                       }
+               },
+               
+               /**
+                * Toggles the visibility of the `fromVersion` form field based on
+                * the selected instructions type.
+                */
+               _toggleFromVersionFormField: function() {
+                       var instructionsTypeList = this._instructionsType.closest('dl').classList;
+                       var fromVersionDl = this._fromVersion.closest('dl');
+                       
+                       if (this._instructionsType.value === 'update') {
+                               instructionsTypeList.remove('col-md-10');
+                               instructionsTypeList.add('col-md-5');
+                               elShow(fromVersionDl);
+                       }
+                       else {
+                               instructionsTypeList.remove('col-md-5');
+                               instructionsTypeList.add('col-md-10');
+                               elHide(fromVersionDl);
+                       }
+               },
+               
+               /**
+                * Returns `true` if the currently entered update "from version"
+                * is valid. Otherwise `false` is returned and an error message
+                * is shown.
+                * 
+                * @return      {boolean}
+                */
+               _validateFromVersion: function(inputField) {
+                       var version = inputField.value;
+                       
+                       if (version === '') {
+                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.global.form.error.empty');
+                               
+                               return false;
+                       }
+                       
+                       if (version.length > 50) {
+                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.maximumLength');
+                               
+                               return false;
+                       }
+                       
+                       // wildcard versions are checked on the server side
+                       if (version.indexOf('*') === -1) {
+                               // 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)) {
+                                       this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
+                                       
+                                       return false;
+                               }
+                       }
+                       else if (!version.replace('*', '0').match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$/i)) {
+                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
+                               
+                               return false;
+                       }
+                       
+                       // remove outdated errors
+                       var error = this._getFromVersionErrorElement(inputField);
+                       if (error !== null) {
+                               elRemove(error);
+                       }
+                       
+                       return true;
+               },
+               
+               /**
+                * Returns `true` if the entered update instructions type is valid.
+                * Otherwise `false` is returned and an error message is shown.
+                * 
+                * @return      {boolean}
+                */
+               _validateInstructionsType: function() {
+                       if (this._instructionsType.value !== 'install' && this._instructionsType.value !== 'update') {
+                               if (this._instructionsType.value === '') {
+                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.global.form.error.empty');
+                               }
+                               else {
+                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.global.form.error.noValidSelection');
+                               }
+                               
+                               return false;
+                       }
+                       
+                       // there may only be one set of installation instructions 
+                       if (this._instructionsType.value === 'install') {
+                               var hasInstall = false;
+                               [].forEach.call(this._instructionsList.children, function(instructions) {
+                                       if (elData(instructions, 'type') === 'install') {
+                                               hasInstall = true;
+                                       }
+                               });
+                               
+                               if (hasInstall) {
+                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.instructions.type.update.error.duplicate');
+                                       
+                                       return false;
+                               }
+                       }
+                       
+                       // remove outdated errors
+                       var error = this._getInstructionsTypeErrorElement();
+                       if (error !== null) {
+                               elRemove(error);
+                       }
+                       
+                       return true;
+               }
+       };
+       
+       return Instructions;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/OptionalPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/OptionalPackages.js
new file mode 100644 (file)
index 0000000..73a102c
--- /dev/null
@@ -0,0 +1,35 @@
+/**
+ * Manages the packages entered in a devtools project optional package form field.
+ * 
+ * @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/OptionalPackages
+ * @see        module:WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
+ * @since      5.2
+ */
+define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function OptionalPackages(formFieldId, existingPackages) {
+               this.init(formFieldId, existingPackages);
+       };
+       Core.inherit(OptionalPackages, AbstractPackageList, {
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
+                */
+               _populateListItem: function(listItem, packageData) {
+                       OptionalPackages._super.prototype._populateListItem.call(this, listItem, packageData);
+                       
+                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.optionalPackage.optionalPackage', {
+                               file: packageData.file,
+                               packageIdentifier: packageData.packageIdentifier
+                       });
+               }
+       });
+       
+       return OptionalPackages;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/RequiredPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/RequiredPackages.js
new file mode 100644 (file)
index 0000000..0930e2a
--- /dev/null
@@ -0,0 +1,118 @@
+/**
+ * Manages the packages entered in a devtools project required package form field.
+ * 
+ * @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/Acp/Builder/Field/Devtools/Project/RequiredPackages
+ * @see        module:WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
+ * @since      5.2
+ */
+define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function RequiredPackages(formFieldId, existingPackages) {
+               this.init(formFieldId, existingPackages);
+       };
+       Core.inherit(RequiredPackages, AbstractPackageList, {
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#init
+                */
+               init: function(formFieldId, existingPackages) {
+                       RequiredPackages._super.prototype.init.call(this, formFieldId, existingPackages);
+                       
+                       this._minVersion = elById(this._formFieldId + '_minVersion');
+                       if (this._minVersion === null) {
+                               throw new Error("Cannot find minimum version form field for packages field with id '" + this._formFieldId + "'.");
+                       }
+                       this._minVersion.addEventListener('keypress', this._keyPress.bind(this));
+                       
+                       this._file = elById(this._formFieldId + '_file');
+                       if (this._file === null) {
+                               throw new Error("Cannot find file form field for required field with id '" + this._formFieldId + "'.");
+                       }
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_createSubmitFields
+                */
+               _createSubmitFields: function(listElement, index) {
+                       RequiredPackages._super.prototype._createSubmitFields.call(this, listElement, index);
+                       
+                       var minVersion = elCreate('input');
+                       elAttr(minVersion, 'type', 'hidden');
+                       elAttr(minVersion, 'name', this._formFieldId + '[' + index + '][minVersion]')
+                       minVersion.value = elData(listElement, 'min-version');
+                       this._form.appendChild(minVersion);
+                       
+                       var file = elCreate('input');
+                       elAttr(file, 'type', 'hidden');
+                       elAttr(file, 'name', this._formFieldId + '[' + index + '][file]')
+                       file.value = elData(listElement, 'file');
+                       this._form.appendChild(file);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_emptyInput
+                */
+               _emptyInput: function() {
+                       RequiredPackages._super.prototype._emptyInput.call(this);
+                       
+                       this._minVersion.value = '';
+                       this._file.checked = false;
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_getInputData
+                */
+               _getInputData: function() {
+                       return Core.extend(RequiredPackages._super.prototype._getInputData.call(this), {
+                               file: this._file.checked,
+                               minVersion: this._minVersion.value
+                       });
+               },
+               
+               /**
+                * Returns the error element for the minimum version 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}
+                */
+               _getMinVersionErrorElement: function(createIfNonExistent) {
+                       return this._getErrorElement(this._minVersion, createIfNonExistent);
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
+                */
+               _populateListItem: function(listItem, packageData) {
+                       RequiredPackages._super.prototype._populateListItem.call(this, listItem, packageData);
+                       
+                       elData(listItem, 'min-version', packageData.minVersion);
+                       elData(listItem, 'file', ~~packageData.file);
+                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.requiredPackage.requiredPackage', {
+                               file: ~~packageData.file,
+                               minVersion: packageData.minVersion,
+                               packageIdentifier: packageData.packageIdentifier
+                       });
+               },
+               
+               /**
+                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_validateInput
+                */
+               _validateInput: function() {
+                       return RequiredPackages._super.prototype._validateInput.call(this) && this._validateVersion(
+                               this._minVersion.value,
+                               this._getMinVersionErrorElement.bind(this)
+                       );
+               }
+       });
+       
+       return RequiredPackages;
+});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList.js
deleted file mode 100644 (file)
index 3abd854..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/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/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/ExcludedPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/ExcludedPackages.js
deleted file mode 100644 (file)
index 80a090c..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Manages the packages entered in a devtools project excluded package form field.
- *
- * @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/Devtools/Project/ExcludedPackages
- * @see        module:WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList
- * @since      5.2
- */
-define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function ExcludedPackages(formFieldId, existingPackages) {
-               this.init(formFieldId, existingPackages);
-       };
-       Core.inherit(ExcludedPackages, AbstractPackageList, {
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#init
-                */
-               init: function(formFieldId, existingPackages) {
-                       ExcludedPackages._super.prototype.init.call(this, formFieldId, existingPackages);
-                       
-                       this._version = elById(this._formFieldId + '_version');
-                       if (this._version === null) {
-                               throw new Error("Cannot find version form field for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       this._version.addEventListener('keypress', this._keyPress.bind(this));
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_createSubmitFields
-                */
-               _createSubmitFields: function(listElement, index) {
-                       ExcludedPackages._super.prototype._createSubmitFields.call(this, listElement, index);
-                       
-                       var version = elCreate('input');
-                       elAttr(version, 'type', 'hidden');
-                       elAttr(version, 'name', this._formFieldId + '[' + index + '][version]')
-                       version.value = elData(listElement, 'version');
-                       this._form.appendChild(version);
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_emptyInput
-                */
-               _emptyInput: function() {
-                       ExcludedPackages._super.prototype._emptyInput.call(this);
-                       
-                       this._version.value = '';
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_getInputData
-                */
-               _getInputData: function() {
-                       return Core.extend(ExcludedPackages._super.prototype._getInputData.call(this), {
-                               version: this._version.value
-                       });
-               },
-               
-               /**
-                * Returns the error element for the version 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}
-                */
-               _getVersionErrorElement: function(createIfNonExistent) {
-                       return this._getErrorElement(this._version, createIfNonExistent);
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
-                */
-               _populateListItem: function(listItem, packageData) {
-                       ExcludedPackages._super.prototype._populateListItem.call(this, listItem, packageData);
-                       
-                       elData(listItem, 'version', packageData.version);
-                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.excludedPackage.excludedPackage', {
-                               packageIdentifier: packageData.packageIdentifier,
-                               version: packageData.version
-                       });
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_validateInput
-                */
-               _validateInput: function() {
-                       return ExcludedPackages._super.prototype._validateInput.call(this) && this._validateVersion(
-                               this._version.value,
-                               this._getVersionErrorElement.bind(this)
-                       );
-               }
-       });
-       
-       return ExcludedPackages;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/Instructions.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/Instructions.js
deleted file mode 100644 (file)
index 257864c..0000000
+++ /dev/null
@@ -1,775 +0,0 @@
-/**
- * Manages the instructions entered in a devtools project instructions form field. 
- * 
- * @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/Devtools/Project/Instructions
- * @since      5.2
- */
-define([
-       'Dom/ChangeListener',
-       'Dom/Traverse',
-       'Dom/Util',
-       'EventKey',
-       'Language',
-       'Ui/Confirmation',
-       'Ui/Dialog',
-       'WoltLabSuite/Core/Ui/Sortable/List'
-], function(
-       DomChangeListener,
-       DomTraverse,
-       DomUtil,
-       EventKey,
-       Language,
-       UiConfirmation,
-       UiDialog,
-       UiSortableList
-) {
-       "use strict";
-       
-       var _applicationPips = ['acpTemplate', 'file', 'script', 'template'];
-       
-       /**
-        * @constructor
-        */
-       function Instructions(
-               formFieldId,
-               instructionsTemplate,
-               instructionsEditDialogTemplate,
-               instructionEditDialogTemplate,
-               pipDefaultFilenames,
-               existingInstructions
-       ) {
-               this.init(
-                       formFieldId,
-                       instructionsTemplate,
-                       instructionsEditDialogTemplate,
-                       instructionEditDialogTemplate,
-                       pipDefaultFilenames,
-                       existingInstructions || []
-               );
-       };
-       Instructions.prototype = {
-               /**
-                * Initializes the instructions handler.
-                * 
-                * @param       {string}        formFieldId                     id of the associated form field
-                * @param       {Template}      instructionsTemplate            template used for a new set of instructions
-                * @param       {Template}      instructionsEditDialogTemplate  template used for instructions edit dialogs
-                * @param       {Template}      instructionEditDialogTemplate   template used for instruction edit dialogs
-                * @param       {object}        pipDefaultFilenames             maps pip names to their default filenames
-                * @param       {object[]}      existingInstructions            data of existing instructions
-                */
-               init: function(
-                       formFieldId,
-                       instructionsTemplate,
-                       instructionsEditDialogTemplate,
-                       instructionEditDialogTemplate,
-                       pipDefaultFilenames,
-                       existingInstructions
-               ) {
-                       this._formFieldId = formFieldId;
-                       this._instructionsTemplate = instructionsTemplate;
-                       this._instructionsEditDialogTemplate = instructionsEditDialogTemplate;
-                       this._instructionEditDialogTemplate = instructionEditDialogTemplate;
-                       this._instructionsCounter = 0;
-                       this._pipDefaultFilenames = pipDefaultFilenames;
-                       this._instructionCounter = 0;
-                       
-                       this._instructionsList = elById(this._formFieldId + '_instructionsList');
-                       if (this._instructionsList === null) {
-                               throw new Error("Cannot find package list for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       
-                       this._instructionsType = elById(this._formFieldId + '_instructionsType');
-                       if (this._instructionsType === null) {
-                               throw new Error("Cannot find instruction type form field for instructions field with id '" + this._formFieldId + "'.");
-                       }
-                       this._instructionsType.addEventListener('change', this._toggleFromVersionFormField.bind(this));
-                       
-                       this._fromVersion = elById(this._formFieldId + '_fromVersion');
-                       if (this._fromVersion === null) {
-                               throw new Error("Cannot find from version form field for instructions field with id '" + this._formFieldId + "'.");
-                       }
-                       this._fromVersion.addEventListener('keypress', this._instructionsKeyPress.bind(this));
-                       
-                       this._addButton = elById(this._formFieldId + '_addButton');
-                       if (this._addButton === null) {
-                               throw new Error("Cannot find add button for instructions field with id '" + this._formFieldId + "'.");
-                       }
-                       this._addButton.addEventListener('click', this._addInstructions.bind(this));
-                       
-                       this._form = this._instructionsList.closest('form');
-                       if (this._form === null) {
-                               throw new Error("Cannot find form element for instructions field with id '" + this._formFieldId + "'.");
-                       }
-                       this._form.addEventListener('submit', this._submit.bind(this));
-                       
-                       var hasInstallInstructions = false;
-                       
-                       for (var index in existingInstructions) {
-                               var instructions = existingInstructions[index];
-                               
-                               if (instructions.type === 'install') {
-                                       hasInstallInstructions = true;
-                                       break;
-                               }
-                       }
-                       
-                       // ensure that there are always installation instructions
-                       if (!hasInstallInstructions) {
-                               this._addInstructionsByData({
-                                       fromVersion: '',
-                                       type: 'install'
-                               });
-                       }
-                       
-                       existingInstructions.forEach(this._addInstructionsByData.bind(this));
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Adds an instruction to a set of instructions as a consequence
-                * of the given event. If the instruction data is invalid, an
-                * error message is shown and no instruction is added.
-                * 
-                * @param       {Event}         event   event that triggered trying to add the instruction
-                */
-               _addInstruction: function(event) {
-                       event.preventDefault();
-                       event.stopPropagation();
-                       
-                       var instructionsId = elData(event.currentTarget.closest('li.section'), 'instructions-id');
-                       
-                       // note: data will be validated/filtered by the server
-                       
-                       var pipField = elById(this._formFieldId + '_instructions' + instructionsId + '_pip');
-                       
-                       // ignore pressing button if no PIP has been selected
-                       if (!pipField.value) {
-                               return;
-                       }
-                       
-                       var valueField = elById(this._formFieldId + '_instructions' + instructionsId + '_value');
-                       var runStandaloneField = elById(this._formFieldId + '_instructions' + instructionsId + '_runStandalone');
-                       var applicationField = elById(this._formFieldId + '_instructions' + instructionsId + '_application');
-                       
-                       this._addInstructionByData(instructionsId, {
-                               application: _applicationPips.indexOf(pipField.value) !== -1 ? applicationField.value : '',
-                               pip: pipField.value,
-                               runStandalone: ~~runStandaloneField.checked,
-                               value: valueField.value
-                       });
-                       
-                       // empty fields
-                       pipField.value = '';
-                       valueField.value = '';
-                       runStandaloneField.checked = false;
-                       applicationField.value = '';
-                       elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription').innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
-                       this._toggleApplicationFormField(instructionsId);
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Adds an instruction to the set of instructions with the given id.
-                * 
-                * @param       {int}           instructionsId
-                * @param       {object}        instructionData
-                */
-               _addInstructionByData: function(instructionsId, instructionData) {
-                       var instructionId = ++this._instructionCounter;
-                       
-                       var instructionList = elById(this._formFieldId + '_instructions' + instructionsId + '_instructionList');
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'sortableNode';
-                       listItem.id = this._formFieldId + '_instruction' + instructionId;
-                       elData(listItem, 'instruction-id', instructionId);
-                       elData(listItem, 'application', instructionData.application);
-                       elData(listItem, 'pip', instructionData.pip);
-                       elData(listItem, 'runStandalone', instructionData.runStandalone);
-                       elData(listItem, 'value', instructionData.value);
-                       
-                       var content = '' +
-                               '<div class="sortableNodeLabel">' +
-                               '       <div class="jsDevtoolsProjectInstruction">' +
-                               '               ' + Language.get('wcf.acp.devtools.project.instruction.instruction', instructionData);
-                       
-                       if (instructionData.errors) {
-                               for (var index in instructionData.errors) {
-                                       content += '<small class="innerError">' + instructionData.errors[index] + '</small>';
-                               }
-                       }
-                       
-                       content += '' +
-                                       '       </div>' +
-                               '       <span class="statusDisplay sortableButtonContainer">' +
-                               '               <span class="icon icon16 fa-pencil pointer jsTooltip" id="' + this._formFieldId + '_instruction' + instructionId + '_editButton" title="' + Language.get('wcf.global.button.edit') + '"></span>' +
-                               '               <span class="icon icon16 fa-times pointer jsTooltip" id="' + this._formFieldId + '_instruction' + instructionId + '_deleteButton" title="' + Language.get('wcf.global.button.delete') + '"></span>' +
-                               '       </span>' +
-                               '</div>';
-                       
-                       listItem.innerHTML = content;
-                       
-                       instructionList.appendChild(listItem);
-                       
-                       elById(this._formFieldId + '_instruction' + instructionId + '_deleteButton').addEventListener('click', this._removeInstruction.bind(this));
-                       elById(this._formFieldId + '_instruction' + instructionId + '_editButton').addEventListener('click', this._editInstruction.bind(this));
-               },
-               
-               /**
-                * Adds a set of instructions as a consequenc of the given event.
-                * If the instructions data is invalid, an error message is shown
-                * and no instruction set is added.
-                * 
-                * @param       {Event}         event   event that triggered trying to add the instructions
-                */
-               _addInstructions: function(event) {
-                       event.preventDefault();
-                       event.stopPropagation();
-                       
-                       // validate data
-                       if (!this._validateInstructionsType() || (this._instructionsType.value === 'update' && !this._validateFromVersion(this._fromVersion))) {
-                               return;
-                       }
-                       
-                       this._addInstructionsByData({
-                               fromVersion: this._instructionsType.value === 'update' ? this._fromVersion.value : '',
-                               type: this._instructionsType.value
-                       });
-                       
-                       // empty fields
-                       this._instructionsType.value = '';
-                       this._fromVersion.value = '';
-                       
-                       this._toggleFromVersionFormField();
-                       
-                       DomChangeListener.trigger();
-               },
-               
-               /**
-                * Adds a set of instructions.
-                * 
-                * @param       {object}        instructionData
-                */
-               _addInstructionsByData: function(instructionsData) {
-                       var instructionsId = ++this._instructionsCounter;
-                       
-                       var listItem = elCreate('li');
-                       listItem.className = 'section';
-                       listItem.innerHTML = this._instructionsTemplate.fetch({
-                               instructionsId: instructionsId,
-                               sectionTitle: Language.get('wcf.acp.devtools.project.instructions.type.' + instructionsData.type + '.title', {
-                                       fromVersion: instructionsData.fromVersion
-                               }),
-                               type: instructionsData.type
-                       });
-                       
-                       listItem.id = this._formFieldId + '_instructions' + instructionsId;
-                       elData(listItem, 'instructions-id', instructionsId);
-                       elData(listItem, 'type', instructionsData.type);
-                       elData(listItem, 'fromVersion', instructionsData.fromVersion);
-                       
-                       elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription')
-                       
-                       this._instructionsList.appendChild(listItem);
-                       
-                       var instructionListContainer = elById(this._formFieldId + '_instructions' + instructionsId + '_instructionListContainer');
-                       if (Array.isArray(instructionsData.errors)) {
-                               instructionsData.errors.forEach(function(errorMessage) {
-                                       var small = elCreate('small');
-                                       small.className = 'innerError';
-                                       small.innerHTML = errorMessage;
-
-                                       instructionListContainer.parentNode.insertBefore(small, instructionListContainer);
-                               });
-                       }
-                       
-                       new UiSortableList({
-                               containerId: instructionListContainer.id,
-                               isSimpleSorting: true,
-                               options: {
-                                       toleranceElement: '> div'
-                               }
-                       });
-                       
-                       var deleteButton = elById(this._formFieldId + '_instructions' + instructionsId + '_deleteButton');
-                       if (instructionsData.type === 'update') {
-                               elById(this._formFieldId + '_instructions' + instructionsId + '_deleteButton').addEventListener('click', this._removeInstructions.bind(this));
-                               elById(this._formFieldId + '_instructions' + instructionsId + '_editButton').addEventListener('click', this._editInstructions.bind(this));
-                       }
-                       
-                       elById(this._formFieldId + '_instructions' + instructionsId + '_pip').addEventListener('change', this._changeInstructionPip.bind(this));
-                       elById(this._formFieldId + '_instructions' + instructionsId + '_value').addEventListener('keypress', this._instructionKeyPress.bind(this));
-                       elById(this._formFieldId + '_instructions' + instructionsId + '_addButton').addEventListener('click', this._addInstruction.bind(this));
-                       
-                       if (instructionsData.instructions) {
-                               for (var index in instructionsData.instructions) {
-                                       this._addInstructionByData(instructionsId, instructionsData.instructions[index]);
-                               }
-                       }
-               },
-               
-               /**
-                * Is called if the selected package installation plugin of an
-                * instruction is changed.
-                * 
-                * @param       {Event}         event           change event
-                */
-               _changeInstructionPip: function(event) {
-                       var pip = event.currentTarget.value;
-                       var instructionsId = elData(event.currentTarget.closest('li.section'), 'instructions-id');
-                       var description = elById(this._formFieldId + '_instructions' + instructionsId + '_valueDescription');
-                       
-                       // update value description
-                       if (this._pipDefaultFilenames[pip] !== '') {
-                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description.defaultFilename', {
-                                       defaultFilename: this._pipDefaultFilenames[pip]
-                               });
-                       }
-                       else {
-                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
-                       }
-                       
-                       var valueDlClassList = elById(this._formFieldId + '_instructions' + instructionsId + '_value').closest('dl').classList;
-                       var applicationDl = elById(this._formFieldId + '_instructions' + instructionsId + '_application').closest('dl');
-                       
-                       // toggle application selector
-                       this._toggleApplicationFormField(instructionsId);
-               },
-               
-               /**
-                * Opens a dialog to edit an existing instruction.
-                * 
-                * @param       {Event}         event   edit button click event
-                */
-               _editInstruction: function(event) {
-                       var listItem = event.currentTarget.closest('li');
-                       
-                       var instructionId = elData(listItem, 'instruction-id');
-                       var application = elData(listItem, 'application');
-                       var pip = elData(listItem, 'pip');
-                       var runStandalone = elDataBool(listItem, 'runStandalone');
-                       var value = elData(listItem, 'value');
-                       
-                       var dialogContent = this._instructionEditDialogTemplate.fetch({
-                               runStandalone: runStandalone,
-                               value: value
-                       });
-                       
-                       var dialogId = 'instructionEditDialog' + instructionId;
-                       if (!UiDialog.getDialog(dialogId)) {
-                               UiDialog.openStatic(dialogId, dialogContent, {
-                                       onSetup: function(content) {
-                                               var applicationSelect = elBySel('select[name=application]', content);
-                                               var pipSelect = elBySel('select[name=pip]', content);
-                                               var runStandaloneInput = elBySel('input[name=runStandalone]', content);
-                                               var valueInput = elBySel('input[name=value]', content);
-                                               
-                                               // set values of `select` elements
-                                               applicationSelect.value = application;
-                                               pipSelect.value = pip;
-                                               
-                                               var submit = function() {
-                                                       var listItem = elById(this._formFieldId + '_instruction' + instructionId);
-                                                       elData(listItem, 'application', _applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : '');
-                                                       elData(listItem, 'pip', pipSelect.value);
-                                                       elData(listItem, 'runStandalone', ~~runStandaloneInput.checked);
-                                                       elData(listItem, 'value', valueInput.value);
-                                                       
-                                                       // note: data will be validated/filtered by the server
-                                                       
-                                                       elByClass('jsDevtoolsProjectInstruction', listItem)[0].innerHTML = Language.get('wcf.acp.devtools.project.instruction.instruction', {
-                                                               application: elData(listItem, 'application'),
-                                                               pip: elData(listItem, 'pip'),
-                                                               runStandalone: elDataBool(listItem, 'runStandalone'),
-                                                               value: elData(listItem, 'value'),
-                                                       });
-                                                       
-                                                       DomChangeListener.trigger();
-                                                       
-                                                       UiDialog.close(dialogId);
-                                               }.bind(this);
-                                               
-                                               valueInput.addEventListener('keypress', function(event) {
-                                                       if (EventKey.Enter(event)) {
-                                                               submit();
-                                                       }
-                                               });
-                                               
-                                               elBySel('button[data-type=submit]', content).addEventListener('click', submit);
-                                               
-                                               var pipChange = function() {
-                                                       var pip = pipSelect.value;
-                                                       
-                                                       if (_applicationPips.indexOf(pip) !== -1) {
-                                                               elShow(applicationSelect.closest('dl'));
-                                                       }
-                                                       else {
-                                                               elHide(applicationSelect.closest('dl'));
-                                                       }
-                                                       
-                                                       var description = DomTraverse.nextByTag(valueInput, 'SMALL');
-                                                       if (this._pipDefaultFilenames[pip] !== '') {
-                                                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description.defaultFilename', {
-                                                                       defaultFilename: this._pipDefaultFilenames[pip]
-                                                               });
-                                                       }
-                                                       else {
-                                                               description.innerHTML = Language.get('wcf.acp.devtools.project.instruction.value.description');
-                                                       }
-                                               }.bind(this);
-                                               
-                                               pipSelect.addEventListener('change', pipChange);
-                                               pipChange();
-                                       }.bind(this),
-                                       title: Language.get('wcf.acp.devtools.project.instruction.edit')
-                               });
-                       }
-                       else {
-                               UiDialog.openStatic(dialogId);
-                       }
-               },
-               
-               /**
-                * Opens a dialog to edit an existing set of instructions.
-                * 
-                * @param       {Event}         event   edit button click event
-                */
-               _editInstructions: function(event) {
-                       var listItem = event.currentTarget.closest('li');
-                       
-                       var instructionsId = elData(listItem, 'instructions-id');
-                       var fromVersion = elData(listItem, 'fromVersion');
-                       
-                       var dialogContent = this._instructionsEditDialogTemplate.fetch({
-                               fromVersion: fromVersion
-                       });
-                       
-                       var dialogId = 'instructionsEditDialog' + instructionsId;
-                       if (!UiDialog.getDialog(dialogId)) {
-                               UiDialog.openStatic(dialogId, dialogContent, {
-                                       onSetup: function (content) {
-                                               var fromVersion = elBySel('input[name=fromVersion]', content);
-                                               
-                                               var submit = function () {
-                                                       if (!this._validateFromVersion(fromVersion)) {
-                                                               return;
-                                                       }
-                                                       
-                                                       var instructions = elById(this._formFieldId + '_instructions' + instructionsId);
-                                                       elData(instructions, 'fromVersion', fromVersion.value);
-                                                       
-                                                       elByClass('jsInstructionsTitle', instructions)[0].textContent = Language.get('wcf.acp.devtools.project.instructions.type.update.title', {
-                                                               fromVersion: fromVersion.value
-                                                       });
-                                                       
-                                                       DomChangeListener.trigger();
-                                                       
-                                                       UiDialog.close(dialogId);
-                                               }.bind(this);
-                                               
-                                               fromVersion.addEventListener('keypress', function (event) {
-                                                       if (EventKey.Enter(event)) {
-                                                               submit();
-                                                       }
-                                               });
-                                               
-                                               elBySel('button[data-type=submit]', content).addEventListener('click', submit);
-                                       }.bind(this),
-                                       title: Language.get('wcf.acp.devtools.project.instructions.edit')
-                               });
-                       }
-                       else {
-                               UiDialog.openStatic(dialogId);
-                       }
-               },
-               
-               /**
-                * 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 error element for the from version 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}
-                */
-               _getFromVersionErrorElement: function(inputField, createIfNonExistent) {
-                       return this._getErrorElement(inputField, createIfNonExistent);
-               },
-               
-               /**
-                * Returns the error element for the instruction type 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}
-                */
-               _getInstructionsTypeErrorElement: function(createIfNonExistent) {
-                       return this._getErrorElement(this._instructionsType, createIfNonExistent);
-               },
-               
-               /**
-                * Adds an instruction after pressing ENTER in a relevant text
-                * field.
-                * 
-                * @param       {Event}         event
-                */
-               _instructionKeyPress: function(event) {
-                       if (EventKey.Enter(event)) {
-                               this._addInstruction(event);
-                       }
-               },
-               
-               /**
-                * Adds a set of instruction after pressing ENTER in a relevant
-                * text field.
-                * 
-                * @param       {Event}         event
-                */
-               _instructionsKeyPress: function(event) {
-                       if (EventKey.Enter(event)) {
-                               this._addInstructions(event);
-                       }
-               },
-               
-               /**
-                * Removes an instruction by clicking on its delete button.
-                * 
-                * @param       {Event}         event           delete button click event
-                */
-               _removeInstruction: function(event) {
-                       var instruction = event.currentTarget.closest('li');
-                       
-                       UiConfirmation.show({
-                               confirm: function() {
-                                       elRemove(instruction);
-                               },
-                               message: Language.get('wcf.acp.devtools.project.instruction.delete.confirmMessages')
-                       });
-               },
-               
-               /**
-                * Removes a set of instructions by clicking on its delete button.
-                * 
-                * @param       {Event}         event           delete button click event
-                */
-               _removeInstructions: function(event) {
-                       var instructions = event.currentTarget.closest('li');
-                       
-                       UiConfirmation.show({
-                               confirm: function() {
-                                       elRemove(instructions);
-                               },
-                               message: Language.get('wcf.acp.devtools.project.instructions.delete.confirmMessages')
-                       });
-               },
-               
-               /**
-                * Adds all necessary (hidden) form fields to the form when
-                * submitting the form.
-                */
-               _submit: function(event) {
-                       DomTraverse.childrenByTag(this._instructionsList, 'LI').forEach(function(instructions, instructionsIndex) {
-                               var namePrefix = this._formFieldId + '[' + instructionsIndex + ']';
-                               
-                               var instructionsType = elCreate('input');
-                               elAttr(instructionsType, 'type', 'hidden');
-                               elAttr(instructionsType, 'name', namePrefix + '[type]')
-                               instructionsType.value = elData(instructions, 'type');
-                               this._form.appendChild(instructionsType);
-                               
-                               if (instructionsType.value === 'update') {
-                                       var fromVersion = elCreate('input');
-                                       elAttr(fromVersion, 'type', 'hidden');
-                                       elAttr(fromVersion, 'name', this._formFieldId + '[' + instructionsIndex + '][fromVersion]')
-                                       fromVersion.value = elData(instructions, 'fromVersion');
-                                       this._form.appendChild(fromVersion);
-                               }
-                               
-                               DomTraverse.childrenByTag(elById(instructions.id + '_instructionList'), 'LI').forEach(function(instruction, instructionIndex) {
-                                       var namePrefix = this._formFieldId + '[' + instructionsIndex + '][instructions][' + instructionIndex + ']';
-                                       
-                                       ['pip', 'value', 'runStandalone'].forEach((function(property) {
-                                               var element = elCreate('input');
-                                               elAttr(element, 'type', 'hidden');
-                                               elAttr(element, 'name', namePrefix + '[' + property + ']');
-                                               element.value = elData(instruction, property);
-                                               this._form.appendChild(element);
-                                       }).bind(this));
-                                       
-                                       if (_applicationPips.indexOf(elData(instruction, 'pip')) !== -1) {
-                                               var application = elCreate('input');
-                                               elAttr(application, 'type', 'hidden');
-                                               elAttr(application, 'name', namePrefix + '[application]');
-                                               application.value = elData(instruction, 'application');
-                                               this._form.appendChild(application);
-                                       }
-                               }.bind(this));
-                       }.bind(this));
-               },
-               
-               /**
-                * Toggles the visibility of the application form field based on
-                * the selected pip for the instructions with the given id.
-                *
-                * @param       {int}   instructionsId          id of the relevant instruction set
-                */
-               _toggleApplicationFormField: function(instructionsId) {
-                       var pip = elById(this._formFieldId + '_instructions' + instructionsId + '_pip').value;
-                       
-                       var valueDlClassList = elById(this._formFieldId + '_instructions' + instructionsId + '_value').closest('dl').classList;
-                       var applicationDl = elById(this._formFieldId + '_instructions' + instructionsId + '_application').closest('dl');
-                       
-                       if (_applicationPips.indexOf(pip) !== -1) {
-                               valueDlClassList.remove('col-md-9');
-                               valueDlClassList.add('col-md-7');
-                               elShow(applicationDl);
-                       }
-                       else {
-                               valueDlClassList.remove('col-md-7');
-                               valueDlClassList.add('col-md-9');
-                               elHide(applicationDl);
-                       }
-               },
-               
-               /**
-                * Toggles the visibility of the `fromVersion` form field based on
-                * the selected instructions type.
-                */
-               _toggleFromVersionFormField: function() {
-                       var instructionsTypeList = this._instructionsType.closest('dl').classList;
-                       var fromVersionDl = this._fromVersion.closest('dl');
-                       
-                       if (this._instructionsType.value === 'update') {
-                               instructionsTypeList.remove('col-md-10');
-                               instructionsTypeList.add('col-md-5');
-                               elShow(fromVersionDl);
-                       }
-                       else {
-                               instructionsTypeList.remove('col-md-5');
-                               instructionsTypeList.add('col-md-10');
-                               elHide(fromVersionDl);
-                       }
-               },
-               
-               /**
-                * Returns `true` if the currently entered update "from version"
-                * is valid. Otherwise `false` is returned and an error message
-                * is shown.
-                * 
-                * @return      {boolean}
-                */
-               _validateFromVersion: function(inputField) {
-                       var version = inputField.value;
-                       
-                       if (version === '') {
-                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.global.form.error.empty');
-                               
-                               return false;
-                       }
-                       
-                       if (version.length > 50) {
-                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.maximumLength');
-                               
-                               return false;
-                       }
-                       
-                       // wildcard versions are checked on the server side
-                       if (version.indexOf('*') === -1) {
-                               // 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)) {
-                                       this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
-                                       
-                                       return false;
-                               }
-                       }
-                       else if (!version.replace('*', '0').match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$/i)) {
-                               this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
-                               
-                               return false;
-                       }
-                       
-                       // remove outdated errors
-                       var error = this._getFromVersionErrorElement(inputField);
-                       if (error !== null) {
-                               elRemove(error);
-                       }
-                       
-                       return true;
-               },
-               
-               /**
-                * Returns `true` if the entered update instructions type is valid.
-                * Otherwise `false` is returned and an error message is shown.
-                * 
-                * @return      {boolean}
-                */
-               _validateInstructionsType: function() {
-                       if (this._instructionsType.value !== 'install' && this._instructionsType.value !== 'update') {
-                               if (this._instructionsType.value === '') {
-                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.global.form.error.empty');
-                               }
-                               else {
-                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.global.form.error.noValidSelection');
-                               }
-                               
-                               return false;
-                       }
-                       
-                       // there may only be one set of installation instructions 
-                       if (this._instructionsType.value === 'install') {
-                               var hasInstall = false;
-                               [].forEach.call(this._instructionsList.children, function(instructions) {
-                                       if (elData(instructions, 'type') === 'install') {
-                                               hasInstall = true;
-                                       }
-                               });
-                               
-                               if (hasInstall) {
-                                       this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.instructions.type.update.error.duplicate');
-                                       
-                                       return false;
-                               }
-                       }
-                       
-                       // remove outdated errors
-                       var error = this._getInstructionsTypeErrorElement();
-                       if (error !== null) {
-                               elRemove(error);
-                       }
-                       
-                       return true;
-               }
-       };
-       
-       return Instructions;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/OptionalPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/OptionalPackages.js
deleted file mode 100644 (file)
index ff64710..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/**
- * Manages the packages entered in a devtools project optional package form field.
- * 
- * @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/Devtools/Project/OptionalPackages
- * @see        module:WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList
- * @since      5.2
- */
-define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function OptionalPackages(formFieldId, existingPackages) {
-               this.init(formFieldId, existingPackages);
-       };
-       Core.inherit(OptionalPackages, AbstractPackageList, {
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
-                */
-               _populateListItem: function(listItem, packageData) {
-                       OptionalPackages._super.prototype._populateListItem.call(this, listItem, packageData);
-                       
-                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.optionalPackage.optionalPackage', {
-                               file: packageData.file,
-                               packageIdentifier: packageData.packageIdentifier
-                       });
-               }
-       });
-       
-       return OptionalPackages;
-});
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/RequiredPackages.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/RequiredPackages.js
deleted file mode 100644 (file)
index 30a4390..0000000
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * Manages the packages entered in a devtools project required package form field.
- * 
- * @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/Devtools/Project/RequiredPackages
- * @see        module:WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList
- * @since      5.2
- */
-define(['./AbstractPackageList', 'Core', 'Language'], function(AbstractPackageList, Core, Language) {
-       "use strict";
-       
-       /**
-        * @constructor
-        */
-       function RequiredPackages(formFieldId, existingPackages) {
-               this.init(formFieldId, existingPackages);
-       };
-       Core.inherit(RequiredPackages, AbstractPackageList, {
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#init
-                */
-               init: function(formFieldId, existingPackages) {
-                       RequiredPackages._super.prototype.init.call(this, formFieldId, existingPackages);
-                       
-                       this._minVersion = elById(this._formFieldId + '_minVersion');
-                       if (this._minVersion === null) {
-                               throw new Error("Cannot find minimum version form field for packages field with id '" + this._formFieldId + "'.");
-                       }
-                       this._minVersion.addEventListener('keypress', this._keyPress.bind(this));
-                       
-                       this._file = elById(this._formFieldId + '_file');
-                       if (this._file === null) {
-                               throw new Error("Cannot find file form field for required field with id '" + this._formFieldId + "'.");
-                       }
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_createSubmitFields
-                */
-               _createSubmitFields: function(listElement, index) {
-                       RequiredPackages._super.prototype._createSubmitFields.call(this, listElement, index);
-                       
-                       var minVersion = elCreate('input');
-                       elAttr(minVersion, 'type', 'hidden');
-                       elAttr(minVersion, 'name', this._formFieldId + '[' + index + '][minVersion]')
-                       minVersion.value = elData(listElement, 'min-version');
-                       this._form.appendChild(minVersion);
-                       
-                       var file = elCreate('input');
-                       elAttr(file, 'type', 'hidden');
-                       elAttr(file, 'name', this._formFieldId + '[' + index + '][file]')
-                       file.value = elData(listElement, 'file');
-                       this._form.appendChild(file);
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_emptyInput
-                */
-               _emptyInput: function() {
-                       RequiredPackages._super.prototype._emptyInput.call(this);
-                       
-                       this._minVersion.value = '';
-                       this._file.checked = false;
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_getInputData
-                */
-               _getInputData: function() {
-                       return Core.extend(RequiredPackages._super.prototype._getInputData.call(this), {
-                               file: this._file.checked,
-                               minVersion: this._minVersion.value
-                       });
-               },
-               
-               /**
-                * Returns the error element for the minimum version 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}
-                */
-               _getMinVersionErrorElement: function(createIfNonExistent) {
-                       return this._getErrorElement(this._minVersion, createIfNonExistent);
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_populateListItem
-                */
-               _populateListItem: function(listItem, packageData) {
-                       RequiredPackages._super.prototype._populateListItem.call(this, listItem, packageData);
-                       
-                       elData(listItem, 'min-version', packageData.minVersion);
-                       elData(listItem, 'file', ~~packageData.file);
-                       listItem.innerHTML = ' ' + Language.get('wcf.acp.devtools.project.requiredPackage.requiredPackage', {
-                               file: ~~packageData.file,
-                               minVersion: packageData.minVersion,
-                               packageIdentifier: packageData.packageIdentifier
-                       });
-               },
-               
-               /**
-                * @see WoltLabSuite/Core/Form/Builder/Field/Devtools/Project/AbstractPackageList#_validateInput
-                */
-               _validateInput: function() {
-                       return RequiredPackages._super.prototype._validateInput.call(this) && this._validateVersion(
-                               this._minVersion.value,
-                               this._getMinVersionErrorElement.bind(this)
-                       );
-               }
-       });
-       
-       return RequiredPackages;
-});