diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index d050def55..9e52b8955 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -5,7 +5,7 @@ function App() { return (
Text input
- +
); } diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index 2d8a92db6..c9115030e 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -318,9 +318,9 @@ export default function App() { cursorColor="dodgerblue" autoCapitalize="sentences" linkRegex={LINK_REGEX} - onChangeText={(e) => handleChangeText(e.nativeEvent)} - onChangeHtml={(e) => handleChangeHtml(e.nativeEvent)} - onChangeState={(e) => handleChangeState(e.nativeEvent)} + onChangeText={handleChangeText} + onChangeHtml={handleChangeHtml} + onChangeState={handleChangeState} onLinkDetected={handleLinkDetected} onMentionDetected={console.log} onStartMention={handleStartMention} @@ -328,8 +328,8 @@ export default function App() { onEndMention={handleEndMention} onFocus={handleFocusEvent} onBlur={handleBlurEvent} - onChangeSelection={(e) => handleSelectionChangeEvent(e.nativeEvent)} - onKeyPress={(e) => handleKeyPress(e.nativeEvent)} + onChangeSelection={handleSelectionChangeEvent} + onKeyPress={handleKeyPress} androidExperimentalSynchronousEvents={ ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS } diff --git a/src/common/defaultProps.ts b/src/common/defaultProps.ts new file mode 100644 index 000000000..ec0f473ca --- /dev/null +++ b/src/common/defaultProps.ts @@ -0,0 +1,8 @@ +export const ENRICHED_TEXT_INPUT_DEFAULTS = { + editable: true, + mentionIndicators: ['@'], + autoCapitalize: 'sentences' as const, + htmlStyle: {}, + androidExperimentalSynchronousEvents: false, + scrollEnabled: true, +}; diff --git a/src/common/types.ts b/src/common/types.ts new file mode 100644 index 000000000..31c775bc2 --- /dev/null +++ b/src/common/types.ts @@ -0,0 +1,67 @@ +// Re-export event types from the NativeComponent spec file (source of truth for Codegen) +export type { + OnChangeTextEvent, + OnChangeHtmlEvent, + OnChangeStateEvent, + OnChangeStateDeprecatedEvent, + OnKeyPressEvent, +} from '../spec/EnrichedTextInputNativeComponent'; + +export interface OnMentionDetected { + text: string; + indicator: string; + attributes: Record; +} + +export interface OnLinkDetected { + text: string; + url: string; + start: number; + end: number; +} + +export interface OnChangeSelectionEvent { + start: number; + end: number; + text: string; +} + +export interface OnChangeMentionEvent { + indicator: string; + text: string; +} + +export interface EnrichedTextInputInstanceBase { + // General commands + focus: () => void; + blur: () => void; + setValue: (value: string) => void; + setSelection: (start: number, end: number) => void; + getHTML: () => Promise; + + // Text formatting commands + toggleBold: () => void; + toggleItalic: () => void; + toggleUnderline: () => void; + toggleStrikeThrough: () => void; + toggleInlineCode: () => void; + toggleH1: () => void; + toggleH2: () => void; + toggleH3: () => void; + toggleH4: () => void; + toggleH5: () => void; + toggleH6: () => void; + toggleCodeBlock: () => void; + toggleBlockQuote: () => void; + toggleOrderedList: () => void; + toggleUnorderedList: () => void; + toggleCheckboxList: (checked: boolean) => void; + setLink: (start: number, end: number, text: string, url: string) => void; + setImage: (src: string, width: number, height: number) => void; + startMention: (indicator: string) => void; + setMention: ( + indicator: string, + text: string, + attributes?: Record + ) => void; +} diff --git a/src/index.native.tsx b/src/index.native.tsx index 7435fe303..2fcdef1b1 100644 --- a/src/index.native.tsx +++ b/src/index.native.tsx @@ -2,11 +2,12 @@ export * from './native/EnrichedTextInput'; export type { OnChangeTextEvent, OnChangeHtmlEvent, + OnChangeMentionEvent, + OnChangeSelectionEvent, OnChangeStateEvent, OnChangeStateDeprecatedEvent, + OnKeyPressEvent, OnLinkDetected, OnMentionDetected, - OnChangeSelectionEvent, - OnKeyPressEvent, -} from './spec/EnrichedTextInputNativeComponent'; -export type { HtmlStyle, MentionStyleProperties } from './types'; +} from './common/types'; +export type { HtmlStyle, MentionStyleProperties } from './native/types'; diff --git a/src/index.tsx b/src/index.tsx index 85f527f6a..8aed07c53 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1 +1,12 @@ export * from './web/EnrichedTextInput'; +export type { + OnChangeTextEvent, + OnChangeHtmlEvent, + OnChangeMentionEvent, + OnChangeSelectionEvent, + OnChangeStateEvent, + OnChangeStateDeprecatedEvent, + OnKeyPressEvent, + OnLinkDetected, + OnMentionDetected, +} from './common/types'; diff --git a/src/native/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx index 595bac126..7c2ae264b 100644 --- a/src/native/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -9,13 +9,9 @@ import { import EnrichedTextInputNativeComponent, { Commands, type NativeProps, - type OnChangeHtmlEvent, - type OnChangeSelectionEvent, - type OnChangeStateEvent, - type OnChangeTextEvent, - type OnLinkDetected, + type OnChangeSelectionNativeEvent, + type OnLinkDetectedNativeEvent, type OnMentionEvent, - type OnMentionDetected, type OnMentionDetectedInternal, type OnRequestHtmlResultEvent, type OnChangeStateDeprecatedEvent, @@ -36,52 +32,27 @@ import type { } from 'react-native'; import { normalizeHtmlStyle } from '../utils/normalizeHtmlStyle'; import { toNativeRegexConfig } from '../utils/regexParser'; +import type { + OnChangeTextEvent, + OnChangeHtmlEvent, + OnChangeStateEvent, + OnMentionDetected, + OnLinkDetected, + OnChangeSelectionEvent, + OnChangeMentionEvent, + EnrichedTextInputInstanceBase, +} from '../common/types'; +import { ENRICHED_TEXT_INPUT_DEFAULTS } from '../common/defaultProps'; import { nullthrows } from '../utils/nullthrows'; -import type { HtmlStyle } from '../types'; +import type { HtmlStyle } from './types'; + +export interface EnrichedTextInputInstance + extends EnrichedTextInputInstanceBase, + NativeMethods {} export type FocusEvent = NativeSyntheticEvent; export type BlurEvent = NativeSyntheticEvent; -export interface EnrichedTextInputInstance extends NativeMethods { - // General commands - focus: () => void; - blur: () => void; - setValue: (value: string) => void; - setSelection: (start: number, end: number) => void; - getHTML: () => Promise; - - // Text formatting commands - toggleBold: () => void; - toggleItalic: () => void; - toggleUnderline: () => void; - toggleStrikeThrough: () => void; - toggleInlineCode: () => void; - toggleH1: () => void; - toggleH2: () => void; - toggleH3: () => void; - toggleH4: () => void; - toggleH5: () => void; - toggleH6: () => void; - toggleCodeBlock: () => void; - toggleBlockQuote: () => void; - toggleOrderedList: () => void; - toggleUnorderedList: () => void; - toggleCheckboxList: (checked: boolean) => void; - setLink: (start: number, end: number, text: string, url: string) => void; - setImage: (src: string, width: number, height: number) => void; - startMention: (indicator: string) => void; - setMention: ( - indicator: string, - text: string, - attributes?: Record - ) => void; -} - -export interface OnChangeMentionEvent { - indicator: string; - text: string; -} - export interface EnrichedTextInputProps extends Omit { ref?: RefObject; autoFocus?: boolean; @@ -99,22 +70,20 @@ export interface EnrichedTextInputProps extends Omit { linkRegex?: RegExp | null; onFocus?: (e: FocusEvent) => void; onBlur?: (e: BlurEvent) => void; - onChangeText?: (e: NativeSyntheticEvent) => void; - onChangeHtml?: (e: NativeSyntheticEvent) => void; - onChangeState?: (e: NativeSyntheticEvent) => void; + onChangeText?: (e: OnChangeTextEvent) => void; + onChangeHtml?: (e: OnChangeHtmlEvent) => void; + onChangeState?: (e: OnChangeStateEvent) => void; /** * @deprecated Use onChangeState prop instead. */ - onChangeStateDeprecated?: ( - e: NativeSyntheticEvent - ) => void; + onChangeStateDeprecated?: (e: OnChangeStateDeprecatedEvent) => void; onLinkDetected?: (e: OnLinkDetected) => void; onMentionDetected?: (e: OnMentionDetected) => void; onStartMention?: (indicator: string) => void; onChangeMention?: (e: OnChangeMentionEvent) => void; onEndMention?: (indicator: string) => void; - onChangeSelection?: (e: NativeSyntheticEvent) => void; - onKeyPress?: (e: NativeSyntheticEvent) => void; + onChangeSelection?: (e: OnChangeSelectionEvent) => void; + onKeyPress?: (e: OnKeyPressEvent) => void; /** * If true, Android will use experimental synchronous events. * This will prevent from input flickering when updating component size. @@ -125,7 +94,7 @@ export interface EnrichedTextInputProps extends Omit { androidExperimentalSynchronousEvents?: boolean; } -const warnAboutMissconfiguredMentions = (indicator: string) => { +const warnAboutMisconfiguredMentions = (indicator: string) => { console.warn( `Looks like you are trying to set a "${indicator}" but it's not in the mentionIndicators prop` ); @@ -141,16 +110,16 @@ type HtmlRequest = { export const EnrichedTextInput = ({ ref, autoFocus, - editable = true, - mentionIndicators = ['@'], + editable = ENRICHED_TEXT_INPUT_DEFAULTS.editable, + mentionIndicators = ENRICHED_TEXT_INPUT_DEFAULTS.mentionIndicators, defaultValue, placeholder, placeholderTextColor, cursorColor, selectionColor, style, - autoCapitalize = 'sentences', - htmlStyle = {}, + autoCapitalize = ENRICHED_TEXT_INPUT_DEFAULTS.autoCapitalize, + htmlStyle = ENRICHED_TEXT_INPUT_DEFAULTS.htmlStyle, linkRegex: _linkRegex, onFocus, onBlur, @@ -165,8 +134,8 @@ export const EnrichedTextInput = ({ onEndMention, onChangeSelection, onKeyPress, - androidExperimentalSynchronousEvents = false, - scrollEnabled = true, + androidExperimentalSynchronousEvents = ENRICHED_TEXT_INPUT_DEFAULTS.androidExperimentalSynchronousEvents, + scrollEnabled = ENRICHED_TEXT_INPUT_DEFAULTS.scrollEnabled, ...rest }: EnrichedTextInputProps) => { const nativeRef = useRef(null); @@ -302,7 +271,7 @@ export const EnrichedTextInput = ({ }, startMention: (indicator: string) => { if (!mentionIndicators?.includes(indicator)) { - warnAboutMissconfiguredMentions(indicator); + warnAboutMisconfiguredMentions(indicator); } Commands.startMention(nullthrows(nativeRef.current), indicator); @@ -327,11 +296,31 @@ export const EnrichedTextInput = ({ } }; - const handleLinkDetected = (e: NativeSyntheticEvent) => { + const handleLinkDetected = ( + e: NativeSyntheticEvent + ) => { const { text, url, start, end } = e.nativeEvent; onLinkDetected?.({ text, url, start, end }); }; + const handleChangeText = (e: NativeSyntheticEvent) => { + onChangeText?.(e.nativeEvent); + }; + + const handleChangeHtml = (e: NativeSyntheticEvent) => { + onChangeHtml?.(e.nativeEvent); + }; + + const handleKeyPress = (e: NativeSyntheticEvent) => { + onKeyPress?.(e.nativeEvent); + }; + + const handleChangeSelection = ( + e: NativeSyntheticEvent + ) => { + onChangeSelection?.(e.nativeEvent); + }; + const handleMentionDetected = ( e: NativeSyntheticEvent ) => { @@ -359,31 +348,28 @@ export const EnrichedTextInput = ({ const onChangeStateWithDeprecated = ( e: NativeSyntheticEvent ) => { - onChangeState?.(e); + onChangeState?.(e.nativeEvent); // TODO: remove in 0.5.0 release onChangeStateDeprecated?.({ - ...e, - nativeEvent: { - isBold: e.nativeEvent.bold.isActive, - isItalic: e.nativeEvent.italic.isActive, - isUnderline: e.nativeEvent.underline.isActive, - isStrikeThrough: e.nativeEvent.strikeThrough.isActive, - isInlineCode: e.nativeEvent.inlineCode.isActive, - isH1: e.nativeEvent.h1.isActive, - isH2: e.nativeEvent.h2.isActive, - isH3: e.nativeEvent.h3.isActive, - isH4: e.nativeEvent.h4.isActive, - isH5: e.nativeEvent.h5.isActive, - isH6: e.nativeEvent.h6.isActive, - isCodeBlock: e.nativeEvent.codeBlock.isActive, - isBlockQuote: e.nativeEvent.blockQuote.isActive, - isOrderedList: e.nativeEvent.orderedList.isActive, - isUnorderedList: e.nativeEvent.unorderedList.isActive, - isCheckboxList: e.nativeEvent.checkboxList.isActive, - isLink: e.nativeEvent.link.isActive, - isImage: e.nativeEvent.image.isActive, - isMention: e.nativeEvent.mention.isActive, - }, + isBold: e.nativeEvent.bold.isActive, + isItalic: e.nativeEvent.italic.isActive, + isUnderline: e.nativeEvent.underline.isActive, + isStrikeThrough: e.nativeEvent.strikeThrough.isActive, + isInlineCode: e.nativeEvent.inlineCode.isActive, + isH1: e.nativeEvent.h1.isActive, + isH2: e.nativeEvent.h2.isActive, + isH3: e.nativeEvent.h3.isActive, + isH4: e.nativeEvent.h4.isActive, + isH5: e.nativeEvent.h5.isActive, + isH6: e.nativeEvent.h6.isActive, + isCodeBlock: e.nativeEvent.codeBlock.isActive, + isBlockQuote: e.nativeEvent.blockQuote.isActive, + isOrderedList: e.nativeEvent.orderedList.isActive, + isUnorderedList: e.nativeEvent.unorderedList.isActive, + isCheckboxList: e.nativeEvent.checkboxList.isActive, + isLink: e.nativeEvent.link.isActive, + isImage: e.nativeEvent.image.isActive, + isMention: e.nativeEvent.mention.isActive, }); }; @@ -404,17 +390,17 @@ export const EnrichedTextInput = ({ linkRegex={linkRegex} onInputFocus={onFocus} onInputBlur={onBlur} - onChangeText={onChangeText} - onChangeHtml={onChangeHtml} + onChangeText={handleChangeText} + onChangeHtml={handleChangeHtml} isOnChangeHtmlSet={onChangeHtml !== undefined} isOnChangeTextSet={onChangeText !== undefined} onChangeState={onChangeStateWithDeprecated} onLinkDetected={handleLinkDetected} onMentionDetected={handleMentionDetected} onMention={handleMentionEvent} - onChangeSelection={onChangeSelection} + onChangeSelection={handleChangeSelection} onRequestHtmlResult={handleRequestHtmlResult} - onInputKeyPress={onKeyPress} + onInputKeyPress={handleKeyPress} androidExperimentalSynchronousEvents={ androidExperimentalSynchronousEvents } diff --git a/src/types.ts b/src/native/types.ts similarity index 100% rename from src/types.ts rename to src/native/types.ts diff --git a/src/spec/EnrichedTextInputNativeComponent.ts b/src/spec/EnrichedTextInputNativeComponent.ts index f7ce7b833..6228fc02c 100644 --- a/src/spec/EnrichedTextInputNativeComponent.ts +++ b/src/spec/EnrichedTextInputNativeComponent.ts @@ -146,7 +146,11 @@ export interface OnChangeStateDeprecatedEvent { isMention: boolean; } -export interface OnLinkDetected { +export interface OnKeyPressEvent { + key: string; +} + +export interface OnLinkDetectedNativeEvent { text: string; url: string; start: Int32; @@ -158,19 +162,12 @@ export interface OnMentionDetectedInternal { indicator: string; payload: string; } - -export interface OnMentionDetected { - text: string; - indicator: string; - attributes: Record; -} - export interface OnMentionEvent { indicator: string; text: UnsafeMixed; } -export interface OnChangeSelectionEvent { +export interface OnChangeSelectionNativeEvent { start: Int32; end: Int32; text: string; @@ -181,10 +178,6 @@ export interface OnRequestHtmlResultEvent { html: UnsafeMixed; } -export interface OnKeyPressEvent { - key: string; -} - interface TargetedEvent { target: Int32; } @@ -264,10 +257,10 @@ export interface NativeProps extends ViewProps { onChangeText?: DirectEventHandler; onChangeHtml?: DirectEventHandler; onChangeState?: DirectEventHandler; - onLinkDetected?: DirectEventHandler; + onLinkDetected?: DirectEventHandler; onMentionDetected?: DirectEventHandler; onMention?: DirectEventHandler; - onChangeSelection?: DirectEventHandler; + onChangeSelection?: DirectEventHandler; onRequestHtmlResult?: DirectEventHandler; onInputKeyPress?: DirectEventHandler; diff --git a/src/utils/normalizeHtmlStyle.ts b/src/utils/normalizeHtmlStyle.ts index 3392d080e..629815980 100644 --- a/src/utils/normalizeHtmlStyle.ts +++ b/src/utils/normalizeHtmlStyle.ts @@ -1,6 +1,6 @@ import { type ColorValue, processColor } from 'react-native'; import type { HtmlStyleInternal } from '../spec/EnrichedTextInputNativeComponent'; -import type { HtmlStyle, MentionStyleProperties } from '../types'; +import type { HtmlStyle, MentionStyleProperties } from '../native/types'; const defaultStyle: Required = { h1: { diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx index 5c06b3e7a..f3a13b766 100644 --- a/src/web/EnrichedTextInput.tsx +++ b/src/web/EnrichedTextInput.tsx @@ -1,3 +1,183 @@ -export const EnrichedTextInput = () => { - return ; +import { + useImperativeHandle, + useRef, + type CSSProperties, + type RefObject, +} from 'react'; + +import type { + EnrichedTextInputInstanceBase, + OnChangeHtmlEvent, + OnChangeMentionEvent, + OnChangeStateDeprecatedEvent, + OnChangeStateEvent, + OnChangeTextEvent, + OnLinkDetected, + OnMentionDetected, + OnChangeSelectionEvent, + OnKeyPressEvent, +} from '../common/types'; +import { ENRICHED_TEXT_INPUT_DEFAULTS } from '../common/defaultProps'; + +export type EnrichedTextInputInstance = EnrichedTextInputInstanceBase; + +export interface MentionStyleProperties { + color?: string; + backgroundColor?: string; + textDecorationLine?: 'underline' | 'none'; +} + +type HeadingStyle = { + fontSize?: number; + bold?: boolean; +}; + +export interface HtmlStyle { + h1?: HeadingStyle; + h2?: HeadingStyle; + h3?: HeadingStyle; + h4?: HeadingStyle; + h5?: HeadingStyle; + h6?: HeadingStyle; + blockquote?: { + borderColor?: string; + borderWidth?: number; + gapWidth?: number; + color?: string; + }; + codeblock?: { + color?: string; + borderRadius?: number; + backgroundColor?: string; + }; + code?: { + color?: string; + backgroundColor?: string; + }; + a?: { + color?: string; + textDecorationLine?: 'underline' | 'none'; + }; + mention?: Record | MentionStyleProperties; + ol?: { + gapWidth?: number; + marginLeft?: number; + markerFontWeight?: string | number; + markerColor?: string; + }; + ul?: { + bulletColor?: string; + bulletSize?: number; + marginLeft?: number; + gapWidth?: number; + }; + ulCheckbox?: { + boxSize?: number; + gapWidth?: number; + marginLeft?: number; + boxColor?: string; + }; +} + +export interface EnrichedTextInputProps { + ref?: RefObject; + autoFocus?: boolean; + editable?: boolean; + mentionIndicators?: string[]; + defaultValue?: string; + placeholder?: string; + placeholderTextColor?: string; + cursorColor?: string; + selectionColor?: string; + autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; + htmlStyle?: HtmlStyle; + style?: CSSProperties; + scrollEnabled?: boolean; + linkRegex?: RegExp | null; + onFocus?: () => void; + onBlur?: () => void; + onChangeText?: (e: OnChangeTextEvent) => void; + onChangeHtml?: (e: OnChangeHtmlEvent) => void; + onChangeState?: (e: OnChangeStateEvent) => void; + /** + * @deprecated Use onChangeState prop instead. + */ + onChangeStateDeprecated?: (e: OnChangeStateDeprecatedEvent) => void; + onLinkDetected?: (e: OnLinkDetected) => void; + onMentionDetected?: (e: OnMentionDetected) => void; + onStartMention?: (indicator: string) => void; + onChangeMention?: (e: OnChangeMentionEvent) => void; + onEndMention?: (indicator: string) => void; + onChangeSelection?: (e: OnChangeSelectionEvent) => void; + onKeyPress?: (e: OnKeyPressEvent) => void; + /** + * Unused for web, but kept for parity with native + */ + androidExperimentalSynchronousEvents?: boolean; +} + +export const EnrichedTextInput = ({ + ref, + autoFocus, + editable = ENRICHED_TEXT_INPUT_DEFAULTS.editable, + defaultValue, + placeholder, + style, +}: EnrichedTextInputProps) => { + const inputRef = useRef(null); + + useImperativeHandle(ref, () => ({ + // General commands + focus: () => { + inputRef.current?.focus(); + }, + blur: () => { + inputRef.current?.blur(); + }, + setValue: (value: string) => { + if (inputRef.current) { + inputRef.current.value = value; + } + }, + setSelection: (start: number, end: number) => { + inputRef.current?.setSelectionRange(start, end); + }, + getHTML: () => { + return Promise.resolve(''); + }, + + // Text formatting commands + toggleBold: () => {}, + toggleItalic: () => {}, + toggleUnderline: () => {}, + toggleStrikeThrough: () => {}, + toggleInlineCode: () => {}, + toggleH1: () => {}, + toggleH2: () => {}, + toggleH3: () => {}, + toggleH4: () => {}, + toggleH5: () => {}, + toggleH6: () => {}, + toggleCodeBlock: () => {}, + toggleBlockQuote: () => {}, + toggleOrderedList: () => {}, + toggleUnorderedList: () => {}, + toggleCheckboxList: () => {}, + setLink: () => {}, + setImage: () => {}, + startMention: () => {}, + setMention: () => {}, + })); + + return ( + + ); };