import { WebView } from 'react-native-webview'; import React, { ComponentType, ReactElement } from 'react'; import { StyleSheet, Text, View, StyleProp, ViewStyle, TextStyle } from 'react-native'; import * as Events from '../../bindings/Events'; import { postMessage } from '../../bindings/RNWebView'; import { emitter } from './Event'; import { useTheme } from '../ThemeContext'; import MentionList from './MentionList'; import * as Platform from '../../helpers/platform'; import Toolbar from './Toolbar'; const isAndroid = Platform.isAndroid; const noop = () => undefined; const isEmpty = (str: string | undefined) => !str || str.length === 0; const emptyStyle = {}; const defaultValue = JSON.parse(` [{ "type": "paragraph", "children": [{ "text": "" }] }] `); export type Props = { readonly error?: string; readonly errorStyle?: StyleProp; readonly initialValue?: any; readonly name: string; readonly onChange?: (any) => void; readonly onCursorChange?: (any) => void; readonly placeholder?: string; readonly style?: StyleProp; readonly wrapperStyle?: StyleProp; }; const RichTextEditor: ComponentType = ({ name, placeholder = '', initialValue = defaultValue, onChange = noop, onCursorChange = noop, error = '', style = emptyStyle, wrapperStyle = emptyStyle, errorStyle = emptyStyle, }: Props): ReactElement => { const theme = useTheme(); const webview = React.useRef(null); const [webviewHeight, setWebviewHeight] = React.useState(undefined); const normalizeEventName = (event: string) => `${name}/${event}`; const postMessageToWebview = (message: { type: string; data: any }) => { if (webview && webview.current) { postMessage(webview.current, message); } }; const html = React.useMemo(() => { const initialValueString = JSON.stringify(initialValue); const padding = theme.richTextEditor.editor.padding; const fontSize = theme.richTextEditor.editor.fontSize; return `
`; }, []); /* Forward events from Toolbar and MentionList to webview */ React.useEffect(() => { const removeBoldListener = Events.on(emitter, normalizeEventName('bold'), _ => postMessageToWebview({ type: '@hero-editor/webview/bold', data: null }), ); const removeItalicListener = Events.on(emitter, normalizeEventName('italic'), _ => postMessageToWebview({ type: '@hero-editor/webview/italic', data: null }), ); const removeUnderlineListener = Events.on(emitter, normalizeEventName('underline'), _ => postMessageToWebview({ type: '@hero-editor/webview/underline', data: null }), ); const removeBulletedListListener = Events.on(emitter, normalizeEventName('bulleted-list'), _ => postMessageToWebview({ type: '@hero-editor/webview/bulleted-list', data: null }), ); const removeNumberedListListener = Events.on(emitter, normalizeEventName('numbered-list'), _ => postMessageToWebview({ type: '@hero-editor/webview/numbered-list', data: null }), ); const removeHeadingOneListener = Events.on(emitter, normalizeEventName('heading-one'), _ => postMessageToWebview({ type: '@hero-editor/webview/heading-one', data: null }), ); const removeHeadingTwoListener = Events.on(emitter, normalizeEventName('heading-two'), _ => postMessageToWebview({ type: '@hero-editor/webview/heading-two', data: null }), ); const removeMentionApplyListener = Events.on(emitter, normalizeEventName('mention-apply'), data => postMessageToWebview({ type: '@hero-editor/webview/mention-apply', data: data }), ); return () => { removeBoldListener(); removeItalicListener(); removeUnderlineListener(); removeBulletedListListener(); removeNumberedListListener(); removeMentionApplyListener(); removeHeadingOneListener(); removeHeadingTwoListener(); }; }, []); /* Handle events from webview */ const onMessage = React.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': Events.emit(emitter, normalizeEventName('editor-focus'), undefined); break; case '@hero-editor/webview/editor-blur': 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': { const value = messageData ? messageData['value'] : undefined; onChange(value); } break; case '@hero-editor/webview/cursor-change': onCursorChange(messageData); break; case '@hero-editor/webview/editor-layout': { const editorLayout = messageData ? { width: Number(messageData['width']), height: Number(messageData['height']) } : undefined; if (editorLayout && editorLayout.height > 120.0) { setWebviewHeight(editorLayout.height); } } break; default: break; } }, []); return ( {!isEmpty(error) ? ( {error} ) : null} ); }; const CompoundRichTextEditor = RichTextEditor as typeof RichTextEditor & { Toolbar: typeof Toolbar; MentionList: typeof MentionList; }; CompoundRichTextEditor.Toolbar = Toolbar; CompoundRichTextEditor.MentionList = MentionList; CompoundRichTextEditor.displayName = 'RichTextEditor'; export default CompoundRichTextEditor;