@hero-design/rn 8.108.2 → 8.109.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,301 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useImperativeHandle,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
9
+
10
+ import { TouchableWithoutFeedback, StyleSheet } from 'react-native';
11
+ import type { WebView } from 'react-native-webview';
12
+ import type { ReactElement, Ref } from 'react';
13
+ import type { StyleProp, ViewStyle } from 'react-native';
14
+ import { useTheme } from '../../theme';
15
+ import heroEditorApp from './heroEditorApp';
16
+ import { isAndroid } from '../../utils/helpers';
17
+ import { ToolbarEvents } from './constants';
18
+ import { emitter } from './EditorEvent';
19
+ import { StyledWebView } from './StyledRichTextEditor';
20
+ import * as Events from './utils/events';
21
+ import { postMessage, requestBlurEditor } from './utils/rnWebView';
22
+ import type { WebViewEventMessage } from './utils/rnWebView';
23
+ import type { TextUnit } from './types';
24
+
25
+ export interface RichTextEditorRef {
26
+ requestBlur: VoidFunction;
27
+ insertNodes: (nodes: Record<string, unknown>[]) => void;
28
+ deleteBackward: (unit?: TextUnit) => void;
29
+ setReadOnly: (readOnly: boolean) => void;
30
+ }
31
+
32
+ export type EditorValue = {
33
+ type: string;
34
+ children: any;
35
+ }[];
36
+
37
+ export interface BaseRichTextEditorProps {
38
+ /**
39
+ * If true, the editor will be focused when the user enters the screen
40
+ */
41
+ autoFocus?: boolean;
42
+ /**
43
+ * Error message
44
+ */
45
+ error?: string;
46
+ /**
47
+ * Field value
48
+ */
49
+ value?: EditorValue;
50
+ /**
51
+ * Unique name used to communicate with webview
52
+ */
53
+ name: string;
54
+ /**
55
+ * Callback function called when the field value changed
56
+ */
57
+ onChange: (data: EditorValue) => void;
58
+ /**
59
+ * Callback function called when the cursor position changed
60
+ */
61
+ onCursorChange?: (params: { position: { top: number } }) => void;
62
+ /**
63
+ * Field placeholder
64
+ */
65
+ placeholder?: string;
66
+ /**
67
+ * Additional styles
68
+ */
69
+ style?: StyleProp<ViewStyle>;
70
+ /**
71
+ * Testing ID of the component
72
+ */
73
+ testID?: string;
74
+ /**
75
+ * Imperative ref to expose the component method
76
+ */
77
+ editorRef?: Ref<RichTextEditorRef>;
78
+ /**
79
+ * Callback function called when the editor is focused
80
+ */
81
+ onFocus?: () => void;
82
+ /**
83
+ * Callback function called when the editor is blurred
84
+ */
85
+ onBlur?: () => void;
86
+ }
87
+
88
+ const defaultValue: EditorValue = [
89
+ {
90
+ type: 'paragraph',
91
+ children: [{ text: '' }],
92
+ },
93
+ ];
94
+
95
+ const BaseRichTextEditor = ({
96
+ autoFocus = true,
97
+ name,
98
+ placeholder = '',
99
+ onChange,
100
+ onCursorChange,
101
+ style = {},
102
+ testID,
103
+ editorRef,
104
+ value = defaultValue,
105
+ onFocus,
106
+ onBlur,
107
+ }: BaseRichTextEditorProps): ReactElement => {
108
+ const theme = useTheme();
109
+
110
+ const webview = useRef<WebView | null>(null);
111
+ const [webviewHeight, setWebviewHeight] = useState<number | undefined>();
112
+
113
+ const [isFocused, setIsFocused] = useState(false);
114
+
115
+ const normalizeEventName = (event: string) => `${name}/${event}`;
116
+ const postMessageToWebview = (message: WebViewEventMessage) => {
117
+ if (webview && webview.current) {
118
+ postMessage(webview.current, message);
119
+ }
120
+ };
121
+
122
+ const html = useMemo(() => {
123
+ const initialValueString = JSON.stringify(value);
124
+
125
+ return `
126
+ <!DOCTYPE html>
127
+ <html>
128
+ <head>
129
+ <meta charset="utf-8">
130
+ <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0">
131
+ <style>
132
+ body {
133
+ margin: 0;
134
+ }
135
+ </style>
136
+ </head>
137
+ <body>
138
+ <div id="root"></div>
139
+ <script>
140
+ window.__editorConfigs = {
141
+ placeholder: "${placeholder}",
142
+ initialValue: ${initialValueString},
143
+ isAndroid: ${isAndroid ? 'true' : 'false'},
144
+ autoFocus: ${autoFocus},
145
+ style: {
146
+ padding: '0 !important',
147
+ fontSize: ${theme.__hd__.richTextEditor.fontSizes.editor},
148
+ color: '${theme.__hd__.richTextEditor.colors.text}'
149
+ }
150
+ };
151
+ ${heroEditorApp}
152
+ </script>
153
+ </body>
154
+ </html>
155
+ `;
156
+ }, []);
157
+
158
+ const requestBlur = useCallback(() => {
159
+ if (webview.current && isFocused) {
160
+ requestBlurEditor(webview.current);
161
+ }
162
+ }, [isFocused]);
163
+
164
+ const insertNodes = useCallback((nodes: Record<string, unknown>[]) => {
165
+ postMessageToWebview({
166
+ type: '@hero-editor/webview/editor-insert-nodes',
167
+ data: { nodes },
168
+ });
169
+ }, []);
170
+
171
+ const deleteBackward = useCallback((unit?: TextUnit) => {
172
+ postMessageToWebview({
173
+ type: '@hero-editor/webview/editor-delete-backward',
174
+ data: { unit },
175
+ });
176
+ }, []);
177
+
178
+ const setReadOnly = useCallback((readOnly: boolean) => {
179
+ postMessageToWebview({
180
+ type: '@hero-editor/webview/editor-read-only',
181
+ data: { readOnly },
182
+ });
183
+ }, []);
184
+
185
+ useImperativeHandle(
186
+ editorRef,
187
+ () => ({ requestBlur, insertNodes, deleteBackward, setReadOnly }),
188
+ [requestBlur, deleteBackward, insertNodes, setReadOnly]
189
+ );
190
+
191
+ /* Forward events from Toolbar and MentionList to webview */
192
+ useEffect(() => {
193
+ const toolbarEventListenerRemovers = Object.values(ToolbarEvents).map(
194
+ (eventName) =>
195
+ Events.on(emitter, normalizeEventName(eventName), (data) => {
196
+ postMessageToWebview({
197
+ type: `@hero-editor/webview/${eventName}`,
198
+ data,
199
+ });
200
+ })
201
+ );
202
+
203
+ const removeMentionApplyListener = Events.on(
204
+ emitter,
205
+ normalizeEventName('mention-apply'),
206
+ (data) =>
207
+ postMessageToWebview({
208
+ type: '@hero-editor/webview/mention-apply',
209
+ data,
210
+ })
211
+ );
212
+
213
+ return () => {
214
+ removeMentionApplyListener();
215
+ toolbarEventListenerRemovers.forEach((remover) => remover());
216
+ };
217
+ }, []);
218
+
219
+ const handleEditorLayoutEvent = useCallback((messageData: any) => {
220
+ const editorLayout = messageData
221
+ ? {
222
+ width: Number(messageData.width),
223
+ height: Number(messageData.height),
224
+ }
225
+ : undefined;
226
+
227
+ if (editorLayout) {
228
+ setWebviewHeight(editorLayout.height);
229
+ }
230
+ }, []);
231
+
232
+ /* Handle events from webview */
233
+ const onMessage = useCallback(
234
+ (event?: { nativeEvent?: { data?: string } }) => {
235
+ const message = event?.nativeEvent?.data
236
+ ? JSON.parse(event?.nativeEvent?.data)
237
+ : undefined;
238
+
239
+ const messageType = message?.type;
240
+ const messageData = message?.data;
241
+
242
+ switch (messageType) {
243
+ case '@hero-editor/webview/editor-focus':
244
+ onFocus?.();
245
+ setIsFocused(true);
246
+ Events.emit(emitter, normalizeEventName('editor-focus'), undefined);
247
+
248
+ break;
249
+ case '@hero-editor/webview/editor-blur':
250
+ onBlur?.();
251
+ setIsFocused(false);
252
+ Events.emit(emitter, normalizeEventName('editor-blur'), undefined);
253
+
254
+ break;
255
+ case '@hero-editor/webview/mention-search':
256
+ Events.emit(
257
+ emitter,
258
+ normalizeEventName('mention-search'),
259
+ messageData
260
+ );
261
+
262
+ break;
263
+ case '@hero-editor/webview/editor-change':
264
+ if (messageData) {
265
+ onChange(messageData.value);
266
+ }
267
+
268
+ break;
269
+ case '@hero-editor/webview/cursor-change':
270
+ onCursorChange?.(messageData);
271
+ break;
272
+
273
+ case '@hero-editor/webview/editor-layout':
274
+ handleEditorLayoutEvent(messageData);
275
+ break;
276
+
277
+ default:
278
+ break;
279
+ }
280
+ },
281
+ [onFocus, onBlur]
282
+ );
283
+
284
+ return (
285
+ <TouchableWithoutFeedback onPress={(e) => e.stopPropagation()}>
286
+ <StyledWebView
287
+ hideKeyboardAccessoryView
288
+ ref={webview}
289
+ testID={testID}
290
+ source={{ html }}
291
+ onMessage={onMessage}
292
+ scrollEnabled={false}
293
+ originWhitelist={['*']}
294
+ keyboardDisplayRequiresUserAction={false}
295
+ style={StyleSheet.flatten([style, { height: webviewHeight }])}
296
+ />
297
+ </TouchableWithoutFeedback>
298
+ );
299
+ };
300
+
301
+ export default BaseRichTextEditor;
@@ -1,16 +1,15 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import type { ComponentType } from 'react';
3
3
  import Icon from '../Icon';
