Rewrite recipient-input not using contentEditable elements

pull/5742/merge
Aleksander Machniak 7 years ago
parent 757423373d
commit 8826cd3fb5

@ -103,6 +103,7 @@
// Forms // Forms
@color-input-border: rgba(0, 0, 0, 0.15); /* from Bootstrap's .form-control */ @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-input-addon-background: #e9ecef; /* from Bootstrap's .input-group-addon */
@color-recipient-input-border: @color-input-border; @color-recipient-input-border: @color-input-border;
@color-recipient-input-background: #f4f4f4; @color-recipient-input-background: #f4f4f4;

@ -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" */ /* Some common icons for "iconized inputs" */
.input-group-addon.icon { .input-group-addon.icon {
@ -240,23 +253,35 @@ td.editfield { width: 99%; /* TODO */ }
/*** Smart recipient input field ***/ /*** Smart recipient input field ***/
.recipient-input { .recipient-input {
display: block !important; /* overwrites Bootstrap */ display: flex;
min-height: 2.4em; flex-wrap: wrap;
overflow: hidden; padding: .25rem .75rem .5rem .75rem;
/* padding: .2em .5em; */
&.focus {
border-color: @color-input-border-focus;
}
.recipient { .recipient {
flex: 1;
display: flex;
position: relative;
max-width: 50%;
border: 1px solid @color-recipient-input-border; border: 1px solid @color-recipient-input-border;
background-color: @color-recipient-input-background; background-color: @color-recipient-input-background;
border-radius: .25em; border-radius: .25em;
padding: 0 .25em; padding: 0 .25em;
margin-top: .25rem;
margin-right: .2em; margin-right: .2em;
display: inline-block;
white-space: nowrap; white-space: nowrap;
@media screen and (max-width: 450px) {
width: 100%;
max-width: 100%;
}
} }
.name { .name {
max-width: 25em; flex-grow: 1;
display: inline-block; display: inline-block;
line-height: 1.1; line-height: 1.1;
padding: .25em; padding: .25em;
@ -280,6 +305,7 @@ td.editfield { width: 99%; /* TODO */ }
a.button.icon { a.button.icon {
font-size: .8em; font-size: .8em;
cursor: pointer; cursor: pointer;
line-height: 1.5;
&:before { &:before {
float: none; float: none;
@ -288,8 +314,18 @@ td.editfield { width: 99%; /* TODO */ }
margin: 0; margin: 0;
} }
} }
input {
flex-grow: 1;
min-width: 25%;
background: transparent !important;
border: 0 !important;
margin-top: .25rem;
outline: 0;
}
} }
/*** Skin selection widget ***/ /*** Skin selection widget ***/
.skinselection { .skinselection {

@ -1930,7 +1930,7 @@ function rcube_elastic_ui()
$(this)[$(target).is(':visible') ? 'removeClass' : 'addClass']('active') $(this)[$(target).is(':visible') ? 'removeClass' : 'addClass']('active')
.off().on('click', function() { .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) function recipient_input(obj)
{ {
var input, ac_props; var area, input, ac_props,
apply_func = function() {
var insert_recipient = function(name, email) { // update the original input
var recipient = $('<span>'), $(obj).val(area.text());
last = input.children('span:last'), },
name_element = $('<span>').attr({'class': 'name', contenteditable: false}) focus_func = function() {
.html(recipient_input_name(name || email)), area.addClass('focus');
email_element = $('<span>').attr({'class': 'email', contenteditable: false}), },
// TODO: should the 'close' link have tabindex? insert_recipient = function(name, email) {
link = $('<a>').attr({'class': 'button icon remove', contenteditable: false}) var recipient = $('<span class="recipient">'),
.click(function() { name_element = $('<span class="name">').html(recipient_input_name(name || email)),
recipient.remove(); email_element = $('<span class="email">'),
// update the original input // TODO: should the 'close' link have tabindex?
$(obj).val(input.text()); link = $('<a>').attr({'class': 'button icon remove'})
// no need to propagate the event .click(function() {
return false; recipient.remove();
}); apply_func();
input.focus();
if (name) { return false;
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($('<br>').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 <br type="_moz"> 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 <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 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, '');
});
if (recipients.length) { if (name) {
// update text node email = ' <' + email + '>';
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));
}
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) { $.each(recipients, function() {
// Note it can be also executed when autocomplete inserts a recipient insert_recipient(this.name, this.email);
update_func(); });
// update the original input input.val('');
$(obj).val(input.text()); apply_func();
// fix cursor position return recipients.length > 0;
if (e.type != 'blur') { },
focus_func(); parse_func = function(e) {
} // Note it can be also executed when autocomplete inserts a recipient
}; update_func();
var keydown_func = function(e) { if (e.type == 'blur') {
// Backspace removes all recipients in Chrome, but in Firefox area.removeClass('focus');
// 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;
} }
} },
// Here we add a recipient box when the separator character (,;) was pressed keydown_func = function(e) {
else if (e.keyCode == 188 || e.keyCode == 59) { // On Backspace remove the last recipient
if (update_func()) { if (e.keyCode == 8 && !input.val().length) {
focus_func(); area.children('span.recipient:last').remove();
apply_func();
return false; 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 // Create the content-editable div
input = $('<div>') input = $('<input>').attr({type: 'text', tabindex: $(obj).attr('tabindex')})
.attr({contenteditable: true, tabindex: $(obj).attr('tabindex')})
.addClass('form-control recipient-input')
.on('paste change blur', parse_func) .on('paste change blur', parse_func)
.on('keydown', keydown_func) .on('keydown', keydown_func)
.on('focus mousedown', focus_func) .on('focus mousedown', focus_func);
.on('keyup', function() {
// Backspace key can add <br type="_moz"> in Firefox area = $('<div>').addClass('form-control recipient-input')
// TODO: this fixes that, but causes input height jump effect .append(input)
$('br[type=\"_moz\"]', this).remove(); .on('click', function() { input.focus(); });
});
// "Replace" the original input/textarea with the content-editable div // "Replace" the original input/textarea with the content-editable div
// Note: we do not remove the original element, and we do not use // Note: we do not remove the original element, and we do not use
// display: none, because we want to handle onfocus event // display: none, because we want to handle onfocus event
// Note: tabindex:-1 to make Shift+TAB working on these widgets // Note: tabindex:-1 to make Shift+TAB working on these widgets
$(obj).css({position: 'absolute', opacity: 0, left: '-5000px', width: '10px'}) $(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 // some core code sometimes focuses or changes the original node
// in such cases we wan't to parse it's value and apply changes // in such cases we wan't to parse it's value and apply changes
// to the widget element // to the widget element
.on('focus', function(e) { input.focus(); }) .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 // 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 // Copy and parse the value already set
input.text($(obj).val()).change(); input.text($(obj).val()).change();
@ -2305,7 +2239,6 @@ function rcube_elastic_ui()
$(this).tab('show'); $(this).tab('show');
if (is_table) { if (is_table) {
console.log(sw[0]);
sw.prop('checked', is_html); sw.prop('checked', is_html);
} }
} }

Loading…
Cancel
Save