From 2883b4c35b12d35272d4cebb1185cf1938d402c2 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Wed, 14 Jun 2023 03:47:18 +1000 Subject: [PATCH] Fix editor bugs (#1281) * focus editor on reply click * fix emoji and sticker img object-fit * fix cursor not moving with autocomplete * stop sanitizing sending plain text body * improve autocomplete query parsing * add escape to turn off active editor toolbar item --- src/app/components/editor/Editor.tsx | 3 +- .../autocomplete/EmoticonAutocomplete.tsx | 2 +- .../editor/autocomplete/autocompleteQuery.ts | 13 +++--- src/app/components/editor/common.ts | 22 +++++++++- src/app/components/editor/keyboard.ts | 43 ++++++++++++++----- src/app/components/editor/output.ts | 2 +- .../components/emoji-board/EmojiBoard.css.tsx | 2 + src/app/components/emoji-board/EmojiBoard.tsx | 1 + src/app/organisms/room/RoomInput.tsx | 3 +- 9 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/app/components/editor/Editor.tsx b/src/app/components/editor/Editor.tsx index edf1ac6..2657b21 100644 --- a/src/app/components/editor/Editor.tsx +++ b/src/app/components/editor/Editor.tsx @@ -93,7 +93,8 @@ export const CustomEditor = forwardRef( const handleKeydown: KeyboardEventHandler = useCallback( (evt) => { onKeyDown?.(evt); - toggleKeyboardShortcut(editor, evt); + const shortcutToggled = toggleKeyboardShortcut(editor, evt); + if (shortcutToggled) evt.preventDefault(); }, [editor, onKeyDown] ); diff --git a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx index e5af3fa..17712b8 100644 --- a/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx +++ b/src/app/components/editor/autocomplete/EmoticonAutocomplete.tsx @@ -104,7 +104,7 @@ export function EmoticonAutocomplete({ as="img" src={mx.mxcUrlToHttp(key) || key} alt={emoticon.shortcode} - style={{ width: toRem(24), height: toRem(24) }} + style={{ width: toRem(24), height: toRem(24), objectFit: 'contain' }} /> ) : ( ( validPrefixes: readonly TPrefix[] ): TPrefix | undefined => { const world = Editor.string(editor, queryRange); - const prefix = world[0] as TPrefix | undefined; - if (!prefix) return undefined; - return validPrefixes.includes(prefix) ? prefix : undefined; + return validPrefixes.find((p) => world.startsWith(p)); }; -export const getAutocompleteQueryText = (editor: Editor, queryRange: BaseRange): string => - Editor.string(editor, queryRange).slice(1); +export const getAutocompleteQueryText = ( + editor: Editor, + queryRange: BaseRange, + prefix: string +): string => Editor.string(editor, queryRange).slice(prefix.length); export const getAutocompleteQuery = ( editor: Editor, @@ -41,6 +42,6 @@ export const getAutocompleteQuery = ( return { range: queryRange, prefix, - text: getAutocompleteQueryText(editor, queryRange), + text: getAutocompleteQueryText(editor, queryRange, prefix), }; }; diff --git a/src/app/components/editor/common.ts b/src/app/components/editor/common.ts index c9cf086..619a1bf 100644 --- a/src/app/components/editor/common.ts +++ b/src/app/components/editor/common.ts @@ -2,11 +2,25 @@ import { BasePoint, BaseRange, Editor, Element, Point, Range, Transforms } from import { BlockType, MarkType } from './Elements'; import { EmoticonElement, FormattedText, HeadingLevel, LinkElement, MentionElement } from './slate'; +const ALL_MARK_TYPE: MarkType[] = [ + MarkType.Bold, + MarkType.Code, + MarkType.Italic, + MarkType.Spoiler, + MarkType.StrikeThrough, + MarkType.Underline, +]; + export const isMarkActive = (editor: Editor, format: MarkType) => { const marks = Editor.marks(editor); return marks ? marks[format] === true : false; }; +export const isAnyMarkActive = (editor: Editor) => { + const marks = Editor.marks(editor); + return marks && !!ALL_MARK_TYPE.find((type) => marks[type] === true); +}; + export const toggleMark = (editor: Editor, format: MarkType) => { const isActive = isMarkActive(editor, format); @@ -17,6 +31,10 @@ export const toggleMark = (editor: Editor, format: MarkType) => { } }; +export const removeAllMark = (editor: Editor) => { + ALL_MARK_TYPE.forEach((mark) => Editor.removeMark(editor, mark)); +}; + export const isBlockActive = (editor: Editor, format: BlockType) => { const [match] = Editor.nodes(editor, { match: (node) => Element.isElement(node) && node.type === format, @@ -140,11 +158,11 @@ export const replaceWithElement = (editor: Editor, selectRange: BaseRange, eleme }; export const moveCursor = (editor: Editor, withSpace?: boolean) => { - // without timeout it works properly when we select autocomplete with Tab or Space + // without timeout move cursor doesn't works properly. setTimeout(() => { Transforms.move(editor); if (withSpace) editor.insertText(' '); - }, 1); + }, 100); }; interface PointUntilCharOptions { diff --git a/src/app/components/editor/keyboard.ts b/src/app/components/editor/keyboard.ts index 52217dd..3fbe536 100644 --- a/src/app/components/editor/keyboard.ts +++ b/src/app/components/editor/keyboard.ts @@ -1,7 +1,7 @@ import { isHotkey } from 'is-hotkey'; import { KeyboardEvent } from 'react'; import { Editor } from 'slate'; -import { isBlockActive, toggleBlock, toggleMark } from './common'; +import { isAnyMarkActive, isBlockActive, removeAllMark, toggleBlock, toggleMark } from './common'; import { BlockType, MarkType } from './Elements'; export const INLINE_HOTKEYS: Record = { @@ -22,19 +22,42 @@ export const BLOCK_HOTKEYS: Record = { }; const BLOCK_KEYS = Object.keys(BLOCK_HOTKEYS); -export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent) => { - BLOCK_KEYS.forEach((hotkey) => { +/** + * @return boolean true if shortcut is toggled. + */ +export const toggleKeyboardShortcut = (editor: Editor, event: KeyboardEvent): boolean => { + if (isHotkey('escape', event)) { + if (isAnyMarkActive(editor)) { + removeAllMark(editor); + return true; + } + console.log(isBlockActive(editor, BlockType.Paragraph)); + if (!isBlockActive(editor, BlockType.Paragraph)) { + toggleBlock(editor, BlockType.Paragraph); + return true; + } + return false; + } + + const blockToggled = BLOCK_KEYS.find((hotkey) => { if (isHotkey(hotkey, event)) { event.preventDefault(); toggleBlock(editor, BLOCK_HOTKEYS[hotkey]); + return true; } + return false; }); + if (blockToggled) return true; - if (!isBlockActive(editor, BlockType.CodeBlock)) - INLINE_KEYS.forEach((hotkey) => { - if (isHotkey(hotkey, event)) { - event.preventDefault(); - toggleMark(editor, INLINE_HOTKEYS[hotkey]); - } - }); + const inlineToggled = isBlockActive(editor, BlockType.CodeBlock) + ? false + : INLINE_KEYS.find((hotkey) => { + if (isHotkey(hotkey, event)) { + event.preventDefault(); + toggleMark(editor, INLINE_HOTKEYS[hotkey]); + return true; + } + return false; + }); + return !!inlineToggled; }; diff --git a/src/app/components/editor/output.ts b/src/app/components/editor/output.ts index 091dab7..38c5449 100644 --- a/src/app/components/editor/output.ts +++ b/src/app/components/editor/output.ts @@ -88,7 +88,7 @@ const elementToPlainText = (node: CustomElement, children: string): string => { export const toPlainText = (node: Descendant | Descendant[]): string => { if (Array.isArray(node)) return node.map((n) => toPlainText(n)).join(''); - if (Text.isText(node)) return sanitizeText(node.text); + if (Text.isText(node)) return node.text; const children = node.children.map((n) => toPlainText(n)).join(''); return elementToPlainText(node, children); diff --git a/src/app/components/emoji-board/EmojiBoard.css.tsx b/src/app/components/emoji-board/EmojiBoard.css.tsx index 0fefc5b..adeb250 100644 --- a/src/app/components/emoji-board/EmojiBoard.css.tsx +++ b/src/app/components/emoji-board/EmojiBoard.css.tsx @@ -122,6 +122,7 @@ export const CustomEmojiImg = style([ { width: toRem(32), height: toRem(32), + objectFit: 'contain', }, ]); @@ -130,5 +131,6 @@ export const StickerImg = style([ { width: toRem(96), height: toRem(96), + objectFit: 'contain', }, ]); diff --git a/src/app/components/emoji-board/EmojiBoard.tsx b/src/app/components/emoji-board/EmojiBoard.tsx index c5f5038..3b1ccc5 100644 --- a/src/app/components/emoji-board/EmojiBoard.tsx +++ b/src/app/components/emoji-board/EmojiBoard.tsx @@ -373,6 +373,7 @@ function ImagePackSidebarStack({ style={{ width: toRem(24), height: toRem(24), + objectFit: 'contain', }} src={mx.mxcUrlToHttp(pack.getPackAvatarUrl(usage) ?? '') || pack.avatarUrl} alt={label || 'Unknown Pack'} diff --git a/src/app/organisms/room/RoomInput.tsx b/src/app/organisms/room/RoomInput.tsx index 17830ad..e79f488 100644 --- a/src/app/organisms/room/RoomInput.tsx +++ b/src/app/organisms/room/RoomInput.tsx @@ -184,12 +184,13 @@ export const RoomInput = forwardRef( body, formattedBody, }); + ReactEditor.focus(editor); }; navigation.on(cons.events.navigation.REPLY_TO_CLICKED, handleReplyTo); return () => { navigation.removeListener(cons.events.navigation.REPLY_TO_CLICKED, handleReplyTo); }; - }, [setReplyDraft]); + }, [setReplyDraft, editor]); const handleRemoveUpload = useCallback( (upload: TUploadContent | TUploadContent[]) => {