diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index e1dba3231..1c07abfa7 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -8655,8 +8655,13 @@ button.module-image__border-overlay:focus { height: 100%; .ql-editor { + caret-color: transparent; padding: 0; + &--loaded { + caret-color: auto; + } + &.ql-blank::before { left: 0; right: 0; diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx index e0ae8fb27..3fb396a51 100644 --- a/ts/components/CompositionInput.tsx +++ b/ts/components/CompositionInput.tsx @@ -24,7 +24,11 @@ import { matchReactEmoji, } from '../quill/emoji/matchers'; import { matchMention } from '../quill/mentions/matchers'; -import { MemberRepository, getDeltaToRemoveStaleMentions } from '../quill/util'; +import { + MemberRepository, + getDeltaToRemoveStaleMentions, + getTextAndMentionsFromOps, +} from '../quill/util'; Quill.register('formats/emoji', EmojiBlot); Quill.register('formats/mention', MentionBlot); @@ -221,32 +225,7 @@ export const CompositionInput: React.ComponentType = props => { return ['', []]; } - const mentions: Array = []; - - const text = ops.reduce((acc, { insert }) => { - if (typeof insert === 'string') { - return acc + insert; - } - - if (insert.emoji) { - return acc + insert.emoji; - } - - if (insert.mention) { - mentions.push({ - length: 1, // The length of `\uFFFC` - mentionUuid: insert.mention.uuid, - replacementText: insert.mention.title, - start: acc.length, - }); - - return `${acc}\uFFFC`; - } - - return acc; - }, ''); - - return [text.trim(), mentions]; + return getTextAndMentionsFromOps(ops); }; const focus = () => { @@ -368,7 +347,7 @@ export const CompositionInput: React.ComponentType = props => { return false; } - if (large) { + if (propsRef.current.large) { return true; } @@ -435,6 +414,26 @@ export const CompositionInput: React.ComponentType = props => { return true; }; + const onCtrlA = () => { + const quill = quillRef.current; + + if (quill === undefined) { + return; + } + + quill.setSelection(0, 0); + }; + + const onCtrlE = () => { + const quill = quillRef.current; + + if (quill === undefined) { + return; + } + + quill.setSelection(quill.getLength(), 0); + }; + const onChange = () => { const quill = quillRef.current; @@ -564,6 +563,8 @@ export const CompositionInput: React.ComponentType = props => { handler: onShortKeyEnter, }, onEscape: { key: 27, handler: onEscape }, // 27 = Escape + onCtrlA: { key: 65, ctrlKey: true, handler: onCtrlA }, // 65 = a + onCtrlE: { key: 69, ctrlKey: true, handler: onCtrlE }, // 69 = e }, }, emojiCompletion: { @@ -603,6 +604,7 @@ export const CompositionInput: React.ComponentType = props => { setTimeout(() => { quill.setSelection(quill.getLength(), 0); + quill.root.classList.add('ql-editor--loaded'); }, 0); }); diff --git a/ts/quill/util.ts b/ts/quill/util.ts index fc5aae7dc..d858becad 100644 --- a/ts/quill/util.ts +++ b/ts/quill/util.ts @@ -6,6 +6,7 @@ import Delta from 'quill-delta'; import { DeltaOperation } from 'quill'; import { ConversationType } from '../state/ducks/conversations'; +import { BodyRangeType } from '../types/Util'; const FUSE_OPTIONS = { shouldSort: true, @@ -15,6 +16,52 @@ const FUSE_OPTIONS = { keys: ['name', 'firstName', 'profileName', 'title'], }; +export const getTextAndMentionsFromOps = ( + ops: Array +): [string, Array] => { + const mentions: Array = []; + + const text = ops.reduce((acc, { insert }, index) => { + if (typeof insert === 'string') { + let textToAdd; + switch (index) { + case 0: { + textToAdd = insert.trimLeft(); + break; + } + case ops.length - 1: { + textToAdd = insert.trimRight(); + break; + } + default: { + textToAdd = insert; + break; + } + } + return acc + textToAdd; + } + + if (insert.emoji) { + return acc + insert.emoji; + } + + if (insert.mention) { + mentions.push({ + length: 1, // The length of `\uFFFC` + mentionUuid: insert.mention.uuid, + replacementText: insert.mention.title, + start: acc.length, + }); + + return `${acc}\uFFFC`; + } + + return acc; + }, ''); + + return [text, mentions]; +}; + export const getDeltaToRemoveStaleMentions = ( ops: Array, memberUuids: Array diff --git a/ts/test/quill/util_test.ts b/ts/test/quill/util_test.ts index bc63948ca..b2356a361 100644 --- a/ts/test/quill/util_test.ts +++ b/ts/test/quill/util_test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { MemberRepository, getDeltaToRemoveStaleMentions, + getTextAndMentionsFromOps, } from '../../quill/util'; import { ConversationType } from '../../state/ducks/conversations'; @@ -162,3 +163,72 @@ describe('getDeltaToRemoveStaleMentions', () => { }); }); }); + +describe('getTextAndMentionsFromOps', () => { + describe('given only text', () => { + it('returns only text trimmed', () => { + const ops = [{ insert: ' The ' }, { insert: ' text ' }]; + const [resultText, resultMentions] = getTextAndMentionsFromOps(ops); + assert.equal(resultText, 'The text'); + assert.equal(resultMentions.length, 0); + }); + }); + + describe('given text, emoji, and mentions', () => { + it('returns the trimmed text with placeholders and mentions', () => { + const ops = [ + { + insert: { + emoji: '😂', + }, + }, + { + insert: ' wow, funny, ', + }, + { + insert: { + mention: { + uuid: 'abcdef', + title: '@fred', + }, + }, + }, + ]; + const [resultText, resultMentions] = getTextAndMentionsFromOps(ops); + assert.equal(resultText, '😂 wow, funny, \uFFFC'); + assert.deepEqual(resultMentions, [ + { + length: 1, + mentionUuid: 'abcdef', + replacementText: '@fred', + start: 15, + }, + ]); + }); + }); + + describe('given only mentions', () => { + it('returns the trimmed text with placeholders and mentions', () => { + const ops = [ + { + insert: { + mention: { + uuid: 'abcdef', + title: '@fred', + }, + }, + }, + ]; + const [resultText, resultMentions] = getTextAndMentionsFromOps(ops); + assert.equal(resultText, '\uFFFC'); + assert.deepEqual(resultMentions, [ + { + length: 1, + mentionUuid: 'abcdef', + replacementText: '@fred', + start: 0, + }, + ]); + }); + }); +});