Merge pull request #5989 from WoltLab/wsc-rpc-api-const
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / js / WoltLabSuite / Core / Language / Input.js
CommitLineData
dbd9f599
AE
1/**
2 * I18n interface for input and textarea fields.
50aa3a01 3 *
2cd1e7d3 4 * @author Alexander Ebert
092eba81 5 * @copyright 2001-2019 WoltLab GmbH
2cd1e7d3 6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
dbd9f599 7 */
092eba81 8define(["require", "exports", "tslib", "../Dom/Util", "../Language", "../Ui/Dropdown/Simple", "../StringUtil"], function (require, exports, tslib_1, Util_1, Language, Simple_1, StringUtil) {
50aa3a01 9 "use strict";
092eba81
AE
10 Object.defineProperty(exports, "__esModule", { value: true });
11 exports.validate = exports.isEnabled = exports.enable = exports.disable = exports.setValues = exports.getValues = exports.unregister = exports.registerCallback = exports.init = void 0;
716617cf
TD
12 Util_1 = tslib_1.__importDefault(Util_1);
13 Language = tslib_1.__importStar(Language);
14 Simple_1 = tslib_1.__importDefault(Simple_1);
15 StringUtil = tslib_1.__importStar(StringUtil);
092eba81
AE
16 const _elements = new Map();
17 const _forms = new WeakMap();
18 const _values = new Map();
50aa3a01 19 /**
092eba81 20 * Sets up DOM and event listeners for an input field.
50aa3a01 21 */
092eba81
AE
22 function initElement(elementId, element, values, availableLanguages, forceSelection) {
23 let container = element.parentElement;
24 if (!container.classList.contains("inputAddon")) {
25 container = document.createElement("div");
26 container.className = "inputAddon";
27 if (element.nodeName === "TEXTAREA") {
28 container.classList.add("inputAddonTextarea");
50aa3a01 29 }
092eba81
AE
30 container.dataset.inputId = elementId;
31 const hasFocus = document.activeElement === element;
32 // DOM manipulation causes focused element to lose focus
33 element.insertAdjacentElement("beforebegin", container);
34 container.appendChild(element);
35 if (hasFocus) {
36 element.focus();
50aa3a01 37 }
092eba81
AE
38 }
39 container.classList.add("dropdown");
40 const button = document.createElement("span");
41 button.className = "button dropdownToggle inputPrefix";
42 const buttonLabel = document.createElement("span");
43 buttonLabel.textContent = Language.get("wcf.global.button.disabledI18n");
44 button.appendChild(buttonLabel);
45 container.insertBefore(button, element);
46 const dropdownMenu = document.createElement("ul");
47 dropdownMenu.className = "dropdownMenu";
48 button.insertAdjacentElement("afterend", dropdownMenu);
49 const callbackClick = (event) => {
50 let target;
51 if (event instanceof HTMLElement) {
52 target = event;
50aa3a01 53 }
092eba81
AE
54 else {
55 target = event.currentTarget;
50aa3a01 56 }
092eba81
AE
57 const languageId = ~~target.dataset.languageId;
58 const activeItem = dropdownMenu.querySelector(".active");
59 if (activeItem !== null) {
60 activeItem.classList.remove("active");
50aa3a01 61 }
092eba81
AE
62 if (languageId) {
63 target.classList.add("active");
50aa3a01 64 }
092eba81
AE
65 const isInit = event instanceof HTMLElement;
66 select(elementId, languageId, isInit);
67 };
68 // build language dropdown
69 Object.entries(availableLanguages).forEach(([languageId, languageName]) => {
70 const listItem = document.createElement("li");
71 listItem.dataset.languageId = languageId;
72 const span = document.createElement("span");
73 span.textContent = languageName;
74 listItem.appendChild(span);
75 listItem.addEventListener("click", callbackClick);
76 dropdownMenu.appendChild(listItem);
77 });
78 if (!forceSelection) {
79 const divider = document.createElement("li");
80 divider.className = "dropdownDivider";
81 dropdownMenu.appendChild(divider);
82 const listItem = document.createElement("li");
83 listItem.dataset.languageId = "0";
84 listItem.addEventListener("click", callbackClick);
85 const span = document.createElement("span");
86 span.textContent = Language.get("wcf.global.button.disabledI18n");
87 listItem.appendChild(span);
88 dropdownMenu.appendChild(listItem);
89 }
90 let activeItem = undefined;
91 if (forceSelection || values.size) {
92 activeItem = Array.from(dropdownMenu.children).find((element) => {
93 return +element.dataset.languageId === window.LANGUAGE_ID;
50aa3a01 94 });
092eba81
AE
95 }
96 Simple_1.default.init(button);
97 Simple_1.default.registerCallback(container.id, dropdownToggle);
98 _elements.set(elementId, {
99 buttonLabel,
100 callbacks: new Map(),
101 element,
102 languageId: 0,
103 isEnabled: true,
104 forceSelection,
105 });
106 // bind to submit event
107 const form = element.closest("form");
108 if (form !== null) {
109 form.addEventListener("submit", submit);
110 let elementIds = _forms.get(form);
111 if (elementIds === undefined) {
112 elementIds = [];
113 _forms.set(form, elementIds);
50aa3a01 114 }
092eba81
AE
115 elementIds.push(elementId);
116 }
117 if (activeItem) {
118 callbackClick(activeItem);
119 }
120 }
121 /**
122 * Selects a language or non-i18n from the dropdown list.
123 */
124 function select(elementId, languageId, isInit) {
125 const data = _elements.get(elementId);
126 const dropdownMenu = Simple_1.default.getDropdownMenu(data.element.closest(".inputAddon").id);
127 const item = dropdownMenu.querySelector(`[data-language-id="${languageId}"]`);
128 const label = item ? item.textContent : "";
129 // save current value
130 if (data.languageId !== languageId) {
131 const values = _values.get(elementId);
132 if (data.languageId) {
3859b5e6
AE
133 const beforeSelect = data.callbacks.get("beforeSelect");
134 if (beforeSelect) {
135 beforeSelect(data.element);
136 }
092eba81 137 values.set(data.languageId, data.element.value);
50aa3a01 138 }
092eba81
AE
139 if (languageId === 0) {
140 _values.set(elementId, new Map());
50aa3a01 141 }
092eba81
AE
142 else if (data.buttonLabel.classList.contains("active") || isInit) {
143 data.element.value = values.get(languageId) || "";
144 }
145 // update label
146 data.buttonLabel.textContent = label;
2b2ad7a7 147 data.buttonLabel.querySelector("fa-icon")?.remove();
ec7b3be2
AE
148 if (languageId) {
149 data.buttonLabel.classList.add("active");
150 const icon = document.createElement("fa-icon");
ec7b3be2
AE
151 icon.setIcon("caret-down", true);
152 data.buttonLabel.append(icon);
153 }
154 else {
155 data.buttonLabel.classList.remove("active");
156 }
092eba81
AE
157 data.languageId = languageId;
158 }
159 if (!isInit) {
160 data.element.blur();
161 data.element.focus();
162 }
163 if (data.callbacks.has("select")) {
164 data.callbacks.get("select")(data.element);
165 }
166 }
167 /**
168 * Callback for dropdowns being opened, flags items with a missing value for one or more languages.
169 */
170 function dropdownToggle(containerId, action) {
171 if (action !== "open") {
172 return;
173 }
174 const dropdownMenu = Simple_1.default.getDropdownMenu(containerId);
175 const container = document.getElementById(containerId);
176 const elementId = container.dataset.inputId;
177 const data = _elements.get(elementId);
178 const values = _values.get(elementId);
179 Array.from(dropdownMenu.children).forEach((item) => {
180 const languageId = ~~(item.dataset.languageId || "");
181 if (languageId) {
182 let hasMissingValue = false;
50aa3a01 183 if (data.languageId) {
092eba81
AE
184 if (languageId === data.languageId) {
185 hasMissingValue = data.element.value.trim() === "";
186 }
187 else {
188 hasMissingValue = !values.get(languageId);
189 }
50aa3a01 190 }
ec7b3be2 191 const span = item.querySelector("span");
2b2ad7a7 192 span.querySelector("fa-icon")?.remove();
092eba81
AE
193 if (hasMissingValue) {
194 item.classList.add("missingValue");
ec7b3be2 195 const icon = document.createElement("fa-icon");
ec7b3be2
AE
196 icon.setIcon("triangle-exclamation");
197 span.append(icon);
50aa3a01 198 }
092eba81
AE
199 else {
200 item.classList.remove("missingValue");
50aa3a01 201 }
50aa3a01 202 }
092eba81
AE
203 });
204 }
205 /**
206 * Inserts hidden fields for i18n input on submit.
207 */
208 function submit(event) {
209 const form = event.currentTarget;
210 const elementIds = _forms.get(form);
211 elementIds.forEach((elementId) => {
212 const data = _elements.get(elementId);
213 if (!data.isEnabled) {
50aa3a01
TD
214 return;
215 }
092eba81
AE
216 const values = _values.get(elementId);
217 if (data.callbacks.has("submit")) {
218 data.callbacks.get("submit")(data.element);
50aa3a01 219 }
50aa3a01 220 // update with current value
092eba81
AE
221 if (data.languageId) {
222 values.set(data.languageId, data.element.value);
50aa3a01 223 }
092eba81
AE
224 if (values.size) {
225 values.forEach(function (value, languageId) {
226 const input = document.createElement("input");
227 input.type = "hidden";
228 input.name = `${elementId}_i18n[${languageId}]`;
229 input.value = value;
230 form.appendChild(input);
231 });
232 // remove name attribute to enforce i18n values
233 data.element.removeAttribute("name");
50aa3a01 234 }
092eba81
AE
235 });
236 }
237 /**
238 * Initializes an input field.
239 */
240 function init(elementId, values, availableLanguages, forceSelection) {
241 if (_values.has(elementId)) {
242 return;
243 }
244 const element = document.getElementById(elementId);
245 if (element === null) {
246 throw new Error(`Expected a valid element id, cannot find '${elementId}'.`);
247 }
248 // unescape values
249 const unescapedValues = new Map();
250 Object.entries(values).forEach(([languageId, value]) => {
251 unescapedValues.set(+languageId, StringUtil.unescapeHTML(value));
252 });
253 _values.set(elementId, unescapedValues);
254 initElement(elementId, element, unescapedValues, availableLanguages, forceSelection);
255 }
256 exports.init = init;
257 /**
258 * Registers a callback for an element.
259 */
260 function registerCallback(elementId, eventName, callback) {
261 if (!_values.has(elementId)) {
262 throw new Error(`Unknown element id '${elementId}'.`);
263 }
264 _elements.get(elementId).callbacks.set(eventName, callback);
265 }
266 exports.registerCallback = registerCallback;
267 /**
268 * Unregisters the element with the given id.
269 *
270 * @since 5.2
271 */
272 function unregister(elementId) {
273 if (!_values.has(elementId)) {
274 throw new Error(`Unknown element id '${elementId}'.`);
275 }
276 _values.delete(elementId);
277 _elements.delete(elementId);
278 }
279 exports.unregister = unregister;
280 /**
281 * Returns the values of an input field.
282 */
283 function getValues(elementId) {
284 const element = _elements.get(elementId);
285 if (element === undefined) {
286 throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`);
287 }
288 const values = _values.get(elementId);
289 // update with current value
290 values.set(element.languageId, element.element.value);
291 return values;
292 }
293 exports.getValues = getValues;
294 /**
295 * Sets the values of an input field.
296 */
297 function setValues(elementId, newValues) {
298 const element = _elements.get(elementId);
299 if (element === undefined) {
300 throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`);
301 }
302 element.element.value = "";
303 const values = new Map(Object.entries(newValues).map(([languageId, value]) => {
304 return [+languageId, value];
305 }));
306 if (values.has(0)) {
307 element.element.value = values.get(0);
308 values.delete(0);
50aa3a01 309 _values.set(elementId, values);
092eba81
AE
310 select(elementId, 0, true);
311 return;
312 }
313 _values.set(elementId, values);
314 element.languageId = 0;
315 select(elementId, window.LANGUAGE_ID, true);
316 }
317 exports.setValues = setValues;
318 /**
319 * Disables the i18n interface for an input field.
320 */
321 function disable(elementId) {
322 const element = _elements.get(elementId);
323 if (element === undefined) {
324 throw new Error(`Expected a valid element, '${elementId}' is not an i18n input field.`);
325 }
326 if (!element.isEnabled) {
327 return;
328 }
329 element.isEnabled = false;
330 // hide language dropdown
331 const buttonContainer = element.buttonLabel.parentElement;
332 Util_1.default.hide(buttonContainer);
333 const dropdownContainer = buttonContainer.parentElement;
334 dropdownContainer.classList.remove("inputAddon", "dropdown");
335 }
336 exports.disable = disable;
337 /**
338 * Enables the i18n interface for an input field.
339 */
340 function enable(elementId) {
341 const element = _elements.get(elementId);
342 if (element === undefined) {
343 throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`);
344 }
345 if (element.isEnabled) {
346 return;
347 }
348 element.isEnabled = true;
349 // show language dropdown
350 const buttonContainer = element.buttonLabel.parentElement;
351 Util_1.default.show(buttonContainer);
352 const dropdownContainer = buttonContainer.parentElement;
353 dropdownContainer.classList.add("inputAddon", "dropdown");
354 }
355 exports.enable = enable;
356 /**
357 * Returns true if i18n input is enabled for an input field.
358 */
359 function isEnabled(elementId) {
360 const element = _elements.get(elementId);
361 if (element === undefined) {
362 throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`);
363 }
364 return element.isEnabled;
365 }
366 exports.isEnabled = isEnabled;
367 /**
368 * Returns true if the value of an i18n input field is valid.
369 *
370 * If the element is disabled, true is returned.
371 */
372 function validate(elementId, permitEmptyValue) {
373 const element = _elements.get(elementId);
374 if (element === undefined) {
375 throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`);
376 }
377 if (!element.isEnabled) {
378 return true;
379 }
380 const values = _values.get(elementId);
381 const dropdownMenu = Simple_1.default.getDropdownMenu(element.element.parentElement.id);
382 if (element.languageId) {
383 values.set(element.languageId, element.element.value);
384 }
385 let hasEmptyValue = false;
386 let hasNonEmptyValue = false;
387 Array.from(dropdownMenu.children).forEach((item) => {
388 const languageId = ~~item.dataset.languageId;
389 if (languageId) {
390 if (!values.has(languageId) || values.get(languageId).length === 0) {
391 // input has non-empty value for previously checked language
392 if (hasNonEmptyValue) {
393 return false;
50aa3a01 394 }
092eba81
AE
395 hasEmptyValue = true;
396 }
397 else {
398 // input has empty value for previously checked language
399 if (hasEmptyValue) {
400 return false;
50aa3a01 401 }
092eba81 402 hasNonEmptyValue = true;
50aa3a01
TD
403 }
404 }
092eba81
AE
405 });
406 return !hasEmptyValue || permitEmptyValue;
407 }
408 exports.validate = validate;
dbd9f599 409});