diff --git a/src/components/RNMarkdownTextInput.tsx b/src/components/RNMarkdownTextInput.tsx index d36af6e13826..11f8a852dbcf 100644 --- a/src/components/RNMarkdownTextInput.tsx +++ b/src/components/RNMarkdownTextInput.tsx @@ -5,13 +5,14 @@ import React from 'react'; import type {TextInput} from 'react-native'; import Animated from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; +import CONST from '@src/CONST'; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedMarkdownTextInput = Animated.createAnimatedComponent(MarkdownTextInput); type AnimatedMarkdownTextInputRef = typeof AnimatedMarkdownTextInput & TextInput & HTMLInputElement; -function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: ForwardedRef) { +function RNMarkdownTextInputWithRef({maxLength, ...props}: MarkdownTextInputProps, ref: ForwardedRef) { const theme = useTheme(); return ( @@ -27,6 +28,10 @@ function RNMarkdownTextInputWithRef(props: MarkdownTextInputProps, ref: Forwarde }} // eslint-disable-next-line {...props} + /** + * If maxLength is not set, we should set the it to CONST.MAX_COMMENT_LENGTH + 1, to avoid parsing markdown for large text + */ + maxLength={maxLength ?? CONST.MAX_COMMENT_LENGTH + 1} /> ); } diff --git a/src/hooks/useHtmlPaste/index.ts b/src/hooks/useHtmlPaste/index.ts index 6199a36abdca..ebffbf1b54b6 100644 --- a/src/hooks/useHtmlPaste/index.ts +++ b/src/hooks/useHtmlPaste/index.ts @@ -1,17 +1,30 @@ import {useNavigation} from '@react-navigation/native'; import {useCallback, useEffect} from 'react'; import Parser from '@libs/Parser'; +import CONST from '@src/CONST'; import type UseHtmlPaste from './types'; -const insertByCommand = (text: string) => { - document.execCommand('insertText', false, text); -}; +const insertAtCaret = (target: HTMLElement, insertedText: string, maxLength: number) => { + const currentText = target.textContent ?? ''; + + let availableLength = maxLength - currentText.length; + if (availableLength <= 0) { + return; + } + + let text = insertedText; -const insertAtCaret = (target: HTMLElement, text: string) => { const selection = window.getSelection(); if (selection?.rangeCount) { const range = selection.getRangeAt(0); + const selectedText = range.toString(); + availableLength -= selectedText.length; + if (availableLength <= 0) { + return; + } + text = text.slice(0, availableLength); range.deleteContents(); + const node = document.createTextNode(text); range.insertNode(node); @@ -22,40 +35,43 @@ const insertAtCaret = (target: HTMLElement, text: string) => { // Dispatch input event to trigger Markdown Input to parse the new text target.dispatchEvent(new Event('input', {bubbles: true})); - } else { - insertByCommand(text); } }; -const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false) => { +const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeListenerOnScreenBlur = false, maxLength = CONST.MAX_COMMENT_LENGTH + 1) => { const navigation = useNavigation(); /** * Set pasted text to clipboard * @param {String} text */ - const paste = useCallback((text: string) => { - try { - const textInputHTMLElement = textInputRef.current as HTMLElement; - if (textInputHTMLElement?.hasAttribute('contenteditable')) { - insertAtCaret(textInputHTMLElement, text); - } else { - insertByCommand(text); - } + const paste = useCallback( + (text: string) => { + try { + const textInputHTMLElement = textInputRef.current as HTMLElement; + if (textInputHTMLElement?.hasAttribute('contenteditable')) { + insertAtCaret(textInputHTMLElement, text, maxLength); + } else { + const htmlInput = textInputRef.current as unknown as HTMLInputElement; + const availableLength = maxLength - (htmlInput.value?.length ?? 0); + htmlInput.setRangeText(text.slice(0, availableLength)); + } - // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. - // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler - // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered - textInputHTMLElement.dispatchEvent( - new FocusEvent('focusin', { - bubbles: true, - }), - ); - // eslint-disable-next-line no-empty - } catch (e) {} - // We only need to set the callback once. - // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, []); + // Pointer will go out of sight when a large paragraph is pasted on the web. Refocusing the input keeps the cursor in view. + // To avoid the keyboard toggle issue in mWeb if using blur() and focus() functions, we just need to dispatch the event to trigger the onFocus handler + // We need to trigger the bubbled "focusin" event to make sure the onFocus handler is triggered + textInputHTMLElement.dispatchEvent( + new FocusEvent('focusin', { + bubbles: true, + }), + ); + // eslint-disable-next-line no-empty + } catch (e) {} + // We only need to set the callback once. + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, + [maxLength, textInputRef], + ); /** * Manually place the pasted HTML into Composer @@ -64,9 +80,9 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi */ const handlePastedHTML = useCallback( (html: string) => { - paste(Parser.htmlToMarkdown(html)); + paste(Parser.htmlToMarkdown(html.slice(0, maxLength))); }, - [paste], + [paste, maxLength], ); /** diff --git a/src/hooks/useHtmlPaste/types.ts b/src/hooks/useHtmlPaste/types.ts index 305ebe5fbd0f..0aaa12a49ac9 100644 --- a/src/hooks/useHtmlPaste/types.ts +++ b/src/hooks/useHtmlPaste/types.ts @@ -5,6 +5,7 @@ type UseHtmlPaste = ( textInputRef: MutableRefObject<(HTMLTextAreaElement & TextInput) | TextInput | null>, preHtmlPasteCallback?: (event: ClipboardEvent) => boolean, removeListenerOnScreenBlur?: boolean, + maxLength?: number, // Maximum length of the text input value after pasting ) => void; export default UseHtmlPaste;