From dbd9f59981dd1b9b5a21c65be7b349ffc93cbd90 Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Tue, 9 Jun 2015 17:11:16 +0200 Subject: [PATCH] Added `WoltLab/WCF/Language/Input` (I18n) and input suffixes --- com.woltlab.wcf/option.xml | 16 + .../files/acp/templates/integerOptionType.tpl | 11 +- .../multipleLanguageInputJavascript.tpl | 15 +- .../install/files/js/WoltLab/WCF/DOM/Util.js | 15 + .../files/js/WoltLab/WCF/Language/Input.js | 280 ++++++++++++++++++ .../js/WoltLab/WCF/UI/Dropdown/Simple.js | 12 +- wcfsetup/install/files/js/require.config.js | 1 + wcfsetup/install/files/style/dropdown.less | 26 ++ wcfsetup/install/files/style/form.less | 79 ++++- 9 files changed, 443 insertions(+), 12 deletions(-) create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js diff --git a/com.woltlab.wcf/option.xml b/com.woltlab.wcf/option.xml index 0fac46d002..918188fea6 100644 --- a/com.woltlab.wcf/option.xml +++ b/com.woltlab.wcf/option.xml @@ -538,12 +538,14 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]> 1800 600 86400 + seconds @@ -898,12 +902,14 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]> integer 96 + pixel @@ -946,6 +952,7 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]> integer 90 0 + days @@ -1070,6 +1077,7 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]> integer 0 0 + years @@ -1133,12 +1141,14 @@ retro:wcf.acp.option.gravatar_default_type.retro]]> integer 192 96 + pixel @@ -1148,6 +1158,7 @@ retro:wcf.acp.option.gravatar_default_type.retro]]> integer 150 0 + pixel @@ -1172,6 +1183,7 @@ retro:wcf.acp.option.gravatar_default_type.retro]]> integer 182 0 + days diff --git a/wcfsetup/install/files/acp/templates/integerOptionType.tpl b/wcfsetup/install/files/acp/templates/integerOptionType.tpl index 466d77b569..2d17d6300b 100644 --- a/wcfsetup/install/files/acp/templates/integerOptionType.tpl +++ b/wcfsetup/install/files/acp/templates/integerOptionType.tpl @@ -1 +1,10 @@ -minvalue !== null} min="{$option->minvalue}"{/if}{if $option->maxvalue !== null} max="{$option->maxvalue}"{/if}{if $inputClass} class="{@$inputClass}"{/if} /> \ No newline at end of file +{if $option->suffix} +
+{/if} + +minvalue !== null} min="{$option->minvalue}"{/if}{if $option->maxvalue !== null} max="{$option->maxvalue}"{/if}{if $inputClass} class="{@$inputClass}"{/if}> + +{if $option->suffix} + {lang}wcf.acp.option.suffix.{@$option->suffix}{/lang} +
+{/if} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl b/wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl index f59328760d..b78c765a37 100644 --- a/wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl +++ b/wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl @@ -1,11 +1,14 @@ {if $availableLanguages|count > 1} {/if} \ No newline at end of file diff --git a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js index 8efa0e855b..91680c179e 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js @@ -167,6 +167,21 @@ define([], function() { } }, + /** + * Inserts an element after an existing element. + * + * @param {Element} newEl element to insert + * @param {Element} el reference element + */ + insertAfter: function(newEl, el) { + if (el.nextElementSibling !== null) { + el.parentNode.insertBefore(newEl, el.nextElementSibling); + } + else { + el.parentNode.appendChild(newEl); + } + }, + /** * Applies a list of CSS properties to an element. * diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js b/wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js new file mode 100644 index 0000000000..0736f2a62e --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js @@ -0,0 +1,280 @@ +/** + * I18n interface for input and textarea fields. + * + * @author Alexander Ebert + * @copyright 2001-2015 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/Language/Input + */ +define(['Dictionary', 'Language', 'ObjectMap', 'StringUtil', 'DOM/Traverse', 'DOM/Util', 'UI/SimpleDropdown'], function(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 WoltLab/WCF/Language/Input + */ + var LanguageInput = { + /** + * 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 = document.getElementById(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); + }, + + /** + * 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 = document.createElement('div'); + container.className = 'inputAddon' + (element.nodeName === 'TEXTAREA' ? ' inputAddonTextarea' : ''); + container.setAttribute('data-input-id', elementId); + + element.parentNode.insertBefore(container, element); + container.appendChild(element); + } + + container.classList.add('dropdown'); + var button = document.createElement('span'); + button.className = 'button dropdownToggle inputPrefix'; + + var span = document.createElement('span'); + span.textContent = Language.get('wcf.global.button.disabledI18n'); + + button.appendChild(span); + container.insertBefore(button, element); + + var dropdownMenu = document.createElement('ul'); + dropdownMenu.className = 'dropdownMenu'; + DOMUtil.insertAfter(dropdownMenu, button); + + var callbackClick = (function(event, isInit) { + var languageId = ~~event.currentTarget.getAttribute('data-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, event.currentTarget.children[0].textContent, isInit || false); + }).bind(this); + + // build language dropdown + for (var languageId in availableLanguages) { + if (availableLanguages.hasOwnProperty(languageId)) { + var listItem = document.createElement('li'); + listItem.setAttribute('data-language-id', languageId); + + span = document.createElement('span'); + span.textContent = availableLanguages[languageId]; + + listItem.appendChild(span); + listItem.addEventListener('click', callbackClick); + dropdownMenu.appendChild(listItem); + } + } + + if (forceSelection !== true) { + var listItem = document.createElement('li'); + listItem.className = 'dropdownDivider'; + listItem.setAttribute('data-language-id', 0); + dropdownMenu.appendChild(listItem); + + listItem = document.createElement('li'); + span = document.createElement('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++) { + if (~~dropdownMenu.children[i].getAttribute('data-language-id') === LANGUAGE_ID) { + activeItem = dropdownMenu.children[i]; + break; + } + } + } + + UISimpleDropdown.init(button); + UISimpleDropdown.registerCallback(container.id, _callbackDropdownToggle); + + _elements.set(elementId, { + buttonLabel: button.children[0], + element: element, + languageId: 0 + }); + + // 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 {integer} languageId language id or `0` to disable i18n + * @param {string} label new dropdown label for selection + * @param {boolean} isInit triggers pre-selection on init + */ + _select: function(elementId, languageId, label, isInit) { + var data = _elements.get(elementId); + + // 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; + } + + data.element.blur(); + data.element.focus(); + }, + + /** + * 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 = document.getElementById(containerId).getAttribute('data-input-id'); + var values = _values.get(elementId); + + var item, languageId; + for (var i = 0, length = dropdownMenu.childElementCount; i < length; i++) { + item = dropdownMenu.children[i]; + languageId = ~~item.getAttribute('data-language-id'); + + if (languageId) { + item.classList[(values.has(languageId) || !values.size ? 'remove' : 'add')]('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); + values = _values.get(elementId); + + // update with current value + if (data.languageId) { + values.set(data.languageId, data.element.value); + } + + if (values.size) { + values.forEach(function(value, languageId) { + input = document.createElement('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'); + } + } + } + }; + + return LanguageInput; +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js index ef996ae0b5..1e8c4726af 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js @@ -14,6 +14,7 @@ define( var _availableDropdowns = null; var _callbacks = new CallbackList(); + var _didInit = false; var _dropdowns = new Dictionary(); var _menus = new Dictionary(); var _menuContainer = null; @@ -26,6 +27,9 @@ define( * Performs initial setup such as setting up dropdowns and binding listeners. */ setup: function() { + if (_didInit) return; + _didInit = true; + _menuContainer = document.createElement('div'); _menuContainer.setAttribute('id', 'dropdownMenuContainer'); document.body.appendChild(_menuContainer); @@ -59,6 +63,8 @@ define( * @param {boolean} isLazyInitialization */ init: function(button, isLazyInitialization) { + this.setup(); + if (button.classList.contains('jsDropdownEnabled') || button.getAttribute('data-target')) { return false; } @@ -103,11 +109,13 @@ define( * @param {Element} menu menu list element */ initFragment: function(dropdown, menu) { - var containerId = DOMUtil.identify(dropdown); + this.setup(); + if (_dropdowns.has(dropdown)) { - throw new Error("Dropdown identified by '" + DOMUtil.identify(dropdown) + "' has already been registered."); + return; } + var containerId = DOMUtil.identify(dropdown); _dropdowns.set(containerId, dropdown); _menuContainer.appendChild(menu); diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 40d94bb474..a0e8dbc821 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -25,6 +25,7 @@ requirejs.config({ 'Language': 'WoltLab/WCF/Language', 'List': 'WoltLab/WCF/List', 'ObjectMap': 'WoltLab/WCF/ObjectMap', + 'StringUtil': 'WoltLab/WCF/StringUtil', 'UI/Alignment': 'WoltLab/WCF/UI/Alignment', 'UI/CloseOverlay': 'WoltLab/WCF/UI/CloseOverlay', 'UI/Confirmation': 'WoltLab/WCF/UI/Confirmation', diff --git a/wcfsetup/install/files/style/dropdown.less b/wcfsetup/install/files/style/dropdown.less index c8755a647e..619a3eb7b7 100644 --- a/wcfsetup/install/files/style/dropdown.less +++ b/wcfsetup/install/files/style/dropdown.less @@ -4,6 +4,19 @@ outline: 0; } + &.inputAddon { + > .dropdownToggle { + padding: @wcfGapTiny; + + > span.active:after { + content: "\f0d7"; + font-family: FontAwesome; + font-size: 14px; + margin-left: 7px; + } + } + } + &.preInput { display: table; width: 100%; @@ -188,6 +201,19 @@ padding-top: 2px; } + &.missingValue > span { + position: relative; + + &:after { + color: @wcfWarningBackgroundColor; + content: @fa-var-exclamation-triangle; + font-family: FontAwesome; + position: absolute; + right: @wcfGapMedium; + top: @wcfGapTiny; + } + } + > a, > span { clear: both; diff --git a/wcfsetup/install/files/style/form.less b/wcfsetup/install/files/style/form.less index f1d8c57a1b..c908cb5ff5 100644 --- a/wcfsetup/install/files/style/form.less +++ b/wcfsetup/install/files/style/form.less @@ -209,6 +209,79 @@ dl:not(.plain) { } } +/* input prefix/suffix */ +.inputAddon { + display: flex; + + > .inputPrefix { + flex: 0 0 auto; + margin: 0 !important; + white-space: nowrap; + } + + > input { + border-radius: 0; + flex: 1 auto; + + &.tiny { + flex: 0 0 80px; + } + + &.short { + flex: 0 1 10%; + } + + &.medium { + flex: 0 1 30%; + } + + &:first-child { + border-radius: 3px 0 0 3px; + } + + &:last-child { + border-radius: 0 3px 3px 0; + } + } + + > .inputSuffix { + background-color: rgba(240, 240, 240, 1); + border: 1px solid @wcfContainerBorderColor; + border-left-width: 0; + border-radius: 0 3px 3px 0; + color: rgba(153, 153, 153, 1); + padding: @wcfGapTiny @wcfGapSmall; + } + + &.inputAddonTextarea { + flex-direction: column; + + > .inputPrefix { + align-self: flex-start; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + border-bottom-width: 0; + } + + > textarea { + border-top-left-radius: 0; + } + } + + &:not(.inputAddonTextarea) { + > .inputPrefix { + border-bottom-right-radius: 0; + border-right-width: 0; + border-top-right-radius: 0; + + &+ input { + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; + } + } + } +} + /* submit buttons */ .formSubmit { font-size: 0; @@ -406,16 +479,16 @@ select[multiple][disabled] { textarea { width: 100%; } - + .tiny { width: 80px; } - + .short { min-width: 80px; width: 10%; } - + .medium { min-width: 150px; width: 30%; -- 2.20.1