955a36a22f0429164def99d46bf94ea1a03ad644
[GitHub/WoltLab/WCF.git] /
1 /**
2 * Abstract implementation of the JavaScript component of a form field handling a list of packages.
3 *
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
8 * @since 5.2
9 */
10
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";
17
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;
24
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_-]+)+$/);
27
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,
31 );
32
33 constructor(formFieldId: string, existingPackages: TPackageData[]) {
34 this.formFieldId = formFieldId;
35
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}'.`);
39 }
40
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}'.`);
44 }
45 this.packageIdentifier.addEventListener("keypress", (ev) => this.keyPress(ev));
46
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}'.`);
50 }
51 this.addButton.addEventListener("click", (ev) => this.addPackage(ev));
52
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}'.`);
56 }
57 this.form.addEventListener("submit", () => this.submit());
58
59 existingPackages.forEach((data) => this.addPackageByData(data));
60 }
61
62 /**
63 * Adds a package to the package list as a consequence of the given event.
64 *
65 * If the package data is invalid, an error message is shown and no package is added.
66 */
67 protected addPackage(event: Event): void {
68 event.preventDefault();
69 event.stopPropagation();
70
71 // validate data
72 if (!this.validateInput()) {
73 return;
74 }
75
76 this.addPackageByData(this.getInputData());
77
78 // empty fields
79 this.emptyInput();
80
81 this.packageIdentifier.focus();
82 }
83
84 /**
85 * Adds a package to the package list using the given package data.
86 */
87 protected addPackageByData(packageData: TPackageData): void {
88 // add package to list
89 const listItem = document.createElement("li");
90 this.populateListItem(listItem, packageData);
91
92 // add delete button
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);
98
99 this.packageList.appendChild(listItem);
100
101 DomChangeListener.trigger();
102 }
103
104 /**
105 * Creates the hidden fields when the form is submitted.
106 */
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);
113 }
114
115 /**
116 * Empties the input fields.
117 */
118 protected emptyInput(): void {
119 this.packageIdentifier.value = "";
120 }
121
122 /**
123 * Returns the current data of the input fields to add a new package.
124 */
125 protected getInputData(): TPackageData {
126 return {
127 packageIdentifier: this.packageIdentifier.value,
128 } as TPackageData;
129 }
130
131 /**
132 * Adds a package to the package list after pressing ENTER in a text field.
133 */
134 protected keyPress(event: KeyboardEvent): void {
135 if (event.key === "Enter") {
136 this.addPackage(event);
137 }
138 }
139
140 /**
141 * Adds all necessary package-relavant data to the given list item.
142 */
143 protected populateListItem(listItem: HTMLLIElement, packageData: TPackageData): void {
144 listItem.dataset.packageIdentifier = packageData.packageIdentifier;
145 }
146
147 /**
148 * Removes a package by clicking on its delete button.
149 */
150 protected removePackage(event: Event): void {
151 (event.currentTarget as HTMLElement).closest("li")!.remove();
152
153 // remove field errors if the last package has been deleted
154 DomUtil.innerError(this.packageList, "");
155 }
156
157 /**
158 * Adds all necessary (hidden) form fields to the form when submitting the form.
159 */
160 protected submit(): void {
161 DomTraverse.childrenByTag(this.packageList, "LI").forEach((listItem, index) =>
162 this.createSubmitFields(listItem, index),
163 );
164 }
165
166 /**
167 * Returns `true` if the currently entered package data is valid. Otherwise `false` is returned and relevant error
168 * messages are shown.
169 */
170 protected validateInput(): boolean {
171 return this.validatePackageIdentifier();
172 }
173
174 /**
175 * Returns `true` if the currently entered package identifier is valid. Otherwise `false` is returned and an error
176 * message is shown.
177 */
178 protected validatePackageIdentifier(): boolean {
179 const packageIdentifier = this.packageIdentifier.value;
180
181 if (packageIdentifier === "") {
182 DomUtil.innerError(this.packageIdentifier, Language.get("wcf.global.form.error.empty"));
183
184 return false;
185 }
186
187 if (packageIdentifier.length < 3) {
188 DomUtil.innerError(
189 this.packageIdentifier,
190 Language.get("wcf.acp.devtools.project.packageIdentifier.error.minimumLength"),
191 );
192
193 return false;
194 } else if (packageIdentifier.length > 191) {
195 DomUtil.innerError(
196 this.packageIdentifier,
197 Language.get("wcf.acp.devtools.project.packageIdentifier.error.maximumLength"),
198 );
199
200 return false;
201 }
202
203 if (!AbstractPackageList.packageIdentifierRegExp.test(packageIdentifier)) {
204 DomUtil.innerError(
205 this.packageIdentifier,
206 Language.get("wcf.acp.devtools.project.packageIdentifier.error.format"),
207 );
208
209 return false;
210 }
211
212 // check if package has already been added
213 const duplicate = DomTraverse.childrenByTag(this.packageList, "LI").some(
214 (listItem) => listItem.dataset.packageIdentifier === packageIdentifier,
215 );
216
217 if (duplicate) {
218 DomUtil.innerError(
219 this.packageIdentifier,
220 Language.get("wcf.acp.devtools.project.packageIdentifier.error.duplicate"),
221 );
222
223 return false;
224 }
225
226 // remove outdated errors
227 DomUtil.innerError(this.packageIdentifier, "");
228
229 return true;
230 }
231
232 /**
233 * Returns `true` if the given version is valid. Otherwise `false` is returned and an error message is shown.
234 */
235 protected validateVersion(versionElement: HTMLInputElement): boolean {
236 const version = versionElement.value;
237
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"));
243
244 return false;
245 }
246
247 if (!AbstractPackageList.versionRegExp.test(version)) {
248 DomUtil.innerError(versionElement, Language.get("wcf.acp.devtools.project.packageVersion.error.format"));
249
250 return false;
251 }
252 }
253
254 // remove outdated errors
255 DomUtil.innerError(versionElement, "");
256
257 return true;
258 }
259 }
260
261 Core.enableLegacyInheritance(AbstractPackageList);
262
263 export = AbstractPackageList;