From 292f1d82843297c27717634717f8b875dd489c06 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 10 Dec 2024 11:19:35 +0800 Subject: [PATCH 01/11] feat: handle context menu format command --- src/MarkdownTextInput.tsx | 3 ++- src/MarkdownTextInput.web.tsx | 45 +++++++++++++++++++++++++++++++++-- src/commonTypes.ts | 4 +++- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index c4ec2467a..51a8368ae 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -10,7 +10,7 @@ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponen import NativeLiveMarkdownModule from './NativeLiveMarkdownModule'; import {mergeMarkdownStyleWithDefault} from './styleUtils'; import type {PartialMarkdownStyle} from './styleUtils'; -import type {InlineImagesInputProps, MarkdownRange} from './commonTypes'; +import type {FormatType, InlineImagesInputProps, MarkdownRange} from './commonTypes'; declare global { // eslint-disable-next-line no-var @@ -53,6 +53,7 @@ function unregisterParser(parserId: number) { interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps { markdownStyle?: PartialMarkdownStyle; + formatSelection?: (selectedText: string, formatType: FormatType) => string; parser: (value: string) => MarkdownRange[]; } diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index b69028012..4db86b687 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -23,13 +23,14 @@ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponen import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils'; import {parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils'; import {forceRefreshAllImages} from './web/inputElements/inlineImage'; -import type {MarkdownRange, InlineImagesInputProps} from './commonTypes'; +import type {MarkdownRange, InlineImagesInputProps, FormatType} from './commonTypes'; const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps { markdownStyle?: MarkdownStyle; parser: (text: string) => MarkdownRange[]; + formatSelection?: (selectedText: string, formatType: FormatType) => string; onClick?: (e: MouseEvent) => void; dir?: string; disabled?: boolean; @@ -85,6 +86,7 @@ const MarkdownTextInput = React.forwardRef { + if (!contentSelection.current) { + return { + text: '', + cursorPosition: 0, + }; + } + + let formatType: FormatType; + switch (formatCommand) { + case 'formatBold': + formatType = 'bold'; + break; + case 'formatItalic': + formatType = 'italic'; + break; + default: + formatType = 'underline'; + break; + } + + const beforeSelectedText = parsedText.slice(0, contentSelection.current.start); + const selectedText = parsedText.slice(contentSelection.current.start, contentSelection.current.end); + const formattedText = formatSelection?.(selectedText, formatType) ?? selectedText; + const formattedTextDiffLength = formattedText.length - selectedText.length; + const afterSelectedText = parsedText.slice(contentSelection.current.end); + const text = `${beforeSelectedText}${formattedText}${afterSelectedText}`; + + return parseText(parser, target, text, processedMarkdownStyle, cursorPosition + formattedTextDiffLength, true); + }, + [parser, parseText, formatSelection, processedMarkdownStyle], + ); + // Placeholder text color logic const updateTextColor = useCallback( (node: HTMLDivElement, text: string) => { @@ -361,6 +397,11 @@ const MarkdownTextInput = React.forwardRef Date: Wed, 11 Dec 2024 19:21:11 +0800 Subject: [PATCH 02/11] chore: rename format function --- src/MarkdownTextInput.web.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 4db86b687..8d6e7a141 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -238,7 +238,7 @@ const MarkdownTextInput = React.forwardRef { if (!contentSelection.current) { return { @@ -400,7 +400,7 @@ const MarkdownTextInput = React.forwardRef Date: Wed, 18 Dec 2024 00:58:38 +0800 Subject: [PATCH 03/11] Early return when selection length is < 1 --- src/MarkdownTextInput.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 8d6e7a141..4304c90ea 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -240,7 +240,7 @@ const MarkdownTextInput = React.forwardRef { - if (!contentSelection.current) { + if (!contentSelection.current || contentSelection.current.end - contentSelection.current.start < 1) { return { text: '', cursorPosition: 0, From 8bc3c479a0c71624ae6e62184d41541269dfc5e2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 18 Dec 2024 00:59:13 +0800 Subject: [PATCH 04/11] Skip slicing text if the formatted text is unchanged --- src/MarkdownTextInput.web.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 4304c90ea..e755c7e66 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -260,14 +260,19 @@ const MarkdownTextInput = React.forwardRef Date: Wed, 18 Dec 2024 01:05:14 +0800 Subject: [PATCH 05/11] chore: move the format command to type conversion to a util function --- src/MarkdownTextInput.web.tsx | 15 ++------------- src/web/utils/blockUtils.ts | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index e755c7e66..8c7b8567f 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -18,6 +18,7 @@ import {updateInputStructure} from './web/utils/parserUtils'; import InputHistory from './web/InputHistory'; import type {TreeNode} from './web/utils/treeUtils'; import {getCurrentCursorPosition, removeSelection, setCursorPosition} from './web/utils/cursorUtils'; +import {getFormatType} from './web/utils/blockUtils'; import './web/MarkdownTextInput.css'; import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils'; @@ -247,19 +248,7 @@ const MarkdownTextInput = React.forwardRef Date: Tue, 7 Jan 2025 19:04:43 +0800 Subject: [PATCH 06/11] remove unnecessary conversion --- example/src/App.tsx | 1 + src/MarkdownTextInput.tsx | 4 ++-- src/MarkdownTextInput.web.tsx | 8 +++----- src/commonTypes.ts | 4 +--- src/web/utils/blockUtils.ts | 15 ++------------- 5 files changed, 9 insertions(+), 23 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 77d5e421d..8cebaae89 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -48,6 +48,7 @@ export default function App() { formatCommand === 'formatBold' ? `*${selectedText}*` : formatCommand === 'formatItalic' ? `_${selectedText}_` : selectedText} autoCapitalize="none" value={value} onChangeText={setValue} diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index 51a8368ae..5122dadda 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -10,7 +10,7 @@ import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponen import NativeLiveMarkdownModule from './NativeLiveMarkdownModule'; import {mergeMarkdownStyleWithDefault} from './styleUtils'; import type {PartialMarkdownStyle} from './styleUtils'; -import type {FormatType, InlineImagesInputProps, MarkdownRange} from './commonTypes'; +import type {InlineImagesInputProps, MarkdownRange} from './commonTypes'; declare global { // eslint-disable-next-line no-var @@ -53,7 +53,7 @@ function unregisterParser(parserId: number) { interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps { markdownStyle?: PartialMarkdownStyle; - formatSelection?: (selectedText: string, formatType: FormatType) => string; + formatSelection?: (selectedText: string, formatCommand: string) => string; parser: (value: string) => MarkdownRange[]; } diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index 8c7b8567f..b2feb40ad 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -18,20 +18,19 @@ import {updateInputStructure} from './web/utils/parserUtils'; import InputHistory from './web/InputHistory'; import type {TreeNode} from './web/utils/treeUtils'; import {getCurrentCursorPosition, removeSelection, setCursorPosition} from './web/utils/cursorUtils'; -import {getFormatType} from './web/utils/blockUtils'; import './web/MarkdownTextInput.css'; import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; import {getElementHeight, getPlaceholderValue, isEventComposing, normalizeValue, parseInnerHTMLToText} from './web/utils/inputUtils'; import {parseToReactDOMStyle, processMarkdownStyle} from './web/utils/webStyleUtils'; import {forceRefreshAllImages} from './web/inputElements/inlineImage'; -import type {MarkdownRange, InlineImagesInputProps, FormatType} from './commonTypes'; +import type {MarkdownRange, InlineImagesInputProps} from './commonTypes'; const useClientEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; interface MarkdownTextInputProps extends TextInputProps, InlineImagesInputProps { markdownStyle?: MarkdownStyle; parser: (text: string) => MarkdownRange[]; - formatSelection?: (selectedText: string, formatType: FormatType) => string; + formatSelection?: (selectedText: string, formatCommand: string) => string; onClick?: (e: MouseEvent) => void; dir?: string; disabled?: boolean; @@ -248,9 +247,8 @@ const MarkdownTextInput = React.forwardRef Date: Tue, 7 Jan 2025 19:06:25 +0800 Subject: [PATCH 07/11] throw error when the selection is invalid --- src/MarkdownTextInput.web.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index b2feb40ad..ea94c0e77 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -241,10 +241,7 @@ const MarkdownTextInput = React.forwardRef { if (!contentSelection.current || contentSelection.current.end - contentSelection.current.start < 1) { - return { - text: '', - cursorPosition: 0, - }; + throw new Error('[react-native-live-markdown] invalid selection'); } const selectedText = parsedText.slice(contentSelection.current.start, contentSelection.current.end); From 6aeb62da5db4465dfc9197542e84c4f0d4f86c74 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 7 Jan 2025 19:45:30 +0800 Subject: [PATCH 08/11] moves function to the top-level --- example/src/App.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 8cebaae89..709a7a675 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -15,6 +15,18 @@ import {PlatformInfo} from './PlatformInfo'; // We don't need this workaround in New Expensify App since Reanimated is imported before Live Markdown. console.log(Animated); +function handleFormatSelection(selectedText: string, formatCommand: string) { + if (formatCommand === 'formatBold') { + return `*${selectedText}*`; + } + + if (formatCommand === 'formatItalic') { + return `_${selectedText}_`; + } + + return selectedText; +} + export default function App() { const [value, setValue] = React.useState(TEST_CONST.EXAMPLE_CONTENT); const [textColorState, setTextColorState] = React.useState(false); @@ -48,7 +60,7 @@ export default function App() { formatCommand === 'formatBold' ? `*${selectedText}*` : formatCommand === 'formatItalic' ? `_${selectedText}_` : selectedText} + formatSelection={handleFormatSelection} autoCapitalize="none" value={value} onChangeText={setValue} From 3d6a4ee9f781e8027e4f8e060ea3c0818afd0bf6 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 7 Jan 2025 23:08:44 +0800 Subject: [PATCH 09/11] convert to switch case --- example/src/App.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 709a7a675..2d48a2bd6 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -16,15 +16,14 @@ import {PlatformInfo} from './PlatformInfo'; console.log(Animated); function handleFormatSelection(selectedText: string, formatCommand: string) { - if (formatCommand === 'formatBold') { - return `*${selectedText}*`; + switch (formatCommand) { + case 'formatBold': + return `*${selectedText}*`; + case 'formatItalic': + return `_${selectedText}_`; + default: + return selectedText; } - - if (formatCommand === 'formatItalic') { - return `_${selectedText}_`; - } - - return selectedText; } export default function App() { From 4cf9a1a90f99312689d72334308767ba27545a8b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 7 Jan 2025 23:09:13 +0800 Subject: [PATCH 10/11] update error message --- src/MarkdownTextInput.web.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index ea94c0e77..bbfd53dbb 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -241,7 +241,7 @@ const MarkdownTextInput = React.forwardRef { if (!contentSelection.current || contentSelection.current.end - contentSelection.current.start < 1) { - throw new Error('[react-native-live-markdown] invalid selection'); + throw new Error('[react-native-live-markdown] Trying to apply format command on empty selection'); } const selectedText = parsedText.slice(contentSelection.current.start, contentSelection.current.end); From 30d5e5d0e129ad128858f36a170e161c8660b6a2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 8 Jan 2025 12:12:56 +0800 Subject: [PATCH 11/11] return early when formatSelection is undefined --- src/MarkdownTextInput.web.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/MarkdownTextInput.web.tsx b/src/MarkdownTextInput.web.tsx index bbfd53dbb..05316223d 100644 --- a/src/MarkdownTextInput.web.tsx +++ b/src/MarkdownTextInput.web.tsx @@ -244,8 +244,12 @@ const MarkdownTextInput = React.forwardRef