From e68636c0c2f491c7ae5f418f724de5e9f0a56565 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 8 Dec 2022 15:06:28 +0100 Subject: [PATCH 1/2] Add emoji handling for plain text mode --- .../wysiwyg_composer/components/Editor.tsx | 3 +- .../components/PlainTextComposer.tsx | 5 +-- .../hooks/useComposerFunctions.ts | 21 ++++++++++-- .../hooks/usePlainTextListeners.ts | 12 ++++--- .../wysiwyg_composer/hooks/useSelection.ts | 34 ++++++++++++------- 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx index b738847ec68..e83f19ce0f5 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/Editor.tsx @@ -34,7 +34,7 @@ export const Editor = memo( function Editor({ disabled, placeholder, leftComponent, rightComponent }: EditorProps, ref, ) { const isExpanded = useIsExpanded(ref as MutableRefObject, HEIGHT_BREAKING_POINT); - const { onFocus, onBlur, selectPreviousSelection } = useSelection(); + const { onFocus, onBlur, selectPreviousSelection, onInput } = useSelection(); return
{ rightComponent?.(selectPreviousSelection) } diff --git a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx index 5339e986cda..7bf453b80bf 100644 --- a/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx +++ b/src/components/views/rooms/wysiwyg_composer/components/PlainTextComposer.tsx @@ -54,8 +54,9 @@ export function PlainTextComposer({ rightComponent, }: PlainTextComposerProps, ) { - const { ref, onInput, onPaste, onKeyDown, content } = usePlainTextListeners(initialContent, onChange, onSend); - const composerFunctions = useComposerFunctions(ref); + const { ref, onInput, onPaste, onKeyDown, content, setContent } = + usePlainTextListeners(initialContent, onChange, onSend); + const composerFunctions = useComposerFunctions(ref, setContent); usePlainTextInitialization(initialContent, ref); useSetCursorPosition(disabled, ref); const { isFocused, onFocus } = useIsFocused(); diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts index abfde035a5f..b9f7146bba7 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useComposerFunctions.ts @@ -16,7 +16,9 @@ limitations under the License. import { RefObject, useMemo } from "react"; -export function useComposerFunctions(ref: RefObject) { +import { setSelection } from "../utils/selection"; + +export function useComposerFunctions(ref: RefObject, setContent: (content: string) => void) { return useMemo(() => ({ clear: () => { if (ref.current) { @@ -24,7 +26,20 @@ export function useComposerFunctions(ref: RefObject) { } }, insertText: (text: string) => { - // TODO + const selection = document.getSelection(); + + if (ref.current && selection) { + const content = ref.current.innerHTML; + const { anchorOffset, focusOffset } = selection; + ref.current.innerHTML = `${content.slice(0, anchorOffset)}${text}${content.slice(focusOffset)}`; + setSelection({ + anchorNode: ref.current.firstChild, + anchorOffset: anchorOffset + text.length, + focusNode: ref.current.firstChild, + focusOffset: focusOffset + text.length, + }); + setContent(ref.current.innerHTML); + } }, - }), [ref]); + }), [ref, setContent]); } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts index bf4678c693b..1931c4a9ca8 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/usePlainTextListeners.ts @@ -36,12 +36,16 @@ export function usePlainTextListeners( onSend?.(); }), [ref, onSend]); + const setText = useCallback((text: string) => { + setContent(text); + onChange?.(text); + }, [onChange]); + const onInput = useCallback((event: SyntheticEvent) => { if (isDivElement(event.target)) { - setContent(event.target.innerHTML); - onChange?.(event.target.innerHTML); + setText(event.target.innerHTML); } - }, [onChange]); + }, [setText]); const isCtrlEnter = useSettingValue("MessageComposerInput.ctrlEnterToSend"); const onKeyDown = useCallback((event: KeyboardEvent) => { @@ -52,5 +56,5 @@ export function usePlainTextListeners( } }, [isCtrlEnter, send]); - return { ref, onInput, onPaste: onInput, onKeyDown, content }; + return { ref, onInput, onPaste: onInput, onKeyDown, content, setContent: setText }; } diff --git a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts index 2ae61790dbf..b27c8c3a4dd 100644 --- a/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts +++ b/src/components/views/rooms/wysiwyg_composer/hooks/useSelection.ts @@ -14,13 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { useCallback, useEffect, useRef } from "react"; +import { MutableRefObject, useCallback, useEffect, useRef } from "react"; import useFocus from "../../../../../hooks/useFocus"; import { setSelection } from "../utils/selection"; type SubSelection = Pick; +function setSelectionRef(selectionRef: MutableRefObject) { + const selection = document.getSelection(); + + if (selection) { + selectionRef.current = { + anchorNode: selection.anchorNode, + anchorOffset: selection.anchorOffset, + focusNode: selection.focusNode, + focusOffset: selection.focusOffset, + }; + } +} + export function useSelection() { const selectionRef = useRef({ anchorNode: null, @@ -32,16 +45,7 @@ export function useSelection() { useEffect(() => { function onSelectionChange() { - const selection = document.getSelection(); - - if (selection) { - selectionRef.current = { - anchorNode: selection.anchorNode, - anchorOffset: selection.anchorOffset, - focusNode: selection.focusNode, - focusOffset: selection.focusOffset, - }; - } + setSelectionRef(selectionRef); } if (isFocused) { @@ -51,9 +55,13 @@ export function useSelection() { return () => document.removeEventListener('selectionchange', onSelectionChange); }, [isFocused]); + const onInput = useCallback(() => { + setSelectionRef(selectionRef); + }, []); + const selectPreviousSelection = useCallback(() => { setSelection(selectionRef.current); - }, [selectionRef]); + }, []); - return { ...focusProps, selectPreviousSelection }; + return { ...focusProps, selectPreviousSelection, onInput }; } From fa6d61ad82f0e50e6c3e1343d79a26efc936d479 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Thu, 8 Dec 2022 16:48:16 +0100 Subject: [PATCH 2/2] Add test to plain text mode emoji handling --- .../views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx index 1b28c6ed2e9..6cb183bb0af 100644 --- a/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx +++ b/test/components/views/rooms/wysiwyg_composer/SendWysiwygComposer-test.tsx @@ -251,7 +251,7 @@ describe('SendWysiwygComposer', () => { describe.each([ { isRichTextEnabled: true }, - // TODO { isRichTextEnabled: false }, + { isRichTextEnabled: false }, ])('Emoji when %s', ({ isRichTextEnabled }) => { let emojiButton: HTMLElement;