@apollohg/react-native-prose-editor 0.5.10 → 0.5.12

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.
@@ -1,11 +1,95 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DEFAULT_EDITOR_TOOLBAR_ITEMS = void 0;
4
+ exports.isEditorToolbarFocusPreservationActive = isEditorToolbarFocusPreservationActive;
5
+ exports.useEditorToolbarFrames = useEditorToolbarFrames;
6
+ exports._setEditorToolbarFrameForTests = _setEditorToolbarFrameForTests;
7
+ exports._resetEditorToolbarFrameRegistryForTests = _resetEditorToolbarFrameRegistryForTests;
8
+ exports._beginEditorToolbarInteractionForTests = _beginEditorToolbarInteractionForTests;
9
+ exports._endEditorToolbarInteractionForTests = _endEditorToolbarInteractionForTests;
4
10
  exports.EditorToolbar = EditorToolbar;
5
11
  const jsx_runtime_1 = require("react/jsx-runtime");
6
12
  const vector_icons_1 = require("@expo/vector-icons");
7
13
  const react_1 = require("react");
8
14
  const react_native_1 = require("react-native");
15
+ const editorToolbarFrames = new Map();
16
+ const editorToolbarFrameListeners = new Set();
17
+ let nextEditorToolbarRegistrationId = 1;
18
+ let activeEditorToolbarInteractions = 0;
19
+ let editorToolbarFocusPreserveUntil = 0;
20
+ const EDITOR_TOOLBAR_FOCUS_PRESERVE_MS = 750;
21
+ function areToolbarFramesEqual(left, right) {
22
+ return (left?.x === right?.x &&
23
+ left?.y === right?.y &&
24
+ left?.width === right?.width &&
25
+ left?.height === right?.height);
26
+ }
27
+ function notifyEditorToolbarFrameListeners() {
28
+ editorToolbarFrameListeners.forEach((listener) => listener());
29
+ }
30
+ function getEditorToolbarFramesSnapshot() {
31
+ return Array.from(editorToolbarFrames.values());
32
+ }
33
+ function registerEditorToolbarFrame(id, frame) {
34
+ if (frame == null || frame.width <= 0 || frame.height <= 0) {
35
+ if (editorToolbarFrames.delete(id)) {
36
+ notifyEditorToolbarFrameListeners();
37
+ }
38
+ return;
39
+ }
40
+ const currentFrame = editorToolbarFrames.get(id);
41
+ if (areToolbarFramesEqual(currentFrame, frame)) {
42
+ return;
43
+ }
44
+ editorToolbarFrames.set(id, frame);
45
+ notifyEditorToolbarFrameListeners();
46
+ }
47
+ function unregisterEditorToolbarFrame(id) {
48
+ if (editorToolbarFrames.delete(id)) {
49
+ notifyEditorToolbarFrameListeners();
50
+ }
51
+ }
52
+ function preserveEditorToolbarFocusForNextBlur() {
53
+ editorToolbarFocusPreserveUntil = Date.now() + EDITOR_TOOLBAR_FOCUS_PRESERVE_MS;
54
+ }
55
+ function beginEditorToolbarInteraction() {
56
+ activeEditorToolbarInteractions += 1;
57
+ preserveEditorToolbarFocusForNextBlur();
58
+ }
59
+ function endEditorToolbarInteraction() {
60
+ activeEditorToolbarInteractions = Math.max(0, activeEditorToolbarInteractions - 1);
61
+ preserveEditorToolbarFocusForNextBlur();
62
+ }
63
+ function isEditorToolbarFocusPreservationActive() {
64
+ return activeEditorToolbarInteractions > 0 || Date.now() <= editorToolbarFocusPreserveUntil;
65
+ }
66
+ function useEditorToolbarFrames() {
67
+ const [frames, setFrames] = (0, react_1.useState)(getEditorToolbarFramesSnapshot);
68
+ (0, react_1.useEffect)(() => {
69
+ const listener = () => setFrames(getEditorToolbarFramesSnapshot());
70
+ editorToolbarFrameListeners.add(listener);
71
+ listener();
72
+ return () => {
73
+ editorToolbarFrameListeners.delete(listener);
74
+ };
75
+ }, []);
76
+ return frames;
77
+ }
78
+ function _setEditorToolbarFrameForTests(id, frame) {
79
+ registerEditorToolbarFrame(id, frame);
80
+ }
81
+ function _resetEditorToolbarFrameRegistryForTests() {
82
+ editorToolbarFrames.clear();
83
+ activeEditorToolbarInteractions = 0;
84
+ editorToolbarFocusPreserveUntil = 0;
85
+ notifyEditorToolbarFrameListeners();
86
+ }
87
+ function _beginEditorToolbarInteractionForTests() {
88
+ beginEditorToolbarInteraction();
89
+ }
90
+ function _endEditorToolbarInteractionForTests() {
91
+ endEditorToolbarInteraction();
92
+ }
9
93
  function defaultIcon(id) {
10
94
  return { type: 'default', id };
11
95
  }
