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 (
);
}
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;
+};