94403ad4ff6826bbebae80a7c49e63a164f77db8
[GitHub/WoltLab/WCF.git] /
1 /**
2 * Abstract implementation of the JavaScript component of a form field handling
3 * a list of packages.
4 *
5 * @author Matthias Schmidt
6 * @copyright 2001-2019 WoltLab GmbH
7 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
8 * @module WoltLabSuite/Core/Acp/Form/Builder/Field/Devtools/Project/AbstractPackageList
9 * @since 5.2
10 */
11 define(['Dom/ChangeListener', 'Dom/Traverse', 'Dom/Util', 'EventKey', 'Language'], function (DomChangeListener, DomTraverse, DomUtil, EventKey, Language) {
12 "use strict";
13 /**
14 * @constructor
15 */
16 function AbstractPackageList(formFieldId, existingPackages) {
17 this.init(formFieldId, existingPackages);
18 }
19 ;
20 AbstractPackageList.prototype = {
21 /**
22 * Initializes the package list handler.
23 *
24 * @param {string} formFieldId id of the associated form field
25 * @param {object[]} existingPackages data of existing packages
26 */
27 init: function (formFieldId, existingPackages) {
28 this._formFieldId = formFieldId;
29 this._packageList = elById(this._formFieldId + '_packageList');
30 if (this._packageList === null) {
31 throw new Error("Cannot find package list for packages field with id '" + this._formFieldId + "'.");
32 }
33 this._packageIdentifier = elById(this._formFieldId + '_packageIdentifier');
34 if (this._packageIdentifier === null) {
35 throw new Error("Cannot find package identifier form field for packages field with id '" + this._formFieldId + "'.");
36 }
37 this._packageIdentifier.addEventListener('keypress', this._keyPress.bind(this));
38 this._addButton = elById(this._formFieldId + '_addButton');
39 if (this._addButton === null) {
40 throw new Error("Cannot find add button for packages field with id '" + this._formFieldId + "'.");
41 }
42 this._addButton.addEventListener('click', this._addPackage.bind(this));
43 this._form = this._packageList.closest('form');
44 if (this._form === null) {
45 throw new Error("Cannot find form element for packages field with id '" + this._formFieldId + "'.");
46 }
47 this._form.addEventListener('submit', this._submit.bind(this));
48 existingPackages.forEach(this._addPackageByData.bind(this));
49 },
50 /**
51 * Adds a package to the package list as a consequence of the given
52 * event. If the package data is invalid, an error message is shown
53 * and no package is added.
54 *
55 * @param {Event} event event that triggered trying to add the package
56 */
57 _addPackage: function (event) {
58 event.preventDefault();
59 event.stopPropagation();
60 // validate data
61 if (!this._validateInput()) {
62 return;
63 }
64 this._addPackageByData(this._getInputData());
65 // empty fields
66 this._emptyInput();
67 this._packageIdentifier.focus();
68 },
69 /**
70 * Adds a package to the package list using the given package data.
71 *
72 * @param {object} packageData
73 */
74 _addPackageByData: function (packageData) {
75 // add package to list
76 var listItem = elCreate('li');
77 this._populateListItem(listItem, packageData);
78 // add delete button
79 var deleteButton = elCreate('span');
80 deleteButton.className = 'icon icon16 fa-times pointer jsTooltip';
81 elAttr(deleteButton, 'title', Language.get('wcf.global.button.delete'));
82 deleteButton.addEventListener('click', this._removePackage.bind(this));
83 DomUtil.prepend(deleteButton, listItem);
84 this._packageList.appendChild(listItem);
85 DomChangeListener.trigger();
86 },
87 /**
88 * Creates the hidden fields when the form is submitted.
89 *
90 * @param {HTMLElement} listElement package list element from the package list
91 * @param {int} index package index
92 */
93 _createSubmitFields: function (listElement, index) {
94 var packageIdentifier = elCreate('input');
95 elAttr(packageIdentifier, 'type', 'hidden');
96 elAttr(packageIdentifier, 'name', this._formFieldId + '[' + index + '][packageIdentifier]');
97 packageIdentifier.value = elData(listElement, 'package-identifier');
98 this._form.appendChild(packageIdentifier);
99 },
100 /**
101 * Empties the input fields.
102 */
103 _emptyInput: function () {
104 this._packageIdentifier.value = '';
105 },
106 /**
107 * Returns the error element for the given form field element.
108 * If `createIfNonExistent` is not given or `false`, `null` is returned
109 * if there is no error element, otherwise an empty error element
110 * is created and returned.
111 *
112 * @param {?boolean} createIfNonExistent
113 * @return {?HTMLElement}
114 */
115 _getErrorElement: function (element, createIfNoNExistent) {
116 var error = DomTraverse.nextByClass(element, 'innerError');
117 if (error === null && createIfNoNExistent) {
118 error = elCreate('small');
119 error.className = 'innerError';
120 DomUtil.insertAfter(error, element);
121 }
122 return error;
123 },
124 /**
125 * Returns the current data of the input fields to add a new package.
126 *
127 * @return {object}
128 */
129 _getInputData: function () {
130 return {
131 packageIdentifier: this._packageIdentifier.value
132 };
133 },
134 /**
135 * Returns the error element for the package identifier form field.
136 * If `createIfNonExistent` is not given or `false`, `null` is returned
137 * if there is no error element, otherwise an empty error element
138 * is created and returned.
139 *
140 * @param {?boolean} createIfNonExistent
141 * @return {?HTMLElement}
142 */
143 _getPackageIdentifierErrorElement: function (createIfNonExistent) {
144 return this._getErrorElement(this._packageIdentifier, createIfNonExistent);
145 },
146 /**
147 * Adds a package to the package list after pressing ENTER in a
148 * text field.
149 *
150 * @param {Event} event
151 */
152 _keyPress: function (event) {
153 if (EventKey.Enter(event)) {
154 this._addPackage(event);
155 }
156 },
157 /**
158 * Adds all necessary package-relavant data to the given list item.
159 *
160 * @param {HTMLElement} listItem package list element holding package data
161 * @param {object} packageData package data
162 */
163 _populateListItem: function (listItem, packageData) {
164 elData(listItem, 'package-identifier', packageData.packageIdentifier);
165 },
166 /**
167 * Removes a package by clicking on its delete button.
168 *
169 * @param {Event} event delete button click event
170 */
171 _removePackage: function (event) {
172 elRemove(event.currentTarget.closest('li'));
173 // remove field errors if the last package has been deleted
174 if (!this._packageList.childElementCount &&
175 this._packageList.nextElementSibling.tagName === 'SMALL' &&
176 this._packageList.nextElementSibling.classList.contains('innerError')) {
177 elRemove(this._packageList.nextElementSibling);
178 }
179 },
180 /**
181 * Adds all necessary (hidden) form fields to the form when
182 * submitting the form.
183 */
184 _submit: function () {
185 DomTraverse.childrenByTag(this._packageList, 'LI').forEach(this._createSubmitFields.bind(this));
186 },
187 /**
188 * Returns `true` if the currently entered package data is valid.
189 * Otherwise `false` is returned and relevant error messages are
190 * shown.
191 *
192 * @return {boolean}
193 */
194 _validateInput: function () {
195 return this._validatePackageIdentifier();
196 },
197 /**
198 * Returns `true` if the currently entered package identifier is
199 * valid. Otherwise `false` is returned and an error message is
200 * shown.
201 *
202 * @return {boolean}
203 */
204 _validatePackageIdentifier: function () {
205 var packageIdentifier = this._packageIdentifier.value;
206 if (packageIdentifier === '') {
207 this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.global.form.error.empty');
208 return false;
209 }
210 if (packageIdentifier.length < 3) {
211 this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.minimumLength');
212 return false;
213 }
214 else if (packageIdentifier.length > 191) {
215 this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.maximumLength');
216 return false;
217 }
218 // see `wcf\data\package\Package::isValidPackageName()`
219 if (!packageIdentifier.match(/^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/)) {
220 this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.format');
221 return false;
222 }
223 // check if package has already been added
224 var duplicate = false;
225 DomTraverse.childrenByTag(this._packageList, 'LI').forEach(function (listItem, index) {
226 if (elData(listItem, 'package-identifier') === packageIdentifier) {
227 duplicate = true;
228 }
229 });
230 if (duplicate) {
231 this._getPackageIdentifierErrorElement(true).textContent = Language.get('wcf.acp.devtools.project.packageIdentifier.error.duplicate');
232 return false;
233 }
234 // remove outdated errors
235 var error = this._getPackageIdentifierErrorElement();
236 if (error !== null) {
237 elRemove(error);
238 }
239 return true;
240 },
241 /**
242 * Returns `true` if the given version is valid. Otherwise `false`
243 * is returned and an error message is shown.
244 *
245 * @param {string} version validated version
246 * @param {function} versionErrorGetter returns the version error element
247 * @return {boolean}
248 */
249 _validateVersion: function (version, versionErrorGetter) {
250 // see `wcf\data\package\Package::isValidVersion()`
251 // the version is no a required attribute
252 if (version !== '') {
253 if (version.length > 255) {
254 versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.maximumLength');
255 return false;
256 }
257 // see `wcf\data\package\Package::isValidVersion()`
258 if (!version.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(\ (a|alpha|b|beta|d|dev|rc|pl)\ ([0-9]+))?$/i)) {
259 versionErrorGetter(true).textContent = Language.get('wcf.acp.devtools.project.packageVersion.error.format');
260 return false;
261 }
262 }
263 // remove outdated errors
264 var error = versionErrorGetter();
265 if (error !== null) {
266 elRemove(error);
267 }
268 return true;
269 }
270 };
271 return AbstractPackageList;
272 });