@@ -52,6 +136,7 @@ const TOOLBAR_PADDING_H = 12;
52
136
  const TOOLBAR_PADDING_V = 4;
53
137
  const MENU_MARGIN = 8;
54
138
  const MENU_WIDTH = 192;
139
+ const KEYBOARD_FRAME_REMEASURE_DELAYS_MS = [50, 150, 300];
55
140
  const ACTIVE_BG = 'rgba(0, 122, 255, 0.12)';
56
141
  const ACTIVE_COLOR = '#007AFF';
57
142
  const DEFAULT_COLOR = '#666666';
@@ -109,16 +194,24 @@ const DEFAULT_MATERIAL_ICONS = {
109
194
  undo: 'undo',
110
195
  redo: 'redo',
111
196
  };
112
- function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, }) {
197
+ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic, onToggleUnderline, onToggleStrike, onToggleBulletList, onToggleHeading, onToggleBlockquote, onToggleOrderedList, onIndentList, onOutdentList, onInsertHorizontalRule, onInsertLineBreak, onUndo, onRedo, onToggleMark, onToggleListType, onInsertNodeType, onRunCommand, onToolbarAction, onRequestLink, onRequestImage, toolbarItems = exports.DEFAULT_EDITOR_TOOLBAR_ITEMS, theme, showTopBorder, preserveEditorFocus = true, }) {
113
198
  const marks = activeState.marks ?? {};
114
199
  const nodes = activeState.nodes ?? {};
115
200
  const commands = activeState.commands ?? {};
116
201
  const allowedMarks = activeState.allowedMarks ?? [];
117
202
  const insertableNodes = activeState.insertableNodes ?? [];
203
+ const rootRef = (0, react_1.useRef)(null);
118
204
  const groupButtonRefs = (0, react_1.useRef)(new Map());
119
205
  const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
120
206
  const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
121
207
  const [menuState, setMenuState] = (0, react_1.useState)(null);
208
+ const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
209
+ const framePublishAnimationFramesRef = (0, react_1.useRef)([]);
210
+ const framePublishTimeoutsRef = (0, react_1.useRef)([]);
211
+ const registrationIdRef = (0, react_1.useRef)(null);
212
+ if (registrationIdRef.current == null) {
213
+ registrationIdRef.current = nextEditorToolbarRegistrationId++;
214
+ }
122
215
  const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
123
216
  const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
124
217
  const canIndentList = isInList && !!commands['indentList'];
@@ -372,6 +465,89 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
372
465
  };
373
466
  }, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
374
467
  const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
