import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { TouchableWithoutFeedback, View } from 'react-native'; import type { WebView } from 'react-native-webview'; import type { ReactElement, Ref } from 'react'; import type { StyleProp, ViewStyle } from 'react-native'; import { useTheme } from '../../theme'; import heroEditorApp from './heroEditorApp'; import { isAndroid } from '../../utils/helpers'; import { ToolbarEvents } from './constants'; import { emitter } from './EditorEvent'; import { StyledWebView } from './StyledRichTextEditor'; import * as Events from './utils/events'; import { postMessage, requestBlurEditor } from './utils/rnWebView'; import type { WebViewEventMessage } from './utils/rnWebView'; import type { TextUnit } from './types'; export interface RichTextEditorRef { requestBlur: VoidFunction; insertNodes: (nodes: Record[]) => void; deleteBackward: (unit?: TextUnit) => void; setReadOnly: (readOnly: boolean) => void; } export type EditorValue = { type: string; children: any; }[]; export interface RichTextEditorInputProps { /** * If true, the editor will be focused when the user enters the screen */ autoFocus?: boolean; /** * Error message */ error?: string; /** * Field value */ value?: EditorValue; /** * Unique name used to communicate with webview */ name: string; /** * Callback function called when the field value changed */ onChange: (data: EditorValue) => void; /** * Callback function called when the cursor position changed */ onCursorChange?: (params: { position: { top: number } }) => void; /** * Field placeholder */ placeholder?: string; /** * Additional styles */ style?: StyleProp; /** * Testing ID of the component */ testID?: string; /** * Imperative ref to expose the component method */ editorRef?: Ref; /** * Callback function called when the editor is focused */ onFocus?: () => void; /** * Callback function called when the editor is blurred */ onBlur?: () => void; } const defaultValue: EditorValue = [ { type: 'paragraph', children: [{ text: '' }], }, ]; const RichTextEditorInput = ({ autoFocus = true, name, placeholder = '', onChange, onCursorChange, style = {}, testID, editorRef, value = defaultValue, onFocus, onBlur, }: RichTextEditorInputProps): ReactElement => { const theme = useTheme(); const webview = useRef(null); const [webviewHeight, setWebviewHeight] = useState(); const [isFocused, setIsFocused] = useState(false); const normalizeEventName = (event: string) => `${name}/${event}`; const postMessageToWebview = (message: WebViewEventMessage) => { if (webview && webview.current) { postMessage(webview.current, message); } }; const html = useMemo(() => { const initialValueString = JSON.stringify(value); return `
`; }, []); const requestBlur = useCallback(() => { if (webview.current && isFocused) { requestBlurEditor(webview.current); } }, [isFocused]); const insertNodes = useCallback((nodes: Record[]) => { postMessageToWebview({ type: '@hero-editor/webview/editor-insert-nodes', data: { nodes }, }); }, []); const deleteBackward = useCallback((unit?: TextUnit) => { postMessageToWebview({ type: '@hero-editor/webview/editor-delete-backward', data: { unit }, }); }, []); const setReadOnly = useCallback((readOnly: boolean) => { postMessageToWebview({ type: '@hero-editor/webview/editor-read-only', data: { readOnly }, }); }, []); useImperativeHandle( editorRef, () => ({ requestBlur, insertNodes, deleteBackward, setReadOnly }), [requestBlur, deleteBackward, insertNodes, setReadOnly] ); /* Forward events from Toolbar and MentionList to webview */ useEffect(() => { const toolbarEventListenerRemovers = Object.values(ToolbarEvents).map( (eventName) => Events.on(emitter, normalizeEventName(eventName), (data) => { postMessageToWebview({ type: `@hero-editor/webview/${eventName}`, data, }); }) ); const removeMentionApplyListener = Events.on( emitter, normalizeEventName('mention-apply'), (data) => postMessageToWebview({ type: '@hero-editor/webview/mention-apply', data, }) ); return () => { removeMentionApplyListener(); toolbarEventListenerRemovers.forEach((remover) => remover()); }; }, []); const handleEditorLayoutEvent = useCallback((messageData: any) => { const editorLayout = messageData ? { width: Number(messageData.width), height: Number(messageData.height), } : undefined; if (editorLayout) { setWebviewHeight(editorLayout.height); } }, []); /* Handle events from webview */ const onMessage = useCallback( (event?: { nativeEvent?: { data?: string } }) => { const message = event?.nativeEvent?.data ? JSON.parse(event?.nativeEvent?.data) : undefined; const messageType = message?.type; const messageData = message?.data; switch (messageType) { case '@hero-editor/webview/editor-focus': onFocus?.(); setIsFocused(true); Events.emit(emitter, normalizeEventName('editor-focus'), undefined); break; case '@hero-editor/webview/editor-blur': onBlur?.(); setIsFocused(false); Events.emit(emitter, normalizeEventName('editor-blur'), undefined); break; case '@hero-editor/webview/mention-search': Events.emit( emitter, normalizeEventName('mention-search'), messageData ); break; case '@hero-editor/webview/editor-change': if (messageData) { onChange(messageData.value); Events.emit( emitter, normalizeEventName('editor-change'), messageData ); } break; case '@hero-editor/webview/cursor-change': onCursorChange?.(messageData); break; case '@hero-editor/webview/editor-layout': handleEditorLayoutEvent(messageData); break; case '@hero-editor/webview/request-upsert-link': Events.emit( emitter, normalizeEventName('request-upsert-link'), messageData ); break; default: break; } }, [onFocus, onBlur] ); return ( e.stopPropagation()}> ); }; export default RichTextEditorInput;