Commit | Line | Data |
---|---|---|
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 | 8 | define(["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 | }); |