2 * Abstract implementation of the JavaScript component of a form field handling a list of packages.
4 * @author Matthias Schmidt
5 * @copyright 2001-2021 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
11 import * as Core from "../../../../../../Core";
12 import * as Language from "../../../../../../Language";
13 import * as DomTraverse from "../../../../../../Dom/Traverse";
14 import DomChangeListener from "../../../../../../Dom/Change/Listener";
15 import DomUtil from "../../../../../../Dom/Util";
16 import { PackageData } from "./Data";
18 abstract class AbstractPackageList<TPackageData extends PackageData = PackageData> {
19 protected readonly addButton: HTMLAnchorElement;
20 protected readonly form: HTMLFormElement;
21 protected readonly formFieldId: string;
22 protected readonly packageList: HTMLOListElement;
23 protected readonly packageIdentifier: HTMLInputElement;
25 // see `wcf\data\package\Package::isValidPackageName()`
26 protected static packageIdentifierRegExp = new RegExp(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/);
28 // see `wcf\data\package\Package::isValidVersion()`
29 protected static versionRegExp = new RegExp(
30 /^([0-9]+).([0-9]+)\.([0-9]+)( (a|alpha|b|beta|d|dev|rc|pl) ([0-9]+))?$/i,
33 constructor(formFieldId: string, existingPackages: TPackageData[]) {
34 this.formFieldId = formFieldId;
36 this.packageList = document.getElementById(`${this.formFieldId}_packageList`) as HTMLOListElement;
37 if (this.packageList === null) {
38 throw new Error(`Cannot find package list for packages field with id '${this.formFieldId}'.`);
41 this.packageIdentifier = document.getElementById(`${this.formFieldId}_packageIdentifier`) as HTMLInputElement;
42 if (this.packageIdentifier === null) {
43 throw new Error(`Cannot find package identifier form field for packages field with id '${this.formFieldId}'.`);
45 this.packageIdentifier.addEventListener("keypress", (ev) => this.keyPress(ev));
47 this.addButton = document.getElementById(`${this.formFieldId}_addButton`) as HTMLAnchorElement;
48 if (this.addButton === null) {
49 throw new Error(`Cannot find add button for packages field with id '${this.formFieldId}'.`);
51 this.addButton.addEventListener("click", (ev) => this.addPackage(ev));
53 this.form = this.packageList.closest("form") as HTMLFormElement;
54 if (this.form === null) {
55 throw new Error(`Cannot find form element for packages field with id '${this.formFieldId}'.`);
57 this.form.addEventListener("submit", () => this.submit());
59 existingPackages.forEach((data) => this.addPackageByData(data));
63 * Adds a package to the package list as a consequence of the given event.
65 * If the package data is invalid, an error message is shown and no package is added.
67 protected addPackage(event: Event): void {
68 event.preventDefault();
69 event.stopPropagation();
72 if (!this.validateInput()) {
76 this.addPackageByData(this.getInputData());
81 this.packageIdentifier.focus();
85 * Adds a package to the package list using the given package data.
87 protected addPackageByData(packageData: TPackageData): void {
88 // add package to list
89 const listItem = document.createElement("li");
90 this.populateListItem(listItem, packageData);
93 const deleteButton = document.createElement("span");
94 deleteButton.className = "icon icon16 fa-times pointer jsTooltip";
95 deleteButton.title = Language.get("wcf.global.button.delete");
96 deleteButton.addEventListener("click", (ev) => this.removePackage(ev));
97 listItem.insertAdjacentElement("afterbegin", deleteButton);
99 this.packageList.appendChild(listItem);
101 DomChangeListener.trigger();
105 * Creates the hidden fields when the form is submitted.
107 protected createSubmitFields(listElement: HTMLLIElement, index: number): void {
108 const packageIdentifier = document.createElement("input");
109 packageIdentifier.type = "hidden";
110 packageIdentifier.name = `${this.formFieldId}[${index}][packageIdentifier]`;
111 packageIdentifier.value = listElement.dataset.packageIdentifier!;
112 this.form.appendChild(packageIdentifier);
116 * Empties the input fields.
118 protected emptyInput(): void {
119 this.packageIdentifier.value = "";
123 * Returns the current data of the input fields to add a new package.
125 protected getInputData(): TPackageData {
127 packageIdentifier: this.packageIdentifier.value,
132 * Adds a package to the package list after pressing ENTER in a text field.
134 protected keyPress(event: KeyboardEvent): void {
135 if (event.key === "Enter") {
136 this.addPackage(event);
141 * Adds all necessary package-relavant data to the given list item.
143 protected populateListItem(listItem: HTMLLIElement, packageData: TPackageData): void {
144 listItem.dataset.packageIdentifier = packageData.packageIdentifier;
148 * Removes a package by clicking on its delete button.
150 protected removePackage(event: Event): void {
151 (event.currentTarget as HTMLElement).closest("li")!.remove();
153 // remove field errors if the last package has been deleted
154 DomUtil.innerError(this.packageList, "");
158 * Adds all necessary (hidden) form fields to the form when submitting the form.
160 protected submit(): void {
161 DomTraverse.childrenByTag(this.packageList, "LI").forEach((listItem, index) =>
162 this.createSubmitFields(listItem, index),
167 * Returns `true` if the currently entered package data is valid. Otherwise `false` is returned and relevant error
168 * messages are shown.
170 protected validateInput(): boolean {
171 return this.validatePackageIdentifier();
175 * Returns `true` if the currently entered package identifier is valid. Otherwise `false` is returned and an error
178 protected validatePackageIdentifier(): boolean {
179 const packageIdentifier = this.packageIdentifier.value;
181 if (packageIdentifier === "") {
182 DomUtil.innerError(this.packageIdentifier, Language.get("wcf.global.form.error.empty"));
187 if (packageIdentifier.length < 3) {
189 this.packageIdentifier,
190 Language.get("wcf.acp.devtools.project.packageIdentifier.error.minimumLength"),
194 } else if (packageIdentifier.length > 191) {
196 this.packageIdentifier,
197 Language.get("wcf.acp.devtools.project.packageIdentifier.error.maximumLength"),
203 if (!AbstractPackageList.packageIdentifierRegExp.test(packageIdentifier)) {
205 this.packageIdentifier,
206 Language.get("wcf.acp.devtools.project.packageIdentifier.error.format"),
212 // check if package has already been added
213 const duplicate = DomTraverse.childrenByTag(this.packageList, "LI").some(
214 (listItem) => listItem.dataset.packageIdentifier === packageIdentifier,
219 this.packageIdentifier,
220 Language.get("wcf.acp.devtools.project.packageIdentifier.error.duplicate"),
226 // remove outdated errors
227 DomUtil.innerError(this.packageIdentifier, "");
233 * Returns `true` if the given version is valid. Otherwise `false` is returned and an error message is shown.
235 protected validateVersion(versionElement: HTMLInputElement): boolean {
236 const version = versionElement.value;
238 // see `wcf\data\package\Package::isValidVersion()`
239 // the version is no a required attribute
240 if (version !== "") {
241 if (version.length > 255) {
242 DomUtil.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.maximumLength"));
247 if (!AbstractPackageList.versionRegExp.test(version)) {
248 DomUtil.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.format"));
254 // remove outdated errors
255 DomUtil.innerError(versionElement, "");
261 Core.enableLegacyInheritance(AbstractPackageList);
263 export = AbstractPackageList;