diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-06-15 18:12:21 +0000 |
| commit | 3fea5b2136627c1d7e83b2a5e111952fe3eb10a1 (patch) | |
| tree | 5eefad07972a87ce4689b83260f981bc87aadad4 /addons/mail/static/src/components/composer_text_input | |
| parent | 0a82848606151ef60eb6523e9c4c161171f214e7 (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')
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> |