468
+ const publishToolbarFrame = (0, react_1.useCallback)(() => {
469
+ const registrationId = registrationIdRef.current;
470
+ const toolbar = rootRef.current;
471
+ if (!preserveEditorFocus || registrationId == null || !toolbar) {
472
+ if (registrationId != null) {
473
+ unregisterEditorToolbarFrame(registrationId);
474
+ }
475
+ return;
476
+ }
477
+ if (typeof toolbar.measureInWindow !== 'function') {
478
+ return;
479
+ }
480
+ toolbar.measureInWindow((x, y, width, height) => {
481
+ registerEditorToolbarFrame(registrationId, { x, y, width, height });
482
+ });
483
+ }, [preserveEditorFocus]);
484
+ const cancelScheduledFramePublishes = (0, react_1.useCallback)(() => {
485
+ framePublishAnimationFramesRef.current.forEach((frame) => cancelAnimationFrame(frame));
486
+ framePublishAnimationFramesRef.current = [];
487
+ framePublishTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
488
+ framePublishTimeoutsRef.current = [];
489
+ }, []);
490
+ const scheduleToolbarFramePublish = (0, react_1.useCallback)(() => {
491
+ if (!preserveEditorFocus) {
492
+ return;
493
+ }
494
+ cancelScheduledFramePublishes();
495
+ publishToolbarFrame();
496
+ framePublishAnimationFramesRef.current.push(requestAnimationFrame(publishToolbarFrame));
497
+ KEYBOARD_FRAME_REMEASURE_DELAYS_MS.forEach((delay) => {
498
+ framePublishTimeoutsRef.current.push(setTimeout(publishToolbarFrame, delay));
499
+ });
500
+ }, [cancelScheduledFramePublishes, preserveEditorFocus, publishToolbarFrame]);
501
+ const handleToolbarLayout = (0, react_1.useCallback)(() => {
502
+ requestAnimationFrame(publishToolbarFrame);
503
+ }, [publishToolbarFrame]);
504
+ (0, react_1.useEffect)(() => {
505
+ if (!preserveEditorFocus) {
506
+ const registrationId = registrationIdRef.current;
507
+ if (registrationId != null) {
508
+ unregisterEditorToolbarFrame(registrationId);
509
+ }
510
+ return;
511
+ }
512
+ const frame = requestAnimationFrame(publishToolbarFrame);
513
+ return () => cancelAnimationFrame(frame);
514
+ }, [
515
+ expandedGroupKey,
516
+ menuState?.groupKey,
517
+ preserveEditorFocus,
518
+ publishToolbarFrame,
519
+ renderedItems.length,
520
+ windowHeight,
521
+ windowWidth,
522
+ ]);
523
+ (0, react_1.useEffect)(() => {
524
+ const registrationId = registrationIdRef.current;
525
+ return () => {
526
+ cancelScheduledFramePublishes();
527
+ if (toolbarInteractionActiveRef.current) {
528
+ toolbarInteractionActiveRef.current = false;
529
+ endEditorToolbarInteraction();
530
+ }
531
+ if (registrationId != null) {
532
+ unregisterEditorToolbarFrame(registrationId);
533
+ }
534
+ };
535
+ }, [cancelScheduledFramePublishes]);
536
+ (0, react_1.useEffect)(() => {
537
+ if (!preserveEditorFocus) {
538
+ cancelScheduledFramePublishes();
539
+ return;
540
+ }
541
+ const subscriptions = [
542
+ react_native_1.Keyboard.addListener('keyboardDidShow', scheduleToolbarFramePublish),
543
+ react_native_1.Keyboard.addListener('keyboardDidHide', scheduleToolbarFramePublish),
544
+ react_native_1.Keyboard.addListener('keyboardDidChangeFrame', scheduleToolbarFramePublish),
545
+ ];
546
+ return () => {
547
+ subscriptions.forEach((subscription) => subscription.remove());
548
+ cancelScheduledFramePublishes();
549
+ };
550
+ }, [cancelScheduledFramePublishes, preserveEditorFocus, scheduleToolbarFramePublish]);
375
551
  (0, react_1.useEffect)(() => {
376
552
  if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
377
553
  setExpandedGroupKey(null);
@@ -389,6 +565,18 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
389
565
  }
390
566
  setMenuState(null);
391
567
  }, []);
568
+ const handleToolbarPressIn = (0, react_1.useCallback)(() => {
569
+ if (preserveEditorFocus && !toolbarInteractionActiveRef.current) {
570
+ toolbarInteractionActiveRef.current = true;
571
+ beginEditorToolbarInteraction();
572
+ }
573
+ }, [preserveEditorFocus]);
574
+ const handleToolbarPressOut = (0, react_1.useCallback)(() => {
575
+ if (preserveEditorFocus && toolbarInteractionActiveRef.current) {
576
+ toolbarInteractionActiveRef.current = false;
577
+ endEditorToolbarInteraction();
578
+ }
579
+ }, [preserveEditorFocus]);
392
580
  const handleGroupPress = (0, react_1.useCallback)((group) => {
393
581
  if (group.isDisabled) {
394
582
  return;
@@ -442,7 +630,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
442
630
  else {
443
631
  groupButtonRefs.current.delete(anchorGroupKey);
444
632
  }
445
- }, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onPress, disabled: button.isDisabled, style: [
633
+ }, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: onPress, disabled: button.isDisabled, style: [
446
634
  styles.button,
447
635
  {
448
636
  borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
@@ -460,7 +648,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
460
648
  styles.separator,
461
649
  theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
462
650
  ] }, key));
