@apollohg/react-native-prose-editor 0.1.1 → 0.3.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.
Files changed (53) hide show
  1. package/README.md +12 -7
  2. package/android/build.gradle +7 -2
  3. package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
  4. package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
  5. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
  6. package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
  7. package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
  8. package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
  9. package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
  10. package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
  11. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
  12. package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
  13. package/dist/EditorTheme.js +29 -0
  14. package/dist/EditorToolbar.d.ts +129 -0
  15. package/dist/EditorToolbar.js +394 -0
  16. package/dist/NativeEditorBridge.d.ts +242 -0
  17. package/dist/NativeEditorBridge.js +647 -0
  18. package/dist/NativeRichTextEditor.d.ts +142 -0
  19. package/dist/NativeRichTextEditor.js +649 -0
  20. package/dist/YjsCollaboration.d.ts +83 -0
  21. package/dist/YjsCollaboration.js +585 -0
  22. package/dist/addons.d.ts +70 -0
  23. package/dist/addons.js +77 -0
  24. package/dist/index.d.ts +8 -0
  25. package/dist/index.js +26 -0
  26. package/dist/schemas.d.ts +35 -0
  27. package/{src/schemas.ts → dist/schemas.js} +62 -27
  28. package/dist/useNativeEditor.d.ts +40 -0
  29. package/dist/useNativeEditor.js +117 -0
  30. package/ios/EditorAddons.swift +26 -3
  31. package/ios/EditorCore.xcframework/Info.plist +5 -5
  32. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  33. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  34. package/ios/EditorLayoutManager.swift +236 -0
  35. package/ios/EditorTheme.swift +51 -1
  36. package/ios/Generated_editor_core.swift +270 -2
  37. package/ios/NativeEditorExpoView.swift +612 -45
  38. package/ios/NativeEditorModule.swift +81 -0
  39. package/ios/PositionBridge.swift +22 -0
  40. package/ios/RenderBridge.swift +427 -39
  41. package/ios/RichTextEditorView.swift +1342 -18
  42. package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
  43. package/package.json +80 -64
  44. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  45. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  46. package/rust/android/x86_64/libeditor_core.so +0 -0
  47. package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +404 -4
  48. package/src/EditorToolbar.tsx +0 -620
  49. package/src/NativeEditorBridge.ts +0 -607
  50. package/src/NativeRichTextEditor.tsx +0 -951
  51. package/src/addons.ts +0 -158
  52. package/src/index.ts +0 -63
  53. package/src/useNativeEditor.ts +0 -173
