From 9706007463894c47c810d0888b818fedaca2decb Mon Sep 17 00:00:00 2001 From: Igor Furgala Date: Fri, 27 Feb 2026 12:04:23 +0100 Subject: [PATCH] chore: experiment with web setup --- apps/example-web/src/App.tsx | 3 +- apps/example-web/tsconfig.app.json | 4 + apps/example-web/vite.config.ts | 6 + package.json | 2 +- src/index.native.tsx | 20 ++ src/index.tsx | 14 +- src/{ => native}/EnrichedTextInput.tsx | 141 ++------------ src/types.ts | 259 ++++++++++++++++++++++++- src/web/EnrichedTextInput.tsx | 52 +++++ 9 files changed, 370 insertions(+), 131 deletions(-) create mode 100644 src/index.native.tsx rename src/{ => native}/EnrichedTextInput.tsx (70%) create mode 100644 src/web/EnrichedTextInput.tsx diff --git a/apps/example-web/src/App.tsx b/apps/example-web/src/App.tsx index b6fa757c8..d050def55 100644 --- a/apps/example-web/src/App.tsx +++ b/apps/example-web/src/App.tsx @@ -1,10 +1,11 @@ import './App.css'; +import { EnrichedTextInput } from 'react-native-enriched'; function App() { return (
Text input
- +
); } diff --git a/apps/example-web/tsconfig.app.json b/apps/example-web/tsconfig.app.json index 690114886..821ce2eea 100644 --- a/apps/example-web/tsconfig.app.json +++ b/apps/example-web/tsconfig.app.json @@ -3,6 +3,10 @@ "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "types": ["vite/client"], + "baseUrl": ".", + "paths": { + "react-native-enriched": ["../../src/index.tsx"] + } }, "include": ["src"] } diff --git a/apps/example-web/vite.config.ts b/apps/example-web/vite.config.ts index 4a5def4c3..5b9d45578 100644 --- a/apps/example-web/vite.config.ts +++ b/apps/example-web/vite.config.ts @@ -1,7 +1,13 @@ +import path from 'node:path'; import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + 'react-native-enriched': path.resolve(__dirname, '../../src/index.tsx'), + }, + }, }); diff --git a/package.json b/package.json index e4735b657..2e46a7c6d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "react-native-enriched", "version": "0.4.1", "description": "Rich Text Editor component for React Native", - "source": "./src/index.tsx", + "source": "./src/index.native.tsx", "main": "./lib/module/index.js", "types": "./lib/typescript/src/index.d.ts", "exports": { diff --git a/src/index.native.tsx b/src/index.native.tsx new file mode 100644 index 000000000..9081123ba --- /dev/null +++ b/src/index.native.tsx @@ -0,0 +1,20 @@ +export { EnrichedTextInput } from './native/EnrichedTextInput'; +export type { + OnChangeTextEvent, + OnChangeHtmlEvent, + OnChangeStateEvent, + OnLinkDetected, + OnMentionDetected, + OnChangeSelectionEvent, + OnKeyPressEvent, + OnPasteImagesEvent, + PastedImage, + HtmlStyle, + MentionStyleProperties, + FocusEvent, + BlurEvent, + EnrichedTextInputInstance, + ContextMenuItem, + OnChangeMentionEvent, + EnrichedTextInputProps, +} from './types'; diff --git a/src/index.tsx b/src/index.tsx index 375534f5a..afda95fdf 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -export * from './EnrichedTextInput'; +export { EnrichedTextInput } from './web/EnrichedTextInput'; export type { OnChangeTextEvent, OnChangeHtmlEvent, @@ -8,5 +8,13 @@ export type { OnChangeSelectionEvent, OnKeyPressEvent, OnPasteImagesEvent, -} from './spec/EnrichedTextInputNativeComponent'; -export type { HtmlStyle, MentionStyleProperties } from './types'; + PastedImage, + HtmlStyle, + MentionStyleProperties, + FocusEvent, + BlurEvent, + EnrichedTextInputInstance, + ContextMenuItem, + OnChangeMentionEvent, + EnrichedTextInputProps, +} from './types'; diff --git a/src/EnrichedTextInput.tsx b/src/native/EnrichedTextInput.tsx similarity index 70% rename from src/EnrichedTextInput.tsx rename to src/native/EnrichedTextInput.tsx index ea1ecdf81..68e9e4df0 100644 --- a/src/EnrichedTextInput.tsx +++ b/src/native/EnrichedTextInput.tsx @@ -1,6 +1,5 @@ import { type Component, - type RefObject, useEffect, useImperativeHandle, useMemo, @@ -10,140 +9,28 @@ import { useCallback } from 'react'; import EnrichedTextInputNativeComponent, { Commands, type NativeProps, - type OnChangeHtmlEvent, - type OnChangeSelectionEvent, - type OnChangeStateEvent, - type OnChangeTextEvent, type OnContextMenuItemPressEvent, - type OnLinkDetected, - type OnMentionEvent, - type OnMentionDetected, type OnMentionDetectedInternal, + type OnMentionEvent, type OnRequestHtmlResultEvent, - type OnKeyPressEvent, - type OnPasteImagesEvent, -} from './spec/EnrichedTextInputNativeComponent'; +} from '../spec/EnrichedTextInputNativeComponent'; import type { - ColorValue, HostInstance, MeasureInWindowOnSuccessCallback, MeasureLayoutOnSuccessCallback, MeasureOnSuccessCallback, NativeMethods, NativeSyntheticEvent, - TargetedEvent, - TextStyle, - ViewProps, - ViewStyle, } from 'react-native'; -import { normalizeHtmlStyle } from './utils/normalizeHtmlStyle'; -import { toNativeRegexConfig } from './utils/regexParser'; -import { nullthrows } from './utils/nullthrows'; -import type { HtmlStyle } from './types'; - -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 ContextMenuItem { - text: string; - onPress: ({ - text, - selection, - styleState, - }: { - text: string; - selection: { start: number; end: number }; - styleState: OnChangeStateEvent; - }) => void; - visible?: boolean; -} - -export interface OnChangeMentionEvent { - indicator: string; - text: string; -} - -export interface EnrichedTextInputProps extends Omit { - ref?: RefObject; - autoFocus?: boolean; - editable?: boolean; - mentionIndicators?: string[]; - defaultValue?: string; - placeholder?: string; - placeholderTextColor?: ColorValue; - cursorColor?: ColorValue; - selectionColor?: ColorValue; - autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; - htmlStyle?: HtmlStyle; - style?: ViewStyle | TextStyle; - scrollEnabled?: boolean; - linkRegex?: RegExp | null; - onFocus?: (e: FocusEvent) => void; - onBlur?: (e: BlurEvent) => void; - onChangeText?: (e: NativeSyntheticEvent) => void; - onChangeHtml?: (e: NativeSyntheticEvent) => void; - onChangeState?: (e: NativeSyntheticEvent) => 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; - onPasteImages?: (e: NativeSyntheticEvent) => void; - contextMenuItems?: ContextMenuItem[]; - /** - * If true, Android will use experimental synchronous events. - * This will prevent from input flickering when updating component size. - * However, this is an experimental feature, which has not been thoroughly tested. - * We may decide to enable it by default in a future release. - * Disabled by default. - */ - androidExperimentalSynchronousEvents?: boolean; - /** - * If true, external HTML (e.g. from Google Docs, Word, web pages) will be - * normalized through the HTML normalizer before being applied. - * This converts arbitrary HTML into the canonical tag subset that the enriched - * parser understands. - * Disabled by default. - */ - useHtmlNormalizer?: boolean; -} +import { normalizeHtmlStyle } from '../utils/normalizeHtmlStyle'; +import { toNativeRegexConfig } from '../utils/regexParser'; +import { nullthrows } from '../utils/nullthrows'; +import type { + ContextMenuItem, + EnrichedTextInputProps, + OnLinkDetected, + OnMentionDetected, +} from '../types'; const warnMentionIndicators = (indicator: string) => { console.warn( @@ -402,7 +289,11 @@ export const EnrichedTextInput = ({ ) => { const { text, indicator, payload } = e.nativeEvent; const attributes = JSON.parse(payload) as Record; - onMentionDetected?.({ text, indicator, attributes }); + onMentionDetected?.({ + text, + indicator, + attributes, + } satisfies OnMentionDetected); }; const handleRequestHtmlResult = ( diff --git a/src/types.ts b/src/types.ts index b7f9591bb..faae48e9b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,13 @@ -import type { ColorValue, TextStyle } from 'react-native'; +import type { RefObject } from 'react'; +import type { + ColorValue, + NativeMethods, + NativeSyntheticEvent, + TargetedEvent, + TextStyle, + ViewProps, + ViewStyle, +} from 'react-native'; interface HeadingStyle { fontSize?: number; @@ -57,3 +66,251 @@ export interface HtmlStyle { boxColor?: ColorValue; }; } + +// Event types + +export interface OnChangeTextEvent { + value: string; +} + +export interface OnChangeHtmlEvent { + value: string; +} + +export interface OnChangeStateEvent { + bold: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + italic: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + underline: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + strikeThrough: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + inlineCode: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h1: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h2: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h3: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h4: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h5: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + h6: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + codeBlock: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + blockQuote: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + orderedList: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + unorderedList: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + link: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + image: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + mention: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; + checkboxList: { + isActive: boolean; + isConflicting: boolean; + isBlocking: boolean; + }; +} + +export interface OnLinkDetected { + text: string; + url: string; + start: number; + end: number; +} + +export interface OnMentionDetected { + text: string; + indicator: string; + attributes: Record; +} + +export interface OnChangeSelectionEvent { + start: number; + end: number; + text: string; +} + +export interface OnKeyPressEvent { + key: string; +} + +export interface PastedImage { + uri: string; + type: string; + width: number; + height: number; +} + +export interface OnPasteImagesEvent { + images: PastedImage[]; +} + +// Component types + +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 ContextMenuItem { + text: string; + onPress: ({ + text, + selection, + styleState, + }: { + text: string; + selection: { start: number; end: number }; + styleState: OnChangeStateEvent; + }) => void; + visible?: boolean; +} + +export interface OnChangeMentionEvent { + indicator: string; + text: string; +} + +export interface EnrichedTextInputProps extends Omit { + ref?: RefObject; + autoFocus?: boolean; + editable?: boolean; + mentionIndicators?: string[]; + defaultValue?: string; + placeholder?: string; + placeholderTextColor?: ColorValue; + cursorColor?: ColorValue; + selectionColor?: ColorValue; + autoCapitalize?: 'none' | 'sentences' | 'words' | 'characters'; + htmlStyle?: HtmlStyle; + style?: ViewStyle | TextStyle; + scrollEnabled?: boolean; + linkRegex?: RegExp | null; + onFocus?: (e: FocusEvent) => void; + onBlur?: (e: BlurEvent) => void; + onChangeText?: (e: NativeSyntheticEvent) => void; + onChangeHtml?: (e: NativeSyntheticEvent) => void; + onChangeState?: (e: NativeSyntheticEvent) => 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; + onPasteImages?: (e: NativeSyntheticEvent) => void; + contextMenuItems?: ContextMenuItem[]; + /** + * If true, Android will use experimental synchronous events. + * This will prevent from input flickering when updating component size. + * However, this is an experimental feature, which has not been thoroughly tested. + * We may decide to enable it by default in a future release. + * Disabled by default. + */ + androidExperimentalSynchronousEvents?: boolean; + /** + * If true, external HTML (e.g. from Google Docs, Word, web pages) will be + * normalized through the HTML normalizer before being applied. + * This converts arbitrary HTML into the canonical tag subset that the enriched + * parser understands. + * Disabled by default. + */ + useHtmlNormalizer?: boolean; +} diff --git a/src/web/EnrichedTextInput.tsx b/src/web/EnrichedTextInput.tsx new file mode 100644 index 000000000..3baed36f3 --- /dev/null +++ b/src/web/EnrichedTextInput.tsx @@ -0,0 +1,52 @@ +import type { + HostInstance, + MeasureInWindowOnSuccessCallback, + MeasureLayoutOnSuccessCallback, + MeasureOnSuccessCallback, +} from 'react-native'; +import { useImperativeHandle } from 'react'; +import type { EnrichedTextInputProps } from '../types'; + +export const EnrichedTextInput = ({ ref }: EnrichedTextInputProps) => { + useImperativeHandle(ref, () => ({ + focus: () => {}, + blur: () => {}, + setValue: () => {}, + setSelection: () => {}, + getHTML: () => Promise.resolve(''), + toggleBold: () => {}, + toggleItalic: () => {}, + toggleUnderline: () => {}, + toggleStrikeThrough: () => {}, + toggleInlineCode: () => {}, + toggleH1: () => {}, + toggleH2: () => {}, + toggleH3: () => {}, + toggleH4: () => {}, + toggleH5: () => {}, + toggleH6: () => {}, + toggleCodeBlock: () => {}, + toggleBlockQuote: () => {}, + toggleOrderedList: () => {}, + toggleUnorderedList: () => {}, + toggleCheckboxList: () => {}, + setLink: () => {}, + setImage: () => {}, + startMention: () => {}, + setMention: () => {}, + measure: (_callback: MeasureOnSuccessCallback) => {}, + measureInWindow: (_callback: MeasureInWindowOnSuccessCallback) => {}, + measureLayout: ( + _relativeToNativeComponentRef: HostInstance | number, + _onSuccess: MeasureLayoutOnSuccessCallback, + _onFail?: () => void + ) => {}, + setNativeProps: (_nativeProps: object) => {}, + })); + + console.error( + 'EnrichedTextInput is not supported on web. Please use a regular text input instead.' + ); + + return null; +};