4
- import { ToolbarEvents } from './constants';
5
- import { emitter } from './EditorEvent';
4
+ import { EditorEvents, ToolbarEvents } from './constants';
6
5
  import {
7
6
  StyledSeparator,
8
7
  StyledToolbar,
9
8
  StyledToolbarButton,
10
9
  } from './StyledToolbar';
11
10
  import type { ToolbarButtonName } from './types';
12
- import * as Events from './utils/events';
13
11
  import type { IconProps } from '../Icon';
12
+ import useRichTextEditorEvents from './hooks/useRichTextEditorEvents';
14
13
 
15
14
  type ToolbarButtonProps = {
16
15
  icon: IconProps['icon'];
@@ -122,22 +121,15 @@ const EditorToolbar = ({
122
121
  initialToolbarButtonArray
123
122
  );
124
123
 
125
- const normalizeEventName = useCallback(
126
- (event: string): string => `${name}/${event}`,
127
- [name]
128
- );
124
+ const { emitEvent, subscribeToEvents } = useRichTextEditorEvents(name);
129
125
 
130
126
  useEffect(() => {
131
- const removeFocusListener = Events.on(
132
- emitter,
133
- normalizeEventName('editor-focus'),
127
+ const removeFocusListener = subscribeToEvents(
128
+ EditorEvents.EditorFocus,
134
129
  () => setShow(true)
135
130
  );
136
-
137
- const removeBlurListener = Events.on(
138
- emitter,
139
- normalizeEventName('editor-blur'),
140
- () => setShow(false)
131
+ const removeBlurListener = subscribeToEvents(EditorEvents.EditorBlur, () =>
132
+ setShow(false)
141
133
  );
142
134
 
143
135
  return () => {
@@ -193,11 +185,10 @@ const EditorToolbar = ({
193
185
  icon={config.icon}
194
186
  onPress={() => {
195
187
  toggleToolbarButton(button);
196
- Events.emit(
197
- emitter,
198
- normalizeEventName(config.eventName),
199
- null
200
- );
188
+ emitEvent({
189
+ type: config.eventName,
190
+ data: null,
191
+ });
201
192
  }}
202
193
  selected={button.selected}
203
194
  />
@@ -1,6 +1,6 @@
1
- import { useTheme } from '@emotion/react';
2
1
  import React, { useCallback, useEffect, useState } from 'react';
3
2
  import { View } from 'react-native';
3
+ import { useTheme } from '../../theme';
4
4
  import { emitter } from './EditorEvent';
5
5
  import * as Events from './utils/events';
6
6
 
@@ -72,7 +72,7 @@ const MentionList = <TMetaData,>({
72
72
  const { highlighted, meta } = options;
73
73
 
74
74
  const highlightStyle = {
75
- color: theme.colors.secondary,
75
+ color: theme.colors.primary,
76
76
  borderRadius: theme.__hd__.richTextEditor.radii.mention,
77
77
  padding: highlighted ? theme.__hd__.richTextEditor.space.mention : 0,
78
78
  background: highlighted