From 092eba8181975adacb85ddabe2ca73467798b955 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Sun, 8 Nov 2020 15:04:17 +0100 Subject: [PATCH] Convert `Language/Input` to TypeScript --- global.d.ts | 1 + .../js/WoltLabSuite/Core/Language/Input.js | 756 ++++++++---------- .../ts/WoltLabSuite/Core/Language/Input.js | 522 ------------ .../ts/WoltLabSuite/Core/Language/Input.ts | 508 ++++++++++++ 4 files changed, 864 insertions(+), 923 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.ts diff --git a/global.d.ts b/global.d.ts index 1136bb7837..f37463b84c 100644 --- a/global.d.ts +++ b/global.d.ts @@ -11,6 +11,7 @@ declare global { Devtools?: typeof Devtools; ENABLE_DEBUG_MODE: boolean; ENABLE_DEVELOPER_TOOLS: boolean; + LANGUAGE_ID: number; REACTION_TYPES: { [key: string]: Reaction; }; diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js index 144f18f844..d2f191dc90 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Language/Input.js @@ -2,437 +2,391 @@ * I18n interface for input and textarea fields. * * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH + * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License * @module WoltLabSuite/Core/Language/Input */ -define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function (Core, Dictionary, Language, ObjectMap, StringUtil, DomTraverse, DomUtil, UiSimpleDropdown) { +define(["require", "exports", "tslib", "../Dom/Util", "../Language", "../Ui/Dropdown/Simple", "../StringUtil"], function (require, exports, tslib_1, Util_1, Language, Simple_1, StringUtil) { "use strict"; - var _elements = new Dictionary(); - var _didInit = false; - var _forms = new ObjectMap(); - var _values = new Dictionary(); - var _callbackDropdownToggle = null; - var _callbackSubmit = null; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.validate = exports.isEnabled = exports.enable = exports.disable = exports.setValues = exports.getValues = exports.unregister = exports.registerCallback = exports.init = void 0; + Util_1 = tslib_1.__importDefault(Util_1); + Language = tslib_1.__importStar(Language); + Simple_1 = tslib_1.__importDefault(Simple_1); + StringUtil = tslib_1.__importStar(StringUtil); + const _elements = new Map(); + const _forms = new WeakMap(); + const _values = new Map(); /** - * @exports WoltLabSuite/Core/Language/Input + * Sets up DOM and event listeners for an input field. */ - return { - /** - * Initializes an input field. - * - * @param {string} elementId input element id - * @param {Object} values preset values per language id - * @param {Object} availableLanguages language names per language id - * @param {boolean} forceSelection require i18n input - */ - init: function (elementId, values, availableLanguages, forceSelection) { - if (_values.has(elementId)) { - return; - } - var element = elById(elementId); - if (element === null) { - throw new Error("Expected a valid element id, cannot find '" + elementId + "'."); - } - this._setup(); - // unescape values - var unescapedValues = new Dictionary(); - for (var key in values) { - if (values.hasOwnProperty(key)) { - unescapedValues.set(~~key, StringUtil.unescapeHTML(values[key])); - } - } - _values.set(elementId, unescapedValues); - this._initElement(elementId, element, unescapedValues, availableLanguages, forceSelection); - }, - /** - * Registers a callback for an element. - * - * @param {string} elementId - * @param {string} eventName - * @param {function} callback - */ - registerCallback: function (elementId, eventName, callback) { - if (!_values.has(elementId)) { - throw new Error("Unknown element id '" + elementId + "'."); + function initElement(elementId, element, values, availableLanguages, forceSelection) { + let container = element.parentElement; + if (!container.classList.contains("inputAddon")) { + container = document.createElement("div"); + container.className = "inputAddon"; + if (element.nodeName === "TEXTAREA") { + container.classList.add("inputAddonTextarea"); } - _elements.get(elementId).callbacks.set(eventName, callback); - }, - /** - * Unregisters the element with the given id. - * - * @param {string} elementId - * @since 5.2 - */ - unregister: function (elementId) { - if (!_values.has(elementId)) { - throw new Error("Unknown element id '" + elementId + "'."); + container.dataset.inputId = elementId; + const hasFocus = document.activeElement === element; + // DOM manipulation causes focused element to lose focus + element.insertAdjacentElement("beforebegin", container); + container.appendChild(element); + if (hasFocus) { + element.focus(); } - _values.delete(elementId); - _elements.delete(elementId); - }, - /** - * Caches common event listener callbacks. - */ - _setup: function () { - if (_didInit) - return; - _didInit = true; - _callbackDropdownToggle = this._dropdownToggle.bind(this); - _callbackSubmit = this._submit.bind(this); - }, - /** - * Sets up DOM and event listeners for an input field. - * - * @param {string} elementId input element id - * @param {Element} element input or textarea element - * @param {Dictionary} values preset values per language id - * @param {Object} availableLanguages language names per language id - * @param {boolean} forceSelection require i18n input - */ - _initElement: function (elementId, element, values, availableLanguages, forceSelection) { - var container = element.parentNode; - if (!container.classList.contains('inputAddon')) { - container = elCreate('div'); - container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : ''); - //noinspection JSCheckFunctionSignatures - elData(container, 'input-id', elementId); - var hasFocus = document.activeElement === element; - // DOM manipulation causes focused element to lose focus - element.parentNode.insertBefore(container, element); - container.appendChild(element); - if (hasFocus) { - element.focus(); - } + } + container.classList.add("dropdown"); + const button = document.createElement("span"); + button.className = "button dropdownToggle inputPrefix"; + const buttonLabel = document.createElement("span"); + buttonLabel.textContent = Language.get("wcf.global.button.disabledI18n"); + button.appendChild(buttonLabel); + container.insertBefore(button, element); + const dropdownMenu = document.createElement("ul"); + dropdownMenu.className = "dropdownMenu"; + button.insertAdjacentElement("afterend", dropdownMenu); + const callbackClick = (event) => { + let target; + if (event instanceof HTMLElement) { + target = event; } - container.classList.add('dropdown'); - var button = elCreate('span'); - button.className = 'button dropdownToggle inputPrefix'; - var span = elCreate('span'); - span.textContent = Language.get('wcf.global.button.disabledI18n'); - button.appendChild(span); - container.insertBefore(button, element); - var dropdownMenu = elCreate('ul'); - dropdownMenu.className = 'dropdownMenu'; - DomUtil.insertAfter(dropdownMenu, button); - var callbackClick = (function (event, isInit) { - var languageId = ~~elData(event.currentTarget, 'language-id'); - var activeItem = DomTraverse.childByClass(dropdownMenu, 'active'); - if (activeItem !== null) - activeItem.classList.remove('active'); - if (languageId) - event.currentTarget.classList.add('active'); - this._select(elementId, languageId, isInit || false); - }).bind(this); - // build language dropdown - var listItem; - for (var languageId in availableLanguages) { - if (availableLanguages.hasOwnProperty(languageId)) { - listItem = elCreate('li'); - elData(listItem, 'language-id', languageId); - span = elCreate('span'); - span.textContent = availableLanguages[languageId]; - listItem.appendChild(span); - listItem.addEventListener('click', callbackClick); - dropdownMenu.appendChild(listItem); - } + else { + target = event.currentTarget; } - if (forceSelection !== true) { - listItem = elCreate('li'); - listItem.className = 'dropdownDivider'; - dropdownMenu.appendChild(listItem); - listItem = elCreate('li'); - elData(listItem, 'language-id', 0); - span = elCreate('span'); - span.textContent = Language.get('wcf.global.button.disabledI18n'); - listItem.appendChild(span); - listItem.addEventListener('click', callbackClick); - dropdownMenu.appendChild(listItem); + const languageId = ~~target.dataset.languageId; + const activeItem = dropdownMenu.querySelector(".active"); + if (activeItem !== null) { + activeItem.classList.remove("active"); } - var activeItem = null; - if (forceSelection === true || values.size) { - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - //noinspection JSUnresolvedVariable - if (~~elData(dropdownMenu.children[i], 'language-id') === LANGUAGE_ID) { - activeItem = dropdownMenu.children[i]; - break; - } - } + if (languageId) { + target.classList.add("active"); } - UiSimpleDropdown.init(button); - UiSimpleDropdown.registerCallback(container.id, _callbackDropdownToggle); - _elements.set(elementId, { - buttonLabel: button.children[0], - callbacks: new Dictionary(), - element: element, - languageId: 0, - isEnabled: true, - forceSelection: forceSelection + const isInit = event instanceof HTMLElement; + select(elementId, languageId, isInit); + }; + // build language dropdown + Object.entries(availableLanguages).forEach(([languageId, languageName]) => { + const listItem = document.createElement("li"); + listItem.dataset.languageId = languageId; + const span = document.createElement("span"); + span.textContent = languageName; + listItem.appendChild(span); + listItem.addEventListener("click", callbackClick); + dropdownMenu.appendChild(listItem); + }); + if (!forceSelection) { + const divider = document.createElement("li"); + divider.className = "dropdownDivider"; + dropdownMenu.appendChild(divider); + const listItem = document.createElement("li"); + listItem.dataset.languageId = "0"; + listItem.addEventListener("click", callbackClick); + const span = document.createElement("span"); + span.textContent = Language.get("wcf.global.button.disabledI18n"); + listItem.appendChild(span); + dropdownMenu.appendChild(listItem); + } + let activeItem = undefined; + if (forceSelection || values.size) { + activeItem = Array.from(dropdownMenu.children).find((element) => { + return +element.dataset.languageId === window.LANGUAGE_ID; }); - // bind to submit event - var submit = DomTraverse.parentByTag(element, 'FORM'); - if (submit !== null) { - submit.addEventListener('submit', _callbackSubmit); - var elementIds = _forms.get(submit); - if (elementIds === undefined) { - elementIds = []; - _forms.set(submit, elementIds); - } - elementIds.push(elementId); + } + Simple_1.default.init(button); + Simple_1.default.registerCallback(container.id, dropdownToggle); + _elements.set(elementId, { + buttonLabel, + callbacks: new Map(), + element, + languageId: 0, + isEnabled: true, + forceSelection, + }); + // bind to submit event + const form = element.closest("form"); + if (form !== null) { + form.addEventListener("submit", submit); + let elementIds = _forms.get(form); + if (elementIds === undefined) { + elementIds = []; + _forms.set(form, elementIds); } - if (activeItem !== null) { - callbackClick({ currentTarget: activeItem }, true); + elementIds.push(elementId); + } + if (activeItem) { + callbackClick(activeItem); + } + } + /** + * Selects a language or non-i18n from the dropdown list. + */ + function select(elementId, languageId, isInit) { + const data = _elements.get(elementId); + const dropdownMenu = Simple_1.default.getDropdownMenu(data.element.closest(".inputAddon").id); + const item = dropdownMenu.querySelector(`[data-language-id="${languageId}"]`); + const label = item ? item.textContent : ""; + // save current value + if (data.languageId !== languageId) { + const values = _values.get(elementId); + if (data.languageId) { + values.set(data.languageId, data.element.value); } - }, - /** - * Selects a language or non-i18n from the dropdown list. - * - * @param {string} elementId input element id - * @param {int} languageId language id or `0` to disable i18n - * @param {boolean} isInit triggers pre-selection on init - */ - _select: function (elementId, languageId, isInit) { - var data = _elements.get(elementId); - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(data.element.closest('.inputAddon').id); - var item, label = ''; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - var itemLanguageId = elData(item, 'language-id'); - if (itemLanguageId.length && languageId === ~~itemLanguageId) { - label = item.children[0].textContent; - } + if (languageId === 0) { + _values.set(elementId, new Map()); } - // save current value - if (data.languageId !== languageId) { - var values = _values.get(elementId); + else if (data.buttonLabel.classList.contains("active") || isInit) { + data.element.value = values.get(languageId) || ""; + } + // update label + data.buttonLabel.textContent = label; + data.buttonLabel.classList[languageId ? "add" : "remove"]("active"); + data.languageId = languageId; + } + if (!isInit) { + data.element.blur(); + data.element.focus(); + } + if (data.callbacks.has("select")) { + data.callbacks.get("select")(data.element); + } + } + /** + * Callback for dropdowns being opened, flags items with a missing value for one or more languages. + */ + function dropdownToggle(containerId, action) { + if (action !== "open") { + return; + } + const dropdownMenu = Simple_1.default.getDropdownMenu(containerId); + const container = document.getElementById(containerId); + const elementId = container.dataset.inputId; + const data = _elements.get(elementId); + const values = _values.get(elementId); + Array.from(dropdownMenu.children).forEach((item) => { + const languageId = ~~(item.dataset.languageId || ""); + if (languageId) { + let hasMissingValue = false; if (data.languageId) { - values.set(data.languageId, data.element.value); + if (languageId === data.languageId) { + hasMissingValue = data.element.value.trim() === ""; + } + else { + hasMissingValue = !values.get(languageId); + } } - if (languageId === 0) { - _values.set(elementId, new Dictionary()); + if (hasMissingValue) { + item.classList.add("missingValue"); } - else if (data.buttonLabel.classList.contains('active') || isInit === true) { - data.element.value = (values.has(languageId)) ? values.get(languageId) : ''; + else { + item.classList.remove("missingValue"); } - // update label - data.buttonLabel.textContent = label; - data.buttonLabel.classList[(languageId ? 'add' : 'remove')]('active'); - data.languageId = languageId; - } - if (!isInit) { - data.element.blur(); - data.element.focus(); } - if (data.callbacks.has('select')) { - data.callbacks.get('select')(data.element); - } - }, - /** - * Callback for dropdowns being opened, flags items with a missing value for one or more languages. - * - * @param {string} containerId dropdown container id - * @param {string} action toggle action, can be `open` or `close` - */ - _dropdownToggle: function (containerId, action) { - if (action !== 'open') { + }); + } + /** + * Inserts hidden fields for i18n input on submit. + */ + function submit(event) { + const form = event.currentTarget; + const elementIds = _forms.get(form); + elementIds.forEach((elementId) => { + const data = _elements.get(elementId); + if (!data.isEnabled) { return; } - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(containerId); - var elementId = elData(elById(containerId), 'input-id'); - var data = _elements.get(elementId); - var values = _values.get(elementId); - var item, languageId; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - languageId = ~~elData(item, 'language-id'); - if (languageId) { - var hasMissingValue = false; - if (data.languageId) { - if (languageId === data.languageId) { - hasMissingValue = (data.element.value.trim() === ''); - } - else { - hasMissingValue = (!values.get(languageId)); - } - } - item.classList[(hasMissingValue ? 'add' : 'remove')]('missingValue'); - } + const values = _values.get(elementId); + if (data.callbacks.has("submit")) { + data.callbacks.get("submit")(data.element); } - }, - /** - * Inserts hidden fields for i18n input on submit. - * - * @param {Object} event event object - */ - _submit: function (event) { - var elementIds = _forms.get(event.currentTarget); - var data, elementId, input, values; - for (var i = 0, length = elementIds.length; i < length; i++) { - elementId = elementIds[i]; - data = _elements.get(elementId); - if (data.isEnabled) { - values = _values.get(elementId); - if (data.callbacks.has('submit')) { - data.callbacks.get('submit')(data.element); - } - // update with current value - if (data.languageId) { - values.set(data.languageId, data.element.value); - } - if (values.size) { - values.forEach(function (value, languageId) { - input = elCreate('input'); - input.type = 'hidden'; - input.name = elementId + '_i18n[' + languageId + ']'; - input.value = value; - event.currentTarget.appendChild(input); - }); - // remove name attribute to enforce i18n values - data.element.removeAttribute('name'); - } - } - } - }, - /** - * Returns the values of an input field. - * - * @param {string} elementId input element id - * @return {Dictionary} values stored for the different languages - */ - getValues: function (elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - var values = _values.get(elementId); // update with current value - values.set(element.languageId, element.element.value); - return values; - }, - /** - * Sets the values of an input field. - * - * @param {string} elementId input element id - * @param {Dictionary} values values for the different languages - */ - setValues: function (elementId, values) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - if (Core.isPlainObject(values)) { - values = Dictionary.fromObject(values); + if (data.languageId) { + values.set(data.languageId, data.element.value); } - element.element.value = ''; - if (values.has(0)) { - element.element.value = values.get(0); - values['delete'](0); - _values.set(elementId, values); - this._select(elementId, 0, true); - return; + if (values.size) { + values.forEach(function (value, languageId) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = `${elementId}_i18n[${languageId}]`; + input.value = value; + form.appendChild(input); + }); + // remove name attribute to enforce i18n values + data.element.removeAttribute("name"); } + }); + } + /** + * Initializes an input field. + */ + function init(elementId, values, availableLanguages, forceSelection) { + if (_values.has(elementId)) { + return; + } + const element = document.getElementById(elementId); + if (element === null) { + throw new Error(`Expected a valid element id, cannot find '${elementId}'.`); + } + // unescape values + const unescapedValues = new Map(); + Object.entries(values).forEach(([languageId, value]) => { + unescapedValues.set(+languageId, StringUtil.unescapeHTML(value)); + }); + _values.set(elementId, unescapedValues); + initElement(elementId, element, unescapedValues, availableLanguages, forceSelection); + } + exports.init = init; + /** + * Registers a callback for an element. + */ + function registerCallback(elementId, eventName, callback) { + if (!_values.has(elementId)) { + throw new Error(`Unknown element id '${elementId}'.`); + } + _elements.get(elementId).callbacks.set(eventName, callback); + } + exports.registerCallback = registerCallback; + /** + * Unregisters the element with the given id. + * + * @since 5.2 + */ + function unregister(elementId) { + if (!_values.has(elementId)) { + throw new Error(`Unknown element id '${elementId}'.`); + } + _values.delete(elementId); + _elements.delete(elementId); + } + exports.unregister = unregister; + /** + * Returns the values of an input field. + */ + function getValues(elementId) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + const values = _values.get(elementId); + // update with current value + values.set(element.languageId, element.element.value); + return values; + } + exports.getValues = getValues; + /** + * Sets the values of an input field. + */ + function setValues(elementId, newValues) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + element.element.value = ""; + const values = new Map(Object.entries(newValues).map(([languageId, value]) => { + return [+languageId, value]; + })); + if (values.has(0)) { + element.element.value = values.get(0); + values.delete(0); _values.set(elementId, values); - element.languageId = 0; - //noinspection JSUnresolvedVariable - this._select(elementId, LANGUAGE_ID, true); - }, - /** - * Disables the i18n interface for an input field. - * - * @param {string} elementId input element id - */ - disable: function (elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid element, '" + elementId + "' is not an i18n input field."); - } - if (!element.isEnabled) - return; - element.isEnabled = false; - // hide language dropdown - //noinspection JSCheckFunctionSignatures - elHide(element.buttonLabel.parentNode); - var dropdownContainer = element.buttonLabel.parentNode.parentNode; - dropdownContainer.classList.remove('inputAddon'); - dropdownContainer.classList.remove('dropdown'); - }, - /** - * Enables the i18n interface for an input field. - * - * @param {string} elementId input element id - */ - enable: function (elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - if (element.isEnabled) - return; - element.isEnabled = true; - // show language dropdown - //noinspection JSCheckFunctionSignatures - elShow(element.buttonLabel.parentNode); - var dropdownContainer = element.buttonLabel.parentNode.parentNode; - dropdownContainer.classList.add('inputAddon'); - dropdownContainer.classList.add('dropdown'); - }, - /** - * Returns true if i18n input is enabled for an input field. - * - * @param {string} elementId input element id - * @return {boolean} - */ - isEnabled: function (elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - return element.isEnabled; - }, - /** - * Returns true if the value of an i18n input field is valid. - * - * If the element is disabled, true is returned. - * - * @param {string} elementId input element id - * @param {boolean} permitEmptyValue if true, input may be empty for all languages - * @return {boolean} true if input is valid - */ - validate: function (elementId, permitEmptyValue) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - if (!element.isEnabled) - return true; - var values = _values.get(elementId); - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(element.element.parentNode.id); - if (element.languageId) { - values.set(element.languageId, element.element.value); - } - var item, languageId; - var hasEmptyValue = false, hasNonEmptyValue = false; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - languageId = ~~elData(item, 'language-id'); - if (languageId) { - if (!values.has(languageId) || values.get(languageId).length === 0) { - // input has non-empty value for previously checked language - if (hasNonEmptyValue) { - return false; - } - hasEmptyValue = true; + select(elementId, 0, true); + return; + } + _values.set(elementId, values); + element.languageId = 0; + select(elementId, window.LANGUAGE_ID, true); + } + exports.setValues = setValues; + /** + * Disables the i18n interface for an input field. + */ + function disable(elementId) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid element, '${elementId}' is not an i18n input field.`); + } + if (!element.isEnabled) { + return; + } + element.isEnabled = false; + // hide language dropdown + const buttonContainer = element.buttonLabel.parentElement; + Util_1.default.hide(buttonContainer); + const dropdownContainer = buttonContainer.parentElement; + dropdownContainer.classList.remove("inputAddon", "dropdown"); + } + exports.disable = disable; + /** + * Enables the i18n interface for an input field. + */ + function enable(elementId) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + if (element.isEnabled) { + return; + } + element.isEnabled = true; + // show language dropdown + const buttonContainer = element.buttonLabel.parentElement; + Util_1.default.show(buttonContainer); + const dropdownContainer = buttonContainer.parentElement; + dropdownContainer.classList.add("inputAddon", "dropdown"); + } + exports.enable = enable; + /** + * Returns true if i18n input is enabled for an input field. + */ + function isEnabled(elementId) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + return element.isEnabled; + } + exports.isEnabled = isEnabled; + /** + * Returns true if the value of an i18n input field is valid. + * + * If the element is disabled, true is returned. + */ + function validate(elementId, permitEmptyValue) { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + if (!element.isEnabled) { + return true; + } + const values = _values.get(elementId); + const dropdownMenu = Simple_1.default.getDropdownMenu(element.element.parentElement.id); + if (element.languageId) { + values.set(element.languageId, element.element.value); + } + let hasEmptyValue = false; + let hasNonEmptyValue = false; + Array.from(dropdownMenu.children).forEach((item) => { + const languageId = ~~item.dataset.languageId; + if (languageId) { + if (!values.has(languageId) || values.get(languageId).length === 0) { + // input has non-empty value for previously checked language + if (hasNonEmptyValue) { + return false; } - else { - // input has empty value for previously checked language - if (hasEmptyValue) { - return false; - } - hasNonEmptyValue = true; + hasEmptyValue = true; + } + else { + // input has empty value for previously checked language + if (hasEmptyValue) { + return false; } + hasNonEmptyValue = true; } } - return (!hasEmptyValue || permitEmptyValue); - } - }; + }); + return !hasEmptyValue || permitEmptyValue; + } + exports.validate = validate; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.js deleted file mode 100644 index 2cadb77ff4..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.js +++ /dev/null @@ -1,522 +0,0 @@ -/** - * I18n interface for input and textarea fields. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Language/Input - */ -define(['Core', 'Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'Dom/Traverse', 'Dom/Util', 'Ui/SimpleDropdown'], function(Core, Dictionary, Language, ObjectMap, StringUtil, DomTraverse, DomUtil, UiSimpleDropdown) { - "use strict"; - - var _elements = new Dictionary(); - var _didInit = false; - var _forms = new ObjectMap(); - var _values = new Dictionary(); - - var _callbackDropdownToggle = null; - var _callbackSubmit = null; - - /** - * @exports WoltLabSuite/Core/Language/Input - */ - return { - /** - * Initializes an input field. - * - * @param {string} elementId input element id - * @param {Object} values preset values per language id - * @param {Object} availableLanguages language names per language id - * @param {boolean} forceSelection require i18n input - */ - init: function(elementId, values, availableLanguages, forceSelection) { - if (_values.has(elementId)) { - return; - } - - var element = elById(elementId); - if (element === null) { - throw new Error("Expected a valid element id, cannot find '" + elementId + "'."); - } - - this._setup(); - - // unescape values - var unescapedValues = new Dictionary(); - for (var key in values) { - if (values.hasOwnProperty(key)) { - unescapedValues.set(~~key, StringUtil.unescapeHTML(values[key])); - } - } - - _values.set(elementId, unescapedValues); - - this._initElement(elementId, element, unescapedValues, availableLanguages, forceSelection); - }, - - /** - * Registers a callback for an element. - * - * @param {string} elementId - * @param {string} eventName - * @param {function} callback - */ - registerCallback: function (elementId, eventName, callback) { - if (!_values.has(elementId)) { - throw new Error("Unknown element id '" + elementId + "'."); - } - - _elements.get(elementId).callbacks.set(eventName, callback); - }, - - /** - * Unregisters the element with the given id. - * - * @param {string} elementId - * @since 5.2 - */ - unregister: function(elementId) { - if (!_values.has(elementId)) { - throw new Error("Unknown element id '" + elementId + "'."); - } - - _values.delete(elementId); - _elements.delete(elementId); - }, - - /** - * Caches common event listener callbacks. - */ - _setup: function() { - if (_didInit) return; - _didInit = true; - - _callbackDropdownToggle = this._dropdownToggle.bind(this); - _callbackSubmit = this._submit.bind(this); - }, - - /** - * Sets up DOM and event listeners for an input field. - * - * @param {string} elementId input element id - * @param {Element} element input or textarea element - * @param {Dictionary} values preset values per language id - * @param {Object} availableLanguages language names per language id - * @param {boolean} forceSelection require i18n input - */ - _initElement: function(elementId, element, values, availableLanguages, forceSelection) { - var container = element.parentNode; - if (!container.classList.contains('inputAddon')) { - container = elCreate('div'); - container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : ''); - //noinspection JSCheckFunctionSignatures - elData(container, 'input-id', elementId); - - var hasFocus = document.activeElement === element; - - // DOM manipulation causes focused element to lose focus - element.parentNode.insertBefore(container, element); - container.appendChild(element); - - if (hasFocus) { - element.focus(); - } - } - - container.classList.add('dropdown'); - var button = elCreate('span'); - button.className = 'button dropdownToggle inputPrefix'; - - var span = elCreate('span'); - span.textContent = Language.get('wcf.global.button.disabledI18n'); - - button.appendChild(span); - container.insertBefore(button, element); - - var dropdownMenu = elCreate('ul'); - dropdownMenu.className = 'dropdownMenu'; - DomUtil.insertAfter(dropdownMenu, button); - - var callbackClick = (function(event, isInit) { - var languageId = ~~elData(event.currentTarget, 'language-id'); - - var activeItem = DomTraverse.childByClass(dropdownMenu, 'active'); - if (activeItem !== null) activeItem.classList.remove('active'); - - if (languageId) event.currentTarget.classList.add('active'); - - this._select(elementId, languageId, isInit || false); - }).bind(this); - - // build language dropdown - var listItem; - for (var languageId in availableLanguages) { - if (availableLanguages.hasOwnProperty(languageId)) { - listItem = elCreate('li'); - elData(listItem, 'language-id', languageId); - - span = elCreate('span'); - span.textContent = availableLanguages[languageId]; - - listItem.appendChild(span); - listItem.addEventListener('click', callbackClick); - dropdownMenu.appendChild(listItem); - } - } - - if (forceSelection !== true) { - listItem = elCreate('li'); - listItem.className = 'dropdownDivider'; - dropdownMenu.appendChild(listItem); - - listItem = elCreate('li'); - elData(listItem, 'language-id', 0); - span = elCreate('span'); - span.textContent = Language.get('wcf.global.button.disabledI18n'); - listItem.appendChild(span); - listItem.addEventListener('click', callbackClick); - dropdownMenu.appendChild(listItem); - } - - var activeItem = null; - if (forceSelection === true || values.size) { - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - //noinspection JSUnresolvedVariable - if (~~elData(dropdownMenu.children[i], 'language-id') === LANGUAGE_ID) { - activeItem = dropdownMenu.children[i]; - break; - } - } - } - - UiSimpleDropdown.init(button); - UiSimpleDropdown.registerCallback(container.id, _callbackDropdownToggle); - - _elements.set(elementId, { - buttonLabel: button.children[0], - callbacks: new Dictionary(), - element: element, - languageId: 0, - isEnabled: true, - forceSelection: forceSelection - }); - - // bind to submit event - var submit = DomTraverse.parentByTag(element, 'FORM'); - if (submit !== null) { - submit.addEventListener('submit', _callbackSubmit); - - var elementIds = _forms.get(submit); - if (elementIds === undefined) { - elementIds = []; - _forms.set(submit, elementIds); - } - - elementIds.push(elementId); - } - - if (activeItem !== null) { - callbackClick({ currentTarget: activeItem }, true); - } - }, - - /** - * Selects a language or non-i18n from the dropdown list. - * - * @param {string} elementId input element id - * @param {int} languageId language id or `0` to disable i18n - * @param {boolean} isInit triggers pre-selection on init - */ - _select: function(elementId, languageId, isInit) { - var data = _elements.get(elementId); - - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(data.element.closest('.inputAddon').id); - var item, label = ''; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - - var itemLanguageId = elData(item, 'language-id'); - if (itemLanguageId.length && languageId === ~~itemLanguageId) { - label = item.children[0].textContent; - } - } - - // save current value - if (data.languageId !== languageId) { - var values = _values.get(elementId); - - if (data.languageId) { - values.set(data.languageId, data.element.value); - } - - if (languageId === 0) { - _values.set(elementId, new Dictionary()); - } - else if (data.buttonLabel.classList.contains('active') || isInit === true) { - data.element.value = (values.has(languageId)) ? values.get(languageId) : ''; - } - - // update label - data.buttonLabel.textContent = label; - data.buttonLabel.classList[(languageId ? 'add' : 'remove')]('active'); - - data.languageId = languageId; - } - - if (!isInit) { - data.element.blur(); - data.element.focus(); - } - - if (data.callbacks.has('select')) { - data.callbacks.get('select')(data.element); - } - }, - - /** - * Callback for dropdowns being opened, flags items with a missing value for one or more languages. - * - * @param {string} containerId dropdown container id - * @param {string} action toggle action, can be `open` or `close` - */ - _dropdownToggle: function(containerId, action) { - if (action !== 'open') { - return; - } - - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(containerId); - var elementId = elData(elById(containerId), 'input-id'); - var data = _elements.get(elementId); - var values = _values.get(elementId); - - var item, languageId; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - languageId = ~~elData(item, 'language-id'); - - if (languageId) { - var hasMissingValue = false; - if (data.languageId) { - if (languageId === data.languageId) { - hasMissingValue = (data.element.value.trim() === ''); - } - else { - hasMissingValue = (!values.get(languageId)); - } - } - - item.classList[(hasMissingValue ? 'add' : 'remove')]('missingValue'); - } - } - }, - - /** - * Inserts hidden fields for i18n input on submit. - * - * @param {Object} event event object - */ - _submit: function(event) { - var elementIds = _forms.get(event.currentTarget); - - var data, elementId, input, values; - for (var i = 0, length = elementIds.length; i < length; i++) { - elementId = elementIds[i]; - data = _elements.get(elementId); - if (data.isEnabled) { - values = _values.get(elementId); - - if (data.callbacks.has('submit')) { - data.callbacks.get('submit')(data.element); - } - - // update with current value - if (data.languageId) { - values.set(data.languageId, data.element.value); - } - - if (values.size) { - values.forEach(function(value, languageId) { - input = elCreate('input'); - input.type = 'hidden'; - input.name = elementId + '_i18n[' + languageId + ']'; - input.value = value; - - event.currentTarget.appendChild(input); - }); - - // remove name attribute to enforce i18n values - data.element.removeAttribute('name'); - } - } - } - }, - - /** - * Returns the values of an input field. - * - * @param {string} elementId input element id - * @return {Dictionary} values stored for the different languages - */ - getValues: function(elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - - var values = _values.get(elementId); - - // update with current value - values.set(element.languageId, element.element.value); - - return values; - }, - - /** - * Sets the values of an input field. - * - * @param {string} elementId input element id - * @param {Dictionary} values values for the different languages - */ - setValues: function(elementId, values) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - - if (Core.isPlainObject(values)) { - values = Dictionary.fromObject(values); - } - - element.element.value = ''; - - if (values.has(0)) { - element.element.value = values.get(0); - values['delete'](0); - _values.set(elementId, values); - this._select(elementId, 0, true); - return; - } - - _values.set(elementId, values); - - element.languageId = 0; - //noinspection JSUnresolvedVariable - this._select(elementId, LANGUAGE_ID, true); - }, - - /** - * Disables the i18n interface for an input field. - * - * @param {string} elementId input element id - */ - disable: function(elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid element, '" + elementId + "' is not an i18n input field."); - } - - if (!element.isEnabled) return; - - element.isEnabled = false; - - // hide language dropdown - //noinspection JSCheckFunctionSignatures - elHide(element.buttonLabel.parentNode); - var dropdownContainer = element.buttonLabel.parentNode.parentNode; - dropdownContainer.classList.remove('inputAddon'); - dropdownContainer.classList.remove('dropdown'); - }, - - /** - * Enables the i18n interface for an input field. - * - * @param {string} elementId input element id - */ - enable: function(elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - - if (element.isEnabled) return; - - element.isEnabled = true; - - // show language dropdown - //noinspection JSCheckFunctionSignatures - elShow(element.buttonLabel.parentNode); - var dropdownContainer = element.buttonLabel.parentNode.parentNode; - dropdownContainer.classList.add('inputAddon'); - dropdownContainer.classList.add('dropdown'); - }, - - /** - * Returns true if i18n input is enabled for an input field. - * - * @param {string} elementId input element id - * @return {boolean} - */ - isEnabled: function(elementId) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - - return element.isEnabled; - }, - - /** - * Returns true if the value of an i18n input field is valid. - * - * If the element is disabled, true is returned. - * - * @param {string} elementId input element id - * @param {boolean} permitEmptyValue if true, input may be empty for all languages - * @return {boolean} true if input is valid - */ - validate: function(elementId, permitEmptyValue) { - var element = _elements.get(elementId); - if (element === undefined) { - throw new Error("Expected a valid i18n input element, '" + elementId + "' is not i18n input field."); - } - - if (!element.isEnabled) return true; - - var values = _values.get(elementId); - - var dropdownMenu = UiSimpleDropdown.getDropdownMenu(element.element.parentNode.id); - - if (element.languageId) { - values.set(element.languageId, element.element.value); - } - - var item, languageId; - var hasEmptyValue = false, hasNonEmptyValue = false; - for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { - item = dropdownMenu.children[i]; - languageId = ~~elData(item, 'language-id'); - - if (languageId) { - if (!values.has(languageId) || values.get(languageId).length === 0) { - // input has non-empty value for previously checked language - if (hasNonEmptyValue) { - return false; - } - - hasEmptyValue = true; - } - else { - // input has empty value for previously checked language - if (hasEmptyValue) { - return false; - } - - hasNonEmptyValue = true; - } - } - } - - return (!hasEmptyValue || permitEmptyValue); - } - }; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.ts new file mode 100644 index 0000000000..dd56ae193c --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Language/Input.ts @@ -0,0 +1,508 @@ +/** + * I18n interface for input and textarea fields. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Language/Input + */ + +import DomUtil from "../Dom/Util"; +import * as Language from "../Language"; +import { NotificationAction } from "../Ui/Dropdown/Data"; +import UiDropdownSimple from "../Ui/Dropdown/Simple"; +import * as StringUtil from "../StringUtil"; + +type LanguageId = number; + +interface I18nValues { + // languageID => value + [key: string]: string; +} + +interface Languages { + // languageID => languageName + [key: string]: string; +} + +type Values = Map; + +type InputOrTextarea = HTMLInputElement | HTMLTextAreaElement; + +type CallbackEvent = "select" | "submit"; +type Callback = (element: InputOrTextarea) => void; + +interface ElementData { + buttonLabel: HTMLElement; + callbacks: Map; + element: InputOrTextarea; + languageId: number; + isEnabled: boolean; + forceSelection: boolean; +} + +const _elements = new Map(); +const _forms = new WeakMap(); +const _values = new Map(); + +/** + * Sets up DOM and event listeners for an input field. + */ +function initElement( + elementId: string, + element: InputOrTextarea, + values: Values, + availableLanguages: Languages, + forceSelection: boolean, +): void { + let container = element.parentElement!; + if (!container.classList.contains("inputAddon")) { + container = document.createElement("div"); + container.className = "inputAddon"; + if (element.nodeName === "TEXTAREA") { + container.classList.add("inputAddonTextarea"); + } + container.dataset.inputId = elementId; + + const hasFocus = document.activeElement === element; + + // DOM manipulation causes focused element to lose focus + element.insertAdjacentElement("beforebegin", container); + container.appendChild(element); + + if (hasFocus) { + element.focus(); + } + } + + container.classList.add("dropdown"); + const button = document.createElement("span"); + button.className = "button dropdownToggle inputPrefix"; + + const buttonLabel = document.createElement("span"); + buttonLabel.textContent = Language.get("wcf.global.button.disabledI18n"); + + button.appendChild(buttonLabel); + container.insertBefore(button, element); + + const dropdownMenu = document.createElement("ul"); + dropdownMenu.className = "dropdownMenu"; + button.insertAdjacentElement("afterend", dropdownMenu); + + const callbackClick = (event: MouseEvent | HTMLElement): void => { + let target: HTMLElement; + if (event instanceof HTMLElement) { + target = event; + } else { + target = event.currentTarget as HTMLElement; + } + + const languageId = ~~target.dataset.languageId!; + + const activeItem = dropdownMenu.querySelector(".active"); + if (activeItem !== null) { + activeItem.classList.remove("active"); + } + + if (languageId) { + target.classList.add("active"); + } + + const isInit = event instanceof HTMLElement; + select(elementId, languageId, isInit); + }; + + // build language dropdown + Object.entries(availableLanguages).forEach(([languageId, languageName]) => { + const listItem = document.createElement("li"); + listItem.dataset.languageId = languageId; + + const span = document.createElement("span"); + span.textContent = languageName; + + listItem.appendChild(span); + listItem.addEventListener("click", callbackClick); + dropdownMenu.appendChild(listItem); + }); + + if (!forceSelection) { + const divider = document.createElement("li"); + divider.className = "dropdownDivider"; + dropdownMenu.appendChild(divider); + + const listItem = document.createElement("li"); + listItem.dataset.languageId = "0"; + listItem.addEventListener("click", callbackClick); + + const span = document.createElement("span"); + span.textContent = Language.get("wcf.global.button.disabledI18n"); + listItem.appendChild(span); + + dropdownMenu.appendChild(listItem); + } + + let activeItem: HTMLElement | undefined = undefined; + if (forceSelection || values.size) { + activeItem = Array.from(dropdownMenu.children).find((element: HTMLElement) => { + return +element.dataset.languageId! === window.LANGUAGE_ID; + }) as HTMLElement; + } + + UiDropdownSimple.init(button); + UiDropdownSimple.registerCallback(container.id, dropdownToggle); + + _elements.set(elementId, { + buttonLabel, + callbacks: new Map(), + element, + languageId: 0, + isEnabled: true, + forceSelection, + }); + + // bind to submit event + const form = element.closest("form"); + if (form !== null) { + form.addEventListener("submit", submit); + + let elementIds = _forms.get(form); + if (elementIds === undefined) { + elementIds = []; + _forms.set(form, elementIds); + } + + elementIds.push(elementId); + } + + if (activeItem) { + callbackClick(activeItem); + } +} + +/** + * Selects a language or non-i18n from the dropdown list. + */ +function select(elementId: string, languageId: number, isInit: boolean): void { + const data = _elements.get(elementId)!; + + const dropdownMenu = UiDropdownSimple.getDropdownMenu(data.element.closest(".inputAddon")!.id)!; + + const item = dropdownMenu.querySelector(`[data-language-id="${languageId}"]`); + const label = item ? item.textContent! : ""; + + // save current value + if (data.languageId !== languageId) { + const values = _values.get(elementId)!; + + if (data.languageId) { + values.set(data.languageId, data.element.value); + } + + if (languageId === 0) { + _values.set(elementId, new Map()); + } else if (data.buttonLabel.classList.contains("active") || isInit) { + data.element.value = values.get(languageId) || ""; + } + + // update label + data.buttonLabel.textContent = label; + data.buttonLabel.classList[languageId ? "add" : "remove"]("active"); + + data.languageId = languageId; + } + + if (!isInit) { + data.element.blur(); + data.element.focus(); + } + + if (data.callbacks.has("select")) { + data.callbacks.get("select")!(data.element); + } +} + +/** + * Callback for dropdowns being opened, flags items with a missing value for one or more languages. + */ +function dropdownToggle(containerId: string, action: NotificationAction): void { + if (action !== "open") { + return; + } + + const dropdownMenu = UiDropdownSimple.getDropdownMenu(containerId)!; + const container = document.getElementById(containerId)!; + const elementId = container.dataset.inputId!; + const data = _elements.get(elementId)!; + const values = _values.get(elementId)!; + + Array.from(dropdownMenu.children).forEach((item: HTMLElement) => { + const languageId = ~~(item.dataset.languageId || ""); + + if (languageId) { + let hasMissingValue = false; + if (data.languageId) { + if (languageId === data.languageId) { + hasMissingValue = data.element.value.trim() === ""; + } else { + hasMissingValue = !values.get(languageId); + } + } + + if (hasMissingValue) { + item.classList.add("missingValue"); + } else { + item.classList.remove("missingValue"); + } + } + }); +} + +/** + * Inserts hidden fields for i18n input on submit. + */ +function submit(event: Event): void { + const form = event.currentTarget as HTMLFormElement; + const elementIds = _forms.get(form)!; + + elementIds.forEach((elementId) => { + const data = _elements.get(elementId)!; + if (!data.isEnabled) { + return; + } + + const values = _values.get(elementId)!; + + if (data.callbacks.has("submit")) { + data.callbacks.get("submit")!(data.element); + } + + // update with current value + if (data.languageId) { + values.set(data.languageId, data.element.value); + } + + if (values.size) { + values.forEach(function (value, languageId) { + const input = document.createElement("input"); + input.type = "hidden"; + input.name = `${elementId}_i18n[${languageId}]`; + input.value = value; + + form.appendChild(input); + }); + + // remove name attribute to enforce i18n values + data.element.removeAttribute("name"); + } + }); +} + +/** + * Initializes an input field. + */ +export function init( + elementId: string, + values: I18nValues, + availableLanguages: Languages, + forceSelection: boolean, +): void { + if (_values.has(elementId)) { + return; + } + + const element = document.getElementById(elementId) as InputOrTextarea; + if (element === null) { + throw new Error(`Expected a valid element id, cannot find '${elementId}'.`); + } + + // unescape values + const unescapedValues = new Map(); + Object.entries(values).forEach(([languageId, value]) => { + unescapedValues.set(+languageId, StringUtil.unescapeHTML(value)); + }); + + _values.set(elementId, unescapedValues); + + initElement(elementId, element, unescapedValues, availableLanguages, forceSelection); +} + +/** + * Registers a callback for an element. + */ +export function registerCallback(elementId: string, eventName: CallbackEvent, callback: Callback): void { + if (!_values.has(elementId)) { + throw new Error(`Unknown element id '${elementId}'.`); + } + + _elements.get(elementId)!.callbacks.set(eventName, callback); +} + +/** + * Unregisters the element with the given id. + * + * @since 5.2 + */ +export function unregister(elementId: string): void { + if (!_values.has(elementId)) { + throw new Error(`Unknown element id '${elementId}'.`); + } + + _values.delete(elementId); + _elements.delete(elementId); +} + +/** + * Returns the values of an input field. + */ +export function getValues(elementId: string): Values { + const element = _elements.get(elementId)!; + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + + const values = _values.get(elementId)!; + + // update with current value + values.set(element.languageId, element.element.value); + + return values; +} + +/** + * Sets the values of an input field. + */ +export function setValues(elementId: string, newValues: Values | I18nValues): void { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + + element.element.value = ""; + + const values = new Map( + Object.entries(newValues).map(([languageId, value]) => { + return [+languageId, value]; + }), + ); + + if (values.has(0)) { + element.element.value = values.get(0)!; + values.delete(0); + + _values.set(elementId, values); + select(elementId, 0, true); + + return; + } + + _values.set(elementId, values); + + element.languageId = 0; + select(elementId, window.LANGUAGE_ID, true); +} + +/** + * Disables the i18n interface for an input field. + */ +export function disable(elementId: string): void { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid element, '${elementId}' is not an i18n input field.`); + } + + if (!element.isEnabled) { + return; + } + + element.isEnabled = false; + + // hide language dropdown + const buttonContainer = element.buttonLabel.parentElement!; + DomUtil.hide(buttonContainer); + const dropdownContainer = buttonContainer.parentElement!; + dropdownContainer.classList.remove("inputAddon", "dropdown"); +} + +/** + * Enables the i18n interface for an input field. + */ +export function enable(elementId: string): void { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + + if (element.isEnabled) { + return; + } + + element.isEnabled = true; + + // show language dropdown + const buttonContainer = element.buttonLabel.parentElement!; + DomUtil.show(buttonContainer); + const dropdownContainer = buttonContainer.parentElement!; + dropdownContainer.classList.add("inputAddon", "dropdown"); +} + +/** + * Returns true if i18n input is enabled for an input field. + */ +export function isEnabled(elementId: string): boolean { + const element = _elements.get(elementId); + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + + return element.isEnabled; +} + +/** + * Returns true if the value of an i18n input field is valid. + * + * If the element is disabled, true is returned. + */ +export function validate(elementId: string, permitEmptyValue: boolean): boolean { + const element = _elements.get(elementId)!; + if (element === undefined) { + throw new Error(`Expected a valid i18n input element, '${elementId}' is not i18n input field.`); + } + + if (!element.isEnabled) { + return true; + } + + const values = _values.get(elementId)!; + + const dropdownMenu = UiDropdownSimple.getDropdownMenu(element.element.parentElement!.id)!; + + if (element.languageId) { + values.set(element.languageId, element.element.value); + } + + let hasEmptyValue = false; + let hasNonEmptyValue = false; + Array.from(dropdownMenu.children).forEach((item: HTMLElement) => { + const languageId = ~~item.dataset.languageId!; + + if (languageId) { + if (!values.has(languageId) || values.get(languageId)!.length === 0) { + // input has non-empty value for previously checked language + if (hasNonEmptyValue) { + return false; + } + + hasEmptyValue = true; + } else { + // input has empty value for previously checked language + if (hasEmptyValue) { + return false; + } + + hasNonEmptyValue = true; + } + } + }); + + return !hasEmptyValue || permitEmptyValue; +} -- 2.20.1