Added `WoltLab/WCF/Language/Input` (I18n) and input suffixes
authorAlexander Ebert <ebert@woltlab.com>
Tue, 9 Jun 2015 15:11:16 +0000 (17:11 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Tue, 9 Jun 2015 15:11:16 +0000 (17:11 +0200)
com.woltlab.wcf/option.xml
wcfsetup/install/files/acp/templates/integerOptionType.tpl
wcfsetup/install/files/acp/templates/multipleLanguageInputJavascript.tpl
wcfsetup/install/files/js/WoltLab/WCF/DOM/Util.js
wcfsetup/install/files/js/WoltLab/WCF/Language/Input.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLab/WCF/UI/Dropdown/Simple.js
wcfsetup/install/files/js/require.config.js
wcfsetup/install/files/style/dropdown.less
wcfsetup/install/files/style/form.less

index 0fac46d00273ce603272656dd47cb2da6510a2fe..918188fea693ead7a6b3511891dd8de66c5a23f7 100644 (file)
@@ -538,12 +538,14 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                                <defaultvalue>1800</defaultvalue>
                                <minvalue>600</minvalue>
                                <maxvalue>86400</maxvalue>
+                               <suffix>seconds</suffix>
                        </option>
                        <option name="user_online_timeout">
                                <categoryname>security.general.session</categoryname>
                                <optiontype>integer</optiontype>
                                <defaultvalue>900</defaultvalue>
                                <minvalue>1</minvalue>
+                               <suffix>seconds</suffix>
                        </option>
                        <option name="session_validate_ip_address">
                                <categoryname>security.general.session</categoryname>
@@ -575,6 +577,7 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                                <defaultvalue>7200</defaultvalue>
                                <minvalue>300</minvalue>
                                <maxvalue>86400</maxvalue>
+                               <suffix>seconds</suffix>
                        </option>
                        <option name="user_authentication_failure_ip_captcha">
                                <categoryname>security.general.authentication</categoryname>
@@ -600,6 +603,7 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                                <defaultvalue>30</defaultvalue>
                                <minvalue>1</minvalue>
                                <maxvalue>365</maxvalue>
+                               <suffix>days</suffix>
                        </option>
                        <!-- /security.general.authentication -->
                        
@@ -898,12 +902,14 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]></enableoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue><![CDATA[210]]></defaultvalue>
                                <minvalue>96</minvalue>
+                               <suffix>pixel</suffix>
                        </option>
                        <option name="attachment_thumbnail_width">
                                <categoryname>message.attachment</categoryname>
                                <optiontype>integer</optiontype>
                                <defaultvalue><![CDATA[280]]></defaultvalue>
                                <minvalue>96</minvalue>
+                               <suffix>pixel</suffix>
                        </option>
                        
                        <!-- message.general -->
@@ -946,6 +952,7 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]></enableoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>90</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>days</suffix>
                        </option>
                        <!-- /message.general.edit -->
                        
@@ -1070,6 +1077,7 @@ redis:cache_source_redis_host,!cache_source_memcached_host]]></enableoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>0</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>years</suffix>
                        </option>
                        <!-- /user.register -->
                        
@@ -1133,12 +1141,14 @@ retro:wcf.acp.option.gravatar_default_type.retro]]></selectoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>192</defaultvalue>
                                <minvalue>96</minvalue>
+                               <suffix>pixel</suffix>
                        </option>
                        <option name="max_avatar_height">
                                <categoryname>user.avatar</categoryname>
                                <optiontype>integer</optiontype>
                                <defaultvalue>192</defaultvalue>
                                <minvalue>96</minvalue>
+                               <suffix>pixel</suffix>
                        </option>
                        <!-- /user.avatar -->
                        
@@ -1148,6 +1158,7 @@ retro:wcf.acp.option.gravatar_default_type.retro]]></selectoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>150</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>pixel</suffix>
                        </option>
                        <!-- /user.signature -->
                        
@@ -1172,6 +1183,7 @@ retro:wcf.acp.option.gravatar_default_type.retro]]></selectoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>182</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>days</suffix>
                        </option>
                        <option name="profile_enable_visitors">
                                <categoryname>user.profile</categoryname>
@@ -1238,6 +1250,7 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>0</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>seconds</suffix>
                        </option>
                        <option name="users_online_record_no_guests">
                                <categoryname>user.list.online</categoryname>