463
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
651
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: rootRef, collapsable: false, onLayout: handleToolbarLayout, style: [
464
652
  styles.container,
465
653
  !resolvedShowTopBorder && styles.containerWithoutTopBorder,
466
654
  theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
@@ -512,7 +700,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
512
700
  : button.isDisabled
513
701
  ? disabledColor
514
702
  : defaultColor;
515
- return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
703
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
516
704
  styles.menuItem,
517
705
  button.isActive && {
518
706
  backgroundColor: theme?.buttonActiveBackgroundColor ?? ACTIVE_BG,
@@ -135,6 +135,8 @@ export interface NativeRichTextEditorRef {
135
135
  getContentJson(): DocumentJSON;
136
136
  /** Get the plain text content (no markup). */
137
137
  getTextContent(): string;
138
+ /** Get the current caret rectangle in editor-local layout coordinates. */
139
+ getCaretRect(): Promise<NativeRichTextEditorCaretRect | null>;
138
140
  /** Undo the last operation. */
139
141
  undo(): void;
140
142
  /** Redo the last undone operation. */
@@ -144,4 +146,18 @@ export interface NativeRichTextEditorRef {
144
146
  /** Check if redo is available. */
145
147
  canRedo(): boolean;
146
148
  }
149
+ export interface NativeRichTextEditorCaretRect {
150
+ /** Left edge of the caret, relative to the editor root view. */
151
+ x: number;
152
+ /** Top edge of the caret, relative to the editor root view. */
153
+ y: number;
154
+ /** Caret width. */
155
+ width: number;
156
+ /** Caret height. */
157
+ height: number;
158
+ /** Current editor root view width. */
159
+ editorWidth: number;
160
+ /** Current editor root view height. */
161
+ editorHeight: number;
162
+ }
147
163
  export declare const NativeRichTextEditor: React.ForwardRefExoticComponent<NativeRichTextEditorProps & React.RefAttributes<NativeRichTextEditorRef>>;
@@ -423,6 +423,44 @@ function serializeRemoteSelections(remoteSelections) {
423
423
  }
424
424
  return stringifyCachedJson(remoteSelections);
425
425
  }
426
+ function areToolbarFramesEqual(left, right) {
427
+ return (left?.x === right?.x &&
428
+ left?.y === right?.y &&
429
+ left?.width === right?.width &&
430
+ left?.height === right?.height);
431
+ }
432
+ function serializeToolbarFrames(frames) {
433
+ if (!frames || frames.length === 0) {
434
+ return undefined;
435
+ }
436
+ return JSON.stringify(frames.length === 1 ? frames[0] : { frames });
437
+ }
438
+ function parseCaretRectJson(raw) {
439
+ if (!raw) {
440
+ return null;
441
+ }
442
+ try {
443
+ const parsed = JSON.parse(raw);
444
+ const x = typeof parsed.x === 'number' ? parsed.x : null;
445
+ const y = typeof parsed.y === 'number' ? parsed.y : null;
446
+ const width = typeof parsed.width === 'number' ? parsed.width : null;
447
+ const height = typeof parsed.height === 'number' ? parsed.height : null;
448
+ const editorWidth = typeof parsed.editorWidth === 'number' ? parsed.editorWidth : null;
449
+ const editorHeight = typeof parsed.editorHeight === 'number' ? parsed.editorHeight : null;
450
+ if (x == null ||
451
+ y == null ||
452
+ width == null ||
453
+ height == null ||
454
+ editorWidth == null ||
455
+ editorHeight == null) {
456
+ return null;
457
+ }
458
+ return { x, y, width, height, editorWidth, editorHeight };
459
+ }
460
+ catch {
461
+ return null;
462
+ }
463
+ }
426
464
  const serializedJsonCache = new WeakMap();
427
465
  function stringifyCachedJson(value) {
428
466
  if (value != null && typeof value === 'object') {
@@ -463,7 +501,9 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
463
501
  const [isReady, setIsReady] = (0, react_1.useState)(false);
464
502
  const [editorInstanceId, setEditorInstanceId] = (0, react_1.useState)(0);
465
503
  const [isFocused, setIsFocused] = (0, react_1.useState)(false);
466
- const [toolbarFrameJson, setToolbarFrameJson] = (0, react_1.useState)(undefined);
504
+ const isFocusedRef = (0, react_1.useRef)(false);
505
+ const [inlineToolbarFrame, setInlineToolbarFrame] = (0, react_1.useState)(null);
506
+ const registeredToolbarFrames = (0, EditorToolbar_1.useEditorToolbarFrames)();
467
507
  const [pendingNativeUpdate, setPendingNativeUpdate] = (0, react_1.useState)({
468
508
  json: undefined,
469
509
  revision: 0,
@@ -745,28 +785,28 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
745
785
  const updateToolbarFrame = (0, react_1.useCallback)(() => {
746
786
  const toolbar = toolbarRef.current;
747
787
  if (!toolbar) {
748
- setToolbarFrameJson(undefined);
788
+ setInlineToolbarFrame(null);
749
789
  return;
750
790
  }
751
791
  toolbar.measureInWindow((x, y, width, height) => {
752
792
  if (width <= 0 || height <= 0) {
753
- setToolbarFrameJson(undefined);
793
+ setInlineToolbarFrame(null);
754
794
  return;
755
795
  }
756
- const nextJson = JSON.stringify({ x, y, width, height });
757
- setToolbarFrameJson((prev) => (prev === nextJson ? prev : nextJson));
796
+ const nextFrame = { x, y, width, height };
797
+ setInlineToolbarFrame((prev) => areToolbarFramesEqual(prev, nextFrame) ? prev : nextFrame);
758
798
  });
759
799
  }, []);
760
800
  (0, react_1.useEffect)(() => {
761
- if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
762
- setToolbarFrameJson(undefined);
801
+ if (!(showToolbar && toolbarPlacement === 'inline' && editable)) {
802
+ setInlineToolbarFrame(null);
763
803
  return;
764
804
  }
765
805
  const frame = requestAnimationFrame(() => {
766
806
  updateToolbarFrame();
767
807
  });
768
808
  return () => cancelAnimationFrame(frame);
769
- }, [editable, isFocused, showToolbar, toolbarPlacement, updateToolbarFrame]);
809
+ }, [editable, showToolbar, toolbarPlacement, updateToolbarFrame]);
770
810
  (0, react_1.useEffect)(() => {
771
811
  if (heightBehavior !== 'autoGrow') {
772
812
  setAutoGrowHeight(null);
@@ -836,19 +876,40 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
836
876
  }
837
877
  onSelectionChangeRef.current?.(nextSelection);
838
878
  }, [syncSelectionStateFromUpdate]);
879
+ const refocusAfterToolbarInteraction = (0, react_1.useCallback)(() => {
880
+ nativeViewRef.current?.focus?.();
881
+ requestAnimationFrame(() => {
882
+ nativeViewRef.current?.focus?.();
883
+ });
884
+ setTimeout(() => {
885
+ nativeViewRef.current?.focus?.();
886
+ }, 50);
887
+ }, []);
839
888
  const handleFocusChange = (0, react_1.useCallback)((event) => {
840
889
  const { isFocused: focused } = event.nativeEvent;
890
+ if (!focused &&
891
+ editable &&
892
+ isFocusedRef.current &&
893
+ (0, EditorToolbar_1.isEditorToolbarFocusPreservationActive)()) {
894
+ setIsFocused(true);
895
+ refocusAfterToolbarInteraction();
896
+ return;
897
+ }
898
+ const wasFocused = isFocusedRef.current;
899
+ isFocusedRef.current = focused;
841
900
  setIsFocused(focused);
842
901
  if (!focused) {
843
902
  setMentionQueryEvent(null);
844
903
  }
845
904
  if (focused) {
846
- onFocusRef.current?.();
905
+ if (!wasFocused) {
906
+ onFocusRef.current?.();
907
+ }
847
908
  }
848
- else {
909
+ else if (wasFocused) {
849
910
  onBlurRef.current?.();
850
911
  }
851
- }, []);
912
+ }, [editable, refocusAfterToolbarInteraction]);
852
913
  (0, react_1.useEffect)(() => {
853
914
  if (addons?.mentions != null) {
854
915
  return;
@@ -1130,6 +1191,13 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1130
1191
  return '';
1131
1192
  return bridgeRef.current.getHtml().replace(/<[^>]+>/g, '');
1132
1193
  },
1194
+ async getCaretRect() {
1195
+ const nativeView = nativeViewRef.current;
1196
+ if (!nativeView?.getCaretRect)
1197
+ return null;
1198
+ const raw = await Promise.resolve(nativeView.getCaretRect());
1199
+ return parseCaretRectJson(raw);
1200
+ },
1133
1201
  undo() {
1134
1202
  runAndApply(() => bridgeRef.current?.undo() ?? null);
1135
1203
  },
@@ -1219,6 +1287,14 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1219
1287
  nativeViewStyleParts.push({ height: autoGrowHeight });
1220
1288
  }
1221
1289
  const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
1290
+ const toolbarFrameJson = serializeToolbarFrames(editable
1291
+ ? [
1292
+ ...(toolbarPlacement === 'inline' && inlineToolbarFrame != null
1293
+ ? [inlineToolbarFrame]
1294
+ : []),
1295
+ ...registeredToolbarFrames,
1296
+ ]
1297
+ : undefined);
1222
1298
  const jsToolbar = ((0, jsx_runtime_1.jsx)(react_native_1.View, { ref: toolbarRef, testID: 'native-editor-js-toolbar', style: [
1223
1299
  styles.inlineToolbar,
1224
1300
  { marginTop: inlineToolbarMarginTop },
@@ -1270,7 +1346,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1270
1346
  '#8E8E93',
1271
1347
  },
1272
1348
  ], children: suggestion.subtitle })) : null] })) }, suggestion.key));
