@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.
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +6 -0
- package/es/index.js +182 -95
- package/lib/index.js +182 -95
- package/package.json +1 -1
- package/src/components/RichTextEditor/BaseRichTextEditor.tsx +301 -0
- package/src/components/RichTextEditor/EditorToolbar.tsx +11 -20
- package/src/components/RichTextEditor/MentionList.tsx +2 -2
- package/src/components/RichTextEditor/RichTextEditor.tsx +42 -261
- package/src/components/RichTextEditor/StyledRichTextEditor.ts +0 -1
- package/src/components/RichTextEditor/__tests__/RichTextEditor.spec.tsx +8 -8
- package/src/components/RichTextEditor/__tests__/__snapshots__/RichTextEditor.spec.tsx.snap +7 -9
- package/src/components/RichTextEditor/constants.ts +5 -0
- package/src/components/RichTextEditor/hooks/useRichTextEditorEvents.ts +60 -0
- package/src/components/RichTextEditor/index.tsx +10 -2
- package/stats/8.108.2/rn-stats.html +3 -1
- package/stats/8.109.0/rn-stats.html +4844 -0
- package/types/components/RichTextEditor/BaseRichTextEditor.d.ts +69 -0
- package/types/components/RichTextEditor/RichTextEditor.d.ts +2 -45
- package/types/components/RichTextEditor/constants.d.ts +4 -0
- package/types/components/RichTextEditor/hooks/useRichTextEditorEvents.d.ts +21 -0
- package/types/components/RichTextEditor/index.d.ts +14 -2
|
@@ -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
|
|
126
|
-
(event: string): string => `${name}/${event}`,
|
|
127
|
-
[name]
|
|
128
|
-
);
|
|
124
|
+
const { emitEvent, subscribeToEvents } = useRichTextEditorEvents(name);
|
|
129
125
|
|
|
130
126
|
useEffect(() => {
|
|
131
|
-
const removeFocusListener =
|
|
132
|
-
|
|
133
|
-
normalizeEventName('editor-focus'),
|
|
127
|
+
const removeFocusListener = subscribeToEvents(
|
|
128
|
+
EditorEvents.EditorFocus,
|
|
134
129
|
() => setShow(true)
|
|
135
130
|
);
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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
|