summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/composer_text_input
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-06-15 18:12:21 +0000
committerSébastien Theys <seb@odoo.com>2020-06-15 18:12:21 +0000
commit3fea5b2136627c1d7e83b2a5e111952fe3eb10a1 (patch)
tree5eefad07972a87ce4689b83260f981bc87aadad4 /addons/mail/static/src/components/composer_text_input
parent0a82848606151ef60eb6523e9c4c161171f214e7 (diff)
[REF] mail, *: refactor messaging with OWL
This commit is a significant rewriting of client-side discuss, chatter, chat window, and messaging menu using OWL. The behavior should be broadly the same, with some slight functional changes here and there. From a technical standpoint, the code of messaging is mainly organized in 2 main groups of modules: - models, which are logical entities that depict the client-side state of messaging as a whole. - components, which are in charge of displaying information from models. This refactoring also introduces new JS guidelines regarding folder structure (/static) and naming rules for JS modules. Community PR: https://github.com/odoo/odoo/pull/39023 Enterprise PR: https://github.com/odoo/enterprise/pull/6249 Task-1914207 This PR is a collaborative work by Alexandre, Julien, Sébastien and Xavier, with the precious help of Lucas to speed it up towards the end. closes odoo/odoo#39023 Related: odoo/enterprise#6249 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com> Co-authored-by: Alexandre Kühn <aku@odoo.com> Co-authored-by: Julien Giannone <jgi@odoo.com> Co-authored-by: Lucas Perais <lpe@odoo.com> Co-authored-by: Sébastien Theys <seb@odoo.com> Co-authored-by: Xavier Dubuc <xdu@odoo.com>
Diffstat (limited to 'addons/mail/static/src/components/composer_text_input')
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.js334
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.scss25
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.xml44
3 files changed, 403 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
new file mode 100644
index 00000000000..198f563d1c7
--- /dev/null
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
@@ -0,0 +1,334 @@
+odoo.define('mail/static/src/components/composer_text_input/composer_text_input.js', function (require) {
+'use strict';
+
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+
+const components = {
+ PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'),
+};
+
+const { Component } = owl;
+const { useRef } = owl.hooks;
+
+/**
+ * ComposerInput relies on a minimal HTML editor in order to support mentions.
+ */
+class ComposerTextInput extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useStore(props => {
+ const composer = this.env.models['mail.composer'].get(props.composerLocalId);
+ return {
+ composer: composer ? composer.__state : undefined,
+ isDeviceMobile: this.env.messaging.device.isMobile,
+ };
+ });
+ /**
+ * Last content of textarea from input event. Useful to determine
+ * whether the current partner is typing something.
+ */
+ this._textareaLastInputValue = "";
+ /**
+ * Reference of the textarea. Useful to set height, selection and content.
+ */
+ this._textareaRef = useRef('textarea');
+ }
+
+ /**
+ * Updates the composer text input content when composer is mounted
+ * as textarea content can't be changed from the DOM.
+ */
+ mounted() {
+ this._update();
+ }
+
+ /**
+ * Updates the composer text input content when composer has changed
+ * as textarea content can't be changed from the DOM.
+ */
+ patched() {
+ this._update();
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.composer}
+ */
+ get composer() {
+ return this.env.models['mail.composer'].get(this.props.composerLocalId);
+ }
+
+ focusout() {
+ this.saveStateInStore();
+ this._textareaRef.el.blur();
+ }
+
+ /**
+ * Saves the composer text input state in store
+ */
+ saveStateInStore() {
+ this.composer.update({
+ textInputContent: this._getContent(),
+ textInputCursorStart: this._getSelectionStart(),
+ textInputCursorEnd: this._getSelectionEnd(),
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns textarea current content.
+ *
+ * @private
+ * @returns {string}
+ */
+ _getContent() {
+ return this._textareaRef.el.value;
+ }
+
+ /**
+ * Returns selection end position.
+ *
+ * @private
+ * @returns {integer}
+ */
+ _getSelectionEnd() {
+ return this._textareaRef.el.selectionEnd;
+ }
+
+ /**
+ * Returns selection start position.
+ *
+ * @private
+ * @returns {integer}
+ *
+ */
+ _getSelectionStart() {
+ return this._textareaRef.el.selectionStart;
+ }
+
+ /**
+ * Determines whether the textarea is empty or not.
+ *
+ * @private
+ * @returns {boolean}
+ */
+ _isEmpty() {
+ return this._getContent() === "";
+ }
+
+ /**
+ * Updates the content and height of a textarea
+ *
+ * @private
+ */
+ _update() {
+ this._textareaRef.el.value = this.composer.textInputContent;
+ this._textareaRef.el.setSelectionRange(
+ this.composer.textInputCursorStart,
+ this.composer.textInputCursorEnd
+ );
+ this._updateHeight();
+ if (this.composer.isDoFocus) {
+ this._textareaRef.el.focus();
+ this.composer.update({ isDoFocus: false });
+ }
+ }
+
+ /**
+ * Updates the textarea height.
+ *
+ * @private
+ */
+ _updateHeight() {
+ this._textareaRef.el.style.height = "0px";
+ this._textareaRef.el.style.height = (this._textareaRef.el.scrollHeight) + "px";
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onInputTextarea() {
+ if (this._textareaLastInputValue !== this._textareaRef.el.value) {
+ this.composer.handleCurrentPartnerIsTyping();
+ }
+ this._textareaLastInputValue = this._textareaRef.el.value;
+ this._updateHeight();
+ this.saveStateInStore();
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydownTextarea(ev) {
+ switch (ev.key) {
+ // UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions
+ case 'ArrowUp':
+ case 'PageUp':
+ case 'ArrowDown':
+ case 'PageDown':
+ case 'Home':
+ case 'End':
+ case 'Tab':
+ if (this.composer.hasSuggestedPartners) {
+ // We use preventDefault here to avoid keys native actions but actions are handled in keyUp
+ ev.preventDefault();
+ }
+ break;
+ // ENTER: submit the message only if the dropdown mention proposition is not displayed
+ case 'Enter':
+ this._onKeydownTextareaEnter(ev);
+ break;
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydownTextareaEnter(ev) {
+ if (this.composer.hasSuggestedPartners) {
+ ev.preventDefault();
+ } else {
+ if (!this.props.hasSendOnEnterEnabled) {
+ return;
+ }
+ if (ev.shiftKey) {
+ return;
+ }
+ if (this.env.messaging.device.isMobile) {
+ return;
+ }
+ this.trigger('o-keydown-enter');
+ ev.preventDefault();
+ }
+ }
+
+ /**
+ * Key events management is performed in a Keyup to avoid intempestive RPC calls
+ *
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeyupTextarea(ev) {
+ switch (ev.key) {
+ // ESCAPE: close mention suggestions
+ case 'Escape':
+ this._onKeyupTextareaEscape(ev);
+ break;
+ // ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
+ case 'Enter':
+ if (this.composer.hasSuggestedPartners) {
+ if (this.composer.activeSuggestedPartner) {
+ this.composer.insertMentionedPartner(this.composer.activeSuggestedPartner);
+ this.composer.closeMentionSuggestions();
+ this.composer.focus();
+ }
+ }
+ break;
+ case 'ArrowUp':
+ case 'PageUp':
+ if (this.composer.hasSuggestedPartners) {
+ this.composer.setPreviousSuggestedPartnerActive();
+ }
+ break;
+ case 'ArrowDown':
+ case 'PageDown':
+ if (this.composer.hasSuggestedPartners) {
+ this.composer.setNextSuggestedPartnerActive();
+ }
+ break;
+ case 'Home':
+ if (this.composer.hasSuggestedPartners) {
+ this.composer.setFirstSuggestedPartnerActive();
+ }
+ break;
+ case 'End':
+ if (this.composer.hasSuggestedPartners) {
+ this.composer.setLastSuggestedPartnerActive();
+ }
+ break;
+ case 'Tab':
+ if (this.composer.hasSuggestedPartners) {
+ if (ev.shiftKey) {
+ this.composer.setPreviousSuggestedPartnerActive();
+ } else {
+ this.composer.setNextSuggestedPartnerActive();
+ }
+ }
+ break;
+ // Otherwise, check if a mention is typed
+ default:
+ this.saveStateInStore();
+ this.composer._detectDelimiter();
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeyupTextareaEscape(ev) {
+ if (this.composer.hasSuggestedPartners) {
+ this.composer.closeMentionSuggestions();
+ } else {
+ if (!this._isEmpty()) {
+ return;
+ }
+ this.composer.discard();
+ }
+ }
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onPartnerMentionSuggestionClicked(ev) {
+ this.composer.insertMentionedPartner(ev.detail.partner);
+ this.composer.closeMentionSuggestions();
+ this.composer.focus();
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onPartnerMentionSuggestionMouseOver(ev) {
+ this.composer.update({
+ activeSuggestedPartner: [['link', ev.detail.partner]],
+ });
+ }
+
+}
+
+Object.assign(ComposerTextInput, {
+ components,
+ defaultProps: {
+ hasMentionSuggestionsBelowPosition: false,
+ hasSendOnEnterEnabled: true,
+ },
+ props: {
+ hasMentionSuggestionsBelowPosition: Boolean,
+ hasSendOnEnterEnabled: Boolean,
+ composerLocalId: String,
+ },
+ template: 'mail.ComposerTextInput',
+});
+
+return ComposerTextInput;
+
+});
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.scss b/addons/mail/static/src/components/composer_text_input/composer_text_input.scss
new file mode 100644
index 00000000000..fe3ad16788d
--- /dev/null
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.scss
@@ -0,0 +1,25 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_ComposerTextInput {
+ min-width: 0;
+ position: relative;
+}
+
+.o_ComposerTextInput_mentionDropdown {
+ position: absolute;
+
+ &.o-lowPosition {
+ bottom: 0;
+ }
+}
+
+.o_ComposerTextInput_textarea {
+ padding: 10px;
+ max-height: 200px;
+ resize: none;
+ border-radius: 10px 0 0 10px;
+ border: none;
+ overflow: auto
+}
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.xml b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
new file mode 100644
index 00000000000..f68b295cb8d
--- /dev/null
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.ComposerTextInput" owl="1">
+ <div class="o_ComposerTextInput">
+ <t t-if="composer">
+ <t t-if="composer.hasSuggestedPartners">
+ <div class="o_ComposerTextInput_mentionDropdown" t-att-class="{ 'o-lowPosition': props.hasMentionSuggestionsBelowPosition }">
+ <div
+ t-att-class="{
+ 'dropdown': props.hasMentionSuggestionsBelowPosition,
+ 'dropup': !props.hasMentionSuggestionsBelowPosition,
+ }"
+ >
+ <div class="o_ComposerTextInput_mentionDropdownPropositionList dropdown-menu show">
+ <t t-foreach="composer.mainSuggestedPartners" t-as="mainSuggestedPartner" t-key="mainSuggestedPartner.localId">
+ <PartnerMentionSuggestion
+ isActive="mainSuggestedPartner === composer.activeSuggestedPartner"
+ partnerLocalId="mainSuggestedPartner.localId"
+ t-on-o-partner-mention-suggestion-clicked="_onPartnerMentionSuggestionClicked"
+ t-on-o-partner-mention-suggestion-mouse-over="_onPartnerMentionSuggestionMouseOver"
+ />
+ </t>
+ <t t-if="composer.mainSuggestedPartners.length > 0 and composer.extraSuggestedPartners.length > 0">
+ <div role="separator" class="dropdown-divider"/>
+ </t>
+ <t t-foreach="composer.extraSuggestedPartners" t-as="extraSuggestedPartner" t-key="extraSuggestedPartner.localId">
+ <PartnerMentionSuggestion
+ isActive="extraSuggestedPartner === composer.activeSuggestedPartner"
+ partnerLocalId="extraSuggestedPartner.localId"
+ t-on-o-partner-mention-suggestion-clicked="_onPartnerMentionSuggestionClicked"
+ t-on-o-partner-mention-suggestion-mouse-over="_onPartnerMentionSuggestionMouseOver"
+ />
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+ <textarea class="o_ComposerTextInput_textarea" t-esc="composer.textInputContent" placeholder="Write something..." t-on-keydown="_onKeydownTextarea" t-on-keyup="_onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
+ </t>
+ </div>
+ </t>
+
+</templates>