1273
- }) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
1349
+ }) }) })) : ((0, jsx_runtime_1.jsx)(EditorToolbar_1.EditorToolbar, { activeState: activeState, historyState: historyState, toolbarItems: toolbarItems, theme: theme?.toolbar, showTopBorder: inlineToolbarShowTopBorder, preserveEditorFocus: false, onToggleMark: (mark) => runAndApply(() => bridgeRef.current?.toggleMark(mark) ?? null, {
1274
1350
  skipNativeApplyIfContentUnchanged: true,
1275
1351
  }), onToggleListType: (listType) => runAndApply(() => bridgeRef.current?.toggleList(listType) ?? null), onToggleHeading: (level) => runAndApply(() => bridgeRef.current?.toggleHeading(level) ?? null), onToggleBlockquote: () => runAndApply(() => bridgeRef.current?.toggleBlockquote() ?? null), onInsertNodeType: (nodeType) => runAndApply(() => bridgeRef.current?.insertNode(nodeType) ?? null), onRunCommand: (command) => {
1276
1352
  switch (command) {
@@ -1296,7 +1372,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1296
1372
  }), onToggleStrike: () => runAndApply(() => bridgeRef.current?.toggleMark('strike') ?? null, {
1297
1373
  skipNativeApplyIfContentUnchanged: true,
1298
1374
  }), 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) })) }));
