summaryrefslogtreecommitdiff
path: root/addons/mail/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/mail/static')
-rw-r--r--addons/mail/static/src/js/activity.js2
-rw-r--r--addons/mail/static/src/js/chat_window.js6
-rw-r--r--addons/mail/static/src/js/chatter.js4
-rw-r--r--addons/mail/static/src/js/chatter_composer.js3
-rw-r--r--addons/mail/static/src/js/composer.js262
-rw-r--r--addons/mail/static/src/js/discuss.js1
-rw-r--r--addons/mail/static/src/js/document_viewer.js16
-rw-r--r--addons/mail/static/src/js/services/chat_manager.js10
-rw-r--r--addons/mail/static/src/js/thread.js1
-rw-r--r--addons/mail/static/src/scss/composer.scss91
-rw-r--r--addons/mail/static/src/scss/extended_chat_window.scss8
-rw-r--r--addons/mail/static/src/xml/composer.xml12
-rw-r--r--addons/mail/static/src/xml/thread.xml2
-rw-r--r--addons/mail/static/tests/chat_window_tests.js78
-rw-r--r--addons/mail/static/tests/chatter_tests.js19
-rw-r--r--addons/mail/static/tests/discuss_tests.js9
16 files changed, 265 insertions, 259 deletions
diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js
index 4bb81489789..fce7eb29f6f 100644
--- a/addons/mail/static/src/js/activity.js
+++ b/addons/mail/static/src/js/activity.js
@@ -32,6 +32,7 @@ function _readActivities(self, ids) {
model: 'mail.activity',
method: 'read',
args: [ids],
+ context: (self.record && self.record.getContext()) || self.getSession().user_context,
}).then(function (activities) {
// convert create_date and date_deadline to moments
_.each(activities, function (activity) {
@@ -108,6 +109,7 @@ var AbstractActivityField = AbstractField.extend({
method: 'action_feedback',
args: [[id]],
kwargs: {feedback: feedback},
+ context: this.record.getContext(),
});
},
_scheduleActivity: function (id, previous_activity_type_id, callback) {
diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js
index 9f09eb390b0..9e080f3a248 100644
--- a/addons/mail/static/src/js/chat_window.js
+++ b/addons/mail/static/src/js/chat_window.js
@@ -16,7 +16,8 @@ var HEIGHT_FOLDED = '34px';
return Widget.extend({
template: "mail.ChatWindow",
custom_events: {
- escape_pressed: '_onEscapePressed'
+ escape_pressed: '_onEscapePressed',
+ document_viewer_closed: '_onDocumentViewerClose',
},
events: {
'click .o_chat_composer': '_onComposerClick',
@@ -182,6 +183,9 @@ return Widget.extend({
}
this.focus_input();
},
+ _onDocumentViewerClose: function (ev) {
+ this.focus_input();
+ },
/**
* @private
*/
diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js
index dfc041325d9..e48a7c60028 100644
--- a/addons/mail/static/src/js/chatter.js
+++ b/addons/mail/static/src/js/chatter.js
@@ -184,7 +184,7 @@ var Chatter = Widget.extend({
input_baseline: 14,
is_log: options && options.is_log,
record_name: this.record_name,
- default_body: old_composer && old_composer.$input && old_composer.$input.html(),
+ default_body: old_composer && old_composer.$input && old_composer.$input.val(),
default_mention_selections: old_composer && old_composer.mention_get_listener_selections(),
});
this.composer.on('input_focused', this, function () {
@@ -247,7 +247,7 @@ var Chatter = Widget.extend({
}).always(function () {
// disable widgets in create mode, otherwise enable
self.isCreateMode ? self._disableChatter() : self._enableChatter();
- $spinner.remove;
+ $spinner.remove();
});
},
/**
diff --git a/addons/mail/static/src/js/chatter_composer.js b/addons/mail/static/src/js/chatter_composer.js
index 7365b89e19e..da85f331863 100644
--- a/addons/mail/static/src/js/chatter_composer.js
+++ b/addons/mail/static/src/js/chatter_composer.js
@@ -46,7 +46,6 @@ var ChatterComposer = composer.BasicComposer.extend({
message = _.extend(message, {
subtype: 'mail.mt_comment',
message_type: 'comment',
- content_subtype: 'html',
context: self.context,
});
@@ -213,7 +212,7 @@ var ChatterComposer = composer.BasicComposer.extend({
recipient_done.then(function (partner_ids) {
var context = {
default_parent_id: self.id,
- default_body: utils.get_text2html(self.$input.html()),
+ default_body: utils.get_text2html(self.$input.val()),
default_attachment_ids: _.pluck(self.get('attachment_ids'), 'id'),
default_partner_ids: partner_ids,
default_is_log: self.options.is_log,
diff --git a/addons/mail/static/src/js/composer.js b/addons/mail/static/src/js/composer.js
index 3a7b9c320e1..f89c0382554 100644
--- a/addons/mail/static/src/js/composer.js
+++ b/addons/mail/static/src/js/composer.js
@@ -7,6 +7,7 @@ var utils = require('mail.utils');
var config = require('web.config');
var core = require('web.core');
var data = require('web.data');
+var dom = require('web.dom');
var session = require('web.session');
var Widget = require('web.Widget');
@@ -29,7 +30,7 @@ var MentionManager = Widget.extend({
events: {
"mouseover .o_mention_proposition": "on_hover_mention_proposition",
- "click .o_mention_proposition": "_onClickMentionItem",
+ "click .o_mention_proposition": "on_click_mention_item",
},
init: function (parent, options) {
@@ -52,6 +53,49 @@ var MentionManager = Widget.extend({
this.$('.o_mention_proposition').removeClass('active');
$elem.addClass('active');
},
+ on_click_mention_item: function (event) {
+ event.preventDefault();
+
+ var text_input = this.composer.$input.val();
+ var id = $(event.currentTarget).data('id');
+ var selected_suggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) {
+ return s.id === id;
+ });
+ var substitution = selected_suggestion.substitution;
+ if (!substitution) { // no substitution string given, so use the mention name instead
+ // replace white spaces with non-breaking spaces to facilitate mentions detection in text
+ selected_suggestion.name = selected_suggestion.name.replace(/ /g, NON_BREAKING_SPACE);
+ substitution = this.active_listener.delimiter + selected_suggestion.name;
+ }
+ var get_mention_index = function (matches, cursor_position) {
+ for (var i=0; i<matches.length; i++) {
+ if (cursor_position <= matches[i].index) {
+ return i;
+ }
+ }
+ return i;
+ };
+
+ // add the selected suggestion to the list
+ if (this.active_listener.selection.length) {
+ // get mention matches (ordered by index in the text)
+ var matches = this._get_match(text_input, this.active_listener);
+ var index = get_mention_index(matches, this._get_selection_positions().start);
+ this.active_listener.selection.splice(index, 0, selected_suggestion);
+ } else {
+ this.active_listener.selection.push(selected_suggestion);
+ }
+
+ // update input text, and reset dropdown
+ var cursor_position = this._get_selection_positions().start;
+ var text_left = text_input.substring(0, cursor_position-(this.mention_word.length+1));
+ var text_right = text_input.substring(cursor_position, text_input.length);
+ var text_input_new = text_left + substitution + ' ' + text_right;
+ this.composer.$input.val(text_input_new);
+ this._set_cursor_position(text_left.length+substitution.length+2);
+ this.set('mention_suggestions', []);
+ this.composer.focus(); // to trigger autoresize
+ },
// Public API
/**
@@ -86,8 +130,8 @@ var MentionManager = Widget.extend({
get_listener_selection: function (delimiter) {
var listener = _.findWhere(this.listeners, {delimiter: delimiter});
if (listener) {
- var inputMentions = this.composer.$input.text().match(new RegExp(delimiter+'[^ ]+(?= |&nbsp;)', 'g'));
- return this._validate_selection(listener.selection, inputMentions);
+ var input_mentions = this.composer.$input.val().match(new RegExp(delimiter+'[^ ]+(?= |&nbsp;)', 'g'));
+ return this._validate_selection(listener.selection, input_mentions);
}
return [];
},
@@ -106,15 +150,15 @@ var MentionManager = Widget.extend({
*/
detect_delimiter: function () {
var self = this;
- var textVal = this.composer.$input.html();
- var cursorPosition = this._getSelectionPositions();
- var leftString = textVal.substring(0, cursorPosition);
+ var text_val = this.composer.$input.val();
+ var cursor_position = this._get_selection_positions().start;
+ var left_string = text_val.substring(0, cursor_position);
function validate_keyword (delimiter, beginning_only) {
- var delimiter_position = leftString.lastIndexOf(delimiter) - 1;
+ var delimiter_position = left_string.lastIndexOf(delimiter) - 1;
if (beginning_only && delimiter_position > 0) {
return false;
}
- var search_str = textVal.substring(delimiter_position, cursorPosition);
+ var search_str = text_val.substring(delimiter_position, cursor_position);
var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))";
var regex_start = new RegExp(pattern, "g");
search_str = search_str.replace(/^\s\s*|^[\n\r]/g, '');
@@ -203,8 +247,6 @@ var MentionManager = Widget.extend({
/**
* Returns the matches (as RexExp.exec does) for the mention in the input text
- *
- * @private
* @param {String} input_text: the text to search matches
* @param {Object} listener: the listener for which we want to find a match
* @returns {Object[]} matches in the same format as RexExp.exec()
@@ -229,32 +271,6 @@ var MentionManager = Widget.extend({
return result;
},
/**
- * Get cursor position in contenteditable div with it's inner html content.
- *
- * @private
- * @return a current cursor position
- */
- _getSelectionPositions: function () {
- if (window.getSelection && window.getSelection().getRangeAt) {
- var range = window.getSelection().getRangeAt(0);
- var selectedObj = window.getSelection();
- var rangeCount = 0;
- var childNodes = selectedObj.anchorNode.parentNode.childNodes;
- for (var i = 0; i < childNodes.length; i++) {
- if (childNodes[i] === selectedObj.anchorNode) {
- break;
- }
- if (childNodes[i].outerHTML)
- rangeCount += childNodes[i].outerHTML.length;
- else if (childNodes[i].nodeType === 3) {
- rangeCount += childNodes[i].textContent.length;
- }
- }
- return range.startOffset + rangeCount;
- }
- return -1;
- },
- /**
* @private
* @param {integer} keycode
*/
@@ -312,23 +328,9 @@ var MentionManager = Widget.extend({
this.open = false;
}
},
- /*
- * Set cursor position after mention is added.
- * @private
- * @paran {int} node - id of childnode to set cursor position after it.
- */
- _setCursorPosition: function (nodeID) {
- var range = document.createRange();
- var selection = window.getSelection();
- range.setStart(document.getElementById(nodeID).nextSibling, 1);
- range.collapse(true);
- selection.removeAllRanges();
- selection.addRange(range);
- this.composer.$input.focus();
- },
- _validate_selection: function (selection, inputMentions) {
+ _validate_selection: function (selection, input_mentions) {
var validated_selection = [];
- _.each(inputMentions, function (mention) {
+ _.each(input_mentions, function (mention) {
var validated_mention = _.findWhere(selection, {name: mention.slice(1)});
if (validated_mention) {
validated_selection.push(validated_mention);
@@ -337,60 +339,15 @@ var MentionManager = Widget.extend({
return validated_selection;
},
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * after click on mention item it set to a contenteditable div with it's new
- * cursor postion
- *
- * @private
- * @param {KeyboardEvent} event - to get a current selected mention from
- * list and eventlistener.
- */
- _onClickMentionItem: function (event) {
- event.preventDefault();
-
- var textInput = this.composer.$input.html();
- var id = $(event.currentTarget).data('id');
- var selectedSuggestion = _.find(_.flatten(this.get('mention_suggestions')), function (s) {
- return s.id === id;
+ // Cursor position and selection utils
+ _get_selection_positions: function () {
+ var el = this.composer.$input.get(0);
+ return el ? dom.getSelectionRange(el) : {start: 0, end: 0};
+ },
+ _set_cursor_position: function (pos) {
+ this.composer.$input.each(function (index, elem) {
+ dom.setSelectionRange(elem, {start: pos, end: pos});
});
- var substitution = selectedSuggestion.substitution;
- if (!substitution) { // no substitution string given, so use the mention name instead
- // replace white spaces with non-breaking spaces to facilitate mentions detection in text
- selectedSuggestion.name = selectedSuggestion.name.replace(/ /g, NON_BREAKING_SPACE);
- substitution = _.escape(this.active_listener.delimiter + selectedSuggestion.name);
- }
- var getMentionIndex = function (matches, cursorPosition) {
- for (var i=0; i<matches.length; i++) {
- if (cursorPosition <= matches[i].index) {
- return i;
- }
- }
- return i;
- };
-
- // add the selected suggestion to the list
- if (this.active_listener.selection.length) {
- // get mention matches (ordered by index in the text)
- var matches = this._get_match(textInput, this.active_listener);
- var index = getMentionIndex(matches, this._getSelectionPositions());
- this.active_listener.selection.splice(index, 0, selectedSuggestion);
- } else {
- this.active_listener.selection.push(selectedSuggestion);
- }
-
- // update input text, and reset dropdown
- var cursorPosition = this._getSelectionPositions();
- var textLeft = textInput.substring(0, cursorPosition-(this.mention_word.length+1));
- var textRight = textInput.substring(cursorPosition, textInput.length);
- var nodeID = _.uniqueId('node');
- var newTextInput = textLeft + "<a id="+ nodeID+">" + substitution + "</a> " + textRight;
- this.composer.$input.html(newTextInput);
- this._setCursorPosition(nodeID);
- this.set('mention_suggestions', []);
},
});
@@ -398,8 +355,8 @@ var MentionManager = Widget.extend({
var BasicComposer = Widget.extend({
template: "mail.ChatComposer",
events: {
- "keydown .o_composer_input": "_onKeydown",
- "keyup .o_composer_input": "_onKeyup",
+ "keydown .o_composer_input textarea": "on_keydown",
+ "keyup .o_composer_input": "on_keyup",
"change input.o_input_file": "on_attachment_change",
"click .o_composer_button_send": "send_message",
"click .o_composer_button_add_attachment": "on_click_add_attachment",
@@ -410,7 +367,6 @@ var BasicComposer = Widget.extend({
'focusout .o_composer_button_emoji': '_onEmojiButtonFocusout',
'focus .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageFocus',
'click .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick',
- 'input .o_composer_input': '_removeMention',
},
// RPCs done to fetch the mention suggestions are throttled with the following value
MENTION_THROTTLE: 200,
@@ -487,12 +443,12 @@ var BasicComposer = Widget.extend({
this.$attachment_button = this.$(".o_composer_button_add_attachment");
this.$attachments_list = this.$('.o_composer_attachments_list');
- this.$input = this.$('.o_composer_input');
+ this.$input = this.$('.o_composer_input textarea');
this.$input.focus(function () {
self.trigger('input_focused');
});
- this.$input.html(this.options.default_body);
- this.$input.css('min-height', this.options.input_min_height);
+ this.$input.val(this.options.default_body);
+ dom.autoresize(this.$input, {parent: this, min_height: this.options.input_min_height});
// Attachments
$(window).on(this.fileupload_id, this.on_attachment_loaded);
@@ -514,7 +470,7 @@ var BasicComposer = Widget.extend({
// behavior for the chatter composer
//Removing unwanted extra spaces from message
- var value = _.escape(this.$input.text()).trim();
+ var value = _.escape(this.$input.val()).trim();
value = value.replace(/(\r|\n){2,}/g, '<br/><br/>');
value = value.replace(/(\r|\n)/g, '<br/>');
@@ -545,7 +501,7 @@ var BasicComposer = Widget.extend({
clear_composer: function() {
// Empty input, selected partners and attachments
- this.$input.html('');
+ this.$input.val('');
this.mention_manager.reset_selections();
this.set('attachment_ids', []);
},
@@ -557,7 +513,7 @@ var BasicComposer = Widget.extend({
getState: function () {
return {
attachments: this.get('attachment_ids'),
- text: this.$input.html(),
+ text: this.$input.val(),
};
},
@@ -565,11 +521,14 @@ var BasicComposer = Widget.extend({
on_click_add_attachment: function () {
this.$('input.o_input_file').click();
this.$input.focus();
+ // set ignoreEscape to avoid escape_pressed event when file selector dialog is opened
+ // when user press escape to cancel file selector dialog then escape_pressed event should not be trigerred
+ this.ignoreEscape = true;
},
setState: function (state) {
this.set('attachment_ids', state.attachments);
- this.$input.html(state.text);
+ this.$input.val(state.text);
},
/**
@@ -578,18 +537,8 @@ var BasicComposer = Widget.extend({
should_send: function (event) {
return !event.shiftKey;
},
- /**
- * _onKeydown event is triggered when is key is pressed
- * - on UP and DOWN arrow is pressed then event prevents it's default
- * behaviour if mention manager is open else it break it.
- * - on ENTER key is pressed and mention manager is open then event
- * prevents it's default behaviour else check if ControlKey is
- * pressed or not if yes then it send a message
- *
- * @private
- * @param {KeyboardEvent} event
- */
- _onKeydown: function (event) {
+
+ on_keydown: function (event) {
switch(event.which) {
// UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
case $.ui.keyCode.UP:
@@ -614,16 +563,7 @@ var BasicComposer = Widget.extend({
}
},
- /**
- * _onKeyup event is triggered when key is released.
- * on ESCAP key it close the mention suggestion dropdown menu.
- * on ENTER key it selects that mention and add to a content editable div.
- * on UP or DOWN key it highlights previous or next mention respectively.
- *
- * @private
- * @param {KeyboardEvent} event
- */
- _onKeyup: function (event) {
+ on_keyup: function (event) {
switch(event.which) {
// ESCAPED KEYS: do nothing
case $.ui.keyCode.END:
@@ -635,6 +575,8 @@ var BasicComposer = Widget.extend({
if (this.mention_manager.is_open()) {
event.stopPropagation();
this.mention_manager.reset_suggestions();
+ } else if (this.ignoreEscape) {
+ this.ignoreEscape = false;
} else {
this.trigger_up("escape_pressed");
}
@@ -738,12 +680,6 @@ var BasicComposer = Widget.extend({
attachments: this.get('attachment_ids'),
}));
},
- // remove mention when user try to edit or remove it.
- _removeMention: function (event) {
- if (window.getSelection().anchorNode.parentNode.tagName === 'A') {
- document.getElementById(window.getSelection().anchorNode.parentNode.id).remove();
- }
- },
// Mention
mention_fetch_throttled: function (model, method, kwargs) {
@@ -829,14 +765,8 @@ var BasicComposer = Widget.extend({
},
// Others
- /**
- * used to check if contenteditable div is empty or not
- *
- * @return - it returns length of attachment and trim content of input box
- * tp check for empty.
- */
is_empty: function () {
- return !this.$input.text().trim() && !this.$('.o_attachments').children().length;
+ return !this.$input.val().trim() && !this.$('.o_attachments').children().length;
},
focus: function () {
this.$input.focus();
@@ -871,6 +801,7 @@ var BasicComposer = Widget.extend({
* @param {MouseEvent} event
*/
_onAttachmentView: function (event) {
+ event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id');
var attachments = this.get('attachment_ids');
if (activeAttachmentID) {
@@ -894,7 +825,7 @@ var BasicComposer = Widget.extend({
if (this.$emojisContainer.parent().length) {
this._hideEmojis();
} else {
- this.$emojisContainer.appendTo(this.$('.o_composer_container'));
+ this.$emojisContainer.appendTo(this.$('.o_composer'));
}
},
/**
@@ -919,31 +850,6 @@ var BasicComposer = Widget.extend({
clearTimeout(this._hideEmojisTimeout);
},
/**
- * set cursor position at the end in input box
- *
- * @todo move that kind of code in a utility file. Maybe dom.js
- *
- * @private
- * @param {Element} el
- */
- _placeCaretAtEnd: function (el) {
- el.focus();
- if (typeof window.getSelection !== "undefined"
- && typeof document.createRange !== "undefined") {
- var range = document.createRange();
- range.selectNodeContents(el);
- range.collapse(false);
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- } else if (typeof document.body.createTextRange !== "undefined") {
- var textRange = document.body.createTextRange();
- textRange.moveToElementText(el);
- textRange.collapse(false);
- textRange.select();
- }
- },
- /**
* Called when an emoji is clicked -> adds it in the <input/>, focuses the
* <input/> and closes the emoji panel.
*
@@ -951,8 +857,8 @@ var BasicComposer = Widget.extend({
* @param {Event} ev
*/
_onEmojiImageClick: function (ev) {
- this.$input.html(this.$input.html() + " " + $(ev.currentTarget).data('emoji') + " ");
- this._placeCaretAtEnd(this.$input[0]);
+ this.$input.val(this.$input.val() + " " + $(ev.currentTarget).data('emoji') + " ");
+ this.$input.focus();
this._hideEmojis();
},
});
diff --git a/addons/mail/static/src/js/discuss.js b/addons/mail/static/src/js/discuss.js
index 3ac9e3b0ed5..3f37a620c80 100644
--- a/addons/mail/static/src/js/discuss.js
+++ b/addons/mail/static/src/js/discuss.js
@@ -861,7 +861,6 @@ var Discuss = AbstractAction.extend(ControlPanelMixin, {
message.subtype = this.selected_message.is_note ? 'mail.mt_note': 'mail.mt_comment';
message.subtype_id = false;
message.message_type = 'comment';
- message.content_subtype = 'html';
}
this.call('chat_manager', 'postMessage', message, options)
.then(function () {
diff --git a/addons/mail/static/src/js/document_viewer.js b/addons/mail/static/src/js/document_viewer.js
index d03b4811c76..3aa7e25c399 100644
--- a/addons/mail/static/src/js/document_viewer.js
+++ b/addons/mail/static/src/js/document_viewer.js
@@ -26,6 +26,7 @@ var DocumentViewer = Widget.extend({
'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox
'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE
'keydown': '_onKeydown',
+ 'keyup': '_onKeyUp',
'mousedown .o_viewer_img': '_onStartDrag',
'mousemove .o_viewer_content': '_onDrag',
'mouseup .o_viewer_content': '_onEndDrag'
@@ -156,6 +157,7 @@ var DocumentViewer = Widget.extend({
_onClose: function (e) {
e.preventDefault();
this.$el.modal('hide');
+ this.trigger_up('document_viewer_closed');
},
/**
* When popup close complete destroyed modal even DOM footprint too
@@ -239,6 +241,20 @@ var DocumentViewer = Widget.extend({
}
},
/**
+ * Close popup on ESCAPE keyup
+ *
+ * @private
+ * @param {KeyEvent} e
+ */
+ _onKeyUp: function (e) {
+ switch (e.which) {
+ case $.ui.keyCode.ESCAPE:
+ e.preventDefault();
+ this._onClose(e);
+ break;
+ }
+ },
+ /**
* @private
* @param {MouseEvent} e
*/
diff --git a/addons/mail/static/src/js/services/chat_manager.js b/addons/mail/static/src/js/services/chat_manager.js
index 6d9a822999d..4bb6e9be6aa 100644
--- a/addons/mail/static/src/js/services/chat_manager.js
+++ b/addons/mail/static/src/js/services/chat_manager.js
@@ -730,7 +730,6 @@ var ChatManager = AbstractService.extend({
args: [options.channelID],
kwargs: _.extend(msg, {
message_type: 'comment',
- content_subtype: 'html',
subtype: 'mail.mt_comment',
command: data.command,
}),
@@ -739,7 +738,6 @@ var ChatManager = AbstractService.extend({
if ('model' in options && 'res_id' in options) {
// post a message in a chatter
_.extend(msg, {
- content_subtype: data.content_subtype,
context: data.context,
message_type: data.message_type,
subtype: data.subtype,
@@ -1357,7 +1355,15 @@ var ChatManager = AbstractService.extend({
//add o_mail_emoji class on each unicode to manage size and font
var unicode = String(value);
var regexp = new RegExp("(?:^|\\s|<[a-z]*>)(" + unicode + ")(?=\\s|$|</[a-z]*>)", "g");
+ var msg_bak = msg.body;
msg.body = msg.body.replace(regexp, ' <span class="o_mail_emoji">'+unicode+'</span> ');
+ // Idiot-proof limit. If the user had the amazing idea of copy-pasting thousands of emojis,
+ // the image rendering can lead to memory overflow errors on some browsers (e.g. Chrome).
+ // Set an arbitrary limit to 200 from which we simply don't replace them (anyway, they are
+ // already replaced by the unicode counterpart).
+ if (_.str.count(msg.body, 'o_mail_emoji') > 200) {
+ msg.body = msg_bak;
+ }
});
function propertyDescr(channel) {
return {
diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js
index d4bd448d2f8..f34823ff9fc 100644
--- a/addons/mail/static/src/js/thread.js
+++ b/addons/mail/static/src/js/thread.js
@@ -380,6 +380,7 @@ var Thread = Widget.extend({
* @param {MouseEvent} event
*/
_onAttachmentView: function (event) {
+ event.stopPropagation();
var activeAttachmentID = $(event.currentTarget).data('id');
if (activeAttachmentID) {
var attachmentViewer = new DocumentViewer(this, this.attachments, activeAttachmentID);
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index e7caf3821d0..cb079f81558 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -24,7 +24,7 @@
position: relative;
background: white;
- input {
+ textarea, input {
border: none;
}
}
@@ -41,19 +41,9 @@
}
.o_composer {
- .o_composer_input[contenteditable=true]:empty:before {
- content: attr(placeholder);
- opacity: 0.4;
- }
- .o_composer_input {
- border: none;
- box-shadow: none;
- cursor: text;
- display: inline-block;
- height: 100%;
- padding: 0px 0px $o-mail-chatter-gap;
- white-space: pre-wrap;
- word-break: break-all;
+ .o_composer_input > textarea {
+ width: 100%;
+ resize: none;
}
.o_composer_mention_dropdown .o_mention_proposition > a {
@include o-text-overflow;
@@ -82,18 +72,19 @@
color: $o-brand-optional;
}
}
- }
-
- .o_mail_emoji_container {
- padding: 0 $o-mail-chatter-gap $o-mail-chatter-gap 0;
- border: 1px solid $gray-lighter-dark;
- background-color: $gray-lighter;
- .o_mail_emoji {
- margin: $o-mail-chatter-gap 0 0 $o-mail-chatter-gap;
- cursor: pointer; // only in the popover
- min-width: 20px;
- min-height: 20px;
+ .o_mail_emoji_container {
+ padding: 0 $o-mail-chatter-gap $o-mail-chatter-gap 0;
+ border: 1px solid $gray-lighter-dark;
+ border-width: 1px 0 0 0;
+ background-color: $gray-lighter;
+
+ .o_mail_emoji {
+ margin: $o-mail-chatter-gap 0 0 $o-mail-chatter-gap;
+ cursor: pointer; // only in the popover
+ min-width: 20px;
+ min-height: 20px;
+ }
}
}
@@ -119,13 +110,19 @@
.o_composer {
border-radius: 2px;
+ textarea {
+ padding-right: $grid-gutter-width; // avoid overlaps betwenn textarea and o_composer_button_full_composer
+ }
+ .o_mail_emoji_container {
+ margin: 0 -$o-mail-chatter-gap;
+ }
}
.o_composer, .o_composer_subject {
border: 1px solid $gray-lighter-dark;
- input {
+ textarea, input {
padding: 0 0 $o-mail-chatter-gap;
border: none;
}
@@ -137,9 +134,6 @@
.o_chatter_composer_tools {
border-top: 1px solid $gray-lighter-dark;
- @media (max-width: $screen-xs-max) {
- margin-top: 10px;
- }
}
&:not(.o_chat_inline_composer) {
@@ -158,24 +152,26 @@
&.o_chat_inline_composer {
.o_composer_container {
- @include o-flex(1, 1, auto);
+ @include o-flex(1, 0, auto);
}
.o_composer {
- display: flex;
- min-height: 40px;
padding: $o-mail-chatter-gap $o-mail-chatter-gap 0 $o-mail-chatter-gap;
border-radius: 2px 0 0 2px;
border-right-width: 0;
- }
- .o_mail_emoji_container {
- text-align: right;
- border-width: 1px;
+
+ .o_mail_emoji_container {
+ text-align: right;
+ border-right-width: 1px;
+ }
}
.o_composer_input {
+ @include o-flex-display();
@include o-flex-flow(row, wrap);
}
.o_composer_text_field {
@include o-flex(1, 1, 50%);
+ margin: 0;
+ padding: 0;
}
.o_chatter_composer_tools {
@include o-flex(0, 1, auto);
@@ -207,25 +203,32 @@
border-top: 1px solid $gray-lighter-dark;
width: 100%;
}
- .o_mail_emoji_container {
- @include o-position-absolute(auto, 0, 100%, 0);
- border-bottom-width: 1px;
+ .o_composer {
+ .o_mail_emoji_container {
+ @include o-position-absolute(auto, 0, 100%, 0);
+ border-bottom-width: 1px;
+ }
}
.o_composer_input {
+ @include o-flex-display();
@include o-flex-flow(row, nowrap);
+ textarea.o_composer_text_field {
+ @include o-flex(1, 1, auto);
+ }
+
.o_chatter_composer_tools {
@include o-flex(0, 0, auto);
}
+ @media (max-width: $screen-xs-max) {
+ .o_composer_button_send {
+ display: inline-block;
+ }
+ }
}
.o_composer_button_send, .o_chatter_avatar {
display: none;
}
- @media (max-width: $screen-xs-max) {
- .o_composer_button_send {
- display: inline-block;
- }
- }
}
}
diff --git a/addons/mail/static/src/scss/extended_chat_window.scss b/addons/mail/static/src/scss/extended_chat_window.scss
index 774a37c6120..6cb930b4488 100644
--- a/addons/mail/static/src/scss/extended_chat_window.scss
+++ b/addons/mail/static/src/scss/extended_chat_window.scss
@@ -42,14 +42,10 @@
}
.o_chatter_composer_tools {
@include o-position-absolute($right: 0px, $top: 0px);
-
- @media (max-width: $screen-xs-max) {
- @include o-position-absolute($right: 6px, $top: 0px);
- }
button {
- padding: 8px;
+ padding: 10px;
@media (max-width: $screen-xs-max) {
- padding: 7px 6px 0px 6px;
+ padding: 13px;
}
&.btn-icon {
font-size: 1.3em;
diff --git a/addons/mail/static/src/xml/composer.xml b/addons/mail/static/src/xml/composer.xml
index 90beedc147f..bb9321a749d 100644
--- a/addons/mail/static/src/xml/composer.xml
+++ b/addons/mail/static/src/xml/composer.xml
@@ -8,11 +8,13 @@
<input tabindex="1" type="text" placeholder="Subject" class="o_input"/>
</div>
<div t-attf-class="o_composer #{widget.extended ? 'o_extended_composer' : ''}">
- <div class="o_composer_input o_composer_text_field form-control" contenteditable="true" placeholder="Write Something..."/>
- <div class="o_chatter_composer_tools">
- <button tabindex="4" class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
- <button tabindex="5" class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
- <button t-if="widget.options.isMobile" tabindex="3" class="btn btn-sm btn-icon fa fa-paper-plane-o o_composer_button_send" type="button"/>
+ <div class="o_composer_input">
+ <textarea class="o_input o_composer_text_field" tabindex="2" placeholder="Write something..."/>
+ <div class="o_chatter_composer_tools">
+ <button tabindex="4" class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
+ <button tabindex="5" class="btn btn-sm btn-icon fa fa-paperclip o_composer_button_add_attachment" type="button"/>
+ <button t-if="widget.options.isMobile" tabindex="3" class="btn btn-sm btn-icon fa fa-paper-plane-o o_composer_button_send" type="button"/>
+ </div>
</div>
</div>
<div class="o_composer_attachments_list"/>
diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml
index c3aa2388154..5ef23d46c43 100644
--- a/addons/mail/static/src/xml/thread.xml
+++ b/addons/mail/static/src/xml/thread.xml
@@ -61,7 +61,7 @@
</t>
<t t-name="DocumentViewer">
- <div class="modal o_modal_fullscreen" tabindex="-1" role="dialog" aria-hidden="true">
+ <div class="modal o_modal_fullscreen" tabindex="-1" data-keyboard="false" role="dialog" aria-hidden="true">
<t t-call="DocumentViewer.Content"/>
<t t-if="widget.attachment.length != 1">
diff --git a/addons/mail/static/tests/chat_window_tests.js b/addons/mail/static/tests/chat_window_tests.js
index 05df8461d18..348b329986a 100644
--- a/addons/mail/static/tests/chat_window_tests.js
+++ b/addons/mail/static/tests/chat_window_tests.js
@@ -62,6 +62,84 @@ QUnit.module('mail', {
parent.destroy();
});
+ QUnit.test('open document viewer and close using ESCAPE key should reset focus to chat window', function (assert) {
+ assert.expect(6);
+
+ function createParent(params) {
+ var widget = new Widget();
+
+ testUtils.addMockEnvironment(widget, params);
+ return widget;
+ }
+ var messages = [{
+ attachment_ids: [{
+ filename: 'image1.jpg',
+ id:1,
+ mimetype: 'image/jpeg',
+ name: 'Test Image 1',
+ url: '/web/content/1?download=true'
+ }],
+ author_id: ["1", "John Doe"],
+ body: "A message",
+ date: moment("2016-12-20 09:35:40"),
+ displayed_author: "John Doe",
+ id: 1,
+ is_note: false,
+ is_starred: false,
+ model: 'partner',
+ res_id: 2
+ }];
+ var parent = createParent({
+ mockRPC: function (route, args) {
+ if(_.str.contains(route, '/mail/attachment/preview/') ||
+ _.str.contains(route, '/web/static/lib/pdfjs/web/viewer.html')){
+ var canvas = document.createElement('canvas');
+ return $.when(canvas.toDataURL());
+ }
+ return this._super.apply(this, arguments);
+ },
+ data: {},
+ });
+
+ var chatWindow = new ChatWindow(parent, 1, "user", false, messages.length, {});
+ chatWindow.appendTo($('#qunit-fixture'));
+ chatWindow.render(messages);
+
+ testUtils.intercept(chatWindow, 'get_messages', function(event) {
+ event.stopPropagation();
+ var requested_msgs = _.filter(messages, function (msg) {
+ return _.contains(event.data.options.ids, msg.id);
+ });
+ event.data.callback($.when(requested_msgs));
+ }, true);
+
+ testUtils.intercept(chatWindow, 'get_bus', function(event) {
+ event.stopPropagation();
+ event.data.callback(new Bus());
+ }, true);
+
+ chatWindow.on('document_viewer_closed', null, function () {
+ assert.ok(true, "chat window should trigger a close document viewer event");
+ });
+ assert.strictEqual(chatWindow.$('.o_thread_message .o_attachment').length, 1,
+ "there should be three attachment on message");
+ // click on first image attachement
+ chatWindow.$('.o_thread_message .o_attachment .o_image_box .o_image_overlay').first().click();
+ // check focus is on document viewer popup and then press escape to close it
+ assert.strictEqual(document.activeElement, $('.o_modal_fullscreen')[0], "Modal popup should have focus");
+ assert.strictEqual($('.o_modal_fullscreen img.o_viewer_img[data-src*="/web/image/1?unique=1"]').length, 1,
+ "Modal popup should open with first image src");
+ // trigger ESCAPE keyup on document viewer popup
+ var upKeyEvent = jQuery.Event("keyup", {which: 27});
+ $('.o_modal_fullscreen').trigger(upKeyEvent);
+ assert.strictEqual(document.activeElement, chatWindow.$input[0],
+ "input should be focused");
+ var upKeyEvent = jQuery.Event( "keyup", {which: 27});
+ chatWindow.$('.o_composer_input').trigger(upKeyEvent);
+ assert.strictEqual(chatWindow.folded, false, "Closed chat Window");
+ parent.destroy();
+ });
+
QUnit.test('chat window\'s input can still be focused when the UI is blocked', function (assert) {
assert.expect(2);
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 8c1edc2f622..46de2e22d3f 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -368,9 +368,7 @@ QUnit.test('kanban activity widget with an activity', function (assert) {
QUnit.test('chatter: post, receive and star messages', function (assert) {
var done = assert.async();
- assert.expect(28);
-
- var unpatchWindowGetSelection = testUtils.patchWindowGetSelection();
+ assert.expect(27);
var bus = new Bus();
var BusService = createBusService(bus);
@@ -475,7 +473,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
// send a message
form.$('.o_chatter_button_new_message').click();
assert.ok(!$('.oe_chatter .o_chat_composer').hasClass('o_hidden'), "chatter should be opened");
- form.$('.oe_chatter .o_composer_text_field:first()').text("My first message");
+ form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
form.$('.oe_chatter .o_composer_button_send').click();
assert.ok($('.oe_chatter .o_chat_composer').hasClass('o_hidden'), "chatter should be closed");
assert.strictEqual(form.$('.o_thread_message').length, 2, "thread should contain two messages");
@@ -489,7 +487,7 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
// log a note
form.$('.o_chatter_button_log_note').click();
assert.ok(!$('.oe_chatter .o_chat_composer').hasClass('o_hidden'), "chatter should be opened");
- form.$('.oe_chatter .o_composer_text_field:first()').text("My first note");
+ form.$('.oe_chatter .o_composer_text_field:first()').val("My first note");
form.$('.oe_chatter .o_composer_button_send').click();
assert.ok($('.oe_chatter .o_chat_composer').hasClass('o_hidden'), "chatter should be closed");
assert.strictEqual(form.$('.o_thread_message').length, 3, "thread should contain three messages");
@@ -515,7 +513,10 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
// very basic test of mention
form.$('.o_chatter_button_new_message').click();
var $input = form.$('.oe_chatter .o_composer_text_field:first()');
- $input.text('@');
+ $input.val('@');
+ // the cursor position must be set for the mention manager to detect that we are mentionning
+ $input[0].selectionStart = 1;
+ $input[0].selectionEnd = 1;
$input.trigger('keyup');
assert.strictEqual(getSuggestionsDef.state(), "pending",
@@ -533,14 +534,10 @@ QUnit.test('chatter: post, receive and star messages', function (assert) {
assert.strictEqual(form.$('.o_mention_proposition .o_mention_info').text(), '(test@odoo.com)',
"suggestion should be displayed correctly");
- $(".o_mention_proposition > a").trigger("click");
- assert.strictEqual($(".o_composer_input a").length, 1, "mention is 'green' in edit mode");
-
BasicComposer.prototype.MENTION_THROTTLE = mentionThrottle;
//cleanup
form.destroy();
- unpatchWindowGetSelection();
done();
});
});
@@ -599,7 +596,7 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert)
// send a message
form.$('.o_chatter_button_new_message').click();
- form.$('.oe_chatter .o_composer_text_field:first()').text("My first message");
+ form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
form.$('.oe_chatter .o_composer_button_send').click();
assert.strictEqual(form.$('.o_thread_message').length, 1, "thread should contain a message");
assert.ok(form.$('.o_thread_message:first() .o_thread_message_core').text().indexOf('My first message') >= 0,
diff --git a/addons/mail/static/tests/discuss_tests.js b/addons/mail/static/tests/discuss_tests.js
index 8d3ac86824e..8b7c72fd21e 100644
--- a/addons/mail/static/tests/discuss_tests.js
+++ b/addons/mail/static/tests/discuss_tests.js
@@ -12,7 +12,6 @@ var testUtils = require('web.test_utils');
var createBusService = mailTestUtils.createBusService;
var createDiscuss = mailTestUtils.createDiscuss;
-var patchWindowGetSelection = testUtils.patchWindowGetSelection;
QUnit.module('mail', {}, function () {
@@ -156,14 +155,13 @@ QUnit.test('@ mention in channel', function (assert) {
// click on general
$general.click();
- var $input = discuss.$('.o_composer_input').first();
+ var $input = discuss.$('textarea.o_composer_text_field').first();
assert.ok($input.length, "should display a composer input");
// Simulate '@' typed by user with mocked Window.getSelection
// Note: focus is needed in order to trigger rpc 'channel_fetch_listeners'
$input.focus();
- $input.text("@");
- var unpatchWindowGetSelection = patchWindowGetSelection();
+ $input.val("@");
$input.trigger('keyup');
fetchListenersDef
@@ -249,7 +247,7 @@ QUnit.test('@ mention in channel', function (assert) {
$input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER}));
assert.strictEqual(discuss.$('.o_mention_proposition').length, 0,
"should not have any partner mention proposition after ENTER");
- assert.strictEqual($input.find('a').text() , "@Admin",
+ assert.strictEqual($input.val().trim() , "@Admin",
"should have the correct mention link in the composer input");
// send message
@@ -266,7 +264,6 @@ QUnit.test('@ mention in channel', function (assert) {
"@Admin", "should have correct mention link in the message content");
// Restore window.getSelection
- unpatchWindowGetSelection();
discuss.destroy();
done();
});