summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/js/composers/basic_composer.js
diff options
context:
space:
mode:
authorAlexandre Kühn <aku@odoo.com>2018-06-12 15:46:46 +0200
committerAlexandre Kühn <aku@odoo.com>2018-07-02 15:06:54 +0200
commitcd34f6de727d5b3858420cff3c9d3c5995c4e75c (patch)
tree8d25861f98a038b71728355375d634a5c178290e /addons/mail/static/src/js/composers/basic_composer.js
parentee9c1f52c316c83775969a3c9038e7e9f560f7e7 (diff)
[REF] mail: JS mail refactoring
---------------------- Summary ---------------------- The purpose of this commit is to improve the JS code of the `mail` module. It applies the new coding guidelines and makes some changes on the design of some modules, such as the old ChatManager module. Here is a short summary of the changes that have been made: 1. New coding guidelines - snake_case to camelCase - prefix private attributes and methods with '_' - jsdoc on most methods - one class per module 2. Rename/Merge some classes - 'chat manager' becomes 'mail manager' (internal) and 'mail service' (external) - 'chat window manager' is now included in mail manager - 'thread' widget now named 'thread widget' 3. New model abstraction for mail objects: - modules 'mail.model.*' - modeling: 0..1 0..1 * * ThreadWindow <------> Thread <-------> Message / \ / \ Thread With Cache Document Thread / | \ / | \ Mailbox Channel Support Channel | | DM - Thread: the superclass of threads. - ThreadWindow: the window component of a thread. - Message: mail objects representing messages. - Thread With Cache: threads that can be used with search view (Discuss app compatible). - Document Thread: represents the thread part of a chatter. - Mailbox: represents what was previously called 'static' channel, e.g. 'Inbox'. - Channel: mail objects representing channels, including livechat. - DM: special kind of Channel for 1:1 communication in the backend. - Support Channel: special channel for im_support module. This new modeling approach let us easily add features on all threads, such as the possibility to put any thread in a small window. ---------------------- Known issues ---------------------- [Already Present in Master] 1. When the Discuss app is in the background with 'Inbox' as the selected Thread, when clicking on a document thread preview in the messaging menu of the systray, the rainbow man appears. 2. When a document with the chatter is in the background, when receiving an inbox notification from this document thread, the document thread is automatically marked as read, which removes the notification right away. 3. Sometimes, opening a DM window from the "blank" thread window does not work. 4. Reply-to feature on Inbox is not working: no message is sent in the document thread. 5. On the first login of admin user with demo data, the inbox counter is wrong (it displays 6, instead of 3). Explanation after investigation: > On page load, it fetches the correct number of Inbox messages (3), but the server notifies of 3 needaction messages right away, so it wrongly assumes these are new needaction messages. > Not possible for web client to detect that these messages should not increment the Inbox counter while keeping same API. 6. Notifications for new document thread messages only work when the user sets 'handle with Odoo' for the Notification Management in the preferences. > due to notifications on the longpoll bus for document thread messages that come from needaction notifications. > requires server-side changes to send notification on the longpoll bus to mentionned user. [New] 7. When receiving a message on a unjoined channel, thread window flickers ('open' > 'close' > 'open') Explanation after investigation: > JS logic: a) On auto-join, ask server to join the channel and get channel infos. b) The info tells the channel is not detached, but JS code makes decision to detach it, and tells server the channel is now detached. c) From (a), server notifies on longpoll bus the state of channel, which is not detached. The web client thinks that the window state of the channel has been changed somewhere else, and the channel is now closed. d) From (b), server notifies on longpoll bus the state of channel, which is detached. The web client opens the thread window of this channel. > The flicker didn't occur before refactoring because the web client was only updating the model of channel when it receives the longpoll notification. > Server behaviour on 1st longpoll notification is necessary for cross-tab synchronization for channel window state. > New design implies that model and view should be synchronized, hence the issue now. > Solution: remove server-side thread window synchronization and replace with client-side synchronization. ---------------------- Hacks ---------------------- The module `im_livechat` now uses mail objects that are compatible with Message and Window objects: Modeling for messages: AbstractMessage / \ / \ LivechatMessage Message - AbstractMessage: message compatible with the thread widget. - LivechatMessage: message used by im_livechat. - Message: message used with the mail manager. Modeling for thread windows: AbstractThreadWindow / \ / \ LivechatWindow ThreadWindow - AbstractThreadWindow: behaviour share between all types of thread windows. - LivechatWindow: window used by im_livechat. - ThreadWindow: window used with mail_manager. The reason for these hacks are twofold: 1. Use the thread widget in the frontend and livechat external lib bundles. 2. Do not have a dependency with the mail manager in the frontend and external lib bundles.
Diffstat (limited to 'addons/mail/static/src/js/composers/basic_composer.js')
-rw-r--r--addons/mail/static/src/js/composers/basic_composer.js640
1 files changed, 640 insertions, 0 deletions
diff --git a/addons/mail/static/src/js/composers/basic_composer.js b/addons/mail/static/src/js/composers/basic_composer.js
new file mode 100644
index 00000000000..ce7c1017ffa
--- /dev/null
+++ b/addons/mail/static/src/js/composers/basic_composer.js
@@ -0,0 +1,640 @@
+odoo.define('mail.composer.Basic', function (require) {
+"use strict";
+
+var emojis = require('mail.emojis');
+var MentionManager = require('mail.composer.MentionManager');
+var DocumentViewer = require('mail.DocumentViewer');
+var mailUtils = 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');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+var BasicComposer = Widget.extend({
+ template: "mail.Composer",
+ events: {
+ 'change input.o_input_file': '_onAttachmentChange',
+ 'click .o_attachment_delete': '_onAttachmentDelete',
+ 'click .o_attachment_download': '_onAttachmentDownload',
+ 'click .o_attachment_view': '_onAttachmentView',
+ 'click .o_composer_button_add_attachment': '_onClickAddAttachment',
+ 'click .o_composer_button_emoji': '_onEmojiButtonClick',
+ 'focusout .o_composer_button_emoji': '_onEmojiButtonFocusout',
+ 'click .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageClick',
+ 'focus .o_mail_emoji_container .o_mail_emoji': '_onEmojiImageFocus',
+ 'keydown .o_composer_input textarea': '_onKeydown',
+ 'keyup .o_composer_input': '_onKeyup',
+ 'click .o_composer_button_send': '_sendMessage',
+ },
+ // RPCs done to fetch the mention suggestions are throttled with the
+ // following value
+ MENTION_THROTTLE: 200,
+
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+ this.options = _.defaults(options || {}, {
+ commandsEnabled: true,
+ context: {},
+ inputMinHeight: 28,
+ mentionFetchLimit: 8,
+ // set to true to only suggest prefetched partners
+ mentionPartnersRestricted: false,
+ sendText: _t("Send"),
+ defaultBody: '',
+ defaultMentionSelections: {},
+ isMobile: config.device.isMobile
+ });
+ this.context = this.options.context;
+
+ // Attachments
+ this._attachmentDataSet = new data.DataSetSearch(this, 'ir.attachment', this.context);
+ this.fileuploadID = _.uniqueId('o_chat_fileupload');
+ this.set('attachment_ids', []);
+
+ // Mention
+ this._mentionManager = new MentionManager(this);
+ this._mentionManager.register({
+ delimiter: '@',
+ fetchCallback: this._mentionFetchPartners.bind(this),
+ generateLinks: true,
+ model: 'res.partner',
+ redirectClassname: 'o_mail_redirect',
+ selection: this.options.defaultMentionSelections['@'],
+ suggestionTemplate: 'mail.MentionSuggestions.Partner',
+ });
+ this._mentionManager.register({
+ delimiter: '#',
+ fetchCallback: this._mentionFetchChannels.bind(this),
+ generateLinks: true,
+ model: 'mail.channel',
+ redirectClassname: 'o_channel_redirect',
+ selection: this.options.defaultMentionSelections['#'],
+ suggestionTemplate: 'mail.MentionSuggestions.Channel',
+ });
+ this._mentionManager.register({
+ delimiter: ':',
+ fetchCallback: this._mentionGetCannedResponses.bind(this),
+ selection: this.options.defaultMentionSelections[':'],
+ suggestionTemplate: 'mail.MentionSuggestions.CannedResponse',
+ });
+ if (this.options.commandsEnabled) {
+ this._mentionManager.register({
+ beginningOnly: true,
+ delimiter: '/',
+ fetchCallback: this._mentionGetCommands.bind(this),
+ selection: this.options.defaultMentionSelections['/'],
+ suggestionTemplate: 'mail.MentionSuggestions.Command',
+ });
+ }
+
+ this.isMini = options.isMini;
+
+ this.avatarURL = session.uid > 0 ? session.url('/web/image', {
+ model: 'res.users',
+ field: 'image_small',
+ id: session.uid,
+ }) : '/web/static/src/img/user_menu_avatar.png';
+ },
+ start: function () {
+ var self = this;
+
+ this._$attachmentButton = this.$('.o_composer_button_add_attachment');
+ this._$attachmentsList = this.$('.o_composer_attachments_list');
+ this.$input = this.$('.o_composer_input textarea');
+ this.$input.focus(function () {
+ self.trigger('input_focused');
+ });
+ this.$input.val(this.options.defaultBody);
+ dom.autoresize(this.$input, {
+ parent: this,
+ min_height: this.options.inputMinHeight
+ });
+
+ // Attachments
+ $(window).on(this.fileuploadID, this._onAttachmentLoaded.bind(this));
+ this.on('change:attachment_ids', this, this._renderAttachments);
+
+ // Mention
+ this._mentionManager.prependTo(this.$('.o_composer'));
+
+ return this._super();
+ },
+ destroy: function () {
+ $(window).off(this.fileuploadID);
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * Empty input, selected partners and attachments
+ */
+ clearComposer: function () {
+ this.$input.val('');
+ this._mentionManager.resetSelections();
+ this.set('attachment_ids', []);
+ },
+ /**
+ * Set the focus on the input
+ */
+ focus: function () {
+ this.$input.focus();
+ },
+ /**
+ * Get the state of the composer.
+ *
+ * This is useful in order to (re)store its state when switchng to another
+ * thread with a composer, and coming back to the thread with this composer.
+ *
+ * @returns {Object}
+ */
+ getState: function () {
+ return {
+ attachments: this.get('attachment_ids'),
+ text: this.$input.val(),
+ };
+ },
+ /**
+ * @returns {integer} it returns length of attachment and trim content of
+ * input box to check for empty.
+ */
+ isEmpty: function () {
+ return !this.$input.val().trim() &&
+ !this.$('.o_attachments').children().length;
+ },
+ /**
+ * Set the state of the composer.
+ *
+ * This is useful in order to (re)store its state when switching to another
+ * thread with a composer, and coming back to the thread with this composer.
+ *
+ * @param {Object} state
+ * @param {Array} state.attachments
+ * @param {string} state.text
+ */
+ setState: function (state) {
+ this.set('attachment_ids', state.attachments);
+ this.$input.val(state.text);
+ },
+ /**
+ * Set the list of command suggestions on the thread.
+ *
+ * When the user partially types a command, matching commands are displayed
+ * to the user (@see _mentionGetCommands).
+ *
+ * @param {Object[]} commands
+ */
+ mentionSetCommands: function (commands) {
+ this._mentionCommands = commands;
+ },
+ /**
+ * Set the list of partner mentions suggestions that has been prefetched.
+ *
+ * When the user partially types a mention, matching prefetched partners are
+ * displayed to the user. If none of them match, then it will fetch for more
+ * partner suggestions (@see _mentionFetchPartners).
+ *
+ * @param {$.Deferred<Object[]>} prefetchedPartners
+ */
+ mentionSetPrefetchedPartners: function (prefetchedPartners) {
+ this._mentionPrefetchedPartners = prefetchedPartners;
+ },
+ /**
+ * @returns {Object} containing listener selections, grouped by delimiter
+ * as key of the object.
+ */
+ getMentionListenerSelections: function () {
+ return this._mentionManager.getListenerSelections();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _clearComposerOnSend: function () {
+ this.clearComposer();
+ },
+ /**
+ * @private
+ */
+ _doCheckAttachmentUpload: function () {
+ if (_.find(this.get('attachment_ids'), function (file) { return file.upload; })) {
+ this.do_warn(_t("Uploading error"), _t("Please, wait while the file is uploading."));
+ return false;
+ }
+ return true;
+ },
+ /**
+ * Hides the emojis container.
+ *
+ * @private
+ */
+ _hideEmojis: function () {
+ this._$emojisContainer.remove();
+ },
+ /**
+ * @private
+ * @param {string} search
+ * @returns {$.Deferred<Object[]>}
+ */
+ _mentionGetCannedResponses: function (search) {
+ var self = this;
+ var def = $.Deferred();
+ clearTimeout(this._cannedTimeout);
+ this._cannedTimeout = setTimeout(function () {
+ var cannedResponses = self.call('mail_service', 'getCannedResponses');
+ var matches = fuzzy.filter(mailUtils.unaccent(search), _.pluck(cannedResponses, 'source'));
+ var indexes = _.pluck(matches.slice(0, self.options.mentionFetchLimit), 'index');
+ def.resolve(_.map(indexes, function (index) {
+ return cannedResponses[index];
+ }));
+ }, 500);
+ return def;
+ },
+ /**
+ * @private
+ * @param {string} search
+ * @returns {Array}
+ */
+ _mentionGetCommands: function (search) {
+ var searchRegexp = new RegExp(_.str.escapeRegExp(mailUtils.unaccent(search)), 'i');
+ return _.filter(this._mentionCommands, function (command) {
+ return searchRegexp.test(command.name);
+ }).slice(0, this.options.mentionFetchLimit);
+ },
+ /**
+ * @private
+ * @param {string} search
+ */
+ _mentionFetchChannels: function (search) {
+ return this._mentionFetchThrottled('mail.channel', 'get_mention_suggestions', {
+ limit: this.options.mentionFetchLimit,
+ search: search,
+ }).then(function (suggestions) {
+ return _.partition(suggestions, function (suggestion) {
+ return _.contains(['public', 'groups'], suggestion.public);
+ });
+ });
+ },
+ /**
+ * @private
+ * @param {string} search
+ */
+ _mentionFetchPartners: function (search) {
+ var self = this;
+ return $.when(this._mentionPrefetchedPartners).then(function (partners) {
+ // filter prefetched partners with the given search string
+ var suggestions = [];
+ var limit = self.options.mentionFetchLimit;
+ var searchRegexp = new RegExp(_.str.escapeRegExp(mailUtils.unaccent(search)), 'i');
+ if (limit > 0) {
+ var filteredPartners = _.filter(partners, function (partner) {
+ return partner.email && searchRegexp.test(partner.email) ||
+ partner.name && searchRegexp.test(mailUtils.unaccent(partner.name));
+ });
+ if (filteredPartners.length) {
+ suggestions.push(filteredPartners.slice(0, limit));
+ limit -= filteredPartners.length;
+ }
+ }
+ if (!suggestions.length && !self.options.mentionPartnersRestricted) {
+ // no result found among prefetched partners, fetch other suggestions
+ suggestions = self._mentionFetchThrottled(
+ 'res.partner',
+ 'get_mention_suggestions',
+ { limit: limit, search: search }
+ );
+ }
+ return suggestions;
+ });
+ },
+ /**
+ * @private
+ * @param {string} model
+ * @param {string} method
+ * @param {Object} kwargs
+ * @return {$.Deferred}
+ */
+ _mentionFetchThrottled: function (model, method, kwargs) {
+ var self = this;
+ // Delays the execution of the RPC to prevent unnecessary RPCs when the user is still typing
+ var def = $.Deferred();
+ clearTimeout(this.mentionFetchTimer);
+ this.mentionFetchTimer = setTimeout(function () {
+ return self._rpc({model: model, method: method, kwargs: kwargs})
+ .then(function (results) {
+ def.resolve(results);
+ });
+ }, this.MENTION_THROTTLE);
+ return def;
+ },
+ /**
+ * @private
+ * @returns {$.Deferred}
+ */
+ _preprocessMessage: function () {
+ // Return a deferred as this function is extended with asynchronous
+ // behavior for the chatter composer
+
+ //Removing unwanted extra spaces from message
+ var value = _.escape(this.$input.val()).trim();
+ value = value.replace(/(\r|\n){2,}/g, '<br/><br/>');
+ value = value.replace(/(\r|\n)/g, '<br/>');
+
+ // prevent html space collapsing
+ value = value.replace(/ /g, '&nbsp;').replace(/([^>])&nbsp;([^<])/g, '$1 $2');
+ var commands = this.options.commandsEnabled ?
+ this._mentionManager.getListenerSelection('/') :
+ [];
+ return $.when({
+ content: this._mentionManager.generateLinks(value),
+ attachment_ids: _.pluck(this.get('attachment_ids'), 'id'),
+ partner_ids: _.uniq(_.pluck(this._mentionManager.getListenerSelection('@'), 'id')),
+ command: commands.length > 0 ? commands[0].name : undefined,
+ });
+ },
+ /**
+ * @private
+ */
+ _renderAttachments: function () {
+ this._$attachmentsList.html(QWeb.render('mail.composer.Attachments', {
+ attachments: this.get('attachment_ids'),
+ }));
+ },
+ /**
+ * @private
+ */
+ _sendMessage: function () {
+ if (this.isEmpty() || !this._doCheckAttachmentUpload()) {
+ return;
+ }
+
+ clearTimeout(this._cannedTimeout);
+ var self = this;
+ this._preprocessMessage().then(function (message) {
+ self.trigger('post_message', message);
+ self._clearComposerOnSend();
+ self.$input.focus();
+ });
+ },
+ /**
+ * Send the message on ENTER, but go to new line on SHIFT+ENTER
+ *
+ * @private
+ */
+ _shouldSend: function (ev) {
+ return !ev.shiftKey;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {jQuery.Event} ev
+ */
+ _onAttachmentChange: function (ev) {
+ var self = this;
+ var files = ev.target.files;
+ var attachments = this.get('attachment_ids');
+
+ _.each(files, function (file){
+ var attachment = _.findWhere(attachments, {name: file.name});
+ // if the files already exits, delete the file before upload
+ if (attachment){
+ self._attachmentDataSet.unlink([attachment.id]);
+ attachments = _.without(attachments, attachment);
+ }
+ });
+
+ this.$('form.o_form_binary_form').submit();
+ this._$attachmentButton.prop('disabled', true);
+ var uploadAttachments = _.map(files, function (file){
+ return {
+ id: 0,
+ name: file.name,
+ filename: file.name,
+ url: '',
+ upload: true,
+ mimetype: '',
+ };
+ });
+ attachments = attachments.concat(uploadAttachments);
+ this.set('attachment_ids', attachments);
+ },
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAttachmentDelete: function (ev){
+ ev.stopPropagation();
+ var self = this;
+ var attachmentID = $(ev.target).data('id');
+ if (attachmentID) {
+ var attachments = [];
+ _.each(this.get('attachment_ids'), function (attachment){
+ if (attachmentID !== attachment.id) {
+ attachments.push(attachment);
+ } else {
+ self._attachmentDataSet.unlink([attachmentID]);
+ }
+ });
+ this.set('attachment_ids', attachments);
+ }
+ },
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAttachmentDownload: function (ev) {
+ ev.stopPropagation();
+ },
+ /**
+ * @private
+ */
+ _onAttachmentLoaded: function () {
+ var attachments = this.get('attachment_ids');
+ var files = Array.prototype.slice.call(arguments, 1);
+
+ _.each(files, function (file) {
+ if (file.error || !file.id){
+ this.do_warn(file.error);
+ attachments = _.filter(attachments, function (attachment) {
+ return !attachment.upload;
+ });
+ } else {
+ var attachment = _.findWhere(attachments, { filename: file.filename, upload: true });
+ if (attachment) {
+ attachments = _.without(attachments, attachment);
+ attachments.push({
+ id: file.id,
+ name: file.name || file.filename,
+ filename: file.filename,
+ mimetype: file.mimetype,
+ url: session.url('/web/content', { id: file.id, download: true }),
+ });
+ }
+ }
+ }.bind(this));
+ this.set('attachment_ids', attachments);
+ this._$attachmentButton.prop('disabled', false);
+ },
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onAttachmentView: function (ev) {
+ var activeAttachmentID = $(ev.currentTarget).data('id');
+ var attachments = this.get('attachment_ids');
+ if (activeAttachmentID) {
+ var attachmentViewer = new DocumentViewer(this, attachments, activeAttachmentID);
+ attachmentViewer.appendTo($('body'));
+ }
+ },
+ _onClickAddAttachment: function () {
+ this.$('input.o_input_file').click();
+ this.$input.focus();
+ },
+ /**
+ * Called when the emoji button is clicked -> opens/hides the emoji panel.
+ * Also, this method is in charge of the rendering of this panel the first
+ * time it is opened.
+ *
+ * @private
+ */
+ _onEmojiButtonClick: function () {
+ if (!this._$emojisContainer) { // lazy rendering
+ this._$emojisContainer = $(QWeb.render('mail.Composer.emojis', {
+ emojis: emojis,
+ }));
+ }
+ if (this._$emojisContainer.parent().length) {
+ this._hideEmojis();
+ } else {
+ this._$emojisContainer.appendTo(this.$('.o_composer'));
+ }
+ },
+ /**
+ * Called when the emoji button is blurred -> closes the emoji panel. The
+ * closing is scheduled to be done at the end of the current execution
+ * stack to allow stoping this closing if the button was focusout to select
+ * an emoji (for example).
+ *
+ * @private
+ */
+ _onEmojiButtonFocusout: function () {
+ if (this._$emojisContainer) {
+ this._hideEmojisTimeout = setTimeout(this._hideEmojis.bind(this), 0);
+ }
+ },
+ /**
+ * Called when an emoji is clicked -> adds it in the <input/>, focuses the
+ * <input/> and closes the emoji panel.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onEmojiImageClick: function (ev) {
+ this.$input.val(this.$input.val() + " " + $(ev.currentTarget).data('emoji') + " ");
+ this.$input.focus();
+ this._hideEmojis();
+ },
+ /**
+ * Called when an emoji is focused -> @see _onEmojiButtonFocusout
+ *
+ * @private
+ */
+ _onEmojiImageFocus: function () {
+ clearTimeout(this._hideEmojisTimeout);
+ },
+ /**
+ * _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} ev
+ */
+ _onKeydown: function (ev) {
+ switch (ev.which) {
+ // UP, DOWN, TAB: prevent moving cursor if navigation in mention propositions
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.TAB:
+ if (this._mentionManager.isOpen()) {
+ ev.preventDefault();
+ }
+ break;
+ // ENTER: submit the message only if the dropdown mention proposition is not displayed
+ case $.ui.keyCode.ENTER:
+ if (this._mentionManager.isOpen()) {
+ ev.preventDefault();
+ } else {
+ var sendMessage = ev.ctrlKey || this._shouldSend(ev);
+ if (sendMessage) {
+ ev.preventDefault();
+ this._sendMessage();
+ }
+ }
+ break;
+ }
+ },
+ /**
+ * _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} ev
+ */
+ _onKeyup: function (ev) {
+ switch (ev.which) {
+ // ESCAPED KEYS: do nothing
+ case $.ui.keyCode.END:
+ case $.ui.keyCode.PAGE_UP:
+ case $.ui.keyCode.PAGE_DOWN:
+ break;
+ // ESCAPE: close mention propositions
+ case $.ui.keyCode.ESCAPE:
+ if (this._mentionManager.isOpen()) {
+ ev.stopPropagation();
+ this._mentionManager.resetSuggestions();
+ } else {
+ this.trigger_up('escape_pressed');
+ }
+ break;
+ // ENTER, UP, DOWN, TAB: check if navigation in mention propositions
+ case $.ui.keyCode.ENTER:
+ case $.ui.keyCode.UP:
+ case $.ui.keyCode.DOWN:
+ case $.ui.keyCode.TAB:
+ if (this._mentionManager.isOpen()) {
+ this._mentionManager.propositionNavigation(ev.which);
+ }
+ break;
+ // Otherwise, check if a mention is typed
+ default:
+ this._mentionManager.detectDelimiter();
+ }
+ },
+});
+
+return BasicComposer;
+
+});