@apollohg/react-native-prose-editor 0.5.9 → 0.5.11

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
  }
@@ -109,16 +193,22 @@ const DEFAULT_MATERIAL_ICONS = {
109
193
  undo: 'undo',
110
194
  redo: 'redo',
111
195
  };
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, }) {
196
+ 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
197
  const marks = activeState.marks ?? {};
114
198
  const nodes = activeState.nodes ?? {};
115
199
  const commands = activeState.commands ?? {};
116
200
  const allowedMarks = activeState.allowedMarks ?? [];
117
201
  const insertableNodes = activeState.insertableNodes ?? [];
202
+ const rootRef = (0, react_1.useRef)(null);
118
203
  const groupButtonRefs = (0, react_1.useRef)(new Map());
119
204
  const { width: windowWidth, height: windowHeight } = (0, react_native_1.useWindowDimensions)();
120
205
  const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
121
206
  const [menuState, setMenuState] = (0, react_1.useState)(null);
207
+ const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
208
+ const registrationIdRef = (0, react_1.useRef)(null);
209
+ if (registrationIdRef.current == null) {
210
+ registrationIdRef.current = nextEditorToolbarRegistrationId++;
211
+ }
122
212
  const isMarkActive = (0, react_1.useCallback)((mark) => !!marks[mark], [marks]);
123
213
  const isInList = !!nodes['bulletList'] || !!nodes['orderedList'];
124
214
  const canIndentList = isInList && !!commands['indentList'];
@@ -372,6 +462,56 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
372
462
  };
373
463
  }, [expandedGroupKey, menuState?.groupKey, resolveButton, toolbarItems]);
374
464
  const resolvedShowTopBorder = showTopBorder ?? theme?.showTopBorder ?? true;
465
+ const publishToolbarFrame = (0, react_1.useCallback)(() => {
466
+ const registrationId = registrationIdRef.current;
467
+ const toolbar = rootRef.current;
468
+ if (!preserveEditorFocus || registrationId == null || !toolbar) {
469
+ if (registrationId != null) {
470
+ unregisterEditorToolbarFrame(registrationId);
471
+ }
472
+ return;
473
+ }
474
+ if (typeof toolbar.measureInWindow !== 'function') {
475
+ return;
476
+ }
477
+ toolbar.measureInWindow((x, y, width, height) => {
478
+ registerEditorToolbarFrame(registrationId, { x, y, width, height });
479
+ });
480
+ }, [preserveEditorFocus]);
481
+ const handleToolbarLayout = (0, react_1.useCallback)(() => {
482
+ requestAnimationFrame(publishToolbarFrame);
483
+ }, [publishToolbarFrame]);
484
+ (0, react_1.useEffect)(() => {
485
+ if (!preserveEditorFocus) {
486
+ const registrationId = registrationIdRef.current;
487
+ if (registrationId != null) {
488
+ unregisterEditorToolbarFrame(registrationId);
489
+ }
490
+ return;
491
+ }
492
+ const frame = requestAnimationFrame(publishToolbarFrame);
493
+ return () => cancelAnimationFrame(frame);
494
+ }, [
495
+ expandedGroupKey,
496
+ menuState?.groupKey,
497
+ preserveEditorFocus,
498
+ publishToolbarFrame,
499
+ renderedItems.length,
500
+ windowHeight,
501
+ windowWidth,
502
+ ]);
503
+ (0, react_1.useEffect)(() => {
504
+ const registrationId = registrationIdRef.current;
505
+ return () => {
506
+ if (toolbarInteractionActiveRef.current) {
507
+ toolbarInteractionActiveRef.current = false;
508
+ endEditorToolbarInteraction();
509
+ }
510
+ if (registrationId != null) {
511
+ unregisterEditorToolbarFrame(registrationId);
512
+ }
513
+ };
514
+ }, []);
375
515
  (0, react_1.useEffect)(() => {
376
516
  if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
377
517
  setExpandedGroupKey(null);
@@ -389,6 +529,18 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
389
529
  }
390
530
  setMenuState(null);
391
531
  }, []);
532
+ const handleToolbarPressIn = (0, react_1.useCallback)(() => {
533
+ if (preserveEditorFocus && !toolbarInteractionActiveRef.current) {
534
+ toolbarInteractionActiveRef.current = true;
535
+ beginEditorToolbarInteraction();
536
+ }
537
+ }, [preserveEditorFocus]);
538
+ const handleToolbarPressOut = (0, react_1.useCallback)(() => {
539
+ if (preserveEditorFocus && toolbarInteractionActiveRef.current) {
540
+ toolbarInteractionActiveRef.current = false;
541
+ endEditorToolbarInteraction();
542
+ }
543
+ }, [preserveEditorFocus]);
392
544
  const handleGroupPress = (0, react_1.useCallback)((group) => {
393
545
  if (group.isDisabled) {
394
546
  return;
@@ -442,7 +594,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
442
594
  else {
443
595
  groupButtonRefs.current.delete(anchorGroupKey);
444
596
  }
445
- }, collapsable: false, style: styles.buttonAnchor, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: onPress, disabled: button.isDisabled, style: [
597
+ }, 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
598
  styles.button,
447
599
  {
448
600
  borderRadius: theme?.buttonBorderRadius ?? BUTTON_RADIUS,
@@ -460,7 +612,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
460
612
  styles.separator,
461
613
  theme?.separatorColor != null ? { backgroundColor: theme.separatorColor } : null,
462
614
  ] }, key));
463
- return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [
615
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { ref: rootRef, collapsable: false, onLayout: handleToolbarLayout, style: [
464
616
  styles.container,
465
617
  !resolvedShowTopBorder && styles.containerWithoutTopBorder,
466
618
  theme?.backgroundColor != null ? { backgroundColor: theme.backgroundColor } : null,
@@ -512,7 +664,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
512
664
  : button.isDisabled
513
665
  ? disabledColor
514
666
  : defaultColor;
515
- return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
667
+ return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPressIn: handleToolbarPressIn, onPressOut: handleToolbarPressOut, onPress: () => handleButtonPress(button), disabled: button.isDisabled, style: ({ pressed }) => [
516
668
  styles.menuItem,
517
669
  button.isActive && {
518
670
  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,21 +785,21 @@ 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
801
  if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
762
- setToolbarFrameJson(undefined);
802
+ setInlineToolbarFrame(null);
763
803
  return;
764
804
  }
765
805
  const frame = requestAnimationFrame(() => {
@@ -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(isFocused && 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';
@@ -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) {