@@ -0,0 +1,649 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NativeRichTextEditor = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const react_native_1 = require("react-native");
7
+ const expo_modules_core_1 = require("expo-modules-core");
8
+ const NativeEditorBridge_1 = require("./NativeEditorBridge");
9
+ const EditorToolbar_1 = require("./EditorToolbar");
10
+ const EditorTheme_1 = require("./EditorTheme");
11
+ const addons_1 = require("./addons");
12
+ const schemas_1 = require("./schemas");
13
+ const schemas_2 = require("./schemas");
14
+ const NativeEditorView = (0, expo_modules_core_1.requireNativeViewManager)('NativeEditor');
15
+ const DEV_NATIVE_VIEW_KEY = __DEV__
16
+ ? `native-editor-dev:${Math.random().toString(36).slice(2)}`
17
+ : 'native-editor';
18
+ const LINK_TOOLBAR_ACTION_KEY = '__native-editor-link__';
19
+ const IMAGE_TOOLBAR_ACTION_KEY = '__native-editor-image__';
20
+ function isImageDataUrl(value) {
21
+ return /^data:image\//i.test(value.trim());
22
+ }
23
+ function isPromiseLike(value) {
24
+ return (value != null &&
25
+ typeof value === 'object' &&
26
+ 'then' in value &&
27
+ typeof value.then === 'function');
28
+ }
29
+ function computeRenderedTextLength(elements) {
30
+ let len = 0;
31
+ let blockCount = 0;
32
+ for (const el of elements) {
33
+ if (el.type === 'blockStart' && el.listContext) {
34
+ len += el.listContext.ordered ? `${el.listContext.index}. `.length : '• '.length;
35
+ }
36
+ else if (el.type === 'textRun' && el.text) {
37
+ len += el.text.length;
38
+ }
39
+ else if (el.type === 'voidInline' ||
40
+ el.type === 'voidBlock' ||
41
+ el.type === 'opaqueInlineAtom' ||
42
+ el.type === 'opaqueBlockAtom') {
43
+ if (el.type === 'opaqueInlineAtom' || el.type === 'opaqueBlockAtom') {
44
+ const visibleText = el.nodeType === 'mention' ? (el.label ?? '?') : `[${el.label ?? '?'}]`;
45
+ len += visibleText.length;
46
+ }
47
+ else {
48
+ // U+FFFC placeholder / hard break
49
+ len += 1;
50
+ }
51
+ }
52
+ else if (el.type === 'blockEnd') {
53
+ blockCount++;
54
+ }
55
+ }
56
+ // Block breaks add 1 scalar each, except the last block
57
+ if (blockCount > 1)
58
+ len += blockCount - 1;
59
+ return len;
60
+ }
61
+ function serializeRemoteSelections(remoteSelections) {
62
+ if (!remoteSelections || remoteSelections.length === 0) {
63
+ return undefined;
64
+ }
65
+ return JSON.stringify(remoteSelections);
66
+ }
67
+ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEditor({ initialContent, initialJSON, value, valueJSON, schema, placeholder, editable = true, maxLength, autoFocus = false, heightBehavior = 'autoGrow', showToolbar = true, toolbarPlacement = 'keyboard', toolbarItems = EditorToolbar_1.DEFAULT_EDITOR_TOOLBAR_ITEMS, onToolbarAction, onRequestLink, onRequestImage, onContentChange, onContentChangeJSON, onSelectionChange, onActiveStateChange, onHistoryStateChange, onFocus, onBlur, style, containerStyle, theme, addons, remoteSelections, allowBase64Images = false, allowImageResizing = true, }, ref) {
68
+ const bridgeRef = (0, react_1.useRef)(null);
69
+ const nativeViewRef = (0, react_1.useRef)(null);
70
+ const [isReady, setIsReady] = (0, react_1.useState)(false);
71
+ const [editorInstanceId, setEditorInstanceId] = (0, react_1.useState)(0);
72
+ const [isFocused, setIsFocused] = (0, react_1.useState)(false);
73
+ const [toolbarFrameJson, setToolbarFrameJson] = (0, react_1.useState)(undefined);
74
+ const [pendingNativeUpdate, setPendingNativeUpdate] = (0, react_1.useState)({
75
+ json: undefined,
76
+ revision: 0,
77
+ });
78
+ const [autoGrowHeight, setAutoGrowHeight] = (0, react_1.useState)(null);
79
+ // Toolbar state from EditorUpdate events
80
+ const [activeState, setActiveState] = (0, react_1.useState)({
81
+ marks: {},
82
+ markAttrs: {},
83
+ nodes: {},
84
+ commands: {},
85
+ allowedMarks: [],
86
+ insertableNodes: [],
87
+ });
88
+ const [historyState, setHistoryState] = (0, react_1.useState)({
89
+ canUndo: false,
90
+ canRedo: false,
91
+ });
92
+ // Selection and rendered text length refs (non-rendering state)
93
+ const selectionRef = (0, react_1.useRef)({ type: 'text', anchor: 0, head: 0 });
94
+ const renderedTextLengthRef = (0, react_1.useRef)(0);
95
+ const toolbarRef = (0, react_1.useRef)(null);
96
+ // Stable callback refs to avoid re-renders
97
+ const onContentChangeRef = (0, react_1.useRef)(onContentChange);
98
+ onContentChangeRef.current = onContentChange;
99
+ const onContentChangeJSONRef = (0, react_1.useRef)(onContentChangeJSON);
100
+ onContentChangeJSONRef.current = onContentChangeJSON;
101
+ const onSelectionChangeRef = (0, react_1.useRef)(onSelectionChange);
102
+ onSelectionChangeRef.current = onSelectionChange;
103
+ const onActiveStateChangeRef = (0, react_1.useRef)(onActiveStateChange);
104
+ onActiveStateChangeRef.current = onActiveStateChange;
105
+ const onHistoryStateChangeRef = (0, react_1.useRef)(onHistoryStateChange);
106
+ onHistoryStateChangeRef.current = onHistoryStateChange;
107
+ const onFocusRef = (0, react_1.useRef)(onFocus);
108
+ onFocusRef.current = onFocus;
109
+ const onBlurRef = (0, react_1.useRef)(onBlur);
110
+ onBlurRef.current = onBlur;
111
+ const addonsRef = (0, react_1.useRef)(addons);
112
+ addonsRef.current = addons;
113
+ const currentLinkHref = typeof activeState.markAttrs?.link?.href === 'string'
114
+ ? activeState.markAttrs.link.href
115
+ : undefined;
116
+ const mentionSuggestionsByKeyRef = (0, react_1.useRef)(new Map());
117
+ mentionSuggestionsByKeyRef.current = new Map((addons?.mentions?.suggestions ?? []).map((suggestion) => [suggestion.key, suggestion]));
118
+ const syncStateFromUpdate = (0, react_1.useCallback)((update) => {
119
+ if (!update)
120
+ return;
121
+ setActiveState(update.activeState);
122
+ setHistoryState(update.historyState);
123
+ selectionRef.current = update.selection;
124
+ renderedTextLengthRef.current = computeRenderedTextLength(update.renderElements);
125
+ }, []);
126
+ // Warn if both value and valueJSON are set
127
+ if (__DEV__ && value != null && valueJSON != null) {
128
+ console.warn('NativeRichTextEditor: value and valueJSON are mutually exclusive. ' +
129
+ 'Only value will be used.');
130
+ }
131
+ const runAndApply = (0, react_1.useCallback)((mutate, options) => {
132
+ const preservedSelection = options?.preserveLiveTextSelection === true ? selectionRef.current : null;
133
+ const shouldCheckForNoopNativeApply = options?.skipNativeApplyIfContentUnchanged === true &&
134
+ bridgeRef.current != null &&
135
+ !bridgeRef.current.isDestroyed;
136
+ const htmlBefore = shouldCheckForNoopNativeApply
137
+ ? bridgeRef.current.getHtml()
138
+ : null;
139
+ const update = mutate();
140
+ if (!update)
141
+ return null;
142
+ if (preservedSelection?.type === 'text' &&
143
+ typeof preservedSelection.anchor === 'number' &&
144
+ typeof preservedSelection.head === 'number' &&
145
+ bridgeRef.current != null &&
146
+ !bridgeRef.current.isDestroyed) {
147
+ bridgeRef.current.setSelection(preservedSelection.anchor, preservedSelection.head);
148
+ update.selection = {
149
+ type: 'text',
150
+ anchor: preservedSelection.anchor,
151
+ head: preservedSelection.head,
152
+ };
153
+ }
154
+ const htmlAfter = shouldCheckForNoopNativeApply
155
+ ? bridgeRef.current.getHtml()
156
+ : null;
157
+ if (!shouldCheckForNoopNativeApply || htmlBefore !== htmlAfter) {
158
+ const updateJson = JSON.stringify(update);
159
+ if (react_native_1.Platform.OS === 'android') {
160
+ setPendingNativeUpdate((current) => ({
161
+ json: updateJson,
162
+ revision: current.revision + 1,
163
+ }));
164
+ }
165
+ else {
166
+ try {
167
+ const applyResult = nativeViewRef.current?.applyEditorUpdate(updateJson);
168
+ if (isPromiseLike(applyResult)) {
169
+ void applyResult.catch(() => {
170
+ // The native view may already be torn down during navigation.
171
+ });
172
+ }
173
+ }
174
+ catch {
175
+ // The native view may already be torn down during navigation.
176
+ }
177
+ }
178
+ }
179
+ syncStateFromUpdate(update);
180
+ onActiveStateChangeRef.current?.(update.activeState);
181
+ onHistoryStateChangeRef.current?.(update.historyState);
182
+ if (!options?.suppressContentCallbacks) {
183
+ if (onContentChangeRef.current && bridgeRef.current) {
184
+ onContentChangeRef.current(bridgeRef.current.getHtml());
185
+ }
186
+ if (onContentChangeJSONRef.current && bridgeRef.current) {
187
+ onContentChangeJSONRef.current(bridgeRef.current.getJson());
188
+ }
189
+ }
190
+ onSelectionChangeRef.current?.(update.selection);
191
+ return update;
192
+ }, [syncStateFromUpdate]);
193
+ (0, react_1.useEffect)(() => {
194
+ const effectiveSchema = addonsRef.current?.mentions != null
195
+ ? (0, addons_1.withMentionsSchema)(schema ?? schemas_1.tiptapSchema)
196
+ : schema;
197
+ const schemaJson = effectiveSchema ? JSON.stringify(effectiveSchema) : undefined;
198
+ const bridgeConfig = maxLength != null || schemaJson || allowBase64Images
199
+ ? { maxLength, schemaJson, allowBase64Images }
200
+ : undefined;
201
+ const bridge = NativeEditorBridge_1.NativeEditorBridge.create(bridgeConfig);
202
+ bridgeRef.current = bridge;
203
+ setEditorInstanceId(bridge.editorId);
204
+ // Four-way content initialization: value > valueJSON > initialJSON > initialContent
205
+ if (value != null) {
206
+ bridge.setHtml(value);
207
+ }
208
+ else if (valueJSON != null) {
209
+ bridge.setJson(valueJSON);
210
+ }
211
+ else if (initialJSON) {
212
+ bridge.setJson(initialJSON);
213
+ }
214
+ else if (initialContent) {
215
+ bridge.setHtml(initialContent);
216
+ }
217
+ syncStateFromUpdate(bridge.getCurrentState());
218
+ setIsReady(true);
219
+ return () => {
220
+ bridge.destroy();
221
+ bridgeRef.current = null;
222
+ nativeViewRef.current = null;
223
+ setEditorInstanceId(0);
224
+ setIsReady(false);
225
+ };
226
+ // eslint-disable-next-line react-hooks/exhaustive-deps
227
+ }, [schema, maxLength, syncStateFromUpdate, Boolean(addons?.mentions), allowBase64Images]);
228
+ (0, react_1.useEffect)(() => {
229
+ if (value == null)
230
+ return;
231
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
232
+ return;
233
+ const currentHtml = bridgeRef.current.getHtml();
234
+ if (currentHtml === value)
235
+ return;
236
+ runAndApply(() => bridgeRef.current.replaceHtml(value), {
237
+ suppressContentCallbacks: true,
238
+ preserveLiveTextSelection: true,
239
+ });
240
+ }, [value, runAndApply]);
241
+ (0, react_1.useEffect)(() => {
242
+ if (valueJSON == null || value != null)
243
+ return;
244
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
245
+ return;
246
+ // No-op if JSON content is identical (avoids churning undo history)
247
+ const currentJson = bridgeRef.current.getJson();
248
+ if (JSON.stringify(currentJson) === JSON.stringify(valueJSON))
249
+ return;
250
+ runAndApply(() => bridgeRef.current.replaceJson(valueJSON), {
251
+ suppressContentCallbacks: true,
252
+ preserveLiveTextSelection: true,
253
+ });
254
+ }, [valueJSON, value, runAndApply]);
255
+ const updateToolbarFrame = (0, react_1.useCallback)(() => {
256
+ const toolbar = toolbarRef.current;
257
+ if (!toolbar) {
258
+ setToolbarFrameJson(undefined);
259
+ return;
260
+ }
261
+ toolbar.measureInWindow((x, y, width, height) => {
262
+ if (width <= 0 || height <= 0) {
263
+ setToolbarFrameJson(undefined);
264
+ return;
265
+ }
266
+ const nextJson = JSON.stringify({ x, y, width, height });
267
+ setToolbarFrameJson((prev) => (prev === nextJson ? prev : nextJson));
268
+ });
269
+ }, []);
270
+ (0, react_1.useEffect)(() => {
271
+ if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
272
+ setToolbarFrameJson(undefined);
273
+ return;
274
+ }
275
+ const frame = requestAnimationFrame(() => {
276
+ updateToolbarFrame();
277
+ });
278
+ return () => cancelAnimationFrame(frame);
279
+ }, [editable, isFocused, showToolbar, toolbarPlacement, updateToolbarFrame]);
280
+ (0, react_1.useEffect)(() => {
281
+ if (heightBehavior !== 'autoGrow') {
282
+ setAutoGrowHeight(null);
283
+ }
284
+ }, [heightBehavior]);
285
+ const handleUpdate = (0, react_1.useCallback)((event) => {
286
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
287
+ return;
288
+ try {
289
+ const update = (0, NativeEditorBridge_1.parseEditorUpdateJson)(event.nativeEvent.updateJson);
290
+ if (!update)
291
+ return;
292
+ syncStateFromUpdate(update);
293
+ onActiveStateChangeRef.current?.(update.activeState);
294
+ onHistoryStateChangeRef.current?.(update.historyState);
295
+ if (onContentChangeRef.current) {
296
+ onContentChangeRef.current(bridgeRef.current.getHtml());
297
+ }
298
+ if (onContentChangeJSONRef.current) {
299
+ onContentChangeJSONRef.current(bridgeRef.current.getJson());
300
+ }
301
+ onSelectionChangeRef.current?.(update.selection);
302
+ }
303
+ catch {
304
+ // Invalid JSON from native — skip
305
+ }
306
+ }, [syncStateFromUpdate]);
307
+ const handleSelectionChange = (0, react_1.useCallback)((event) => {
308
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
309
+ return;
310
+ const { anchor, head } = event.nativeEvent;
311
+ let selection;
312
+ if (anchor === 0 &&
313
+ head >= renderedTextLengthRef.current &&
314
+ renderedTextLengthRef.current > 0) {
315
+ selection = { type: 'all' };
316
+ }
317
+ else {
318
+ selection = { type: 'text', anchor, head };
319
+ }
320
+ bridgeRef.current.updateSelectionFromNative(anchor, head);
321
+ const currentState = bridgeRef.current.getCurrentState();
322
+ syncStateFromUpdate(currentState);
323
+ const nextSelection = selection.type === 'all' ? selection : (currentState?.selection ?? selection);
324
+ selectionRef.current = nextSelection;
325
+ if (currentState) {
326
+ onActiveStateChangeRef.current?.(currentState.activeState);
327
+ onHistoryStateChangeRef.current?.(currentState.historyState);
328
+ }
329
+ onSelectionChangeRef.current?.(nextSelection);
330
+ }, [syncStateFromUpdate]);
331
+ const handleFocusChange = (0, react_1.useCallback)((event) => {
332
+ const { isFocused: focused } = event.nativeEvent;
333
+ setIsFocused(focused);
334
+ if (focused) {
335
+ onFocusRef.current?.();
336
+ }
337
+ else {
338
+ onBlurRef.current?.();
339
+ }
340
+ }, []);
341
+ const handleContentHeightChange = (0, react_1.useCallback)((event) => {
342
+ if (heightBehavior !== 'autoGrow')
343
+ return;
344
+ const density = react_native_1.Platform.OS === 'android' ? react_native_1.PixelRatio.get() : 1;
345
+ const nextHeight = Math.ceil(event.nativeEvent.contentHeight / density);
346
+ if (!(nextHeight > 0))
347
+ return;
348
+ setAutoGrowHeight((prev) => (prev === nextHeight ? prev : nextHeight));
349
+ }, [autoGrowHeight, heightBehavior]);
350
+ const restoreSelection = (0, react_1.useCallback)((selection) => {
351
+ if (selection.type === 'text') {
352
+ const { anchor, head } = selection;
353
+ if (anchor == null || head == null) {
354
+ return;
355
+ }
356
+ bridgeRef.current?.setSelection(anchor, head);
357
+ return;
358
+ }
359
+ if (selection.type === 'node') {
360
+ const { pos } = selection;
361
+ if (pos == null) {
362
+ return;
363
+ }
364
+ bridgeRef.current?.setSelection(pos, pos);
365
+ }
366
+ }, []);
367
+ const insertImage = (0, react_1.useCallback)((src, attrs, selection) => {
368
+ const trimmedSrc = src.trim();
369
+ if (!trimmedSrc)
370
+ return;
371
+ if (!allowBase64Images && isImageDataUrl(trimmedSrc)) {
372
+ return;
373
+ }
374
+ runAndApply(() => {
375
+ if (selection) {
376
+ restoreSelection(selection);
377
+ }
378
+ return (bridgeRef.current?.insertContentJson((0, schemas_2.buildImageFragmentJson)({
379
+ src: trimmedSrc,
380
+ ...(attrs ?? {}),
381
+ })) ?? null);
382
+ });
383
+ }, [allowBase64Images, restoreSelection, runAndApply]);
384
+ const openLinkRequest = (0, react_1.useCallback)(() => {
385
+ const requestSelection = selectionRef.current;
386
+ onRequestLink?.({
387
+ href: currentLinkHref,
388
+ isActive: activeState.marks.link === true,
389
+ selection: requestSelection,
390
+ setLink: (href) => {
391
+ const trimmedHref = href.trim();
392
+ if (!trimmedHref)
393
+ return;
394
+ runAndApply(() => {
395
+ restoreSelection(requestSelection);
396
+ return (bridgeRef.current?.setMark('link', {
397
+ href: trimmedHref,
398
+ }) ?? null);
399
+ }, { skipNativeApplyIfContentUnchanged: true });
400
+ },
401
+ unsetLink: () => {
402
+ runAndApply(() => {
403
+ restoreSelection(requestSelection);
404
+ return bridgeRef.current?.unsetMark('link') ?? null;
405
+ }, { skipNativeApplyIfContentUnchanged: true });
406
+ },
407
+ });
408
+ }, [activeState.marks.link, currentLinkHref, onRequestLink, restoreSelection, runAndApply]);
409
+ const openImageRequest = (0, react_1.useCallback)(() => {
410
+ const requestSelection = selectionRef.current;
411
+ onRequestImage?.({
412
+ selection: requestSelection,
413
+ allowBase64: allowBase64Images,
414
+ insertImage: (src, attrs) => insertImage(src, attrs, requestSelection),
415
+ });
416
+ }, [allowBase64Images, insertImage, onRequestImage]);
417
+ const handleToolbarAction = (0, react_1.useCallback)((event) => {
418
+ if (event.nativeEvent.key === LINK_TOOLBAR_ACTION_KEY) {
419
+ openLinkRequest();
420
+ return;
421
+ }
422
+ if (event.nativeEvent.key === IMAGE_TOOLBAR_ACTION_KEY) {
423
+ openImageRequest();
424
+ return;
425
+ }
426
+ onToolbarAction?.(event.nativeEvent.key);
427
+ }, [onToolbarAction, openImageRequest, openLinkRequest]);
428
+ const handleAddonEvent = (0, react_1.useCallback)((event) => {
429
+ let parsed = null;
430
+ try {
431
+ parsed = JSON.parse(event.nativeEvent.eventJson);
432
+ }
433
+ catch {
434
+ return;
435
+ }
436
+ if (!parsed)
437
+ return;
438
+ if (parsed.type === 'mentionsQueryChange') {
439
+ addonsRef.current?.mentions?.onQueryChange?.({
440
+ query: parsed.query,
441
+ trigger: parsed.trigger,
442
+ range: parsed.range,
443
+ isActive: parsed.isActive,
444
+ });
445
+ return;
446
+ }
447
+ if (parsed.type === 'mentionsSelect') {
448
+ const suggestion = mentionSuggestionsByKeyRef.current.get(parsed.suggestionKey);
449
+ if (!suggestion)
450
+ return;
451
+ addonsRef.current?.mentions?.onSelect?.({
452
+ trigger: parsed.trigger,
453
+ suggestion,
454
+ attrs: parsed.attrs,
455
+ });
456
+ }
457
+ }, []);
458
+ (0, react_1.useImperativeHandle)(ref, () => ({
459
+ focus() {
460
+ nativeViewRef.current?.focus?.();
461
+ },
462
+ blur() {
463
+ nativeViewRef.current?.blur?.();
464
+ },
465
+ toggleMark(markType) {
466
+ runAndApply(() => bridgeRef.current?.toggleMark(markType) ?? null, {
467
+ skipNativeApplyIfContentUnchanged: true,
468
+ });
469
+ },
470
+ setLink(href) {
471
+ const trimmedHref = href.trim();
472
+ if (!trimmedHref)
473
+ return;
474
+ runAndApply(() => bridgeRef.current?.setMark('link', { href: trimmedHref }) ?? null, { skipNativeApplyIfContentUnchanged: true });
475
+ },
476
+ unsetLink() {
477
+ runAndApply(() => bridgeRef.current?.unsetMark('link') ?? null, {
478
+ skipNativeApplyIfContentUnchanged: true,
479
+ });
480
+ },
481
+ toggleBlockquote() {
482
+ runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null);
483
+ },
484
+ toggleList(listType) {
485
+ runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null);
486
+ },
487
+ indentListItem() {
488
+ runAndApply(() => bridgeRef.current?.indentListItem() ?? null);
489
+ },
490
+ outdentListItem() {
491
+ runAndApply(() => bridgeRef.current?.outdentListItem() ?? null);
492
+ },
493
+ insertNode(nodeType) {
494
+ runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null);
495
+ },
496
+ insertImage(src, attrs) {
497
+ insertImage(src, attrs);
498
+ },
499
+ insertText(text) {
500
+ runAndApply(() => bridgeRef.current?.replaceSelectionText(text) ?? null);
501
+ },
502
+ insertContentHtml(html) {
503
+ runAndApply(() => bridgeRef.current?.insertContentHtml(html) ?? null);
504
+ },
505
+ insertContentJson(doc) {
506
+ runAndApply(() => bridgeRef.current?.insertContentJson(doc) ?? null);
507
+ },
508
+ setContent(html) {
509
+ runAndApply(() => bridgeRef.current?.replaceHtml(html) ?? null);
510
+ },
511
+ setContentJson(doc) {
512
+ runAndApply(() => bridgeRef.current?.replaceJson(doc) ?? null);
513
+ },
514
+ getContent() {
515
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
516
+ return '';
517
+ return bridgeRef.current.getHtml();
518
+ },
519
+ getContentJson() {
520
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
521
+ return {};
522
+ return bridgeRef.current.getJson();
523
+ },
524
+ getTextContent() {
525
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
526
+ return '';
527
+ return bridgeRef.current.getHtml().replace(/<[^>]+>/g, '');
528
+ },
529
+ undo() {
530
+ runAndApply(() => bridgeRef.current?.undo() ?? null);
531
+ },
532
+ redo() {
533
+ runAndApply(() => bridgeRef.current?.redo() ?? null);
534
+ },
535
+ canUndo() {
536
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
537
+ return false;
538
+ return bridgeRef.current.canUndo();
539
+ },
540
+ canRedo() {
541
+ if (!bridgeRef.current || bridgeRef.current.isDestroyed)
542
+ return false;
543
+ return bridgeRef.current.canRedo();
544
+ },
545
+ }), [insertImage, runAndApply]);
546
+ if (!isReady)
547
+ return null;
548
+ const toolbarItemsForNative = toolbarItems.map((item) => {
549
+ if (item.type === 'link') {
550
+ return {
551
+ type: 'action',
552
+ key: LINK_TOOLBAR_ACTION_KEY,
553
+ label: item.label,
554
+ icon: item.icon,
555
+ isActive: activeState.marks.link === true,
556
+ isDisabled: !editable || !onRequestLink || !activeState.allowedMarks.includes('link'),
557
+ };
558
+ }
559
+ if (item.type === 'image') {
560
+ return {
561
+ type: 'action',
562
+ key: IMAGE_TOOLBAR_ACTION_KEY,
563
+ label: item.label,
564
+ icon: item.icon,
565
+ isActive: false,
566
+ isDisabled: !editable
567
+ || !onRequestImage
568
+ || !activeState.insertableNodes.includes(schemas_2.IMAGE_NODE_NAME),
569
+ };
570
+ }
571
+ return item;
572
+ });
573
+ const themeJson = (0, EditorTheme_1.serializeEditorTheme)(theme);
574
+ const addonsJson = (0, addons_1.serializeEditorAddons)(addons);
575
+ const toolbarItemsJson = JSON.stringify(toolbarItemsForNative);
576
+ const remoteSelectionsJson = serializeRemoteSelections(remoteSelections);
577
+ const usesNativeKeyboardToolbar = toolbarPlacement === 'keyboard' && (react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android');
578
+ const shouldRenderJsToolbar = !usesNativeKeyboardToolbar && showToolbar && editable;
579
+ const inlineToolbarChrome = {
580
+ backgroundColor: theme?.toolbar?.backgroundColor,
581
+ borderColor: theme?.toolbar?.borderColor,
582
+ borderWidth: theme?.toolbar?.borderWidth,
583
+ borderRadius: theme?.toolbar?.borderRadius,
584
+ };
585
+ const containerMinHeight = react_native_1.StyleSheet.flatten(containerStyle)?.minHeight;
586
+ const nativeViewStyleParts = [];
587
+ if (containerMinHeight != null) {
588
+ nativeViewStyleParts.push({ minHeight: containerMinHeight });
589
+ }
590
+ if (style != null) {
591
+ nativeViewStyleParts.push(style);
592
+ }
593
+ if (heightBehavior === 'autoGrow' && autoGrowHeight != null) {
594
+ nativeViewStyleParts.push({ height: autoGrowHeight });
595
+ }
596
+ const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
597
+ const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
598
+ styles.inlineToolbar,
599
+ inlineToolbarChrome.backgroundColor != null
600
+ ? { backgroundColor: inlineToolbarChrome.backgroundColor }
601
+ : null,
602
+ inlineToolbarChrome.borderColor != null
603
+ ? { borderColor: inlineToolbarChrome.borderColor }
604
+ : null,
605
+ inlineToolbarChrome.borderWidth != null
606
+ ? { borderWidth: inlineToolbarChrome.borderWidth }
607
+ : null,
608
+ inlineToolbarChrome.borderRadius != null
609
+ ? { borderRadius: inlineToolbarChrome.borderRadius }
610
+ : null,
611
+ ], onLayout: updateToolbarFrame, children: (0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: false, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
612
+ skipNativeApplyIfContentUnchanged: true,
613
+ }), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
614
+ switch (command) {
615
+ case 'indentList':
616
+ runAndApply(() => bridgeRef.current?.indentListItem() ?? null);
617
+ break;
618
+ case 'outdentList':
619
+ runAndApply(() => bridgeRef.current?.outdentListItem() ?? null);
620
+ break;
621
+ case 'undo':
622
+ runAndApply(() => bridgeRef.current?.undo() ?? null);
623
+ break;
624
+ case 'redo':
625
+ runAndApply(() => bridgeRef.current?.redo() ?? null);
626
+ break;
627
+ }
628
+ }, onRequestLink: openLinkRequest, onRequestImage: openImageRequest, onToolbarAction: onToolbarAction, onToggleBold: () => runAndApply(() => bridgeRef.current?.toggleMark('bold') ?? null, {
629
+ skipNativeApplyIfContentUnchanged: true,
630
+ }), onToggleItalic: () => runAndApply(() => bridgeRef.current?.toggleMark('italic') ?? null, {
631
+ skipNativeApplyIfContentUnchanged: true,
632
+ }), onToggleUnderline: () => runAndApply(() => bridgeRef.current?.toggleMark('underline') ?? null, {
633
+ skipNativeApplyIfContentUnchanged: true,
634
+ }), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
635
+ skipNativeApplyIfContentUnchanged: true,
636
+ }), onToggleBulletList: () => runAndApply(() => bridgeRef.current?.toggleList('bulletList') ?? null), onToggleOrderedList: () => runAndApply(() => bridgeRef.current?.toggleList('orderedList') ?? null), onIndentList: () => runAndApply(() => bridgeRef.current?.indentListItem() ?? null), onOutdentList: () => runAndApply(() => bridgeRef.current?.outdentListItem() ?? null), onInsertHorizontalRule: () => runAndApply(() => bridgeRef.current?.insertNode('horizontalRule') ?? null), onInsertLineBreak: () => runAndApply(() => bridgeRef.current?.insertNode('hardBreak') ?? null), onUndo: () => runAndApply(() => bridgeRef.current?.undo() ?? null), onRedo: () => runAndApply(() => bridgeRef.current?.redo() ?? null) }) }));
637
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsx)(NativeEditorView, { ref: nativeViewRef, style: nativeViewStyle, editorId: editorInstanceId, placeholder: placeholder, editable: editable, autoFocus: autoFocus, showToolbar: showToolbar, toolbarPlacement: toolbarPlacement, heightBehavior: heightBehavior, allowImageResizing: allowImageResizing, themeJson: themeJson, addonsJson: addonsJson, toolbarItemsJson: toolbarItemsJson, remoteSelectionsJson: remoteSelectionsJson, toolbarFrameJson: toolbarPlacement === 'inline' && isFocused ? toolbarFrameJson : undefined, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
638
+ });
639
+ const styles = react_native_1.StyleSheet.create({
640
+ container: {
641
+ position: 'relative',
642
+ },
643
+ inlineToolbar: {
644
+ marginTop: 8,
645
+ borderWidth: react_native_1.StyleSheet.hairlineWidth,
646
+ borderColor: '#E5E5EA',
647
+ overflow: 'hidden',
648
+ },
649
+ });