Commit | Line | Data |
---|---|---|
dbd9f599 AE |
1 | /** |
2 | * I18n interface for input and textarea fields. | |
3 | * | |
2cd1e7d3 | 4 | * @author Alexander Ebert |
c839bd49 | 5 | * @copyright 2001-2018 WoltLab GmbH |
2cd1e7d3 AE |
6 | * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php> |
7 | * @module WoltLabSuite/Core/Language/Input | |
dbd9f599 | 8 | */ |
c33eb992 | 9 | define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Core, Dictionary, Language, ObjectMap, StringUtil, DomTraverse, DomUtil, UiSimpleDropdown) { |
dbd9f599 AE |
10 | "use strict"; |
11 | ||
12 | var _elements = new Dictionary(); | |
13 | var _didInit = false; | |
14 | var _forms = new ObjectMap(); | |
15 | var _values = new Dictionary(); | |
16 | ||
17 | var _callbackDropdownToggle = null; | |
18 | var _callbackSubmit = null; | |
19 | ||
20 | /** | |
2cd1e7d3 | 21 | * @exports WoltLabSuite/Core/Language/Input |
dbd9f599 | 22 | */ |
2cd1e7d3 | 23 | return { |
dbd9f599 AE |
24 | /** |
25 | * Initializes an input field. | |
26 | * | |
2cd1e7d3 AE |
27 | * @param {string} elementId input element id |
28 | * @param {Object} values preset values per language id | |
29 | * @param {Object} availableLanguages language names per language id | |
30 | * @param {boolean} forceSelection require i18n input | |
dbd9f599 AE |
31 | */ |
32 | init: function(elementId, values, availableLanguages, forceSelection) { | |
33 | if (_values.has(elementId)) { | |
34 | return; | |
35 | } | |
36 | ||
d0023381 | 37 | var element = elById(elementId); |
dbd9f599 AE |
38 | if (element === null) { |
39 | throw new Error("Expected a valid element id, cannot find '" + elementId + "'."); | |
40 | } | |
41 | ||
42 | this._setup(); | |
43 | ||
44 | // unescape values | |
45 | var unescapedValues = new Dictionary(); | |
46 | for (var key in values) { | |
2cd1e7d3 | 47 | if (values.hasOwnProperty(key)) { |
dbd9f599 AE |
48 | unescapedValues.set(~~key, StringUtil.unescapeHTML(values[key])); |
49 | } | |
50 | } | |
51 | ||
52 | _values.set(elementId, unescapedValues); | |
53 | ||
54 | this._initElement(elementId, element, unescapedValues, availableLanguages, forceSelection); | |
55 | }, | |
56 | ||
e1b30522 AE |
57 | /** |
58 | * Registers a callback for an element. | |
59 | * | |
60 | * @param {string} elementId | |
61 | * @param {string} eventName | |
62 | * @param {function} callback | |
63 | */ | |
64 | registerCallback: function (elementId, eventName, callback) { | |
65 | if (!_values.has(elementId)) { | |
66 | throw new Error("Unknown element id '" + elementId + "'."); | |
67 | } | |
68 | ||
69 | _elements.get(elementId).callbacks.set(eventName, callback); | |
70 | }, | |
71 | ||
dbd9f599 AE |
72 | /** |
73 | * Caches common event listener callbacks. | |
74 | */ | |
75 | _setup: function() { | |
76 | if (_didInit) return; | |
77 | _didInit = true; | |
78 | ||
79 | _callbackDropdownToggle = this._dropdownToggle.bind(this); | |
80 | _callbackSubmit = this._submit.bind(this); | |
81 | }, | |
82 | ||
83 | /** | |
84 | * Sets up DOM and event listeners for an input field. | |
85 | * | |
2cd1e7d3 AE |
86 | * @param {string} elementId input element id |
87 | * @param {Element} element input or textarea element | |
88 | * @param {Dictionary} values preset values per language id | |
89 | * @param {Object} availableLanguages language names per language id | |
90 | * @param {boolean} forceSelection require i18n input | |
dbd9f599 AE |
91 | */ |
92 | _initElement: function(elementId, element, values, availableLanguages, forceSelection) { | |
93 | var container = element.parentNode; | |
94 | if (!container.classList.contains('inputAddon')) { | |
d0023381 | 95 | container = elCreate('div'); |
dbd9f599 | 96 | container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : ''); |
2cd1e7d3 | 97 | //noinspection JSCheckFunctionSignatures |
3c2b7e9b | 98 | elData(container, 'input-id', elementId); |
dbd9f599 AE |
99 | |
100 | element.parentNode.insertBefore(container, element); | |
101 | container.appendChild(element); | |
102 | } | |
103 | ||
104 | container.classList.add('dropdown'); | |
d0023381 | 105 | var button = elCreate('span'); |
dbd9f599 AE |
106 | button.className = 'button dropdownToggle inputPrefix'; |
107 | ||
d0023381 | 108 | var span = elCreate('span'); |
dbd9f599 AE |
109 | span.textContent = Language.get('wcf.global.button.disabledI18n'); |
110 | ||
111 | button.appendChild(span); | |
112 | container.insertBefore(button, element); | |
113 | ||
d0023381 | 114 | var dropdownMenu = elCreate('ul'); |
dbd9f599 | 115 | dropdownMenu.className = 'dropdownMenu'; |
9a421cc7 | 116 | DomUtil.insertAfter(dropdownMenu, button); |
dbd9f599 AE |
117 | |
118 | var callbackClick = (function(event, isInit) { | |
f5336f4f | 119 | var languageId = ~~elData(event.currentTarget, 'language-id'); |
dbd9f599 | 120 | |
9a421cc7 | 121 | var activeItem = DomTraverse.childByClass(dropdownMenu, 'active'); |
dbd9f599 AE |
122 | if (activeItem !== null) activeItem.classList.remove('active'); |
123 | ||
124 | if (languageId) event.currentTarget.classList.add('active'); | |
125 | ||
c33eb992 | 126 | this._select(elementId, languageId, isInit || false); |
dbd9f599 AE |
127 | }).bind(this); |
128 | ||
129 | // build language dropdown | |
2cd1e7d3 | 130 | var listItem; |
dbd9f599 | 131 | for (var languageId in availableLanguages) { |
2cd1e7d3 AE |
132 | if (availableLanguages.hasOwnProperty(languageId)) { |
133 | listItem = elCreate('li'); | |
3c2b7e9b | 134 | elData(listItem, 'language-id', languageId); |
dbd9f599 | 135 | |
d0023381 | 136 | span = elCreate('span'); |
dbd9f599 AE |
137 | span.textContent = availableLanguages[languageId]; |
138 | ||
139 | listItem.appendChild(span); | |
ac188fc5 | 140 | listItem.addEventListener(WCF_CLICK_EVENT, callbackClick); |
dbd9f599 AE |
141 | dropdownMenu.appendChild(listItem); |
142 | } | |
143 | } | |
144 | ||
145 | if (forceSelection !== true) { | |
2cd1e7d3 | 146 | listItem = elCreate('li'); |
dbd9f599 | 147 | listItem.className = 'dropdownDivider'; |
dbd9f599 AE |
148 | dropdownMenu.appendChild(listItem); |
149 | ||
d0023381 | 150 | listItem = elCreate('li'); |
3c2b7e9b | 151 | elData(listItem, 'language-id', 0); |
d0023381 | 152 | span = elCreate('span'); |
dbd9f599 AE |
153 | span.textContent = Language.get('wcf.global.button.disabledI18n'); |
154 | listItem.appendChild(span); | |
ac188fc5 | 155 | listItem.addEventListener(WCF_CLICK_EVENT, callbackClick); |
dbd9f599 AE |
156 | dropdownMenu.appendChild(listItem); |
157 | } | |
158 | ||
159 | var activeItem = null; | |
160 | if (forceSelection === true || values.size) { | |
161 | for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { | |
2cd1e7d3 | 162 | //noinspection JSUnresolvedVariable |
0c677c2d | 163 | if (~~elData(dropdownMenu.children[i], 'language-id') === LANGUAGE_ID) { |
dbd9f599 AE |
164 | activeItem = dropdownMenu.children[i]; |
165 | break; | |
166 | } | |
167 | } | |
168 | } | |
169 | ||
9a421cc7 AE |
170 | UiSimpleDropdown.init(button); |
171 | UiSimpleDropdown.registerCallback(container.id, _callbackDropdownToggle); | |
dbd9f599 AE |
172 | |
173 | _elements.set(elementId, { | |
174 | buttonLabel: button.children[0], | |
e1b30522 | 175 | callbacks: new Dictionary(), |
dbd9f599 | 176 | element: element, |
c33eb992 MS |
177 | languageId: 0, |
178 | isEnabled: true, | |
179 | forceSelection: forceSelection | |
dbd9f599 AE |
180 | }); |
181 | ||
182 | // bind to submit event | |
9a421cc7 | 183 | var submit = DomTraverse.parentByTag(element, 'FORM'); |
dbd9f599 AE |
184 | if (submit !== null) { |
185 | submit.addEventListener('submit', _callbackSubmit); | |
186 | ||
187 | var elementIds = _forms.get(submit); | |
188 | if (elementIds === undefined) { | |
189 | elementIds = []; | |
190 | _forms.set(submit, elementIds); | |
191 | } | |
192 | ||
193 | elementIds.push(elementId); | |
194 | } | |
195 | ||
196 | if (activeItem !== null) { | |
197 | callbackClick({ currentTarget: activeItem }, true); | |
198 | } | |
199 | }, | |
200 | ||
201 | /** | |
202 | * Selects a language or non-i18n from the dropdown list. | |
203 | * | |
2cd1e7d3 AE |
204 | * @param {string} elementId input element id |
205 | * @param {int} languageId language id or `0` to disable i18n | |
206 | * @param {boolean} isInit triggers pre-selection on init | |
dbd9f599 | 207 | */ |
c33eb992 | 208 | _select: function(elementId, languageId, isInit) { |
dbd9f599 AE |
209 | var data = _elements.get(elementId); |
210 | ||
e1b30522 | 211 | var dropdownMenu = UiSimpleDropdown.getDropdownMenu(data.element.closest('.inputAddon').id); |
c33eb992 MS |
212 | var item, label = ''; |
213 | for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { | |
214 | item = dropdownMenu.children[i]; | |
215 | ||
3c2b7e9b | 216 | var itemLanguageId = elData(item, 'language-id'); |
c33eb992 MS |
217 | if (itemLanguageId.length && languageId === ~~itemLanguageId) { |
218 | label = item.children[0].textContent; | |
219 | } | |
220 | } | |
221 | ||
dbd9f599 AE |
222 | // save current value |
223 | if (data.languageId !== languageId) { | |
224 | var values = _values.get(elementId); | |
225 | ||
226 | if (data.languageId) { | |
227 | values.set(data.languageId, data.element.value); | |
228 | } | |
229 | ||
230 | if (languageId === 0) { | |
231 | _values.set(elementId, new Dictionary()); | |
232 | } | |
233 | else if (data.buttonLabel.classList.contains('active') || isInit === true) { | |
234 | data.element.value = (values.has(languageId)) ? values.get(languageId) : ''; | |
235 | } | |
236 | ||
237 | // update label | |
238 | data.buttonLabel.textContent = label; | |
239 | data.buttonLabel.classList[(languageId ? 'add' : 'remove')]('active'); | |
240 | ||
241 | data.languageId = languageId; | |
242 | } | |
243 | ||
e1bd11ed AE |
244 | if (!isInit) { |
245 | data.element.blur(); | |
246 | data.element.focus(); | |
247 | } | |
e1b30522 AE |
248 | |
249 | if (data.callbacks.has('select')) { | |
250 | data.callbacks.get('select')(data.element); | |
251 | } | |
dbd9f599 AE |
252 | }, |
253 | ||
254 | /** | |
255 | * Callback for dropdowns being opened, flags items with a missing value for one or more languages. | |
256 | * | |
2cd1e7d3 AE |
257 | * @param {string} containerId dropdown container id |
258 | * @param {string} action toggle action, can be `open` or `close` | |
dbd9f599 AE |
259 | */ |
260 | _dropdownToggle: function(containerId, action) { | |
261 | if (action !== 'open') { | |
262 | return; | |
263 | } | |
264 | ||
9a421cc7 | 265 | var dropdownMenu = UiSimpleDropdown.getDropdownMenu(containerId); |
f5336f4f | 266 | var elementId = elData(elById(containerId), 'input-id'); |
48c945aa | 267 | var data = _elements.get(elementId); |
dbd9f599 AE |
268 | var values = _values.get(elementId); |
269 | ||
270 | var item, languageId; | |
271 | for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { | |
272 | item = dropdownMenu.children[i]; | |
f5336f4f | 273 | languageId = ~~elData(item, 'language-id'); |
dbd9f599 AE |
274 | |
275 | if (languageId) { | |
48c945aa AE |
276 | var hasMissingValue = false; |
277 | if (data.languageId) { | |
278 | if (languageId === data.languageId) { | |
279 | hasMissingValue = (data.element.value.trim() === ''); | |
280 | } | |
281 | else { | |
282 | hasMissingValue = (!values.get(languageId)); | |
283 | } | |
284 | } | |
285 | ||
286 | item.classList[(hasMissingValue ? 'add' : 'remove')]('missingValue'); | |
dbd9f599 AE |
287 | } |
288 | } | |
289 | }, | |
290 | ||
291 | /** | |
292 | * Inserts hidden fields for i18n input on submit. | |
293 | * | |
2cd1e7d3 | 294 | * @param {Object} event event object |
dbd9f599 AE |
295 | */ |
296 | _submit: function(event) { | |
297 | var elementIds = _forms.get(event.currentTarget); | |
298 | ||
299 | var data, elementId, input, values; | |
300 | for (var i = 0, length = elementIds.length; i < length; i++) { | |
301 | elementId = elementIds[i]; | |
302 | data = _elements.get(elementId); | |
c33eb992 MS |
303 | if (data.isEnabled) { |
304 | values = _values.get(elementId); | |
305 | ||
e1b30522 AE |
306 | if (data.callbacks.has('submit')) { |
307 | data.callbacks.get('submit')(data.element); | |
308 | } | |
309 | ||
c33eb992 MS |
310 | // update with current value |
311 | if (data.languageId) { | |
312 | values.set(data.languageId, data.element.value); | |
313 | } | |
314 | ||
315 | if (values.size) { | |
316 | values.forEach(function(value, languageId) { | |
317 | input = elCreate('input'); | |
318 | input.type = 'hidden'; | |
319 | input.name = elementId + '_i18n[' + languageId + ']'; | |
320 | input.value = value; | |
321 | ||
322 | event.currentTarget.appendChild(input); | |
323 | }); | |
324 | ||
325 | // remove name attribute to enforce i18n values | |
326 | data.element.removeAttribute('name'); | |
327 | } | |
dbd9f599 | 328 | } |
c33eb992 MS |
329 | } |
330 | }, | |
331 | ||
332 | /** | |
333 | * Returns the values of an input field. | |
334 | * | |
2cd1e7d3 AE |
335 | * @param {string} elementId input element id |
336 | * @return {Dictionary} values stored for the different languages | |
c33eb992 MS |
337 | */ |
338 | getValues: function(elementId) { | |
339 | var element = _elements.get(elementId); | |
340 | if (element === undefined) { | |
341 | throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); | |
342 | } | |
343 | ||
344 | var values = _values.get(elementId); | |
345 | ||
346 | // update with current value | |
347 | values.set(element.languageId, element.element.value); | |
348 | ||
349 | return values; | |
350 | }, | |
351 | ||
352 | /** | |
353 | * Sets the values of an input field. | |
354 | * | |
2cd1e7d3 AE |
355 | * @param {string} elementId input element id |
356 | * @param {Dictionary} values values for the different languages | |
c33eb992 MS |
357 | */ |
358 | setValues: function(elementId, values) { | |
359 | var element = _elements.get(elementId); | |
360 | if (element === undefined) { | |
361 | throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); | |
362 | } | |
363 | ||
364 | if (Core.isPlainObject(values)) { | |
365 | values = Dictionary.fromObject(values); | |
366 | } | |
367 | ||
368 | element.element.value = ''; | |
369 | ||
370 | if (values.has(0)) { | |
371 | element.element.value = values.get(0); | |
372 | values['delete'](0); | |
0eb0962e JR |
373 | _values.set(elementId, values); |
374 | this._select(elementId, 0, true); | |
375 | return; | |
c33eb992 MS |
376 | } |
377 | ||
378 | _values.set(elementId, values); | |
379 | ||
380 | element.languageId = 0; | |
2cd1e7d3 | 381 | //noinspection JSUnresolvedVariable |
c33eb992 MS |
382 | this._select(elementId, LANGUAGE_ID, true); |
383 | }, | |
384 | ||
385 | /** | |
386 | * Disables the i18n interface for an input field. | |
387 | * | |
2cd1e7d3 | 388 | * @param {string} elementId input element id |
c33eb992 MS |
389 | */ |
390 | disable: function(elementId) { | |
391 | var element = _elements.get(elementId); | |
392 | if (element === undefined) { | |
38d4624b | 393 | throw new Error("Expected a valid element, '" + elementId + "' is not an i18n input field."); |
c33eb992 MS |
394 | } |
395 | ||
396 | if (!element.isEnabled) return; | |
397 | ||
398 | element.isEnabled = false; | |
399 | ||
400 | // hide language dropdown | |
2cd1e7d3 | 401 | //noinspection JSCheckFunctionSignatures |
f5336f4f | 402 | elHide(element.buttonLabel.parentNode); |
c33eb992 MS |
403 | var dropdownContainer = element.buttonLabel.parentNode.parentNode; |
404 | dropdownContainer.classList.remove('inputAddon'); | |
405 | dropdownContainer.classList.remove('dropdown'); | |
406 | }, | |
407 | ||
408 | /** | |
409 | * Enables the i18n interface for an input field. | |
410 | * | |
2cd1e7d3 | 411 | * @param {string} elementId input element id |
c33eb992 MS |
412 | */ |
413 | enable: function(elementId) { | |
414 | var element = _elements.get(elementId); | |
415 | if (element === undefined) { | |
416 | throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); | |
417 | } | |
418 | ||
419 | if (element.isEnabled) return; | |
420 | ||
421 | element.isEnabled = true; | |
422 | ||
423 | // show language dropdown | |
2cd1e7d3 | 424 | //noinspection JSCheckFunctionSignatures |
f5336f4f | 425 | elShow(element.buttonLabel.parentNode); |
c33eb992 MS |
426 | var dropdownContainer = element.buttonLabel.parentNode.parentNode; |
427 | dropdownContainer.classList.add('inputAddon'); | |
428 | dropdownContainer.classList.add('dropdown'); | |
429 | }, | |
430 | ||
431 | /** | |
432 | * Returns true if i18n input is enabled for an input field. | |
433 | * | |
2cd1e7d3 AE |
434 | * @param {string} elementId input element id |
435 | * @return {boolean} | |
c33eb992 MS |
436 | */ |
437 | isEnabled: function(elementId) { | |
438 | var element = _elements.get(elementId); | |
439 | if (element === undefined) { | |
440 | throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); | |
441 | } | |
442 | ||
443 | return element.isEnabled; | |
444 | }, | |
445 | ||
446 | /** | |
447 | * Returns true if the value of an i18n input field is valid. | |
448 | * | |
449 | * If the element is disabled, true is returned. | |
450 | * | |
2cd1e7d3 AE |
451 | * @param {string} elementId input element id |
452 | * @param {boolean} permitEmptyValue if true, input may be empty for all languages | |
453 | * @return {boolean} true if input is valid | |
c33eb992 MS |
454 | */ |
455 | validate: function(elementId, permitEmptyValue) { | |
456 | var element = _elements.get(elementId); | |
457 | if (element === undefined) { | |
458 | throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); | |
459 | } | |
460 | ||
461 | if (!element.isEnabled) return true; | |
462 | ||
463 | var values = _values.get(elementId); | |
464 | ||
465 | var dropdownMenu = UiSimpleDropdown.getDropdownMenu(element.element.parentNode.id); | |
466 | ||
467 | if (element.languageId) { | |
468 | values.set(element.languageId, element.element.value); | |
469 | } | |
470 | ||
471 | var item, languageId; | |
472 | var hasEmptyValue = false, hasNonEmptyValue = false; | |
473 | for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { | |
474 | item = dropdownMenu.children[i]; | |
f5336f4f | 475 | languageId = ~~elData(item, 'language-id'); |
dbd9f599 | 476 | |
c33eb992 MS |
477 | if (languageId) { |
478 | if (!values.has(languageId) || values.get(languageId).length === 0) { | |
479 | // input has non-empty value for previously checked language | |
480 | if (hasNonEmptyValue) { | |
481 | return false; | |
482 | } | |
dbd9f599 | 483 | |
c33eb992 MS |
484 | hasEmptyValue = true; |
485 | } | |
486 | else { | |
487 | // input has empty value for previously checked language | |
488 | if (hasEmptyValue) { | |
489 | return false; | |
490 | } | |
491 | ||
492 | hasNonEmptyValue = true; | |
493 | } | |
dbd9f599 AE |
494 | } |
495 | } | |
c33eb992 | 496 | |
2cd1e7d3 | 497 | return (!hasEmptyValue || permitEmptyValue); |
dbd9f599 AE |
498 | } |
499 | }; | |
dbd9f599 | 500 | }); |