@@ -1269,18 +1282,21 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <optiontype>integer</optiontype>
                                <defaultvalue>14</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>days</suffix>
                        </option>
                        <option name="user_cleanup_activity_event_lifetime">
                                <categoryname>user.cleanup</categoryname>
                                <optiontype>integer</optiontype>
                                <defaultvalue>60</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>days</suffix>
                        </option>
                        <option name="user_cleanup_profile_visitor_lifetime">
                                <categoryname>user.cleanup</categoryname>
                                <optiontype>integer</optiontype>
                                <defaultvalue>60</defaultvalue>
                                <minvalue>0</minvalue>
+                               <suffix>days</suffix>
                        </option>
                        <!-- /user.cleanup -->
                        
index 466d77b569d8873fdeaa3e032d438bed41bcd5fe..2d17d6300befee322bb47978014d49132ee4a315 100644 (file)
@@ -1 +1,10 @@
-<input type="number" id="{$option->optionName}" name="values[{$option->optionName}]" value="{$value}"{if $option->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}
+       <div class="inputAddon">
+{/if}
+
+<input type="number" id="{$option->optionName}" name="values[{$option->optionName}]" value="{$value}"{if $option->minvalue !== null} min="{$option->minvalue}"{/if}{if $option->maxvalue !== null} max="{$option->maxvalue}"{/if}{if $inputClass} class="{@$inputClass}"{/if}>
+
+{if $option->suffix}
+               <span class="inputSuffix">{lang}wcf.acp.option.suffix.{@$option->suffix}{/lang}</span>
+       </div>
+{/if}
\ No newline at end of file
index f59328760d2fd65b0c6a705853bf6c20c818052f..b78c765a37eceb92012f1825ba9ce5a8ea749819 100644 (file)
@@ -1,11 +1,14 @@
 {if $availableLanguages|count > 1}
        <script data-relocate="true">
-               //<![CDATA[
-               $(function() {
-                       var $availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{@$languageID}: '{$languageName}'{/implode} };
-                       var $values = { {implode from=$i18nValues[$elementIdentifier] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
-                       new WCF.MultipleLanguageInput('{@$elementIdentifier}', {if $forceSelection}true{else}false{/if}, $values, $availableLanguages);
+               require(['Language', 'WoltLab/WCF/Language/Input'], function(Language, LanguageInput) {
+                       Language.addObject({
+                               'wcf.global.button.disabledI18n': '{lang}wcf.global.button.disabledI18n{/lang}'
+                       });
+                       
+                       var availableLanguages = { {implode from=$availableLanguages key=languageID item=languageName}{@$languageID}: '{$languageName}'{/implode} };
+                       var values = { {implode from=$i18nValues[$elementIdentifier] key=languageID item=value}'{@$languageID}': '{$value}'{/implode} };
+                       
+                       LanguageInput.init('{@$elementIdentifier}', values, availableLanguages, {if $forceSelection}true{else}false{/if});
                });
-               //]]>
        </script>
 {/if}
\ No newline at end of file
index 8efa0e855b5e1935453b2f17d780130bbe9da8d6..91680c179ecfdf9719c454745a0ba01c3514e2f4 100644 (file)
@@ -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 (file)
index 0000000..0736f2a
--- /dev/null
@@ -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 <http://opensource.org/licenses/lgpl-license.php>
+ * @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<integer, string>}       values                  preset values per language id
+                * @param       {object<integer, string>}       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<integer, string>}       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;
+});
index ef996ae0b5ff7efe3c463649591a4fdf8f4cb1e2..1e8c4726af584ca96bc6bcf6d20c0d2efc60c3d2 100644 (file)
@@ -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);
                        
index 40d94bb474cfd2b8656398b2b1a28e72f1e07bb4..a0e8dbc821ac2be986bbcc51f7c25fed8830ea5c 100644 (file)
@@ -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',
index c8755a647e4420a88e26d375bd704a5cbac66b81..619a3eb7b7f033ddb85f5f75ece8ddc2b51e41ef 100644 (file)
@@ -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%;
                        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;
index f1d8c57a1b2fa31a1ccce6c21402f48ee57dd05f..c908cb5ff5d4a22f4a99a8e98a04f2380fa37d1e 100644 (file)
@@ -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%;