From 2e4eb30e5a098ef551486d048fa8362a116fdc7a Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 9 Apr 2017 17:14:42 +0200 Subject: [PATCH] Smart recipient input widget (to be improved) --- skins/elastic/styles/styles.less | 31 +--- skins/elastic/styles/widgets/buttons.less | 3 + skins/elastic/styles/widgets/forms.less | 77 ++++++++++ skins/elastic/templates/compose.html | 12 +- skins/elastic/ui.js | 177 +++++++++++++++++++++- 5 files changed, 263 insertions(+), 37 deletions(-) create mode 100644 skins/elastic/styles/widgets/forms.less diff --git a/skins/elastic/styles/styles.less b/skins/elastic/styles/styles.less index 400c8b5a2..b76451dd4 100644 --- a/skins/elastic/styles/styles.less +++ b/skins/elastic/styles/styles.less @@ -179,36 +179,6 @@ html.iframe body { } -/* TODO: review when decided what css framework to use */ -table.propform { - width: 100%; - - .row { - margin-right: 0; /* without these the form is too wide causing horizontal scrollbar appearence */ - margin-left: 0; - } -} - -/* Some common icons for "iconized inputs" */ -.input-group-addon.icon { - - &:before { - &:extend(.font-icon-class); - margin: 0; - line-height: 1; - } - &.user:before { - content: @fa-var-user; - } - &.pass:before { - content: @fa-var-lock; - } - &.host:before { - content: @fa-var-home; - } -} - - /*** Widgets ***/ @import "widgets/buttons.less"; @@ -218,6 +188,7 @@ table.propform { @import "widgets/toolbar.less"; @import "widgets/searchbar.less"; @import "widgets/lists.less"; +@import "widgets/forms.less"; @import "widgets/messagebody.less"; @import "widgets/googiespell.less"; diff --git a/skins/elastic/styles/widgets/buttons.less b/skins/elastic/styles/widgets/buttons.less index 0a26a0147..dea7e7c4a 100644 --- a/skins/elastic/styles/widgets/buttons.less +++ b/skins/elastic/styles/widgets/buttons.less @@ -79,6 +79,9 @@ a.button.icon { &.back-list-button:before { content: @fa-var-arrow-left; } + &.remove:before { + content: @fa-var-close; + } &.dropdown:before { content: @fa-var-caret-down; font-size: 1em; diff --git a/skins/elastic/styles/widgets/forms.less b/skins/elastic/styles/widgets/forms.less new file mode 100644 index 000000000..ae893c559 --- /dev/null +++ b/skins/elastic/styles/widgets/forms.less @@ -0,0 +1,77 @@ +/*** Common form elements style ***/ + + +/* TODO: review when decided what css framework to use */ +table.propform { + width: 100%; + + .row { + margin-right: 0; /* without these the form is too wide causing horizontal scrollbar appearence */ + margin-left: 0; + } +} + +/* Some common icons for "iconized inputs" */ +.input-group-addon.icon { + + &:before { + &:extend(.font-icon-class); + margin: 0; + line-height: 1; + } + &.user:before { + content: @fa-var-user; + } + &.pass:before { + content: @fa-var-lock; + } + &.host:before { + content: @fa-var-home; + } +} + + +td.editfield { width: 99%; /* TODO */ } + + +/*** Smart recipient input field ***/ + +.recipient-input { + min-height: 2.4em; +/* padding: 0.2em 0.5em; */ + + .recipient { + border: 1px solid #ddd; /* TODO */ + background-color: #f4f4f4; /* TODO */ + border-radius: 0.25em; + padding: 0 0.25em; + margin-right: 0.2em; + display: inline-block; + white-space: nowrap; + } + + .name { + max-width: 25em; + display: inline-block; + line-height: 1.1; + padding: 0.25em; + vertical-align: middle; + overflow: hidden; + text-overflow: ellipsis; + } + + .email { + text-indent: -5000rem; + display: inline-block; + width: 0; + } + + a.button.icon:before { + font-size: 0.9em; + float: initial; + display: inline-block; + width: 1em; + margin: 0; + cursor: pointer; + } +} diff --git a/skins/elastic/templates/compose.html b/skins/elastic/templates/compose.html index f9f7b90cb..72e61c8cc 100644 --- a/skins/elastic/templates/compose.html +++ b/skins/elastic/templates/compose.html @@ -101,7 +101,7 @@ - + @@ -112,10 +112,10 @@ - + - + @@ -123,7 +123,7 @@ - + @@ -134,7 +134,7 @@ - + @@ -145,7 +145,7 @@ - + diff --git a/skins/elastic/ui.js b/skins/elastic/ui.js index 47c41caa0..2e7617cff 100644 --- a/skins/elastic/ui.js +++ b/skins/elastic/ui.js @@ -144,6 +144,8 @@ function rcube_elastic_ui() register_frame_buttons(form_buttons); } } + + $('[data-recipient-input]').each(function() { recipient_input(this); }); }; /** @@ -1048,12 +1050,185 @@ function rcube_elastic_ui() 'class': 'button icon dropdown skip-content', 'data-popup': 'attachment-menu', }) - .append($('').text('Show options')) // TODO: Localize "Show options" below + .append($('').text('Show options')) // TODO: Localize "Show options" .appendTo(item); popup_init(button); } }; + + /** + * Replaces recipient input with content-editable element that uses "recipient boxes" + */ + function recipient_input(obj) + { + var input; + + var insert_recipient = function(name, email) { + var name_element = $('').attr({'class': 'name', contenteditable: false}) + .text(recipient_input_name(name || email)), + email_element = $('').attr({'class': 'email', contenteditable: false}) + .text(' <' + email + '>' + rcmail.env.recipients_separator), + // TODO: should the 'close' link have tabindex? + link = $('').attr({'class': 'button icon remove', contenteditable: false}) + .click(function() { $(this).parent().remove(); }), + last = input.children('span:last'), + recipient = $('') + .attr({ + 'class': 'recipient', + contenteditable: false, + title: name ? (name + ' <' + email + '>') : '' + }) + .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)); + } + }; + + // Puts cursor at proper place of the content editable element + var focus_func = function() { + var obj, range = document.createRange(); + + // if there's a text node, put cursor at the end of it + if (obj = $(input).contents().filter(function() { return this.nodeType == 3; }).last()[0]) { + 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 parse_func = function(e) { + // TODO: BUG: backspace removes all recipients in Chrome + // TODO: it is possible to put cursor between recipient boxes, we should block this + // TODO: in onkeyup add recipient element on separator character? + // TODO: selecting signatures can modify the original input, need to + // update the contentEditable element too + + // Note it can be also executed when autocomplete inserts a recipient + if (e.type.match(/^(change|paste|blur)$/)) { + var node, text, recipients = [], cloned = input.clone(); + + cloned.find('span').remove(); + text = cloned.text(); + recipients = recipient_input_parser(text); + + $.each(recipients, function() { + insert_recipient(this.name, this.email); + text = text.replace(this.text, ''); + }); + + 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)); + + // update original input + $(obj).val(input.text()); + } + + // fix cursor position + if (e.type != 'blur') { + focus_func(); + } + } + + // Backspace key can add
in Firefox + $('br[type=\"_moz\"]', this).remove(); + }; + + input = $('
') + .attr({contenteditable: true, tabindex: $(obj).attr('tabindex')}) + // todo aria attributes + .addClass('form-control recipient-input') + .on('paste change blur keyup', parse_func) + .on('focus', focus_func); + + $(obj).hide().after(input).on('focus', function() { input.focus(); }) + + setTimeout(function() { + var ac_props; + + // Copy and parse the value already set + input.text($(obj).val()).change(); + + if (rcmail.env.autocomplete_threads > 0) { + ac_props = { + threads: rcmail.env.autocomplete_threads, + sources: rcmail.env.autocomplete_sources + }; + } + + // Init autocompletion + rcmail.init_address_input_events(input, ac_props); + }, 5); + }; + + /** + * Parses recipient address input and extracts recipients from it + */ + function recipient_input_parser(text) + { + var recipients = [], + delim = rcmail.env.recipients_delimiter + ';', + address_rx_part = '(\\S+|("[^"]+"))@\\S+', + recipient_rx1 = new RegExp('(<' + address_rx_part + '>)'), + recipient_rx2 = new RegExp('(' + address_rx_part + ')'), + global_rx = /(?=\S)[^",;]*(?:"[^\\"]*(?:\\[,;\S][^\\"]*)*"[^",;]*)*/g, + matches = text.match(global_rx); + + $.each(matches || [], function() { + if (this.length && (recipient_rx1.test(this) || recipient_rx2.test(this))) { + var email = RegExp.$1, + name = $.trim(this.replace(email, '')); + + recipients.push({ + name: name, + email: email.replace(/(^<|>$)/g, ''), + text: this + }); + } + }); + + return recipients; + }; + + /** + * Generates HTML for a text adding