From 8f6df1d355f657f64d283593d3714a2363f6764b Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Tue, 12 Jan 2021 10:59:37 +0100 Subject: [PATCH] Convert `Acp/Form/Builder/Field/Devtools/Project/Instructions` to TypeScript --- .../Field/Devtools/Project/Instructions.js | 823 ++++++++---------- .../Field/Devtools/Project/Instructions.js | 775 ----------------- .../Field/Devtools/Project/Instructions.ts | 712 +++++++++++++++ 3 files changed, 1078 insertions(+), 1232 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts 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 index dd78c563c5..0540afe1ba 100644 --- 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 @@ -1,626 +1,535 @@ /** * 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 - * @module WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions - * @since 5.2 + * @author Matthias Schmidt + * @copyright 2001-2021 WoltLab GmbH + * @license GNU Lesser General Public License + * @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) { +define(["require", "exports", "tslib", "../../../../../../Core", "../../../../../../Language", "../../../../../../Dom/Traverse", "../../../../../../Dom/Change/Listener", "../../../../../../Dom/Util", "../../../../../../Ui/Sortable/List", "../../../../../../Ui/Dialog", "../../../../../../Ui/Confirmation"], function (require, exports, tslib_1, Core, Language, DomTraverse, Listener_1, Util_1, List_1, Dialog_1, UiConfirmation) { "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; - } - } + Core = tslib_1.__importStar(Core); + Language = tslib_1.__importStar(Language); + DomTraverse = tslib_1.__importStar(DomTraverse); + Listener_1 = tslib_1.__importDefault(Listener_1); + Util_1 = tslib_1.__importDefault(Util_1); + List_1 = tslib_1.__importDefault(List_1); + Dialog_1 = tslib_1.__importDefault(Dialog_1); + UiConfirmation = tslib_1.__importStar(UiConfirmation); + class Instructions { + constructor(formFieldId, instructionsTemplate, instructionsEditDialogTemplate, instructionEditDialogTemplate, pipDefaultFilenames, existingInstructions) { + this.instructionCounter = 0; + this.instructionsCounter = 0; + this.formFieldId = formFieldId; + this.instructionsTemplate = instructionsTemplate; + this.instructionsEditDialogTemplate = instructionsEditDialogTemplate; + this.instructionEditDialogTemplate = instructionEditDialogTemplate; + this.pipDefaultFilenames = pipDefaultFilenames; + this.instructionsList = document.getElementById(`${this.formFieldId}_instructionsList`); + if (this.instructionsList === null) { + throw new Error(`Cannot find package list for packages field with id '${this.formFieldId}'.`); + } + this.instructionsType = document.getElementById(`${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()); + this.fromVersion = document.getElementById(`${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", (ev) => this.instructionsKeyPress(ev)); + this.addButton = document.getElementById(`${this.formFieldId}_addButton`); + if (this.addButton === null) { + throw new Error(`Cannot find add button form field for instructions field with id '${this.formFieldId}'.`); + } + this.addButton.addEventListener("click", (ev) => this.addInstructions(ev)); + 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()); + const hasInstallInstructions = existingInstructions.some((instructions) => instructions.type === "install"); // ensure that there are always installation instructions if (!hasInstallInstructions) { - this._addInstructionsByData({ - fromVersion: '', - type: 'install' + this.addInstructionsByData({ + fromVersion: "", + type: "install", }); } - existingInstructions.forEach(this._addInstructionsByData.bind(this)); - DomChangeListener.trigger(); - }, + existingInstructions.forEach((instructions) => this.addInstructionsByData(instructions)); + Listener_1.default.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 + * 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. */ - _addInstruction: function (event) { + addInstruction(event) { event.preventDefault(); event.stopPropagation(); - var instructionsId = elData(event.currentTarget.closest('li.section'), 'instructions-id'); + const instructionsId = event.currentTarget.closest("li.section").dataset + .instrictionsId; // note: data will be validated/filtered by the server - var pipField = elById(this._formFieldId + '_instructions' + instructionsId + '_pip'); + const pipField = document.getElementById(`${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 : '', + const valueField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_value`); + const runStandaloneField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_runStandalone`); + const applicationField = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_application`); + this.addInstructionByData(instructionsId, { + application: Instructions.applicationPips.indexOf(pipField.value) !== -1 ? applicationField.value : "", pip: pipField.value, runStandalone: ~~runStandaloneField.checked, - value: valueField.value + value: valueField.value, }); // empty fields - pipField.value = ''; - valueField.value = ''; + 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(); - }, + applicationField.value = ""; + document.getElementById(`${this.formFieldId}_instructions${instructionsId}_valueDescription`).innerHTML = Language.get("wcf.acp.devtools.project.instruction.value.description"); + this.toggleApplicationFormField(instructionsId); + Listener_1.default.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 = '' + - '
' + - '
' + - ' ' + Language.get('wcf.acp.devtools.project.instruction.instruction', instructionData); + addInstructionByData(instructionsId, instructionData) { + const instructionId = ++this.instructionCounter; + const instructionList = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_instructionList`); + const listItem = document.createElement("li"); + listItem.className = "sortableNode"; + listItem.id = `${this.formFieldId}_instructions${instructionsId}`; + listItem.dataset.instructionId = instructionId.toString(); + listItem.dataset.application = instructionData.application; + listItem.dataset.pip = instructionData.pip; + listItem.dataset.runStandalone = instructionData.runStandalone ? "true" : "false"; + listItem.dataset.value = instructionData.value; + let content = ` +
+
+ ${Language.get("wcf.acp.devtools.project.instruction.instruction", instructionData)} + `; if (instructionData.errors) { - for (var index in instructionData.errors) { - content += '' + instructionData.errors[index] + ''; - } + instructionData.errors.forEach((error) => { + content += `${error}`; + }); } - content += '' + - '
' + - ' ' + - ' ' + - ' ' + - ' ' + - '
'; + content += ` +
+ + + + +
+ `; 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)); - }, + document + .getElementById(`${this.formFieldId}_instruction${instructionsId}_deleteButton`) + .addEventListener("click", (ev) => this.removeInstruction(ev)); + document + .getElementById(`${this.formFieldId}_instruction${instructionsId}_editButton`) + .addEventListener("click", (ev) => this.editInstruction(ev)); + } /** - * 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. + * Adds a set of instructions. * - * @param {Event} event event that triggered trying to add the instructions + * If the instructions data is invalid, an error message is shown and no instruction set is added. */ - _addInstructions: function (event) { + addInstructions(event) { event.preventDefault(); event.stopPropagation(); // validate data - if (!this._validateInstructionsType() || (this._instructionsType.value === 'update' && !this._validateFromVersion(this._fromVersion))) { + 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 + 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(); - }, + this.instructionsType.value = ""; + this.fromVersion.value = ""; + this.toggleFromVersionFormField(); + Listener_1.default.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({ + addInstructionsByData(instructionsData) { + const instructionsId = ++this.instructionsCounter; + const listItem = document.createElement("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 + sectionTitle: Language.get(`wcf.acp.devtools.project.instructions.type.${instructionsData.type}.title`, { + fromVersion: instructionsData.fromVersion, }), - type: instructionsData.type + 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'); + listItem.id = `${this.formFieldId}_instructions${instructionsId}`; + listItem.dataset.instructionsId = instructionsId.toString(); + listItem.dataset.type = instructionsData.type; + listItem.dataset.fromVersion = instructionsData.fromVersion; + this.instructionsList.appendChild(listItem); + const instructionListContainer = document.getElementById(`${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); + instructionsData.errors.forEach((errorMessage) => { + Util_1.default.innerError(instructionListContainer, errorMessage, true); }); } - new UiSortableList({ + new List_1.default({ containerId: instructionListContainer.id, isSimpleSorting: true, options: { - toleranceElement: '> div' - } + 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.type === "update") { + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_deleteButton`) + .addEventListener("click", (ev) => this.removeInstructions(ev)); + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_editButton`) + .addEventListener("click", (ev) => this.editInstructions(ev)); + } + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_pip`) + .addEventListener("change", (ev) => this.changeInstructionPip(ev)); + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_value`) + .addEventListener("keypress", (ev) => this.instructionKeyPress(ev)); + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_addButton`) + .addEventListener("click", (ev) => this.addInstruction(ev)); if (instructionsData.instructions) { - for (var index in instructionsData.instructions) { - this._addInstructionByData(instructionsId, instructionsData.instructions[index]); - } + instructionsData.instructions.forEach((instruction) => { + this.addInstructionByData(instructionsId, instruction); + }); } - }, + } /** - * Is called if the selected package installation plugin of an - * instruction is changed. - * - * @param {Event} event change event + * Is called if the selected package installation plugin of an instruction is changed. */ - _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'); + changeInstructionPip(event) { + const target = event.currentTarget; + const pip = target.value; + const instructionsId = target.closest("li.section").dataset.instructionsId; + const description = document.getElementById(`${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] + 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'); + 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); - }, + 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({ + editInstruction(event) { + const listItem = event.currentTarget.closest("li"); + const instructionId = listItem.dataset.instructionId; + const application = listItem.dataset.application; + const pip = listItem.dataset.pip; + const runStandalone = Core.stringToBool(listItem.dataset.runStandalone); + const value = listItem.dataset.value; + const dialogContent = this.instructionEditDialogTemplate.fetch({ runStandalone: runStandalone, - value: value + 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); + const dialogId = "instructionEditDialog" + instructionId; + if (!Dialog_1.default.getDialog(dialogId)) { + Dialog_1.default.openStatic(dialogId, dialogContent, { + onSetup: (content) => { + const applicationSelect = content.querySelector("select[name=application]"); + const pipSelect = content.querySelector("select[name=pip]"); + const runStandaloneInput = content.querySelector("input[name=runStandalone]"); + const valueInput = content.querySelector("input[name=value]"); // 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); + const submit = () => { + const listItem = document.getElementById(`${this.formFieldId}_instruction${instructionId}`); + listItem.dataset.application = + Instructions.applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : ""; + listItem.dataset.pip = pipSelect.value; + listItem.dataset.runStandalone = runStandaloneInput.checked ? "1" : "0"; + listItem.dataset.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'), + listItem.querySelector(".jsDevtoolsProjectInstruction").innerHTML = Language.get("wcf.acp.devtools.project.instruction.instruction", { + application: listItem.dataset.application, + pip: listItem.dataset.pip, + runStandalone: listItem.dataset.runStandalone, + value: listItem.dataset.value, }); - DomChangeListener.trigger(); - UiDialog.close(dialogId); - }.bind(this); - valueInput.addEventListener('keypress', function (event) { - if (EventKey.Enter(event)) { + Listener_1.default.trigger(); + Dialog_1.default.close(dialogId); + }; + valueInput.addEventListener("keypress", (event) => { + if (event.key === "Enter") { 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')); + content.querySelector("button[data-type=submit]").addEventListener("click", submit); + const pipChange = () => { + const pip = pipSelect.value; + if (Instructions.applicationPips.indexOf(pip) !== -1) { + Util_1.default.show(applicationSelect.closest("dl")); } else { - elHide(applicationSelect.closest('dl')); + Util_1.default.hide(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] + const 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'); + description.innerHTML = Language.get("wcf.acp.devtools.project.instruction.value.description"); } - }.bind(this); - pipSelect.addEventListener('change', pipChange); + }; + pipSelect.addEventListener("change", pipChange); pipChange(); - }.bind(this), - title: Language.get('wcf.acp.devtools.project.instruction.edit') + }, + title: Language.get("wcf.acp.devtools.project.instruction.edit"), }); } else { - UiDialog.openStatic(dialogId); + Dialog_1.default.openStatic(dialogId, null); } - }, + } /** * 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 + editInstructions(event) { + const listItem = event.currentTarget.closest("li"); + const instructionsId = listItem.dataset.instructionsId; + const fromVersion = listItem.dataset.fromVersion; + const 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)) { + const dialogId = "instructionsEditDialog" + instructionsId; + if (!Dialog_1.default.getDialog(dialogId)) { + Dialog_1.default.openStatic(dialogId, dialogContent, { + onSetup: (content) => { + const fromVersion = content.querySelector("input[name=fromVersion]"); + const submit = () => { + 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 + const instructions = document.getElementById(`${this.formFieldId}_instructions${instructionsId}`); + instructions.dataset.fromVersion = fromVersion.value; + instructions.querySelector(".jsInstructionsTitle").innerHTML = 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)) { + Listener_1.default.trigger(); + Dialog_1.default.close(dialogId); + }; + fromVersion.addEventListener("keypress", (event) => { + if (event.key === "Enter") { submit(); } }); - elBySel('button[data-type=submit]', content).addEventListener('click', submit); - }.bind(this), - title: Language.get('wcf.acp.devtools.project.instructions.edit') + content.querySelector("button[data-type=submit]").addEventListener("click", submit); + }, + title: Language.get("wcf.acp.devtools.project.instructions.edit"), }); } else { - UiDialog.openStatic(dialogId); + Dialog_1.default.openStatic(dialogId, null); } - }, - /** - * 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 + * Adds an instruction after pressing ENTER in a relevant text field. */ - _instructionKeyPress: function (event) { - if (EventKey.Enter(event)) { - this._addInstruction(event); + instructionKeyPress(event) { + if (event.key === "Enter") { + this.addInstruction(event); } - }, + } /** - * Adds a set of instruction after pressing ENTER in a relevant - * text field. - * - * @param {Event} event + * Adds a set of instruction after pressing ENTER in a relevant text field. */ - _instructionsKeyPress: function (event) { - if (EventKey.Enter(event)) { - this._addInstructions(event); + instructionsKeyPress(event) { + if (event.key === "Enter") { + 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'); + removeInstruction(event) { + const instruction = event.currentTarget.closest("li"); UiConfirmation.show({ - confirm: function () { - elRemove(instruction); + confirm: () => { + instruction.remove(); }, - message: Language.get('wcf.acp.devtools.project.instruction.delete.confirmMessages') + 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'); + removeInstructions(event) { + const instructions = event.currentTarget.closest("li"); UiConfirmation.show({ - confirm: function () { - elRemove(instructions); + confirm: () => { + instructions.remove(); }, - message: Language.get('wcf.acp.devtools.project.instructions.delete.confirmMessages') + message: Language.get("wcf.acp.devtools.project.instructions.delete.confirmMessages"), }); - }, + } /** - * Adds all necessary (hidden) form fields to the form when - * submitting the form. + * Adds all necessary (hidden) form fields to the form when submitting the form. */ - _submit: function (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); + submit() { + DomTraverse.childrenByTag(this.instructionsList, "LI").forEach((instructions, instructionsIndex) => { + const namePrefix = `${this.formFieldId}[${instructionsIndex}]`; + const instructionsType = document.createElement("input"); + instructionsType.type = "hidden"; + instructionsType.name = `${namePrefix}[type]`; + instructionsType.value = instructions.dataset.type; + this.form.appendChild(instructionsType); + if (instructionsType.value === "update") { + const fromVersion = document.createElement("input"); + fromVersion.type = "hidden"; + fromVersion.name = `${this.formFieldId}[${instructionsIndex}][fromVersion]`; + fromVersion.value = instructions.dataset.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); + DomTraverse.childrenByTag(document.getElementById(`${instructions.id}_instructionList`), "LI").forEach((instruction, instructionIndex) => { + const namePrefix = `${this.formFieldId}[${instructionsIndex}][instructions][${instructionIndex}]`; + ["pip", "value", "runStandalone"].forEach((property) => { + const element = document.createElement("input"); + element.type = "hidden"; + element.name = `${namePrefix}[${property}]`; + element.value = instruction.dataset[property]; + this.form.appendChild(element); + }); + if (Instructions.applicationPips.indexOf(instruction.dataset.pip) !== -1) { + const application = document.createElement("input"); + application.type = "hidden"; + application.name = `${namePrefix}[application]`; + application.value = instruction.dataset.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 + * Toggles the visibility of the application form field based on the selected pip for the instructions with the given id. */ - _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); + toggleApplicationFormField(instructionsId) { + const pip = document.getElementById(`${this.formFieldId}_instructions${instructionsId}_pip`) + .value; + const valueDlClassList = document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_value`) + .closest("dl").classList; + const applicationDl = document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_application`) + .closest("dl"); + if (Instructions.applicationPips.indexOf(pip) !== -1) { + valueDlClassList.remove("col-md-9"); + valueDlClassList.add("col-md-7"); + Util_1.default.show(applicationDl); } else { - valueDlClassList.remove('col-md-7'); - valueDlClassList.add('col-md-9'); - elHide(applicationDl); + valueDlClassList.remove("col-md-7"); + valueDlClassList.add("col-md-9"); + Util_1.default.hide(applicationDl); } - }, + } /** - * Toggles the visibility of the `fromVersion` form field based on - * the selected instructions type. + * 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); + toggleFromVersionFormField() { + const instructionsTypeList = this.instructionsType.closest("dl").classList; + const fromVersionDl = this.fromVersion.closest("dl"); + if (this.instructionsType.value === "update") { + instructionsTypeList.remove("col-md-10"); + instructionsTypeList.add("col-md-5"); + Util_1.default.show(fromVersionDl); } else { - instructionsTypeList.remove('col-md-5'); - instructionsTypeList.add('col-md-10'); - elHide(fromVersionDl); + instructionsTypeList.remove("col-md-5"); + instructionsTypeList.add("col-md-10"); + Util_1.default.hide(fromVersionDl); } - }, + } /** - * Returns `true` if the currently entered update "from version" - * is valid. Otherwise `false` is returned and an error message - * is shown. - * - * @return {boolean} + * Returns `true` if the currently entered update "from version" is valid. Otherwise `false` is returned and an error + * message is shown. */ - _validateFromVersion: function (inputField) { - var version = inputField.value; - if (version === '') { - this._getFromVersionErrorElement(inputField, true).textContent = Language.get('wcf.global.form.error.empty'); + validateFromVersion(inputField) { + const version = inputField.value; + if (version === "") { + Util_1.default.innerError(inputField, 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'); + Util_1.default.innerError(inputField, 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'); + if (version.indexOf("*") === -1) { + if (!Instructions.versionRegExp.test(version)) { + Util_1.default.innerError(inputField, 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'); + else if (!Instructions.versionRegExp.test(version.replace("*", "0"))) { + Util_1.default.innerError(inputField, Language.get("wcf.acp.devtools.project.packageVersion.error.format")); return false; } // remove outdated errors - var error = this._getFromVersionErrorElement(inputField); - if (error !== null) { - elRemove(error); - } + Util_1.default.innerError(inputField, ""); 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'); + validateInstructionsType() { + if (this.instructionsType.value !== "install" && this.instructionsType.value !== "update") { + if (this.instructionsType.value === "") { + Util_1.default.innerError(this.instructionsType, Language.get("wcf.global.form.error.empty")); } else { - this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.global.form.error.noValidSelection'); + Util_1.default.innerError(this.instructionsType, 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; - } - }); + // there may only be one set of installation instructions + if (this.instructionsType.value === "install") { + const hasInstall = Array.from(this.instructionsList.children).some((instructions) => instructions.dataset.type === "install"); if (hasInstall) { - this._getInstructionsTypeErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.instructions.type.update.error.duplicate'); + Util_1.default.innerError(this.instructionsType, 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); - } + Util_1.default.innerError(this.instructionsType, ""); return true; } - }; + } + Instructions.applicationPips = ["acpTemplate", "file", "script", "template"]; + // see `wcf\data\package\Package::isValidPackageName()` + Instructions.packageIdentifierRegExp = new RegExp(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/); + // see `wcf\data\package\Package::isValidVersion()` + Instructions.versionRegExp = new RegExp(/^([0-9]+).([0-9]+)\.([0-9]+)( (a|alpha|b|beta|d|dev|rc|pl) ([0-9]+))?$/i); + Core.enableLegacyInheritance(Instructions); return Instructions; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js deleted file mode 100644 index 3e73c417b4..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.js +++ /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 - * @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 = '' + - '
' + - '
' + - ' ' + Language.get('wcf.acp.devtools.project.instruction.instruction', instructionData); - - if (instructionData.errors) { - for (var index in instructionData.errors) { - content += '' + instructionData.errors[index] + ''; - } - } - - content += '' + - '
' + - ' ' + - ' ' + - ' ' + - ' ' + - '
'; - - 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/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts new file mode 100644 index 0000000000..1c782b862f --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions.ts @@ -0,0 +1,712 @@ +/** + * Manages the instructions entered in a devtools project instructions form field. + * + * @author Matthias Schmidt + * @copyright 2001-2021 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/Instructions + * @since 5.2 + */ + +import * as Core from "../../../../../../Core"; +import Template from "../../../../../../Template"; +import * as Language from "../../../../../../Language"; +import * as DomTraverse from "../../../../../../Dom/Traverse"; +import DomChangeListener from "../../../../../../Dom/Change/Listener"; +import DomUtil from "../../../../../../Dom/Util"; +import UiSortableList from "../../../../../../Ui/Sortable/List"; +import UiDialog from "../../../../../../Ui/Dialog"; +import * as UiConfirmation from "../../../../../../Ui/Confirmation"; + +interface Instruction { + application: string; + errors?: string[]; + pip: string; + runStandalone: number; + value: string; +} + +interface InstructionsData { + errors?: string[]; + fromVersion?: string; + instructions?: Instruction[]; + type: "install" | "update"; +} + +type InstructionsId = number | string; + +class Instructions { + protected readonly addButton: HTMLAnchorElement; + protected readonly form: HTMLFormElement; + protected readonly formFieldId: string; + protected readonly fromVersion: HTMLInputElement; + protected instructionCounter = 0; + protected instructionsCounter = 0; + protected readonly instructionsEditDialogTemplate: Template; + protected readonly instructionsList: HTMLUListElement; + protected readonly instructionsType: HTMLSelectElement; + protected readonly instructionsTemplate: Template; + protected readonly instructionEditDialogTemplate: Template; + protected readonly pipDefaultFilenames: { [k: string]: string }; + + protected static applicationPips = ["acpTemplate", "file", "script", "template"]; + + // see `wcf\data\package\Package::isValidPackageName()` + protected static packageIdentifierRegExp = new RegExp(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/); + + // see `wcf\data\package\Package::isValidVersion()` + protected static versionRegExp = new RegExp( + /^([0-9]+).([0-9]+)\.([0-9]+)( (a|alpha|b|beta|d|dev|rc|pl) ([0-9]+))?$/i, + ); + + constructor( + formFieldId: string, + instructionsTemplate: Template, + instructionsEditDialogTemplate: Template, + instructionEditDialogTemplate: Template, + pipDefaultFilenames: { [k: string]: string }, + existingInstructions: InstructionsData[], + ) { + this.formFieldId = formFieldId; + this.instructionsTemplate = instructionsTemplate; + this.instructionsEditDialogTemplate = instructionsEditDialogTemplate; + this.instructionEditDialogTemplate = instructionEditDialogTemplate; + this.pipDefaultFilenames = pipDefaultFilenames; + + this.instructionsList = document.getElementById(`${this.formFieldId}_instructionsList`) as HTMLUListElement; + if (this.instructionsList === null) { + throw new Error(`Cannot find package list for packages field with id '${this.formFieldId}'.`); + } + + this.instructionsType = document.getElementById(`${this.formFieldId}_instructionsType`) as HTMLSelectElement; + 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()); + + this.fromVersion = document.getElementById(`${this.formFieldId}_fromVersion`) as HTMLInputElement; + if (this.fromVersion === null) { + throw new Error(`Cannot find from version form field for instructions field with id '${this.formFieldId}'.`); + } + this.fromVersion.addEventListener("keypress", (ev) => this.instructionsKeyPress(ev)); + + this.addButton = document.getElementById(`${this.formFieldId}_addButton`) as HTMLAnchorElement; + if (this.addButton === null) { + throw new Error(`Cannot find add button form field for instructions field with id '${this.formFieldId}'.`); + } + this.addButton.addEventListener("click", (ev) => this.addInstructions(ev)); + + 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()); + + const hasInstallInstructions = existingInstructions.some((instructions) => instructions.type === "install"); + + // ensure that there are always installation instructions + if (!hasInstallInstructions) { + this.addInstructionsByData({ + fromVersion: "", + type: "install", + }); + } + + existingInstructions.forEach((instructions) => this.addInstructionsByData(instructions)); + + 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. + */ + protected addInstruction(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + + const instructionsId = ((event.currentTarget as HTMLElement).closest("li.section") as HTMLElement).dataset + .instrictionsId!; + + // note: data will be validated/filtered by the server + + const pipField = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_pip`, + ) as HTMLInputElement; + + // ignore pressing button if no PIP has been selected + if (!pipField.value) { + return; + } + + const valueField = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_value`, + ) as HTMLInputElement; + const runStandaloneField = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_runStandalone`, + ) as HTMLInputElement; + const applicationField = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_application`, + ) as HTMLSelectElement; + + this.addInstructionByData(instructionsId, { + application: Instructions.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 = ""; + document.getElementById( + `${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. + */ + protected addInstructionByData(instructionsId: InstructionsId, instructionData: Instruction): void { + const instructionId = ++this.instructionCounter; + + const instructionList = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_instructionList`, + )!; + + const listItem = document.createElement("li"); + listItem.className = "sortableNode"; + listItem.id = `${this.formFieldId}_instructions${instructionsId}`; + listItem.dataset.instructionId = instructionId.toString(); + listItem.dataset.application = instructionData.application; + listItem.dataset.pip = instructionData.pip; + listItem.dataset.runStandalone = instructionData.runStandalone ? "true" : "false"; + listItem.dataset.value = instructionData.value; + + let content = ` +
+
+ ${Language.get("wcf.acp.devtools.project.instruction.instruction", instructionData)} + `; + + if (instructionData.errors) { + instructionData.errors.forEach((error) => { + content += `${error}`; + }); + } + + content += ` +
+ + + + +
+ `; + + listItem.innerHTML = content; + + instructionList.appendChild(listItem); + + document + .getElementById(`${this.formFieldId}_instruction${instructionsId}_deleteButton`)! + .addEventListener("click", (ev) => this.removeInstruction(ev)); + document + .getElementById(`${this.formFieldId}_instruction${instructionsId}_editButton`)! + .addEventListener("click", (ev) => this.editInstruction(ev)); + } + + /** + * Adds a set of instructions. + * + * If the instructions data is invalid, an error message is shown and no instruction set is added. + */ + protected addInstructions(event: Event): void { + 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 as "install" | "update", + }); + + // empty fields + this.instructionsType.value = ""; + this.fromVersion.value = ""; + + this.toggleFromVersionFormField(); + + DomChangeListener.trigger(); + } + + /** + * Adds a set of instructions. + */ + protected addInstructionsByData(instructionsData: InstructionsData): void { + const instructionsId = ++this.instructionsCounter; + + const listItem = document.createElement("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}`; + listItem.dataset.instructionsId = instructionsId.toString(); + listItem.dataset.type = instructionsData.type; + listItem.dataset.fromVersion = instructionsData.fromVersion; + + this.instructionsList.appendChild(listItem); + + const instructionListContainer = document.getElementById( + `${this.formFieldId}_instructions${instructionsId}_instructionListContainer`, + )!; + if (Array.isArray(instructionsData.errors)) { + instructionsData.errors.forEach((errorMessage) => { + DomUtil.innerError(instructionListContainer, errorMessage, true); + }); + } + + new UiSortableList({ + containerId: instructionListContainer.id, + isSimpleSorting: true, + options: { + toleranceElement: "> div", + }, + }); + + if (instructionsData.type === "update") { + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_deleteButton`)! + .addEventListener("click", (ev) => this.removeInstructions(ev)); + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_editButton`)! + .addEventListener("click", (ev) => this.editInstructions(ev)); + } + + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_pip`)! + .addEventListener("change", (ev) => this.changeInstructionPip(ev)); + + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_value`)! + .addEventListener("keypress", (ev) => this.instructionKeyPress(ev)); + + document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_addButton`)! + .addEventListener("click", (ev) => this.addInstruction(ev)); + + if (instructionsData.instructions) { + instructionsData.instructions.forEach((instruction) => { + this.addInstructionByData(instructionsId, instruction); + }); + } + } + + /** + * Is called if the selected package installation plugin of an instruction is changed. + */ + protected changeInstructionPip(event: Event): void { + const target = event.currentTarget as HTMLInputElement; + + const pip = target.value; + const instructionsId = (target.closest("li.section") as HTMLElement).dataset.instructionsId!; + const description = document.getElementById(`${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"); + } + + // toggle application selector + this.toggleApplicationFormField(instructionsId); + } + + /** + * Opens a dialog to edit an existing instruction. + */ + protected editInstruction(event: Event): void { + const listItem = (event.currentTarget as HTMLElement).closest("li")!; + + const instructionId = listItem.dataset.instructionId!; + const application = listItem.dataset.application!; + const pip = listItem.dataset.pip!; + const runStandalone = Core.stringToBool(listItem.dataset.runStandalone!); + const value = listItem.dataset.value!; + + const dialogContent = this.instructionEditDialogTemplate.fetch({ + runStandalone: runStandalone, + value: value, + }); + + const dialogId = "instructionEditDialog" + instructionId; + if (!UiDialog.getDialog(dialogId)) { + UiDialog.openStatic(dialogId, dialogContent, { + onSetup: (content) => { + const applicationSelect = content.querySelector("select[name=application]") as HTMLSelectElement; + const pipSelect = content.querySelector("select[name=pip]") as HTMLInputElement; + const runStandaloneInput = content.querySelector("input[name=runStandalone]") as HTMLInputElement; + const valueInput = content.querySelector("input[name=value]") as HTMLInputElement; + + // set values of `select` elements + applicationSelect.value = application; + pipSelect.value = pip; + + const submit = () => { + const listItem = document.getElementById(`${this.formFieldId}_instruction${instructionId}`)!; + listItem.dataset.application = + Instructions.applicationPips.indexOf(pipSelect.value) !== -1 ? applicationSelect.value : ""; + listItem.dataset.pip = pipSelect.value; + listItem.dataset.runStandalone = runStandaloneInput.checked ? "1" : "0"; + listItem.dataset.value = valueInput.value; + + // note: data will be validated/filtered by the server + + listItem.querySelector(".jsDevtoolsProjectInstruction")!.innerHTML = Language.get( + "wcf.acp.devtools.project.instruction.instruction", + { + application: listItem.dataset.application, + pip: listItem.dataset.pip, + runStandalone: listItem.dataset.runStandalone, + value: listItem.dataset.value, + }, + ); + + DomChangeListener.trigger(); + + UiDialog.close(dialogId); + }; + + valueInput.addEventListener("keypress", (event) => { + if (event.key === "Enter") { + submit(); + } + }); + + content.querySelector("button[data-type=submit]")!.addEventListener("click", submit); + + const pipChange = () => { + const pip = pipSelect.value; + + if (Instructions.applicationPips.indexOf(pip) !== -1) { + DomUtil.show(applicationSelect.closest("dl")!); + } else { + DomUtil.hide(applicationSelect.closest("dl")!); + } + + const 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"); + } + }; + + pipSelect.addEventListener("change", pipChange); + pipChange(); + }, + title: Language.get("wcf.acp.devtools.project.instruction.edit"), + }); + } else { + UiDialog.openStatic(dialogId, null); + } + } + + /** + * Opens a dialog to edit an existing set of instructions. + */ + protected editInstructions(event: Event): void { + const listItem = (event.currentTarget as HTMLElement).closest("li")!; + + const instructionsId = listItem.dataset.instructionsId!; + const fromVersion = listItem.dataset.fromVersion; + + const dialogContent = this.instructionsEditDialogTemplate.fetch({ + fromVersion: fromVersion, + }); + + const dialogId = "instructionsEditDialog" + instructionsId; + if (!UiDialog.getDialog(dialogId)) { + UiDialog.openStatic(dialogId, dialogContent, { + onSetup: (content) => { + const fromVersion = content.querySelector("input[name=fromVersion]") as HTMLInputElement; + + const submit = () => { + if (!this.validateFromVersion(fromVersion)) { + return; + } + + const instructions = document.getElementById(`${this.formFieldId}_instructions${instructionsId}`)!; + instructions.dataset.fromVersion = fromVersion.value; + + instructions.querySelector(".jsInstructionsTitle")!.innerHTML = Language.get( + "wcf.acp.devtools.project.instructions.type.update.title", + { + fromVersion: fromVersion.value, + }, + ); + + DomChangeListener.trigger(); + + UiDialog.close(dialogId); + }; + + fromVersion.addEventListener("keypress", (event) => { + if (event.key === "Enter") { + submit(); + } + }); + + content.querySelector("button[data-type=submit]")!.addEventListener("click", submit); + }, + title: Language.get("wcf.acp.devtools.project.instructions.edit"), + }); + } else { + UiDialog.openStatic(dialogId, null); + } + } + + /** + * Adds an instruction after pressing ENTER in a relevant text field. + */ + protected instructionKeyPress(event: KeyboardEvent): void { + if (event.key === "Enter") { + this.addInstruction(event); + } + } + + /** + * Adds a set of instruction after pressing ENTER in a relevant text field. + */ + protected instructionsKeyPress(event: KeyboardEvent): void { + if (event.key === "Enter") { + this.addInstructions(event); + } + } + + /** + * Removes an instruction by clicking on its delete button. + */ + protected removeInstruction(event: Event): void { + const instruction = (event.currentTarget as HTMLElement).closest("li")!; + + UiConfirmation.show({ + confirm: () => { + instruction.remove(); + }, + 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 + */ + protected removeInstructions(event: Event): void { + const instructions = (event.currentTarget as HTMLElement).closest("li")!; + + UiConfirmation.show({ + confirm: () => { + instructions.remove(); + }, + message: Language.get("wcf.acp.devtools.project.instructions.delete.confirmMessages"), + }); + } + + /** + * Adds all necessary (hidden) form fields to the form when submitting the form. + */ + protected submit(): void { + DomTraverse.childrenByTag(this.instructionsList, "LI").forEach((instructions, instructionsIndex) => { + const namePrefix = `${this.formFieldId}[${instructionsIndex}]`; + + const instructionsType = document.createElement("input"); + instructionsType.type = "hidden"; + instructionsType.name = `${namePrefix}[type]`; + instructionsType.value = instructions.dataset.type!; + this.form.appendChild(instructionsType); + + if (instructionsType.value === "update") { + const fromVersion = document.createElement("input"); + fromVersion.type = "hidden"; + fromVersion.name = `${this.formFieldId}[${instructionsIndex}][fromVersion]`; + fromVersion.value = instructions.dataset.fromVersion!; + this.form.appendChild(fromVersion); + } + + DomTraverse.childrenByTag(document.getElementById(`${instructions.id}_instructionList`)!, "LI").forEach( + (instruction, instructionIndex) => { + const namePrefix = `${this.formFieldId}[${instructionsIndex}][instructions][${instructionIndex}]`; + + ["pip", "value", "runStandalone"].forEach((property) => { + const element = document.createElement("input"); + element.type = "hidden"; + element.name = `${namePrefix}[${property}]`; + element.value = instruction.dataset[property]!; + this.form.appendChild(element); + }); + + if (Instructions.applicationPips.indexOf(instruction.dataset.pip!) !== -1) { + const application = document.createElement("input"); + application.type = "hidden"; + application.name = `${namePrefix}[application]`; + application.value = instruction.dataset.application!; + this.form.appendChild(application); + } + }, + ); + }); + } + + /** + * Toggles the visibility of the application form field based on the selected pip for the instructions with the given id. + */ + protected toggleApplicationFormField(instructionsId: InstructionsId): void { + const pip = (document.getElementById(`${this.formFieldId}_instructions${instructionsId}_pip`) as HTMLInputElement) + .value; + + const valueDlClassList = document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_value`)! + .closest("dl")!.classList; + const applicationDl = document + .getElementById(`${this.formFieldId}_instructions${instructionsId}_application`)! + .closest("dl")!; + + if (Instructions.applicationPips.indexOf(pip) !== -1) { + valueDlClassList.remove("col-md-9"); + valueDlClassList.add("col-md-7"); + DomUtil.show(applicationDl); + } else { + valueDlClassList.remove("col-md-7"); + valueDlClassList.add("col-md-9"); + DomUtil.hide(applicationDl); + } + } + + /** + * Toggles the visibility of the `fromVersion` form field based on the selected instructions type. + */ + protected toggleFromVersionFormField(): void { + const instructionsTypeList = this.instructionsType.closest("dl")!.classList; + const fromVersionDl = this.fromVersion.closest("dl")!; + + if (this.instructionsType.value === "update") { + instructionsTypeList.remove("col-md-10"); + instructionsTypeList.add("col-md-5"); + DomUtil.show(fromVersionDl); + } else { + instructionsTypeList.remove("col-md-5"); + instructionsTypeList.add("col-md-10"); + DomUtil.hide(fromVersionDl); + } + } + + /** + * Returns `true` if the currently entered update "from version" is valid. Otherwise `false` is returned and an error + * message is shown. + */ + protected validateFromVersion(inputField: HTMLInputElement): boolean { + const version = inputField.value; + + if (version === "") { + DomUtil.innerError(inputField, Language.get("wcf.global.form.error.empty")); + + return false; + } + + if (version.length > 50) { + DomUtil.innerError(inputField, Language.get("wcf.acp.devtools.project.packageVersion.error.maximumLength")); + + return false; + } + + // wildcard versions are checked on the server side + if (version.indexOf("*") === -1) { + if (!Instructions.versionRegExp.test(version)) { + DomUtil.innerError(inputField, Language.get("wcf.acp.devtools.project.packageVersion.error.format")); + + return false; + } + } else if (!Instructions.versionRegExp.test(version.replace("*", "0"))) { + DomUtil.innerError(inputField, Language.get("wcf.acp.devtools.project.packageVersion.error.format")); + + return false; + } + + // remove outdated errors + DomUtil.innerError(inputField, ""); + + return true; + } + + /** + * Returns `true` if the entered update instructions type is valid. + * Otherwise `false` is returned and an error message is shown. + */ + protected validateInstructionsType(): boolean { + if (this.instructionsType.value !== "install" && this.instructionsType.value !== "update") { + if (this.instructionsType.value === "") { + DomUtil.innerError(this.instructionsType, Language.get("wcf.global.form.error.empty")); + } else { + DomUtil.innerError(this.instructionsType, Language.get("wcf.global.form.error.noValidSelection")); + } + + return false; + } + + // there may only be one set of installation instructions + if (this.instructionsType.value === "install") { + const hasInstall = Array.from(this.instructionsList.children).some( + (instructions: HTMLElement) => instructions.dataset.type === "install", + ); + + if (hasInstall) { + DomUtil.innerError( + this.instructionsType, + Language.get("wcf.acp.devtools.project.instructions.type.update.error.duplicate"), + ); + + return false; + } + } + + // remove outdated errors + DomUtil.innerError(this.instructionsType, ""); + + return true; + } +} + +Core.enableLegacyInheritance(Instructions); + +export = Instructions; -- 2.20.1