2 * I18n interface for input and textarea fields.
4 * @author Alexander Ebert
5 * @copyright 2001-2018 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module WoltLabSuite/Core/Language/Input
9 define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Core
, Dictionary
, Language
, ObjectMap
, StringUtil
, DomTraverse
, DomUtil
, UiSimpleDropdown
) {
12 var _elements
= new Dictionary();
14 var _forms
= new ObjectMap();
15 var _values
= new Dictionary();
17 var _callbackDropdownToggle
= null;
18 var _callbackSubmit
= null;
21 * @exports WoltLabSuite/Core/Language/Input
25 * Initializes an input field.
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
32 init: function(elementId
, values
, availableLanguages
, forceSelection
) {
33 if (_values
.has(elementId
)) {
37 var element
= elById(elementId
);
38 if (element
=== null) {
39 throw new Error("Expected a valid element id, cannot find '" + elementId
+ "'.");
45 var unescapedValues
= new Dictionary();
46 for (var key
in values
) {
47 if (values
.hasOwnProperty(key
)) {
48 unescapedValues
.set(~~key
, StringUtil
.unescapeHTML(values
[key
]));
52 _values
.set(elementId
, unescapedValues
);
54 this._initElement(elementId
, element
, unescapedValues
, availableLanguages
, forceSelection
);
58 * Registers a callback for an element.
60 * @param {string} elementId
61 * @param {string} eventName
62 * @param {function} callback
64 registerCallback: function (elementId
, eventName
, callback
) {
65 if (!_values
.has(elementId
)) {
66 throw new Error("Unknown element id '" + elementId
+ "'.");
69 _elements
.get(elementId
).callbacks
.set(eventName
, callback
);
73 * Caches common event listener callbacks.
79 _callbackDropdownToggle
= this._dropdownToggle
.bind(this);
80 _callbackSubmit
= this._submit
.bind(this);
84 * Sets up DOM and event listeners for an input field.
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
92 _initElement: function(elementId
, element
, values
, availableLanguages
, forceSelection
) {
93 var container
= element
.parentNode
;
94 if (!container
.classList
.contains('inputAddon')) {
95 container
= elCreate('div');
96 container
.className
= 'inputAddon' + (element
.nodeName
=== 'TEXTAREA' ? ' inputAddonTextarea' : '');
97 //noinspection JSCheckFunctionSignatures
98 elData(container
, 'input-id', elementId
);
100 element
.parentNode
.insertBefore(container
, element
);
101 container
.appendChild(element
);
104 container
.classList
.add('dropdown');
105 var button
= elCreate('span');
106 button
.className
= 'button dropdownToggle inputPrefix';
108 var span
= elCreate('span');
109 span
.textContent
= Language
.get('wcf.global.button.disabledI18n');
111 button
.appendChild(span
);
112 container
.insertBefore(button
, element
);
114 var dropdownMenu
= elCreate('ul');
115 dropdownMenu
.className
= 'dropdownMenu';
116 DomUtil
.insertAfter(dropdownMenu
, button
);
118 var callbackClick
= (function(event
, isInit
) {
119 var languageId
= ~~elData(event
.currentTarget
, 'language-id');
121 var activeItem
= DomTraverse
.childByClass(dropdownMenu
, 'active');
122 if (activeItem
!== null) activeItem
.classList
.remove('active');
124 if (languageId
) event
.currentTarget
.classList
.add('active');
126 this._select(elementId
, languageId
, isInit
|| false);
129 // build language dropdown
131 for (var languageId
in availableLanguages
) {
132 if (availableLanguages
.hasOwnProperty(languageId
)) {
133 listItem
= elCreate('li');
134 elData(listItem
, 'language-id', languageId
);
136 span
= elCreate('span');
137 span
.textContent
= availableLanguages
[languageId
];
139 listItem
.appendChild(span
);
140 listItem
.addEventListener(WCF_CLICK_EVENT
, callbackClick
);
141 dropdownMenu
.appendChild(listItem
);
145 if (forceSelection
!== true) {
146 listItem
= elCreate('li');
147 listItem
.className
= 'dropdownDivider';
148 dropdownMenu
.appendChild(listItem
);
150 listItem
= elCreate('li');
151 elData(listItem
, 'language-id', 0);
152 span
= elCreate('span');
153 span
.textContent
= Language
.get('wcf.global.button.disabledI18n');
154 listItem
.appendChild(span
);
155 listItem
.addEventListener(WCF_CLICK_EVENT
, callbackClick
);
156 dropdownMenu
.appendChild(listItem
);
159 var activeItem
= null;
160 if (forceSelection
=== true || values
.size
) {
161 for (var i
= 0, length
= dropdownMenu
.childElementCount
; i
< length
; i
++) {
162 //noinspection JSUnresolvedVariable
163 if (~~elData(dropdownMenu
.children
[i
], 'language-id') === LANGUAGE_ID
) {
164 activeItem
= dropdownMenu
.children
[i
];
170 UiSimpleDropdown
.init(button
);
171 UiSimpleDropdown
.registerCallback(container
.id
, _callbackDropdownToggle
);
173 _elements
.set(elementId
, {
174 buttonLabel
: button
.children
[0],
175 callbacks
: new Dictionary(),
179 forceSelection
: forceSelection
182 // bind to submit event
183 var submit
= DomTraverse
.parentByTag(element
, 'FORM');
184 if (submit
!== null) {
185 submit
.addEventListener('submit', _callbackSubmit
);
187 var elementIds
= _forms
.get(submit
);
188 if (elementIds
=== undefined) {
190 _forms
.set(submit
, elementIds
);
193 elementIds
.push(elementId
);
196 if (activeItem
!== null) {
197 callbackClick({ currentTarget
: activeItem
}, true);
202 * Selects a language or non-i18n from the dropdown list.
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
208 _select: function(elementId
, languageId
, isInit
) {
209 var data
= _elements
.get(elementId
);
211 var dropdownMenu
= UiSimpleDropdown
.getDropdownMenu(data
.element
.closest('.inputAddon').id
);
212 var item
, label
= '';
213 for (var i
= 0, length
= dropdownMenu
.childElementCount
; i
< length
; i
++) {
214 item
= dropdownMenu
.children
[i
];
216 var itemLanguageId
= elData(item
, 'language-id');
217 if (itemLanguageId
.length
&& languageId
=== ~~itemLanguageId
) {
218 label
= item
.children
[0].textContent
;
222 // save current value
223 if (data
.languageId
!== languageId
) {
224 var values
= _values
.get(elementId
);
226 if (data
.languageId
) {
227 values
.set(data
.languageId
, data
.element
.value
);
230 if (languageId
=== 0) {
231 _values
.set(elementId
, new Dictionary());
233 else if (data
.buttonLabel
.classList
.contains('active') || isInit
=== true) {
234 data
.element
.value
= (values
.has(languageId
)) ? values
.get(languageId
) : '';
238 data
.buttonLabel
.textContent
= label
;
239 data
.buttonLabel
.classList
[(languageId
? 'add' : 'remove')]('active');
241 data
.languageId
= languageId
;
246 data
.element
.focus();
249 if (data
.callbacks
.has('select')) {
250 data
.callbacks
.get('select')(data
.element
);
255 * Callback for dropdowns being opened, flags items with a missing value for one or more languages.
257 * @param {string} containerId dropdown container id
258 * @param {string} action toggle action, can be `open` or `close`
260 _dropdownToggle: function(containerId
, action
) {
261 if (action
!== 'open') {
265 var dropdownMenu
= UiSimpleDropdown
.getDropdownMenu(containerId
);
266 var elementId
= elData(elById(containerId
), 'input-id');
267 var data
= _elements
.get(elementId
);
268 var values
= _values
.get(elementId
);
270 var item
, languageId
;
271 for (var i
= 0, length
= dropdownMenu
.childElementCount
; i
< length
; i
++) {
272 item
= dropdownMenu
.children
[i
];
273 languageId
= ~~elData(item
, 'language-id');
276 var hasMissingValue
= false;
277 if (data
.languageId
) {
278 if (languageId
=== data
.languageId
) {
279 hasMissingValue
= (data
.element
.value
.trim() === '');
282 hasMissingValue
= (!values
.get(languageId
));
286 item
.classList
[(hasMissingValue
? 'add' : 'remove')]('missingValue');
292 * Inserts hidden fields for i18n input on submit.
294 * @param {Object} event event object
296 _submit: function(event
) {
297 var elementIds
= _forms
.get(event
.currentTarget
);
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
);
303 if (data
.isEnabled
) {
304 values
= _values
.get(elementId
);
306 if (data
.callbacks
.has('submit')) {
307 data
.callbacks
.get('submit')(data
.element
);
310 // update with current value
311 if (data
.languageId
) {
312 values
.set(data
.languageId
, data
.element
.value
);
316 values
.forEach(function(value
, languageId
) {
317 input
= elCreate('input');
318 input
.type
= 'hidden';
319 input
.name
= elementId
+ '_i18n[' + languageId
+ ']';
322 event
.currentTarget
.appendChild(input
);
325 // remove name attribute to enforce i18n values
326 data
.element
.removeAttribute('name');
333 * Returns the values of an input field.
335 * @param {string} elementId input element id
336 * @return {Dictionary} values stored for the different languages
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.");
344 var values
= _values
.get(elementId
);
346 // update with current value
347 values
.set(element
.languageId
, element
.element
.value
);
353 * Sets the values of an input field.
355 * @param {string} elementId input element id
356 * @param {Dictionary} values values for the different languages
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.");
364 if (Core
.isPlainObject(values
)) {
365 values
= Dictionary
.fromObject(values
);
368 element
.element
.value
= '';
371 element
.element
.value
= values
.get(0);
373 _values
.set(elementId
, values
);
374 this._select(elementId
, 0, true);
378 _values
.set(elementId
, values
);
380 element
.languageId
= 0;
381 //noinspection JSUnresolvedVariable
382 this._select(elementId
, LANGUAGE_ID
, true);
386 * Disables the i18n interface for an input field.
388 * @param {string} elementId input element id
390 disable: function(elementId
) {
391 var element
= _elements
.get(elementId
);
392 if (element
=== undefined) {
393 throw new Error("Expected a valid element, '" + elementId
+ "' is not an i18n input field.");
396 if (!element
.isEnabled
) return;
398 element
.isEnabled
= false;
400 // hide language dropdown
401 //noinspection JSCheckFunctionSignatures
402 elHide(element
.buttonLabel
.parentNode
);
403 var dropdownContainer
= element
.buttonLabel
.parentNode
.parentNode
;
404 dropdownContainer
.classList
.remove('inputAddon');
405 dropdownContainer
.classList
.remove('dropdown');
409 * Enables the i18n interface for an input field.
411 * @param {string} elementId input element id
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.");
419 if (element
.isEnabled
) return;
421 element
.isEnabled
= true;
423 // show language dropdown
424 //noinspection JSCheckFunctionSignatures
425 elShow(element
.buttonLabel
.parentNode
);
426 var dropdownContainer
= element
.buttonLabel
.parentNode
.parentNode
;
427 dropdownContainer
.classList
.add('inputAddon');
428 dropdownContainer
.classList
.add('dropdown');
432 * Returns true if i18n input is enabled for an input field.
434 * @param {string} elementId input element id
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.");
443 return element
.isEnabled
;
447 * Returns true if the value of an i18n input field is valid.
449 * If the element is disabled, true is returned.
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
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.");
461 if (!element
.isEnabled
) return true;
463 var values
= _values
.get(elementId
);
465 var dropdownMenu
= UiSimpleDropdown
.getDropdownMenu(element
.element
.parentNode
.id
);
467 if (element
.languageId
) {
468 values
.set(element
.languageId
, element
.element
.value
);
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
];
475 languageId
= ~~elData(item
, 'language-id');
478 if (!values
.has(languageId
) || values
.get(languageId
).length
=== 0) {
479 // input has non-empty value for previously checked language
480 if (hasNonEmptyValue
) {
484 hasEmptyValue
= true;
487 // input has empty value for previously checked language
492 hasNonEmptyValue
= true;
497 return (!hasEmptyValue
|| permitEmptyValue
);