Smart recipient input widget (to be improved)

pull/5742/merge
Aleksander Machniak 8 years ago
parent 1d35d1a20c
commit 2e4eb30e5a

@ -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";

@ -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;

@ -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;
}
}

@ -101,7 +101,7 @@
<label for="_to"><roundcube:label name="to" /></label>
</td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="to" form="form" id="_to" cols="70" rows="1" tabindex="1" aria-required="true" />
<roundcube:object name="composeHeaders" part="to" form="form" id="_to" cols="70" rows="1" tabindex="1" aria-required="true" data-recipient-input="true" />
</td>
<td class="button">
<a href="#add-header" onclick="" class="button add-recipient" tabindex="1"><roundcube:label name="addheader" /></a>
@ -112,10 +112,10 @@
<label for="_cc"><roundcube:label name="cc" /></label>
</td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="1" />
<roundcube:object name="composeHeaders" part="cc" form="form" id="_cc" cols="70" rows="1" tabindex="1" data-recipient-input="true" />
</td>
<td class="button">
<a href="#delete" onclick="return UI.hide_header_row('cc');" class="button cancel" title="<roundcube:label name='delete' />" tabindex="3"><roundcube:label name="delete" /></a>
<a href="#delete" onclick="return UI.hide_header_row('cc');" class="button cancel" title="<roundcube:label name='delete' />" tabindex="1"><roundcube:label name="delete" /></a>
</td>
</tr>
<tr id="compose-bcc" class="hidden">
@ -123,7 +123,7 @@
<label for="_bcc"><roundcube:label name="bcc" /></label>
</td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="1" />
<roundcube:object name="composeHeaders" part="bcc" form="form" id="_bcc" cols="70" rows="1" tabindex="1" data-recipient-input="true" />
</td>
<td class="button">
<a href="#delete" onclick="return UI.hide_header_row('bcc');" class="button cancel" title="<roundcube:label name='delete' />" tabindex="1"><roundcube:label name="delete" /></a>
@ -134,7 +134,7 @@
<label for="_replyto"><roundcube:label name="replyto" /></label>
</td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="1" />
<roundcube:object name="composeHeaders" part="replyto" form="form" id="_replyto" size="70" tabindex="1" data-recipient-input="true" />
</td>
<td class="button">
<a href="#delete" onclick="return UI.hide_header_row('replyto');" class="button cancel" title="<roundcube:label name='delete' />" tabindex="1"><roundcube:label name="delete" /></a>
@ -145,7 +145,7 @@
<label for="_followupto"><roundcube:label name="followupto" /></label>
</td>
<td class="editfield">
<roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="1" />
<roundcube:object name="composeHeaders" part="followupto" form="form" id="_followupto" size="70" tabindex="1" data-recipient-input="true" />
</td>
<td class="button">
<a href="#delete" onclick="return UI.hide_header_row('followupto');" class="button cancel" title="<roundcube:label name='delete' />" tabindex="1"><roundcube:label name="delete" /></a>

@ -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($('<span class="inner">').text('Show options')) // TODO: Localize "Show options" below
.append($('<span class="inner">').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 = $('<span>').attr({'class': 'name', contenteditable: false})
.text(recipient_input_name(name || email)),
email_element = $('<span>').attr({'class': 'email', contenteditable: false})
.text(' <' + email + '>' + rcmail.env.recipients_separator),
// TODO: should the 'close' link have tabindex?
link = $('<a>').attr({'class': 'button icon remove', contenteditable: false})
.click(function() { $(this).parent().remove(); }),
last = input.children('span:last'),
recipient = $('<span>')
.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($('<br>').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 <br> 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 <br type="_moz"> in Firefox
$('br[type=\"_moz\"]', this).remove();
};
input = $('<div>')
.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 <span class="hidden">
* for quote/backslash characters, so it can be hidden from the user,
* but still in place to make copying simpler
*/
function recipient_input_name(text)
{
// TODO
return text;
};
}
var UI = new rcube_elastic_ui();

Loading…
Cancel
Save