@apollohg/react-native-prose-editor 0.1.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/LICENSE +160 -0
- package/README.md +143 -0
- package/android/build.gradle +39 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +131 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +1057 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +191 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +325 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +647 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +257 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +714 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +76 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +1044 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +211 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +228 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +254 -0
- package/ios/EditorTheme.swift +372 -0
- package/ios/Generated_editor_core.swift +1143 -0
- package/ios/NativeEditorExpoView.swift +1417 -0
- package/ios/NativeEditorModule.swift +263 -0
- package/ios/PositionBridge.swift +278 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +825 -0
- package/ios/RichTextEditorView.swift +1559 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1014 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +66 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2014 -0
- package/src/EditorTheme.ts +130 -0
- package/src/EditorToolbar.tsx +620 -0
- package/src/NativeEditorBridge.ts +607 -0
- package/src/NativeRichTextEditor.tsx +951 -0
- package/src/addons.ts +158 -0
- package/src/index.ts +63 -0
- package/src/schemas.ts +153 -0
- package/src/useNativeEditor.ts +173 -0
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useCallback,
|
|
5
|
+
useImperativeHandle,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
PixelRatio,
|
|
11
|
+
Platform,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
View,
|
|
14
|
+
type NativeSyntheticEvent,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
import { requireNativeViewManager } from 'expo-modules-core';
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
NativeEditorBridge,
|
|
22
|
+
type ActiveState,
|
|
23
|
+
type DocumentJSON,
|
|
24
|
+
type EditorUpdate,
|
|
25
|
+
type HistoryState,
|
|
26
|
+
type RenderElement,
|
|
27
|
+
type Selection,
|
|
28
|
+
parseEditorUpdateJson,
|
|
29
|
+
} from './NativeEditorBridge';
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_EDITOR_TOOLBAR_ITEMS,
|
|
32
|
+
EditorToolbar,
|
|
33
|
+
type EditorToolbarCommand,
|
|
34
|
+
type EditorToolbarItem,
|
|
35
|
+
type EditorToolbarListType,
|
|
36
|
+
} from './EditorToolbar';
|
|
37
|
+
import {
|
|
38
|
+
serializeEditorTheme,
|
|
39
|
+
type EditorTheme,
|
|
40
|
+
} from './EditorTheme';
|
|
41
|
+
import {
|
|
42
|
+
serializeEditorAddons,
|
|
43
|
+
type EditorAddonEvent,
|
|
44
|
+
type EditorAddons,
|
|
45
|
+
type MentionSuggestion,
|
|
46
|
+
withMentionsSchema,
|
|
47
|
+
} from './addons';
|
|
48
|
+
import { tiptapSchema, type SchemaDefinition } from './schemas';
|
|
49
|
+
|
|
50
|
+
interface NativeEditorViewHandle {
|
|
51
|
+
focus?: () => void;
|
|
52
|
+
blur?: () => void;
|
|
53
|
+
applyEditorUpdate: (updateJson: string) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface NativeEditorViewProps {
|
|
57
|
+
style?: StyleProp<ViewStyle>;
|
|
58
|
+
editorId: number;
|
|
59
|
+
placeholder?: string;
|
|
60
|
+
editable: boolean;
|
|
61
|
+
autoFocus: boolean;
|
|
62
|
+
showToolbar: boolean;
|
|
63
|
+
toolbarPlacement: NativeRichTextEditorToolbarPlacement;
|
|
64
|
+
heightBehavior: NativeRichTextEditorHeightBehavior;
|
|
65
|
+
themeJson?: string;
|
|
66
|
+
addonsJson?: string;
|
|
67
|
+
toolbarItemsJson?: string;
|
|
68
|
+
toolbarFrameJson?: string;
|
|
69
|
+
editorUpdateJson?: string;
|
|
70
|
+
editorUpdateRevision?: number;
|
|
71
|
+
onEditorUpdate: (event: NativeSyntheticEvent<NativeUpdateEvent>) => void;
|
|
72
|
+
onSelectionChange: (event: NativeSyntheticEvent<NativeSelectionEvent>) => void;
|
|
73
|
+
onFocusChange: (event: NativeSyntheticEvent<NativeFocusEvent>) => void;
|
|
74
|
+
onContentHeightChange: (event: NativeSyntheticEvent<NativeContentHeightEvent>) => void;
|
|
75
|
+
onToolbarAction: (event: NativeSyntheticEvent<NativeToolbarActionEvent>) => void;
|
|
76
|
+
onAddonEvent: (event: NativeSyntheticEvent<NativeAddonEvent>) => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const NativeEditorView = requireNativeViewManager(
|
|
80
|
+
'NativeEditor'
|
|
81
|
+
) as React.ComponentType<
|
|
82
|
+
NativeEditorViewProps & React.RefAttributes<NativeEditorViewHandle>
|
|
83
|
+
>;
|
|
84
|
+
|
|
85
|
+
const DEV_NATIVE_VIEW_KEY = __DEV__
|
|
86
|
+
? `native-editor-dev:${Math.random().toString(36).slice(2)}`
|
|
87
|
+
: 'native-editor';
|
|
88
|
+
|
|
89
|
+
interface NativeUpdateEvent {
|
|
90
|
+
updateJson: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface NativeSelectionEvent {
|
|
94
|
+
anchor: number;
|
|
95
|
+
head: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface NativeFocusEvent {
|
|
99
|
+
isFocused: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface NativeContentHeightEvent {
|
|
103
|
+
contentHeight: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface NativeToolbarActionEvent {
|
|
107
|
+
key: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface NativeAddonEvent {
|
|
111
|
+
eventJson: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function computeRenderedTextLength(elements: RenderElement[]): number {
|
|
115
|
+
let len = 0;
|
|
116
|
+
let blockCount = 0;
|
|
117
|
+
for (const el of elements) {
|
|
118
|
+
if (el.type === 'blockStart' && el.listContext) {
|
|
119
|
+
len += el.listContext.ordered
|
|
120
|
+
? `${el.listContext.index}. `.length
|
|
121
|
+
: '• '.length;
|
|
122
|
+
} else if (el.type === 'textRun' && el.text) {
|
|
123
|
+
len += el.text.length;
|
|
124
|
+
} else if (
|
|
125
|
+
el.type === 'voidInline' ||
|
|
126
|
+
el.type === 'voidBlock' ||
|
|
127
|
+
el.type === 'opaqueInlineAtom' ||
|
|
128
|
+
el.type === 'opaqueBlockAtom'
|
|
129
|
+
) {
|
|
130
|
+
if (el.type === 'opaqueInlineAtom' || el.type === 'opaqueBlockAtom') {
|
|
131
|
+
const visibleText =
|
|
132
|
+
el.nodeType === 'mention'
|
|
133
|
+
? (el.label ?? '?')
|
|
134
|
+
: `[${el.label ?? '?'}]`;
|
|
135
|
+
len += visibleText.length;
|
|
136
|
+
} else {
|
|
137
|
+
// U+FFFC placeholder / hard break
|
|
138
|
+
len += 1;
|
|
139
|
+
}
|
|
140
|
+
} else if (el.type === 'blockEnd') {
|
|
141
|
+
blockCount++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Block breaks add 1 scalar each, except the last block
|
|
145
|
+
if (blockCount > 1) len += blockCount - 1;
|
|
146
|
+
return len;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type NativeRichTextEditorHeightBehavior = 'fixed' | 'autoGrow';
|
|
150
|
+
export type NativeRichTextEditorToolbarPlacement = 'keyboard' | 'inline';
|
|
151
|
+
|
|
152
|
+
export interface NativeRichTextEditorProps {
|
|
153
|
+
/** Initial content as HTML (uncontrolled mode). */
|
|
154
|
+
initialContent?: string;
|
|
155
|
+
/** Initial content as ProseMirror JSON (uncontrolled mode). */
|
|
156
|
+
initialJSON?: DocumentJSON;
|
|
157
|
+
/** Controlled HTML content. External changes are diffed and applied. */
|
|
158
|
+
value?: string;
|
|
159
|
+
/** Controlled ProseMirror JSON content. Ignored if value is set. */
|
|
160
|
+
valueJSON?: DocumentJSON;
|
|
161
|
+
/** Schema definition. Defaults to tiptapSchema if not provided. */
|
|
162
|
+
schema?: SchemaDefinition;
|
|
163
|
+
/** Placeholder text shown when editor is empty. */
|
|
164
|
+
placeholder?: string;
|
|
165
|
+
/** Whether the editor is editable. */
|
|
166
|
+
editable?: boolean;
|
|
167
|
+
/** Maximum character length. */
|
|
168
|
+
maxLength?: number;
|
|
169
|
+
/** Whether to auto-focus on mount. */
|
|
170
|
+
autoFocus?: boolean;
|
|
171
|
+
/** Controls whether the editor scrolls internally or grows with content. */
|
|
172
|
+
heightBehavior?: NativeRichTextEditorHeightBehavior;
|
|
173
|
+
/** Whether to show the formatting toolbar. Defaults to true. */
|
|
174
|
+
showToolbar?: boolean;
|
|
175
|
+
/** Whether the toolbar is attached to the keyboard natively or rendered inline in React. */
|
|
176
|
+
toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
|
|
177
|
+
/** Displayed toolbar buttons, in order. Supports custom marks/nodes. */
|
|
178
|
+
toolbarItems?: readonly EditorToolbarItem[];
|
|
179
|
+
/** Called when a custom `action` toolbar item is pressed. */
|
|
180
|
+
onToolbarAction?: (key: string) => void;
|
|
181
|
+
/** Called when content changes with the current HTML. */
|
|
182
|
+
onContentChange?: (html: string) => void;
|
|
183
|
+
/** Called when content changes with the current ProseMirror JSON. */
|
|
184
|
+
onContentChangeJSON?: (json: DocumentJSON) => void;
|
|
185
|
+
/** Called when selection changes. */
|
|
186
|
+
onSelectionChange?: (selection: Selection) => void;
|
|
187
|
+
/** Called when active formatting state changes. */
|
|
188
|
+
onActiveStateChange?: (state: ActiveState) => void;
|
|
189
|
+
/** Called when the editor gains focus. */
|
|
190
|
+
onFocus?: () => void;
|
|
191
|
+
/** Called when the editor loses focus. */
|
|
192
|
+
onBlur?: () => void;
|
|
193
|
+
/** Style applied to the native view container. */
|
|
194
|
+
style?: StyleProp<ViewStyle>;
|
|
195
|
+
/** Optional native content theme applied to rendered blocks and typing attrs. */
|
|
196
|
+
theme?: EditorTheme;
|
|
197
|
+
/** Optional addon configuration. */
|
|
198
|
+
addons?: EditorAddons;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface NativeRichTextEditorRef {
|
|
202
|
+
/** Programmatically focus the editor. */
|
|
203
|
+
focus(): void;
|
|
204
|
+
/** Programmatically blur the editor. */
|
|
205
|
+
blur(): void;
|
|
206
|
+
/** Toggle a formatting mark (e.g. 'bold', 'italic'). */
|
|
207
|
+
toggleMark(markType: string): void;
|
|
208
|
+
/** Toggle a list type (bulletList or orderedList). */
|
|
209
|
+
toggleList(listType: 'bulletList' | 'orderedList'): void;
|
|
210
|
+
/** Indent the current list item. */
|
|
211
|
+
indentListItem(): void;
|
|
212
|
+
/** Outdent the current list item. */
|
|
213
|
+
outdentListItem(): void;
|
|
214
|
+
/** Insert a void node (e.g. 'horizontalRule'). */
|
|
215
|
+
insertNode(nodeType: string): void;
|
|
216
|
+
/** Insert text at the current cursor position. */
|
|
217
|
+
insertText(text: string): void;
|
|
218
|
+
/** Insert HTML content at the current selection. */
|
|
219
|
+
insertContentHtml(html: string): void;
|
|
220
|
+
/** Insert JSON content at the current selection. */
|
|
221
|
+
insertContentJson(doc: DocumentJSON): void;
|
|
222
|
+
/** Replace entire document with HTML (preserves undo history). */
|
|
223
|
+
setContent(html: string): void;
|
|
224
|
+
/** Replace entire document with JSON (preserves undo history). */
|
|
225
|
+
setContentJson(doc: DocumentJSON): void;
|
|
226
|
+
/** Get the current HTML content. */
|
|
227
|
+
getContent(): string;
|
|
228
|
+
/** Get the current content as ProseMirror JSON. */
|
|
229
|
+
getContentJson(): DocumentJSON;
|
|
230
|
+
/** Get the plain text content (no markup). */
|
|
231
|
+
getTextContent(): string;
|
|
232
|
+
/** Undo the last operation. */
|
|
233
|
+
undo(): void;
|
|
234
|
+
/** Redo the last undone operation. */
|
|
235
|
+
redo(): void;
|
|
236
|
+
/** Check if undo is available. */
|
|
237
|
+
canUndo(): boolean;
|
|
238
|
+
/** Check if redo is available. */
|
|
239
|
+
canRedo(): boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface RunAndApplyOptions {
|
|
243
|
+
/** If true, suppress onContentChange/onContentChangeJSON callbacks. */
|
|
244
|
+
suppressContentCallbacks?: boolean;
|
|
245
|
+
/** If true, skip the native view apply when the Rust HTML is unchanged. */
|
|
246
|
+
skipNativeApplyIfContentUnchanged?: boolean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const NativeRichTextEditor = forwardRef<
|
|
250
|
+
NativeRichTextEditorRef,
|
|
251
|
+
NativeRichTextEditorProps
|
|
252
|
+
>(function NativeRichTextEditor(
|
|
253
|
+
{
|
|
254
|
+
initialContent,
|
|
255
|
+
initialJSON,
|
|
256
|
+
value,
|
|
257
|
+
valueJSON,
|
|
258
|
+
schema,
|
|
259
|
+
placeholder,
|
|
260
|
+
editable = true,
|
|
261
|
+
maxLength,
|
|
262
|
+
autoFocus = false,
|
|
263
|
+
heightBehavior = 'autoGrow',
|
|
264
|
+
showToolbar = true,
|
|
265
|
+
toolbarPlacement = 'keyboard',
|
|
266
|
+
toolbarItems = DEFAULT_EDITOR_TOOLBAR_ITEMS,
|
|
267
|
+
onToolbarAction,
|
|
268
|
+
onContentChange,
|
|
269
|
+
onContentChangeJSON,
|
|
270
|
+
onSelectionChange,
|
|
271
|
+
onActiveStateChange,
|
|
272
|
+
onFocus,
|
|
273
|
+
onBlur,
|
|
274
|
+
style,
|
|
275
|
+
theme,
|
|
276
|
+
addons,
|
|
277
|
+
},
|
|
278
|
+
ref
|
|
279
|
+
) {
|
|
280
|
+
const bridgeRef = useRef<NativeEditorBridge | null>(null);
|
|
281
|
+
const nativeViewRef = useRef<NativeEditorViewHandle | null>(null);
|
|
282
|
+
const [isReady, setIsReady] = useState(false);
|
|
283
|
+
const [editorInstanceId, setEditorInstanceId] = useState(0);
|
|
284
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
285
|
+
const [toolbarFrameJson, setToolbarFrameJson] = useState<string | undefined>(
|
|
286
|
+
undefined
|
|
287
|
+
);
|
|
288
|
+
const [pendingNativeUpdate, setPendingNativeUpdate] = useState<{
|
|
289
|
+
json?: string;
|
|
290
|
+
revision: number;
|
|
291
|
+
}>({
|
|
292
|
+
json: undefined,
|
|
293
|
+
revision: 0,
|
|
294
|
+
});
|
|
295
|
+
const [autoGrowHeight, setAutoGrowHeight] = useState<number | null>(null);
|
|
296
|
+
|
|
297
|
+
// Toolbar state from EditorUpdate events
|
|
298
|
+
const [activeState, setActiveState] = useState<ActiveState>({
|
|
299
|
+
marks: {},
|
|
300
|
+
nodes: {},
|
|
301
|
+
commands: {},
|
|
302
|
+
allowedMarks: [],
|
|
303
|
+
insertableNodes: [],
|
|
304
|
+
});
|
|
305
|
+
const [historyState, setHistoryState] = useState<HistoryState>({
|
|
306
|
+
canUndo: false,
|
|
307
|
+
canRedo: false,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Selection and rendered text length refs (non-rendering state)
|
|
311
|
+
const selectionRef = useRef<Selection>({ type: 'text', anchor: 0, head: 0 });
|
|
312
|
+
const renderedTextLengthRef = useRef(0);
|
|
313
|
+
const toolbarRef = useRef<View | null>(null);
|
|
314
|
+
|
|
315
|
+
// Stable callback refs to avoid re-renders
|
|
316
|
+
const onContentChangeRef = useRef(onContentChange);
|
|
317
|
+
onContentChangeRef.current = onContentChange;
|
|
318
|
+
const onContentChangeJSONRef = useRef(onContentChangeJSON);
|
|
319
|
+
onContentChangeJSONRef.current = onContentChangeJSON;
|
|
320
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
321
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
322
|
+
const onActiveStateChangeRef = useRef(onActiveStateChange);
|
|
323
|
+
onActiveStateChangeRef.current = onActiveStateChange;
|
|
324
|
+
const onFocusRef = useRef(onFocus);
|
|
325
|
+
onFocusRef.current = onFocus;
|
|
326
|
+
const onBlurRef = useRef(onBlur);
|
|
327
|
+
onBlurRef.current = onBlur;
|
|
328
|
+
const addonsRef = useRef(addons);
|
|
329
|
+
addonsRef.current = addons;
|
|
330
|
+
|
|
331
|
+
const mentionSuggestionsByKeyRef = useRef<Map<string, MentionSuggestion>>(new Map());
|
|
332
|
+
mentionSuggestionsByKeyRef.current = new Map(
|
|
333
|
+
(addons?.mentions?.suggestions ?? []).map((suggestion) => [suggestion.key, suggestion])
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const syncStateFromUpdate = useCallback((update: EditorUpdate | null) => {
|
|
337
|
+
if (!update) return;
|
|
338
|
+
setActiveState(update.activeState);
|
|
339
|
+
setHistoryState(update.historyState);
|
|
340
|
+
selectionRef.current = update.selection;
|
|
341
|
+
renderedTextLengthRef.current = computeRenderedTextLength(
|
|
342
|
+
update.renderElements
|
|
343
|
+
);
|
|
344
|
+
}, []);
|
|
345
|
+
|
|
346
|
+
// Warn if both value and valueJSON are set
|
|
347
|
+
if (__DEV__ && value != null && valueJSON != null) {
|
|
348
|
+
console.warn(
|
|
349
|
+
'NativeRichTextEditor: value and valueJSON are mutually exclusive. ' +
|
|
350
|
+
'Only value will be used.'
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const runAndApply = useCallback(
|
|
355
|
+
(
|
|
356
|
+
mutate: () => EditorUpdate | null,
|
|
357
|
+
options?: RunAndApplyOptions
|
|
358
|
+
): EditorUpdate | null => {
|
|
359
|
+
const shouldCheckForNoopNativeApply =
|
|
360
|
+
options?.skipNativeApplyIfContentUnchanged === true &&
|
|
361
|
+
bridgeRef.current != null &&
|
|
362
|
+
!bridgeRef.current.isDestroyed;
|
|
363
|
+
const htmlBefore = shouldCheckForNoopNativeApply
|
|
364
|
+
? bridgeRef.current!.getHtml()
|
|
365
|
+
: null;
|
|
366
|
+
const update = mutate();
|
|
367
|
+
if (!update) return null;
|
|
368
|
+
|
|
369
|
+
const htmlAfter = shouldCheckForNoopNativeApply
|
|
370
|
+
? bridgeRef.current!.getHtml()
|
|
371
|
+
: null;
|
|
372
|
+
if (!shouldCheckForNoopNativeApply || htmlBefore !== htmlAfter) {
|
|
373
|
+
const updateJson = JSON.stringify(update);
|
|
374
|
+
if (Platform.OS === 'android') {
|
|
375
|
+
setPendingNativeUpdate((current) => ({
|
|
376
|
+
json: updateJson,
|
|
377
|
+
revision: current.revision + 1,
|
|
378
|
+
}));
|
|
379
|
+
} else {
|
|
380
|
+
nativeViewRef.current?.applyEditorUpdate(updateJson);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
syncStateFromUpdate(update);
|
|
385
|
+
|
|
386
|
+
onActiveStateChangeRef.current?.(update.activeState);
|
|
387
|
+
|
|
388
|
+
if (!options?.suppressContentCallbacks) {
|
|
389
|
+
if (onContentChangeRef.current && bridgeRef.current) {
|
|
390
|
+
onContentChangeRef.current(bridgeRef.current.getHtml());
|
|
391
|
+
}
|
|
392
|
+
if (onContentChangeJSONRef.current && bridgeRef.current) {
|
|
393
|
+
onContentChangeJSONRef.current(bridgeRef.current.getJson());
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
onSelectionChangeRef.current?.(update.selection);
|
|
398
|
+
|
|
399
|
+
return update;
|
|
400
|
+
},
|
|
401
|
+
[syncStateFromUpdate]
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
const effectiveSchema =
|
|
406
|
+
addonsRef.current?.mentions != null
|
|
407
|
+
? withMentionsSchema(schema ?? tiptapSchema)
|
|
408
|
+
: schema;
|
|
409
|
+
const schemaJson = effectiveSchema ? JSON.stringify(effectiveSchema) : undefined;
|
|
410
|
+
const bridge = NativeEditorBridge.create(
|
|
411
|
+
maxLength != null || schemaJson ? { maxLength, schemaJson } : undefined
|
|
412
|
+
);
|
|
413
|
+
bridgeRef.current = bridge;
|
|
414
|
+
setEditorInstanceId(bridge.editorId);
|
|
415
|
+
|
|
416
|
+
// Four-way content initialization: value > valueJSON > initialJSON > initialContent
|
|
417
|
+
if (value != null) {
|
|
418
|
+
bridge.setHtml(value);
|
|
419
|
+
} else if (valueJSON != null) {
|
|
420
|
+
bridge.setJson(valueJSON);
|
|
421
|
+
} else if (initialJSON) {
|
|
422
|
+
bridge.setJson(initialJSON);
|
|
423
|
+
} else if (initialContent) {
|
|
424
|
+
bridge.setHtml(initialContent);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
syncStateFromUpdate(bridge.getCurrentState());
|
|
428
|
+
setIsReady(true);
|
|
429
|
+
|
|
430
|
+
return () => {
|
|
431
|
+
bridge.destroy();
|
|
432
|
+
bridgeRef.current = null;
|
|
433
|
+
setEditorInstanceId(0);
|
|
434
|
+
setIsReady(false);
|
|
435
|
+
};
|
|
436
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
437
|
+
}, [schema, maxLength, syncStateFromUpdate, Boolean(addons?.mentions)]);
|
|
438
|
+
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (value == null) return;
|
|
441
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
442
|
+
|
|
443
|
+
const currentHtml = bridgeRef.current.getHtml();
|
|
444
|
+
if (currentHtml === value) return;
|
|
445
|
+
|
|
446
|
+
runAndApply(() => bridgeRef.current!.replaceHtml(value), {
|
|
447
|
+
suppressContentCallbacks: true,
|
|
448
|
+
});
|
|
449
|
+
}, [value, runAndApply]);
|
|
450
|
+
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (valueJSON == null || value != null) return;
|
|
453
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
454
|
+
|
|
455
|
+
// No-op if JSON content is identical (avoids churning undo history)
|
|
456
|
+
const currentJson = bridgeRef.current.getJson();
|
|
457
|
+
if (JSON.stringify(currentJson) === JSON.stringify(valueJSON)) return;
|
|
458
|
+
|
|
459
|
+
runAndApply(() => bridgeRef.current!.replaceJson(valueJSON), {
|
|
460
|
+
suppressContentCallbacks: true,
|
|
461
|
+
});
|
|
462
|
+
}, [valueJSON, value, runAndApply]);
|
|
463
|
+
|
|
464
|
+
const updateToolbarFrame = useCallback(() => {
|
|
465
|
+
const toolbar = toolbarRef.current;
|
|
466
|
+
if (!toolbar) {
|
|
467
|
+
setToolbarFrameJson(undefined);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
toolbar.measureInWindow((x, y, width, height) => {
|
|
472
|
+
if (width <= 0 || height <= 0) {
|
|
473
|
+
setToolbarFrameJson(undefined);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const nextJson = JSON.stringify({ x, y, width, height });
|
|
478
|
+
setToolbarFrameJson((prev) => (prev === nextJson ? prev : nextJson));
|
|
479
|
+
});
|
|
480
|
+
}, []);
|
|
481
|
+
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
|
|
484
|
+
setToolbarFrameJson(undefined);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const frame = requestAnimationFrame(() => {
|
|
489
|
+
updateToolbarFrame();
|
|
490
|
+
});
|
|
491
|
+
return () => cancelAnimationFrame(frame);
|
|
492
|
+
}, [
|
|
493
|
+
editable,
|
|
494
|
+
isFocused,
|
|
495
|
+
showToolbar,
|
|
496
|
+
toolbarPlacement,
|
|
497
|
+
updateToolbarFrame,
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
if (heightBehavior !== 'autoGrow') {
|
|
502
|
+
setAutoGrowHeight(null);
|
|
503
|
+
}
|
|
504
|
+
}, [heightBehavior]);
|
|
505
|
+
|
|
506
|
+
const handleUpdate = useCallback(
|
|
507
|
+
(event: NativeSyntheticEvent<NativeUpdateEvent>) => {
|
|
508
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const update = parseEditorUpdateJson(event.nativeEvent.updateJson);
|
|
512
|
+
if (!update) return;
|
|
513
|
+
syncStateFromUpdate(update);
|
|
514
|
+
|
|
515
|
+
onActiveStateChangeRef.current?.(update.activeState);
|
|
516
|
+
|
|
517
|
+
if (onContentChangeRef.current) {
|
|
518
|
+
onContentChangeRef.current(bridgeRef.current.getHtml());
|
|
519
|
+
}
|
|
520
|
+
if (onContentChangeJSONRef.current) {
|
|
521
|
+
onContentChangeJSONRef.current(bridgeRef.current.getJson());
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
onSelectionChangeRef.current?.(update.selection);
|
|
525
|
+
} catch {
|
|
526
|
+
// Invalid JSON from native — skip
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
[syncStateFromUpdate]
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const handleSelectionChange = useCallback(
|
|
533
|
+
(event: NativeSyntheticEvent<NativeSelectionEvent>) => {
|
|
534
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed) return;
|
|
535
|
+
|
|
536
|
+
const { anchor, head } = event.nativeEvent;
|
|
537
|
+
let selection: Selection;
|
|
538
|
+
|
|
539
|
+
if (
|
|
540
|
+
anchor === 0 &&
|
|
541
|
+
head >= renderedTextLengthRef.current &&
|
|
542
|
+
renderedTextLengthRef.current > 0
|
|
543
|
+
) {
|
|
544
|
+
selection = { type: 'all' };
|
|
545
|
+
} else {
|
|
546
|
+
selection = { type: 'text', anchor, head };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
bridgeRef.current.updateSelectionFromNative(anchor, head);
|
|
550
|
+
const currentState = bridgeRef.current.getCurrentState();
|
|
551
|
+
syncStateFromUpdate(currentState);
|
|
552
|
+
const nextSelection =
|
|
553
|
+
selection.type === 'all'
|
|
554
|
+
? selection
|
|
555
|
+
: (currentState?.selection ?? selection);
|
|
556
|
+
selectionRef.current = nextSelection;
|
|
557
|
+
if (currentState) {
|
|
558
|
+
onActiveStateChangeRef.current?.(currentState.activeState);
|
|
559
|
+
}
|
|
560
|
+
onSelectionChangeRef.current?.(nextSelection);
|
|
561
|
+
},
|
|
562
|
+
[syncStateFromUpdate]
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
const handleFocusChange = useCallback(
|
|
566
|
+
(event: NativeSyntheticEvent<NativeFocusEvent>) => {
|
|
567
|
+
const { isFocused: focused } = event.nativeEvent;
|
|
568
|
+
setIsFocused(focused);
|
|
569
|
+
if (focused) {
|
|
570
|
+
onFocusRef.current?.();
|
|
571
|
+
} else {
|
|
572
|
+
onBlurRef.current?.();
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
[]
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
const handleContentHeightChange = useCallback(
|
|
579
|
+
(event: NativeSyntheticEvent<NativeContentHeightEvent>) => {
|
|
580
|
+
if (heightBehavior !== 'autoGrow') return;
|
|
581
|
+
const density = Platform.OS === 'android' ? PixelRatio.get() : 1;
|
|
582
|
+
const nextHeight = Math.ceil(event.nativeEvent.contentHeight / density);
|
|
583
|
+
if (!(nextHeight > 0)) return;
|
|
584
|
+
setAutoGrowHeight((prev) => (prev === nextHeight ? prev : nextHeight));
|
|
585
|
+
},
|
|
586
|
+
[autoGrowHeight, heightBehavior]
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const handleToolbarAction = useCallback(
|
|
590
|
+
(event: NativeSyntheticEvent<NativeToolbarActionEvent>) => {
|
|
591
|
+
onToolbarAction?.(event.nativeEvent.key);
|
|
592
|
+
},
|
|
593
|
+
[onToolbarAction]
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const handleAddonEvent = useCallback(
|
|
597
|
+
(event: NativeSyntheticEvent<NativeAddonEvent>) => {
|
|
598
|
+
let parsed: EditorAddonEvent | null = null;
|
|
599
|
+
try {
|
|
600
|
+
parsed = JSON.parse(event.nativeEvent.eventJson) as EditorAddonEvent;
|
|
601
|
+
} catch {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!parsed) return;
|
|
605
|
+
|
|
606
|
+
if (parsed.type === 'mentionsQueryChange') {
|
|
607
|
+
addonsRef.current?.mentions?.onQueryChange?.({
|
|
608
|
+
query: parsed.query,
|
|
609
|
+
trigger: parsed.trigger,
|
|
610
|
+
range: parsed.range,
|
|
611
|
+
isActive: parsed.isActive,
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (parsed.type === 'mentionsSelect') {
|
|
617
|
+
const suggestion = mentionSuggestionsByKeyRef.current.get(parsed.suggestionKey);
|
|
618
|
+
if (!suggestion) return;
|
|
619
|
+
addonsRef.current?.mentions?.onSelect?.({
|
|
620
|
+
trigger: parsed.trigger,
|
|
621
|
+
suggestion,
|
|
622
|
+
attrs: parsed.attrs,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
[]
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
useImperativeHandle(
|
|
630
|
+
ref,
|
|
631
|
+
() => ({
|
|
632
|
+
focus() {
|
|
633
|
+
nativeViewRef.current?.focus?.();
|
|
634
|
+
},
|
|
635
|
+
blur() {
|
|
636
|
+
nativeViewRef.current?.blur?.();
|
|
637
|
+
},
|
|
638
|
+
toggleMark(markType: string) {
|
|
639
|
+
runAndApply(
|
|
640
|
+
() => bridgeRef.current?.toggleMark(markType) ?? null,
|
|
641
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
642
|
+
);
|
|
643
|
+
},
|
|
644
|
+
toggleList(listType: 'bulletList' | 'orderedList') {
|
|
645
|
+
runAndApply(
|
|
646
|
+
() => bridgeRef.current?.toggleList(listType) ?? null
|
|
647
|
+
);
|
|
648
|
+
},
|
|
649
|
+
indentListItem() {
|
|
650
|
+
runAndApply(
|
|
651
|
+
() => bridgeRef.current?.indentListItem() ?? null
|
|
652
|
+
);
|
|
653
|
+
},
|
|
654
|
+
outdentListItem() {
|
|
655
|
+
runAndApply(
|
|
656
|
+
() => bridgeRef.current?.outdentListItem() ?? null
|
|
657
|
+
);
|
|
658
|
+
},
|
|
659
|
+
insertNode(nodeType: string) {
|
|
660
|
+
runAndApply(
|
|
661
|
+
() => bridgeRef.current?.insertNode(nodeType) ?? null
|
|
662
|
+
);
|
|
663
|
+
},
|
|
664
|
+
insertText(text: string) {
|
|
665
|
+
runAndApply(
|
|
666
|
+
() => bridgeRef.current?.replaceSelectionText(text) ?? null
|
|
667
|
+
);
|
|
668
|
+
},
|
|
669
|
+
insertContentHtml(html: string) {
|
|
670
|
+
runAndApply(
|
|
671
|
+
() => bridgeRef.current?.insertContentHtml(html) ?? null
|
|
672
|
+
);
|
|
673
|
+
},
|
|
674
|
+
insertContentJson(doc: DocumentJSON) {
|
|
675
|
+
runAndApply(
|
|
676
|
+
() => bridgeRef.current?.insertContentJson(doc) ?? null
|
|
677
|
+
);
|
|
678
|
+
},
|
|
679
|
+
setContent(html: string) {
|
|
680
|
+
runAndApply(
|
|
681
|
+
() => bridgeRef.current?.replaceHtml(html) ?? null
|
|
682
|
+
);
|
|
683
|
+
},
|
|
684
|
+
setContentJson(doc: DocumentJSON) {
|
|
685
|
+
runAndApply(
|
|
686
|
+
() => bridgeRef.current?.replaceJson(doc) ?? null
|
|
687
|
+
);
|
|
688
|
+
},
|
|
689
|
+
getContent(): string {
|
|
690
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
691
|
+
return '';
|
|
692
|
+
return bridgeRef.current.getHtml();
|
|
693
|
+
},
|
|
694
|
+
getContentJson(): DocumentJSON {
|
|
695
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
696
|
+
return {};
|
|
697
|
+
return bridgeRef.current.getJson();
|
|
698
|
+
},
|
|
699
|
+
getTextContent(): string {
|
|
700
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
701
|
+
return '';
|
|
702
|
+
return bridgeRef.current.getHtml().replace(/<[^>]+>/g, '');
|
|
703
|
+
},
|
|
704
|
+
undo() {
|
|
705
|
+
runAndApply(() => bridgeRef.current?.undo() ?? null);
|
|
706
|
+
},
|
|
707
|
+
redo() {
|
|
708
|
+
runAndApply(() => bridgeRef.current?.redo() ?? null);
|
|
709
|
+
},
|
|
710
|
+
canUndo(): boolean {
|
|
711
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
712
|
+
return false;
|
|
713
|
+
return bridgeRef.current.canUndo();
|
|
714
|
+
},
|
|
715
|
+
canRedo(): boolean {
|
|
716
|
+
if (!bridgeRef.current || bridgeRef.current.isDestroyed)
|
|
717
|
+
return false;
|
|
718
|
+
return bridgeRef.current.canRedo();
|
|
719
|
+
},
|
|
720
|
+
}),
|
|
721
|
+
[runAndApply]
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
if (!isReady) return null;
|
|
725
|
+
|
|
726
|
+
const themeJson = serializeEditorTheme(theme);
|
|
727
|
+
const addonsJson = serializeEditorAddons(addons);
|
|
728
|
+
const toolbarItemsJson = JSON.stringify(toolbarItems);
|
|
729
|
+
const usesNativeKeyboardToolbar =
|
|
730
|
+
toolbarPlacement === 'keyboard' &&
|
|
731
|
+
(Platform.OS === 'ios' || Platform.OS === 'android');
|
|
732
|
+
const shouldRenderJsToolbar =
|
|
733
|
+
!usesNativeKeyboardToolbar && showToolbar && editable;
|
|
734
|
+
const inlineToolbarChrome = {
|
|
735
|
+
backgroundColor: theme?.toolbar?.backgroundColor,
|
|
736
|
+
borderColor: theme?.toolbar?.borderColor,
|
|
737
|
+
borderWidth: theme?.toolbar?.borderWidth,
|
|
738
|
+
borderRadius: theme?.toolbar?.borderRadius,
|
|
739
|
+
};
|
|
740
|
+
const nativeViewStyle =
|
|
741
|
+
heightBehavior === 'autoGrow' && autoGrowHeight != null
|
|
742
|
+
? [style, { height: autoGrowHeight }]
|
|
743
|
+
: style;
|
|
744
|
+
const jsToolbar = (
|
|
745
|
+
<View
|
|
746
|
+
ref={toolbarRef}
|
|
747
|
+
testID="native-editor-js-toolbar"
|
|
748
|
+
style={[
|
|
749
|
+
styles.inlineToolbar,
|
|
750
|
+
inlineToolbarChrome.backgroundColor != null
|
|
751
|
+
? { backgroundColor: inlineToolbarChrome.backgroundColor }
|
|
752
|
+
: null,
|
|
753
|
+
inlineToolbarChrome.borderColor != null
|
|
754
|
+
? { borderColor: inlineToolbarChrome.borderColor }
|
|
755
|
+
: null,
|
|
756
|
+
inlineToolbarChrome.borderWidth != null
|
|
757
|
+
? { borderWidth: inlineToolbarChrome.borderWidth }
|
|
758
|
+
: null,
|
|
759
|
+
inlineToolbarChrome.borderRadius != null
|
|
760
|
+
? { borderRadius: inlineToolbarChrome.borderRadius }
|
|
761
|
+
: null,
|
|
762
|
+
]}
|
|
763
|
+
onLayout={updateToolbarFrame}
|
|
764
|
+
>
|
|
765
|
+
<EditorToolbar
|
|
766
|
+
activeState={activeState}
|
|
767
|
+
historyState={historyState}
|
|
768
|
+
toolbarItems={toolbarItems}
|
|
769
|
+
theme={theme?.toolbar}
|
|
770
|
+
showTopBorder={false}
|
|
771
|
+
onToggleMark={(mark) =>
|
|
772
|
+
runAndApply(
|
|
773
|
+
() => bridgeRef.current?.toggleMark(mark) ?? null,
|
|
774
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
onToggleListType={(
|
|
778
|
+
listType: EditorToolbarListType
|
|
779
|
+
) =>
|
|
780
|
+
runAndApply(
|
|
781
|
+
() => bridgeRef.current?.toggleList(listType) ?? null
|
|
782
|
+
)
|
|
783
|
+
}
|
|
784
|
+
onInsertNodeType={(nodeType) =>
|
|
785
|
+
runAndApply(
|
|
786
|
+
() => bridgeRef.current?.insertNode(nodeType) ?? null
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
onRunCommand={(command: EditorToolbarCommand) => {
|
|
790
|
+
switch (command) {
|
|
791
|
+
case 'indentList':
|
|
792
|
+
runAndApply(
|
|
793
|
+
() =>
|
|
794
|
+
bridgeRef.current?.indentListItem() ??
|
|
795
|
+
null
|
|
796
|
+
);
|
|
797
|
+
break;
|
|
798
|
+
case 'outdentList':
|
|
799
|
+
runAndApply(
|
|
800
|
+
() =>
|
|
801
|
+
bridgeRef.current?.outdentListItem() ??
|
|
802
|
+
null
|
|
803
|
+
);
|
|
804
|
+
break;
|
|
805
|
+
case 'undo':
|
|
806
|
+
runAndApply(
|
|
807
|
+
() => bridgeRef.current?.undo() ?? null
|
|
808
|
+
);
|
|
809
|
+
break;
|
|
810
|
+
case 'redo':
|
|
811
|
+
runAndApply(
|
|
812
|
+
() => bridgeRef.current?.redo() ?? null
|
|
813
|
+
);
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}}
|
|
817
|
+
onToolbarAction={onToolbarAction}
|
|
818
|
+
onToggleBold={() =>
|
|
819
|
+
runAndApply(
|
|
820
|
+
() =>
|
|
821
|
+
bridgeRef.current?.toggleMark('bold') ??
|
|
822
|
+
null,
|
|
823
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
824
|
+
)
|
|
825
|
+
}
|
|
826
|
+
onToggleItalic={() =>
|
|
827
|
+
runAndApply(
|
|
828
|
+
() =>
|
|
829
|
+
bridgeRef.current?.toggleMark('italic') ??
|
|
830
|
+
null,
|
|
831
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
onToggleUnderline={() =>
|
|
835
|
+
runAndApply(
|
|
836
|
+
() =>
|
|
837
|
+
bridgeRef.current?.toggleMark('underline') ??
|
|
838
|
+
null,
|
|
839
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
onToggleStrike={() =>
|
|
843
|
+
runAndApply(
|
|
844
|
+
() =>
|
|
845
|
+
bridgeRef.current?.toggleMark('strike') ??
|
|
846
|
+
null,
|
|
847
|
+
{ skipNativeApplyIfContentUnchanged: true }
|
|
848
|
+
)
|
|
849
|
+
}
|
|
850
|
+
onToggleBulletList={() =>
|
|
851
|
+
runAndApply(
|
|
852
|
+
() =>
|
|
853
|
+
bridgeRef.current?.toggleList(
|
|
854
|
+
'bulletList'
|
|
855
|
+
) ?? null
|
|
856
|
+
)
|
|
857
|
+
}
|
|
858
|
+
onToggleOrderedList={() =>
|
|
859
|
+
runAndApply(
|
|
860
|
+
() =>
|
|
861
|
+
bridgeRef.current?.toggleList(
|
|
862
|
+
'orderedList'
|
|
863
|
+
) ?? null
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
onIndentList={() =>
|
|
867
|
+
runAndApply(
|
|
868
|
+
() => bridgeRef.current?.indentListItem() ?? null
|
|
869
|
+
)
|
|
870
|
+
}
|
|
871
|
+
onOutdentList={() =>
|
|
872
|
+
runAndApply(
|
|
873
|
+
() => bridgeRef.current?.outdentListItem() ?? null
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
onInsertHorizontalRule={() =>
|
|
877
|
+
runAndApply(
|
|
878
|
+
() =>
|
|
879
|
+
bridgeRef.current?.insertNode(
|
|
880
|
+
'horizontalRule'
|
|
881
|
+
) ?? null
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
onInsertLineBreak={() =>
|
|
885
|
+
runAndApply(
|
|
886
|
+
() =>
|
|
887
|
+
bridgeRef.current?.insertNode(
|
|
888
|
+
'hardBreak'
|
|
889
|
+
) ?? null
|
|
890
|
+
)
|
|
891
|
+
}
|
|
892
|
+
onUndo={() =>
|
|
893
|
+
runAndApply(
|
|
894
|
+
() => bridgeRef.current?.undo() ?? null
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
onRedo={() =>
|
|
898
|
+
runAndApply(
|
|
899
|
+
() => bridgeRef.current?.redo() ?? null
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
/>
|
|
903
|
+
</View>
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
return (
|
|
907
|
+
<View style={styles.container}>
|
|
908
|
+
<NativeEditorView
|
|
909
|
+
key={DEV_NATIVE_VIEW_KEY}
|
|
910
|
+
ref={nativeViewRef}
|
|
911
|
+
style={nativeViewStyle}
|
|
912
|
+
editorId={editorInstanceId}
|
|
913
|
+
placeholder={placeholder}
|
|
914
|
+
editable={editable}
|
|
915
|
+
autoFocus={autoFocus}
|
|
916
|
+
showToolbar={showToolbar}
|
|
917
|
+
toolbarPlacement={toolbarPlacement}
|
|
918
|
+
heightBehavior={heightBehavior}
|
|
919
|
+
themeJson={themeJson}
|
|
920
|
+
addonsJson={addonsJson}
|
|
921
|
+
toolbarItemsJson={toolbarItemsJson}
|
|
922
|
+
toolbarFrameJson={
|
|
923
|
+
toolbarPlacement === 'inline' && isFocused
|
|
924
|
+
? toolbarFrameJson
|
|
925
|
+
: undefined
|
|
926
|
+
}
|
|
927
|
+
editorUpdateJson={pendingNativeUpdate.json}
|
|
928
|
+
editorUpdateRevision={pendingNativeUpdate.revision}
|
|
929
|
+
onEditorUpdate={handleUpdate}
|
|
930
|
+
onSelectionChange={handleSelectionChange}
|
|
931
|
+
onFocusChange={handleFocusChange}
|
|
932
|
+
onContentHeightChange={handleContentHeightChange}
|
|
933
|
+
onToolbarAction={handleToolbarAction}
|
|
934
|
+
onAddonEvent={handleAddonEvent}
|
|
935
|
+
/>
|
|
936
|
+
{shouldRenderJsToolbar && jsToolbar}
|
|
937
|
+
</View>
|
|
938
|
+
);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
const styles = StyleSheet.create({
|
|
942
|
+
container: {
|
|
943
|
+
position: 'relative',
|
|
944
|
+
},
|
|
945
|
+
inlineToolbar: {
|
|
946
|
+
marginTop: 8,
|
|
947
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
948
|
+
borderColor: '#E5E5EA',
|
|
949
|
+
overflow: 'hidden',
|
|
950
|
+
},
|
|
951
|
+
});
|