diff options
Diffstat (limited to 'addons/mail/static')
| -rw-r--r-- | addons/mail/static/src/js/activity.js | 2 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chat_window.js | 6 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chatter.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chatter_composer.js | 3 | ||||
| -rw-r--r-- | addons/mail/static/src/js/composer.js | 262 | ||||
| -rw-r--r-- | addons/mail/static/src/js/discuss.js | 1 | ||||
| -rw-r--r-- | addons/mail/static/src/js/document_viewer.js | 16 | ||||
| -rw-r--r-- | addons/mail/static/src/js/services/chat_manager.js | 10 | ||||
| -rw-r--r-- | addons/mail/static/src/js/thread.js | 1 | ||||
| -rw-r--r-- | addons/mail/static/src/scss/composer.scss | 91 | ||||
| -rw-r--r-- | addons/mail/static/src/scss/extended_chat_window.scss | 8 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/composer.xml | 12 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/thread.xml | 2 | ||||
| -rw-r--r-- | addons/mail/static/tests/chat_window_tests.js | 78 | ||||
| -rw-r--r-- | addons/mail/static/tests/chatter_tests.js | 19 | ||||
| -rw-r--r-- | addons/mail/static/tests/discuss_tests.js | 9 |
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+'[^ ]+(?= | )', 'g')); - return this._validate_selection(listener.selection, inputMentions); + var input_mentions = this.composer.$input.val().match(new RegExp(delimiter+'[^ ]+(?= | )', '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(); }); |
