diff options
| author | Nikunj Ladava <nla@odoo.com> | 2017-05-19 17:48:45 +0530 |
|---|---|---|
| committer | Géry Debongnie <ged@odoo.com> | 2018-01-25 12:18:10 +0100 |
| commit | c778de44c7cd9b7ef5a9a91738164ab0ebd0ca06 (patch) | |
| tree | f41e5e5eaa1bb7e19a4ab052aa7ed65c9bb28c05 /addons/mail/static/src | |
| parent | f50c6b1f6c79e766add1015a393230b525f89bfa (diff) | |
[IMP] mail : display mentions in green color in edit mode
When someone mentions another person in discuss/chat window, the
mentioned name was displayed just as a text, which may be unclear. With
this commit, we display mentions with the same style as mentions in a
message.
To do that, the main idea is to change the input by a div with
contenteditable set to true. This allows us to add <a>...</a> inside.
Diffstat (limited to 'addons/mail/static/src')
| -rw-r--r-- | addons/mail/static/src/js/chat_window.js | 4 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chatter.js | 2 | ||||
| -rw-r--r-- | addons/mail/static/src/js/chatter_composer.js | 2 | ||||
| -rw-r--r-- | addons/mail/static/src/js/composer.js | 256 | ||||
| -rw-r--r-- | addons/mail/static/src/less/composer.less | 83 | ||||
| -rw-r--r-- | addons/mail/static/src/less/extended_chat_window.less | 8 | ||||
| -rw-r--r-- | addons/mail/static/src/xml/composer.xml | 12 |
7 files changed, 232 insertions, 135 deletions
diff --git a/addons/mail/static/src/js/chat_window.js b/addons/mail/static/src/js/chat_window.js index 0c3a2270045..0f728e00ba1 100644 --- a/addons/mail/static/src/js/chat_window.js +++ b/addons/mail/static/src/js/chat_window.js @@ -132,13 +132,13 @@ return Widget.extend({ event.stopPropagation(); // to prevent jquery's blockUI to cancel event // ENTER key (avoid requiring jquery ui for external livechat) if (event.which === 13) { - var content = _.str.trim(this.$input.val()); + var content = _.str.trim(this.$input.text()); var message = { content: content, attachment_ids: [], partner_ids: [], }; - this.$input.val(''); + this.$input.text(''); if (content) { this.trigger('post_message', message, this.channel_id); } diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index cc71046039d..649a7381f7c 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -185,7 +185,7 @@ var Chatter = Widget.extend(chat_mixin, { input_baseline: 14, is_log: options && options.is_log, record_name: this.record_name, - default_body: old_composer && old_composer.$input && old_composer.$input.val(), + default_body: old_composer && old_composer.$input && old_composer.$input.html(), default_mention_selections: old_composer && old_composer.mention_get_listener_selections(), }); this.composer.on('input_focused', this, function () { diff --git a/addons/mail/static/src/js/chatter_composer.js b/addons/mail/static/src/js/chatter_composer.js index e22e3a5ca9f..fe23bc528fe 100644 --- a/addons/mail/static/src/js/chatter_composer.js +++ b/addons/mail/static/src/js/chatter_composer.js @@ -212,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.val()), + default_body: utils.get_text2html(self.$input.html()), 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 744e72742e2..2e2aaa66f74 100644 --- a/addons/mail/static/src/js/composer.js +++ b/addons/mail/static/src/js/composer.js @@ -8,7 +8,6 @@ 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'); @@ -31,7 +30,7 @@ var MentionManager = Widget.extend({ events: { "mouseover .o_mention_proposition": "on_hover_mention_proposition", - "click .o_mention_proposition": "on_click_mention_item", + "click .o_mention_proposition": "_onClickMentionItem", }, init: function (parent, options) { @@ -54,49 +53,6 @@ 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 /** @@ -131,8 +87,8 @@ var MentionManager = Widget.extend({ get_listener_selection: function (delimiter) { var listener = _.findWhere(this.listeners, {delimiter: delimiter}); if (listener) { - var input_mentions = this.composer.$input.val().match(new RegExp(delimiter+'[^ ]+(?= | )', 'g')); - return this._validate_selection(listener.selection, input_mentions); + var inputMentions = this.composer.$input.text().match(new RegExp(delimiter+'[^ ]+(?= | )', 'g')); + return this._validate_selection(listener.selection, inputMentions); } return []; }, @@ -169,15 +125,15 @@ var MentionManager = Widget.extend({ */ detect_delimiter: function () { var self = this; - var text_val = this.composer.$input.val(); - var cursor_position = this._get_selection_positions().start; - var left_string = text_val.substring(0, cursor_position); + var textVal = this.composer.$input.html(); + var cursorPosition = this._getSelectionPositions(); + var leftString = textVal.substring(0, cursorPosition); function validate_keyword (delimiter, beginning_only) { - var delimiter_position = left_string.lastIndexOf(delimiter) - 1; + var delimiter_position = leftString.lastIndexOf(delimiter) - 1; if (beginning_only && delimiter_position > 0) { return false; } - var search_str = text_val.substring(delimiter_position, cursor_position); + var search_str = textVal.substring(delimiter_position, cursorPosition); var pattern = "(^"+delimiter+"|(^\\s"+delimiter+"))"; var regex_start = new RegExp(pattern, "g"); search_str = search_str.replace(/^\s\s*|^[\n\r]/g, ''); @@ -263,6 +219,8 @@ var MentionManager = Widget.extend({ // Private functions /** * 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() @@ -286,6 +244,32 @@ 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; + }, _render_suggestions: function () { var suggestions = []; if (_.find(this.get('mention_suggestions'), _.isArray)) { @@ -318,9 +302,23 @@ var MentionManager = Widget.extend({ this.open = false; } }, - _validate_selection: function (selection, input_mentions) { + /* + * 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) { var validated_selection = []; - _.each(input_mentions, function (mention) { + _.each(inputMentions, function (mention) { var validated_mention = _.findWhere(selection, {name: mention.slice(1)}); if (validated_mention) { validated_selection.push(validated_mention); @@ -329,15 +327,60 @@ var MentionManager = Widget.extend({ return validated_selection; }, - // 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}); + //-------------------------------------------------------------------------- + // 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; }); + 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', []); }, }); @@ -345,8 +388,8 @@ var MentionManager = Widget.extend({ var BasicComposer = Widget.extend(chat_mixin, { template: "mail.ChatComposer", events: { - "keydown .o_composer_input textarea": "on_keydown", - "keyup .o_composer_input": "on_keyup", + "keydown .o_composer_input": "_onKeydown", + "keyup .o_composer_input": "_onKeyup", "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", @@ -357,6 +400,7 @@ var BasicComposer = Widget.extend(chat_mixin, { '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, @@ -433,12 +477,12 @@ var BasicComposer = Widget.extend(chat_mixin, { this.$attachment_button = this.$(".o_composer_button_add_attachment"); this.$attachments_list = this.$('.o_composer_attachments_list'); - this.$input = this.$('.o_composer_input textarea'); + this.$input = this.$('.o_composer_input'); this.$input.focus(function () { self.trigger('input_focused'); }); - this.$input.val(this.options.default_body); - dom.autoresize(this.$input, {parent: this, min_height: this.options.input_min_height}); + this.$input.html(this.options.default_body); + this.$input.css('min-height', this.options.input_min_height) // Attachments $(window).on(this.fileupload_id, this.on_attachment_loaded); @@ -460,7 +504,7 @@ var BasicComposer = Widget.extend(chat_mixin, { // behavior for the chatter composer //Removing unwanted extra spaces from message - var value = _.escape(this.$input.val()).trim(); + var value = _.escape(this.$input.text()).trim(); value = value.replace(/(\r|\n){2,}/g, '<br/><br/>'); value = value.replace(/(\r|\n)/g, '<br/>'); @@ -491,7 +535,7 @@ var BasicComposer = Widget.extend(chat_mixin, { clear_composer: function() { // Empty input, selected partners and attachments - this.$input.val(''); + this.$input.html(''); this.mention_manager.reset_selections(); this.set('attachment_ids', []); }, @@ -503,7 +547,7 @@ var BasicComposer = Widget.extend(chat_mixin, { getState: function () { return { attachments: this.get('attachment_ids'), - text: this.$input.val(), + text: this.$input.html(), }; }, @@ -515,7 +559,7 @@ var BasicComposer = Widget.extend(chat_mixin, { setState: function (state) { this.set('attachment_ids', state.attachments); - this.$input.val(state.text); + this.$input.html(state.text); }, /** @@ -524,8 +568,18 @@ var BasicComposer = Widget.extend(chat_mixin, { should_send: function (event) { return !event.shiftKey; }, - - on_keydown: function (event) { + /** + * _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) { switch(event.which) { // UP, DOWN: prevent moving cursor if navigation in mention propositions case $.ui.keyCode.UP: @@ -549,7 +603,16 @@ var BasicComposer = Widget.extend(chat_mixin, { } }, - on_keyup: function (event) { + /** + * _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) { switch(event.which) { // ESCAPED KEYS: do nothing case $.ui.keyCode.END: @@ -663,6 +726,12 @@ var BasicComposer = Widget.extend(chat_mixin, { 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) { @@ -748,8 +817,14 @@ var BasicComposer = Widget.extend(chat_mixin, { }, // 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.val().trim() && !this.$('.o_attachments').children().length; + return !this.$input.text().trim() && !this.$('.o_attachments').children().length; }, focus: function () { this.$input.focus(); @@ -807,7 +882,7 @@ var BasicComposer = Widget.extend(chat_mixin, { if (this.$emojisContainer.parent().length) { this._hideEmojis(); } else { - this.$emojisContainer.appendTo(this.$('.o_composer')); + this.$emojisContainer.appendTo(this.$('.o_composer_container')); } }, /** @@ -830,6 +905,31 @@ var BasicComposer = Widget.extend(chat_mixin, { 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. * @@ -837,8 +937,8 @@ var BasicComposer = Widget.extend(chat_mixin, { * @param {Event} ev */ _onEmojiImageClick: function (ev) { - this.$input.val(this.$input.val() + " " + $(ev.currentTarget).data('emoji') + " "); - this.$input.focus(); + this.$input.html(this.$input.html() + " " + $(ev.currentTarget).data('emoji') + " "); + this._placeCaretAtEnd(this.$input[0]) this._hideEmojis(); }, }); diff --git a/addons/mail/static/src/less/composer.less b/addons/mail/static/src/less/composer.less index ec3b156af2a..9df25c895a3 100644 --- a/addons/mail/static/src/less/composer.less +++ b/addons/mail/static/src/less/composer.less @@ -10,7 +10,7 @@ position: relative; background: white; - textarea, input { + input { border: none; } } @@ -27,9 +27,15 @@ } .o_composer { - .o_composer_input > textarea { - width: 100%; - resize: none; + .o_composer_input[contenteditable=true]:empty:before { + content: attr(placeholder); + opacity: 0.75; + } + .o_composer_input { + display: inline-block; + height: 100%; + white-space: pre-wrap; + word-break: break-all; } .o_composer_mention_dropdown .o_mention_proposition > a { .o-text-overflow(); @@ -58,17 +64,16 @@ color: @odoo-brand-optional; } } + } - .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_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 - } + .o_mail_emoji { + margin: @o-mail-chatter-gap 0 0 @o-mail-chatter-gap; + cursor: pointer; // only in the popover } } @@ -94,19 +99,13 @@ .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; - textarea, input { + input { padding: 0 0 @o-mail-chatter-gap; border: none; } @@ -118,6 +117,9 @@ .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) { @@ -136,26 +138,26 @@ &.o_chat_inline_composer { .o_composer_container { - .o-flex(1, 0, auto); + .o-flex(1, 1, 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-right-width: 1px; - } + } + .o_mail_emoji_container { + text-align: right; + border-width: 1px; } .o_composer_input { - .o-flex-display(); .o-flex-flow(row, wrap); } .o_composer_text_field { .o-flex(1, 1, 50%); - margin: 0; - padding: 0; + margin: -4px 7px 5px 0px; + padding: 2.5px 0px 0px 5px; } .o_chatter_composer_tools { .o-flex(0, 1, auto); @@ -187,32 +189,25 @@ border-top: 1px solid @gray-lighter-dark; width: 100%; } - .o_composer { - .o_mail_emoji_container { - .o-position-absolute(auto, 0, 100%, 0); - border-bottom-width: 1px; - } + .o_mail_emoji_container { + .o-position-absolute(auto, 0, 100%, 0); + border-bottom-width: 1px; } .o_composer_input { - .o-flex-display(); .o-flex-flow(row, nowrap); - textarea.o_composer_text_field { - .o-flex(1, 1, auto); - } - .o_chatter_composer_tools { .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/less/extended_chat_window.less b/addons/mail/static/src/less/extended_chat_window.less index 3ae73dee861..fcf92cf052a 100644 --- a/addons/mail/static/src/less/extended_chat_window.less +++ b/addons/mail/static/src/less/extended_chat_window.less @@ -42,10 +42,14 @@ } .o_chatter_composer_tools { .o-position-absolute(@right: 0px, @top: 0px); + + @media (max-width: @screen-xs-max) { + .o-position-absolute(@right: 6px, @top: 0px); + } button { - padding: 10px; + padding: 8px; @media (max-width: @screen-xs-max) { - padding: 13px; + padding: 7px 6px 0px 6px; } &.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 b46af297928..2795a0bb724 100644 --- a/addons/mail/static/src/xml/composer.xml +++ b/addons/mail/static/src/xml/composer.xml @@ -8,13 +8,11 @@ <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"> - <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 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> </div> <div class="o_composer_attachments_list"/> |
