From 8826cd3fb51db6f3e94987b36068cbd865bf7bdc Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Mon, 25 Sep 2017 19:32:49 +0200 Subject: [PATCH] Rewrite recipient-input not using contentEditable elements --- skins/elastic/styles/colors.less | 1 + skins/elastic/styles/widgets/forms.less | 48 +++++- skins/elastic/ui.js | 209 ++++++++---------------- 3 files changed, 114 insertions(+), 144 deletions(-) diff --git a/skins/elastic/styles/colors.less b/skins/elastic/styles/colors.less index a22a75903..8fe020a8d 100644 --- a/skins/elastic/styles/colors.less +++ b/skins/elastic/styles/colors.less @@ -103,6 +103,7 @@ // Forms @color-input-border: rgba(0, 0, 0, 0.15); /* from Bootstrap's .form-control */ +@color-input-border-focus: #80bdff; /* from Bootstrap's .form-control:focus */ @color-input-addon-background: #e9ecef; /* from Bootstrap's .input-group-addon */ @color-recipient-input-border: @color-input-border; @color-recipient-input-background: #f4f4f4; diff --git a/skins/elastic/styles/widgets/forms.less b/skins/elastic/styles/widgets/forms.less index cd2d54eeb..7bad014b7 100644 --- a/skins/elastic/styles/widgets/forms.less +++ b/skins/elastic/styles/widgets/forms.less @@ -121,6 +121,19 @@ html.ms .propform { } } +@media screen and (max-width: @screen-width-mini) { + .formcontent { + .col-form-label { + flex: auto; + max-width: 100%; + } + .col-10 { + flex: auto; + max-width: 100%; + } + } +} + /* Some common icons for "iconized inputs" */ .input-group-addon.icon { @@ -240,23 +253,35 @@ td.editfield { width: 99%; /* TODO */ } /*** Smart recipient input field ***/ .recipient-input { - display: block !important; /* overwrites Bootstrap */ - min-height: 2.4em; - overflow: hidden; -/* padding: .2em .5em; */ + display: flex; + flex-wrap: wrap; + padding: .25rem .75rem .5rem .75rem; + + &.focus { + border-color: @color-input-border-focus; + } .recipient { + flex: 1; + display: flex; + position: relative; + max-width: 50%; border: 1px solid @color-recipient-input-border; background-color: @color-recipient-input-background; border-radius: .25em; padding: 0 .25em; + margin-top: .25rem; margin-right: .2em; - display: inline-block; white-space: nowrap; + + @media screen and (max-width: 450px) { + width: 100%; + max-width: 100%; + } } .name { - max-width: 25em; + flex-grow: 1; display: inline-block; line-height: 1.1; padding: .25em; @@ -280,6 +305,7 @@ td.editfield { width: 99%; /* TODO */ } a.button.icon { font-size: .8em; cursor: pointer; + line-height: 1.5; &:before { float: none; @@ -288,8 +314,18 @@ td.editfield { width: 99%; /* TODO */ } margin: 0; } } + + input { + flex-grow: 1; + min-width: 25%; + background: transparent !important; + border: 0 !important; + margin-top: .25rem; + outline: 0; + } } + /*** Skin selection widget ***/ .skinselection { diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js index 53393252c..f153839bf 100644 --- a/skins/elastic/ui.js +++ b/skins/elastic/ui.js @@ -1930,7 +1930,7 @@ function rcube_elastic_ui() $(this)[$(target).is(':visible') ? 'removeClass' : 'addClass']('active') .off().on('click', function() { - $(target).removeClass('hidden').find('.recipient-input').focus(); + $(target).removeClass('hidden').find('.recipient-input > input').focus(); }); }); }; @@ -1940,167 +1940,101 @@ function rcube_elastic_ui() */ function recipient_input(obj) { - var input, ac_props; - - var insert_recipient = function(name, email) { - var recipient = $(''), - last = input.children('span:last'), - name_element = $('').attr({'class': 'name', contenteditable: false}) - .html(recipient_input_name(name || email)), - email_element = $('').attr({'class': 'email', contenteditable: false}), - // TODO: should the 'close' link have tabindex? - link = $('').attr({'class': 'button icon remove', contenteditable: false}) - .click(function() { - recipient.remove(); - // update the original input - $(obj).val(input.text()); - // no need to propagate the event - return false; - }); - - if (name) { - email = ' <' + email + '>'; - } - - email_element.text((name ? email : '') + ','); - recipient.attr({'class': 'recipient', contenteditable: false, title: name ? (name + email) : null}) - .append([name_element, email_element, link]); - - if (last.length) { - (last).after(recipient); - } - else { - input.html('').append(recipient) - // contentEditable BR is required as a workaround for cursor issues in Chrome - .append($('
').attr('contenteditable', false)); - } - }; - - // get text input node from inside of the widget - var get_text_node = function() { - return $(input).contents().filter(function() { return this.nodeType == 3; }).last()[0]; - }; - - // Backspace key can add
in Firefox - // Puts cursor at proper place of the content editable element - var focus_func = function() { - var obj, range = document.createRange(); - - rcmail.env.focused_field = obj; - - // if there's a text node, put cursor at the end of it - if (obj = get_text_node()) { - range.setStart(obj, $(obj).text().length); - } - // else if there's
put the cursor before it - else if (obj = input.children('br:last')[0]) { - range.setStartBefore(obj); - } - // else if there's at least one recipient box put the cursor after the last one - else if (obj = input.children('span:last')[0]) { - range.setStartAfter(obj); - } - // else if there's any node, put the cursor after it - else if (obj = input.lastChild) { - range.setStartAfter(obj); - } - // else do nothing - else { - return; - } - - range.collapse(true); - var selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - }; - - var update_func = function() { - var node, text, recipients = [], cloned = input.clone(); - - cloned.find('span').remove(); - text = cloned.text().replace(/[,;\s]+$/, ''); - recipients = recipient_input_parser(text); - - $.each(recipients, function() { - insert_recipient(this.name, this.email); - text = text.replace(this.text, ''); - }); + var area, input, ac_props, + apply_func = function() { + // update the original input + $(obj).val(area.text()); + }, + focus_func = function() { + area.addClass('focus'); + }, + insert_recipient = function(name, email) { + var recipient = $(''), + name_element = $('').html(recipient_input_name(name || email)), + email_element = $('
').attr({'class': 'button icon remove'}) + .click(function() { + recipient.remove(); + apply_func(); + input.focus(); + return false; + }); - if (recipients.length) { - // update text node - text = $.trim(text.replace(/[,]{1,}/g, ',').replace(/(^,|,$)/g, '')); - $(input).contents().each(function() { if (this.nodeType == 3) $(this).remove(); }); - input.children('span:last').after(document.createTextNode(text)); - } + if (name) { + email = ' <' + email + '>'; + } - return recipients.length > 0; - }; + email_element.text((name ? email : '') + ','); + recipient.attr('title', name ? (name + email) : null) + .append([name_element, email_element, link]) + .insertBefore(input); + }, + update_func = function() { + var text = input.val().replace(/[,;\s]+$/, ''), + recipients = recipient_input_parser(text); - var parse_func = function(e) { - // Note it can be also executed when autocomplete inserts a recipient - update_func(); + $.each(recipients, function() { + insert_recipient(this.name, this.email); + }); - // update the original input - $(obj).val(input.text()); + input.val(''); + apply_func(); - // fix cursor position - if (e.type != 'blur') { - focus_func(); - } - }; + return recipients.length > 0; + }, + parse_func = function(e) { + // Note it can be also executed when autocomplete inserts a recipient + update_func(); - var keydown_func = function(e) { - // Backspace removes all recipients in Chrome, but in Firefox - // it does nothing. We'll consistently remove the last recipient - if (e.keyCode == 8) { - // check if we're on the far left side of the text entry node - var node = get_text_node(), selection = window.getSelection(); - if ((node == selection.focusNode && !selection.focusOffset) || selection.anchorNode == input[0]) { - input.children('span:last').remove(); - // update the original input - $(obj).val(input.text()); - focus_func(); - return false; + if (e.type == 'blur') { + area.removeClass('focus'); } - } - // Here we add a recipient box when the separator character (,;) was pressed - else if (e.keyCode == 188 || e.keyCode == 59) { - if (update_func()) { - focus_func(); + }, + keydown_func = function(e) { + // On Backspace remove the last recipient + if (e.keyCode == 8 && !input.val().length) { + area.children('span.recipient:last').remove(); + apply_func(); return false; } - } - }; + // Here we add a recipient box when the separator character (,;) was pressed + else if (e.key == ',' || e.key == ';') { + if (update_func()) { + return false; + } + } + }; // Create the content-editable div - input = $('
') - .attr({contenteditable: true, tabindex: $(obj).attr('tabindex')}) - .addClass('form-control recipient-input') + input = $('').attr({type: 'text', tabindex: $(obj).attr('tabindex')}) .on('paste change blur', parse_func) .on('keydown', keydown_func) - .on('focus mousedown', focus_func) - .on('keyup', function() { - // Backspace key can add
in Firefox - // TODO: this fixes that, but causes input height jump effect - $('br[type=\"_moz\"]', this).remove(); - }); + .on('focus mousedown', focus_func); + + area = $('
').addClass('form-control recipient-input') + .append(input) + .on('click', function() { input.focus(); }); // "Replace" the original input/textarea with the content-editable div // Note: we do not remove the original element, and we do not use // display: none, because we want to handle onfocus event // Note: tabindex:-1 to make Shift+TAB working on these widgets $(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'}) - .attr('tabindex', -1).after(input) + .attr('tabindex', -1) + .after(area) // some core code sometimes focuses or changes the original node // in such cases we wan't to parse it's value and apply changes // to the widget element .on('focus', function(e) { input.focus(); }) - .on('change', function(e) { input.text(this.value).change(); }); + .on('change', function(e) { + $('span.recipient', area).remove(); + input.val(this.value).change(); + }); - // this one line is here to fix border of Bootstrap's input-group + // this one line is here to fix border of Bootstrap's input-group, // input-group should not contain any hidden elements - $(obj).detach().insertBefore(input.parent()); + $(obj).detach().insertBefore(area.parent()); // Copy and parse the value already set input.text($(obj).val()).change(); @@ -2305,7 +2239,6 @@ function rcube_elastic_ui() $(this).tab('show'); if (is_table) { - console.log(sw[0]); sw.prop('checked', is_html); } }