1299
- 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] }));
1375
+ 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: toolbarFrameJson, editorUpdateJson: pendingNativeUpdate.json, editorUpdateRevision: pendingNativeUpdate.revision, onEditorUpdate: handleUpdate, onSelectionChange: handleSelectionChange, onFocusChange: handleFocusChange, onContentHeightChange: handleContentHeightChange, onToolbarAction: handleToolbarAction, onAddonEvent: handleAddonEvent }, DEV_NATIVE_VIEW_KEY), shouldRenderJsToolbar && jsToolbar] }));
1300
1376
  });
1301
1377
  const styles = react_native_1.StyleSheet.create({
1302
1378
  container: {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
1
+ export { NativeRichTextEditor, type NativeRichTextEditorProps, type NativeRichTextEditorRef, type NativeRichTextEditorCaretRect, type NativeRichTextEditorHeightBehavior, type NativeRichTextEditorToolbarPlacement, type RemoteSelectionDecoration, type LinkRequestContext, type ImageRequestContext, } from './NativeRichTextEditor';
2
2
  export { NativeProseViewer, type NativeProseViewerProps, type NativeProseViewerAddons, type NativeProseViewerMentionsAddonConfig, type NativeProseViewerMentionPrefix, type NativeProseViewerLinkPressEvent, type NativeProseViewerMentionRenderContext, type NativeProseViewerMentionPressEvent, } from './NativeProseViewer';
3
3
  export { EditorToolbar, DEFAULT_EDITOR_TOOLBAR_ITEMS, type EditorToolbarProps, type EditorToolbarItem, type EditorToolbarLeafItem, type EditorToolbarGroupChildItem, type EditorToolbarGroupItem, type EditorToolbarGroupPresentation, type EditorToolbarIcon, type EditorToolbarDefaultIconId, type EditorToolbarSFSymbolIcon, type EditorToolbarMaterialIcon, type EditorToolbarCommand, type EditorToolbarHeadingLevel, type EditorToolbarListType, } from './EditorToolbar';
4
4
  export type { EditorContentInsets, EditorTheme, EditorTextStyle, EditorLinkTheme, EditorHeadingTheme, EditorListTheme, EditorHorizontalRuleTheme, EditorMentionTheme, EditorToolbarTheme, EditorToolbarAppearance, EditorFontStyle, EditorFontWeight, } from './EditorTheme';
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>libeditor_core.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64_x86_64-simulator</string>
11
+ <string>ios-arm64</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>libeditor_core.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
- <string>x86_64</string>
18
17
  </array>
19
18
  <key>SupportedPlatform</key>
20
19
  <string>ios</string>
21
- <key>SupportedPlatformVariant</key>
22
- <string>simulator</string>
23
20
  </dict>
24
21
  <dict>
25
22
  <key>BinaryPath</key>
26
23
  <string>libeditor_core.a</string>
27
24
  <key>LibraryIdentifier</key>
28
- <string>ios-arm64</string>
25
+ <string>ios-arm64_x86_64-simulator</string>
29
26
  <key>LibraryPath</key>
30
27
  <string>libeditor_core.a</string>
31
28
  <key>SupportedArchitectures</key>
32
29
  <array>
33
30
  <string>arm64</string>
31
+ <string>x86_64</string>
34
32
  </array>
35
33
  <key>SupportedPlatform</key>
36
34
  <string>ios</string>
35
+ <key>SupportedPlatformVariant</key>
36
+ <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
@@ -1626,7 +1626,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1626
1626
  inputViewStyle: .keyboard
1627
1627
  )
1628
1628
  private let accessoryPlaceholder = EditorAccessoryPlaceholderView(frame: .zero)
1629
- private var toolbarFrameInWindow: CGRect?
1629
+ private var toolbarFramesInWindow: [CGRect] = []
1630
+ private var lastToolbarTouchUptime: TimeInterval = -Double.infinity
1630
1631
  private var didApplyAutoFocus = false
1631
1632
  private var toolbarState = NativeToolbarState.empty
1632
1633
  private var toolbarItems: [NativeToolbarItem] = NativeToolbarItem.defaults
@@ -1855,17 +1856,32 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1855
1856
  lastToolbarFrameJSON = toolbarFrameJson
1856
1857
  guard let toolbarFrameJson,
1857
1858
  let data = toolbarFrameJson.data(using: .utf8),
1858
- let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
1859
- let x = (raw["x"] as? NSNumber)?.doubleValue,
1859
+ let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
1860
+ else {
1861
+ toolbarFramesInWindow = []
1862
+ return
1863
+ }
1864
+
1865
+ if let frameDictionaries = raw["frames"] as? [[String: Any]] {
1866
+ toolbarFramesInWindow = frameDictionaries.compactMap(Self.toolbarFrame(from:))
1867
+ return
1868
+ }
1869
+
1870
+ toolbarFramesInWindow = Self.toolbarFrame(from: raw).map { [$0] } ?? []
1871
+ }
1872
+
1873
+ private static func toolbarFrame(from raw: [String: Any]) -> CGRect? {
1874
+ guard let x = (raw["x"] as? NSNumber)?.doubleValue,
1860
1875
  let y = (raw["y"] as? NSNumber)?.doubleValue,
1861
1876
  let width = (raw["width"] as? NSNumber)?.doubleValue,
1862
- let height = (raw["height"] as? NSNumber)?.doubleValue
1877
+ let height = (raw["height"] as? NSNumber)?.doubleValue,
1878
+ width > 0,
1879
+ height > 0
1863
1880
  else {
1864
- toolbarFrameInWindow = nil
1865
- return
1881
+ return nil
1866
1882
  }
1867
1883
 
1868
- toolbarFrameInWindow = CGRect(x: x, y: y, width: width, height: height)
1884
+ return CGRect(x: x, y: y, width: width, height: height)
1869
1885
  }
1870
1886
 
1871
1887
  func setPendingEditorUpdateJson(_ editorUpdateJson: String?) {
@@ -1904,6 +1920,30 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1904
1920
  richTextView.textView.resignFirstResponder()
1905
1921
  }
1906
1922
 
1923
+ func getCaretRectJson() -> String? {
1924
+ layoutIfNeeded()
1925
+ richTextView.layoutIfNeeded()
1926
+
1927
+ guard let caretRect = richTextView.currentCaretRect() else {
1928
+ return nil
1929
+ }
1930
+ let editorRect = richTextView.convert(caretRect, to: self)
1931
+ let payload: [String: Any] = [
1932
+ "x": editorRect.minX,
1933
+ "y": editorRect.minY,
1934
+ "width": editorRect.width,
1935
+ "height": editorRect.height,
1936
+ "editorWidth": bounds.width,
1937
+ "editorHeight": bounds.height,
1938
+ ]
1939
+ guard let data = try? JSONSerialization.data(withJSONObject: payload),
1940
+ let json = String(data: data, encoding: .utf8)
1941
+ else {
1942
+ return nil
1943
+ }
1944
+ return json
1945
+ }
1946
+
1907
1947
  // MARK: - Focus Notifications
1908
1948
 
1909
1949
  @objc private func textViewDidBeginEditing(_ notification: Notification) {
@@ -1914,6 +1954,13 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1914
1954
  }
1915
1955
 
1916
1956
  @objc private func textViewDidEndEditing(_ notification: Notification) {
1957
+ if shouldPreserveFocusAfterToolbarTouch() {
1958
+ DispatchQueue.main.async { [weak self] in
1959
+ self?.richTextView.textView.becomeFirstResponder()
1960
+ }
1961
+ return
1962
+ }
1963
+
1917
1964
  uninstallOutsideTapRecognizer()
1918
1965
  richTextView.textView.refreshSelectionVisualState()
1919
1966
  clearMentionQueryStateAndHidePopover()
@@ -1952,6 +1999,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1952
1999
  guard gestureRecognizer === outsideTapGestureRecognizer else { return true }
1953
2000
  guard let tapWindow = gestureWindow ?? window else { return true }
1954
2001
  let locationInWindow = touch.location(in: tapWindow)
2002
+ if isLocationInStandaloneToolbarFrame(locationInWindow) {
2003
+ markRecentToolbarTouch()
2004
+ }
1955
2005
  let result = shouldHandleOutsideTap(
1956
2006
  locationInWindow: locationInWindow,
1957
2007
  touchedView: touch.view
@@ -1959,6 +2009,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1959
2009
  return result
1960
2010
  }
1961
2011
 
2012
+ private func markRecentToolbarTouch() {
2013
+ lastToolbarTouchUptime = ProcessInfo.processInfo.systemUptime
2014
+ }
2015
+
2016
+ private func shouldPreserveFocusAfterToolbarTouch() -> Bool {
2017
+ ProcessInfo.processInfo.systemUptime - lastToolbarTouchUptime <= 0.75
2018
+ }
2019
+
2020
+ private func isLocationInStandaloneToolbarFrame(_ locationInWindow: CGPoint) -> Bool {
2021
+ toolbarFramesInWindow.contains(where: { $0.contains(locationInWindow) })
2022
+ }
2023
+
1962
2024
  private func shouldHandleOutsideTap(
1963
2025
  locationInWindow: CGPoint,
1964
2026
  touchedView: UIView?
@@ -1975,7 +2037,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1975
2037
  if let touchedView, touchedView.isDescendant(of: accessoryToolbar) {
1976
2038
  return false
1977
2039
  }
1978
- if let toolbarFrameInWindow, toolbarFrameInWindow.contains(locationInWindow) {
2040
+ if isLocationInStandaloneToolbarFrame(locationInWindow) {
1979
2041
  return false
1980
2042
  }
1981
2043
  return true
@@ -386,6 +386,9 @@ public class NativeEditorModule: Module {
386
386
  AsyncFunction("blur") { (view: NativeEditorExpoView) in
387
387
  view.blur()
388
388
  }
389
+ AsyncFunction("getCaretRect") { (view: NativeEditorExpoView) -> String? in
390
+ view.getCaretRectJson()
391
+ }
389
392
  }
390
393
 
391
394
  View(NativeProseViewerExpoView.self) {