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

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.
@@ -27,6 +27,7 @@ android {
27
27
  versionCode 1
28
28
  versionName packageJson.version
29
29
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30
+ consumerProguardFiles "consumer-rules.pro"
30
31
  }
31
32
 
32
33
  // Include prebuilt Rust .so files from the package's rust/android/ directory
@@ -0,0 +1,8 @@
1
+ # JNA includes desktop AWT helpers that are not available on Android. The editor
2
+ # bindings do not call those APIs, but R8 still validates the references.
3
+ -dontwarn java.awt.**
4
+
5
+ # UniFFI uses JNA reflection/proxies to bind Kotlin method and structure field
6
+ # names to the Rust library. Keep these names stable in consuming release builds.
7
+ -keep class com.sun.jna.** { *; }
8
+ -keep class uniffi.editor_core.** { *; }
@@ -669,7 +669,7 @@ class EditorEditText @JvmOverloads constructor(
669
669
  cursor > 0 &&
670
670
  currentText.getOrNull(cursor - 1) == EMPTY_BLOCK_PLACEHOLDER
671
671
  ) {
672
- val scalarCursor = PositionBridge.utf16ToScalar(cursor - 1, currentText)
672
+ val scalarCursor = PositionBridge.utf16ToScalar(cursor, currentText)
673
673
  deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
674
674
  return
675
675
  }
@@ -722,7 +722,7 @@ class EditorEditText @JvmOverloads constructor(
722
722
  deleteRangeInRust(scalarStart, scalarEnd)
723
723
  } else if (start > 0) {
724
724
  if (currentText.getOrNull(start - 1) == EMPTY_BLOCK_PLACEHOLDER) {
725
- val scalarCursor = PositionBridge.utf16ToScalar(start - 1, currentText)
725
+ val scalarCursor = PositionBridge.utf16ToScalar(start, currentText)
726
726
  deleteBackwardAtSelectionScalarInRust(scalarCursor, scalarCursor)
727
727
  return
728
728
  }
@@ -64,6 +64,8 @@ class NativeEditorExpoView(
64
64
  private var previousWindowCallback: Window.Callback? = null
65
65
  private var toolbarFramesInWindow: List<RectF> = emptyList()
66
66
  private var lastToolbarTouchUptimeMs: Long? = null
67
+ private var pendingOutsideTapBlur: Runnable? = null
68
+ private var pendingKeyboardDismiss: Runnable? = null
67
69
  private var addons = NativeEditorAddons(null)
68
70
  private var mentionQueryState: MentionQueryState? = null
69
71
  private var lastMentionEventJson: String? = null
@@ -266,6 +268,8 @@ class NativeEditorExpoView(
266
268
  }
267
269
 
268
270
  fun focus() {
271
+ cancelPendingOutsideTapBlur()
272
+ cancelPendingKeyboardDismiss()
269
273
  richTextView.editorEditText.requestFocus()
270
274
  richTextView.editorEditText.post {
271
275
  val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
@@ -274,12 +278,55 @@ class NativeEditorExpoView(
274
278
  }
275
279
 
276
280
  fun blur() {
281
+ cancelPendingOutsideTapBlur()
282
+ cancelPendingKeyboardDismiss()
277
283
  clearRecentToolbarTouch()
278
284
  richTextView.editorEditText.clearFocus()
279
285
  val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
280
286
  imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
281
287
  }
282
288
 
289
+ private fun blurWithDeferredKeyboardDismiss() {
290
+ cancelPendingKeyboardDismiss()
291
+ clearRecentToolbarTouch()
292
+ richTextView.editorEditText.clearFocus()
293
+ val dismiss = Runnable {
294
+ pendingKeyboardDismiss = null
295
+ if (!richTextView.editorEditText.hasFocus()) {
296
+ val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
297
+ imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
298
+ }
299
+ }
300
+ pendingKeyboardDismiss = dismiss
301
+ richTextView.editorEditText.post(dismiss)
302
+ }
303
+
304
+ private fun scheduleOutsideTapBlur() {
305
+ cancelPendingOutsideTapBlur()
306
+ val blur = Runnable {
307
+ pendingOutsideTapBlur = null
308
+ if (richTextView.editorEditText.hasFocus()) {
309
+ blurWithDeferredKeyboardDismiss()
310
+ }
311
+ }
312
+ pendingOutsideTapBlur = blur
313
+ richTextView.editorEditText.postDelayed(blur, OUTSIDE_TAP_BLUR_DELAY_MS)
314
+ }
315
+
316
+ private fun cancelPendingOutsideTapBlur() {
317
+ pendingOutsideTapBlur?.let {
318
+ richTextView.editorEditText.removeCallbacks(it)
319
+ pendingOutsideTapBlur = null
320
+ }
321
+ }
322
+
323
+ private fun cancelPendingKeyboardDismiss() {
324
+ pendingKeyboardDismiss?.let {
325
+ richTextView.editorEditText.removeCallbacks(it)
326
+ pendingKeyboardDismiss = null
327
+ }
328
+ }
329
+
283
330
  fun getCaretRectJson(): String? {
284
331
  if (width <= 0 || height <= 0) return null
285
332
  val rect = richTextView.caretRect() ?: return null
@@ -296,6 +343,8 @@ class NativeEditorExpoView(
296
343
 
297
344
  override fun onDetachedFromWindow() {
298
345
  super.onDetachedFromWindow()
346
+ cancelPendingOutsideTapBlur()
347
+ cancelPendingKeyboardDismiss()
299
348
  uninstallOutsideTapBlurHandler()
300
349
  detachKeyboardToolbarIfNeeded()
301
350
  }
@@ -430,14 +479,17 @@ class NativeEditorExpoView(
430
479
 
431
480
  val wrappedCallback = object : Window.Callback by currentCallback {
432
481
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
433
- if (
482
+ val shouldBlur =
434
483
  event.action == MotionEvent.ACTION_DOWN &&
435
484
  richTextView.editorEditText.hasFocus() &&
436
485
  isTouchOutsideEditor(event)
437
- ) {
438
- blur()
486
+ val result = currentCallback.dispatchTouchEvent(event)
487
+ if (shouldBlur) {
488
+ scheduleOutsideTapBlur()
489
+ } else if (event.action == MotionEvent.ACTION_DOWN) {
490
+ cancelPendingOutsideTapBlur()
439
491
  }
440
- return currentCallback.dispatchTouchEvent(event)
492
+ return result
441
493
  }
442
494
  }
443
495
 
@@ -458,6 +510,7 @@ class NativeEditorExpoView(
458
510
 
459
511
  private fun isTouchOutsideEditor(event: MotionEvent): Boolean {
460
512
  if (isTouchInsideKeyboardToolbar(event)) {
513
+ markRecentToolbarTouch()
461
514
  return false
462
515
  }
463
516
  if (isTouchInsideStandaloneToolbar(event)) {
@@ -553,6 +606,7 @@ class NativeEditorExpoView(
553
606
  private companion object {
554
607
  private const val TOOLBAR_HIT_SLOP_DP = 8f
555
608
  private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
609
+ private const val OUTSIDE_TAP_BLUR_DELAY_MS = 100L
556
610
  }
557
611
 
558
612
  private fun resolveActivity(context: Context): Activity? {
@@ -136,6 +136,7 @@ const TOOLBAR_PADDING_H = 12;
136
136
  const TOOLBAR_PADDING_V = 4;
137
137
  const MENU_MARGIN = 8;
138
138
  const MENU_WIDTH = 192;
139
+ const KEYBOARD_FRAME_REMEASURE_DELAYS_MS = [50, 150, 300];
139
140
  const ACTIVE_BG = 'rgba(0, 122, 255, 0.12)';
140
141
  const ACTIVE_COLOR = '#007AFF';
141
142
  const DEFAULT_COLOR = '#666666';
@@ -205,6 +206,8 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
205
206
  const [expandedGroupKey, setExpandedGroupKey] = (0, react_1.useState)(null);
206
207
  const [menuState, setMenuState] = (0, react_1.useState)(null);
207
208
  const toolbarInteractionActiveRef = (0, react_1.useRef)(false);
209
+ const framePublishAnimationFramesRef = (0, react_1.useRef)([]);
210
+ const framePublishTimeoutsRef = (0, react_1.useRef)([]);
208
211
  const registrationIdRef = (0, react_1.useRef)(null);
209
212
  if (registrationIdRef.current == null) {
210
213
  registrationIdRef.current = nextEditorToolbarRegistrationId++;
@@ -478,6 +481,23 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
478
481
  registerEditorToolbarFrame(registrationId, { x, y, width, height });
479
482
  });
480
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]);
481
501
  const handleToolbarLayout = (0, react_1.useCallback)(() => {
482
502
  requestAnimationFrame(publishToolbarFrame);
483
503
  }, [publishToolbarFrame]);
@@ -503,6 +523,7 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
503
523
  (0, react_1.useEffect)(() => {
504
524
  const registrationId = registrationIdRef.current;
505
525
  return () => {
526
+ cancelScheduledFramePublishes();
506
527
  if (toolbarInteractionActiveRef.current) {
507
528
  toolbarInteractionActiveRef.current = false;
508
529
  endEditorToolbarInteraction();
@@ -511,7 +532,22 @@ function EditorToolbar({ activeState, historyState, onToggleBold, onToggleItalic
511
532
  unregisterEditorToolbarFrame(registrationId);
512
533
  }
513
534
  };
514
- }, []);
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]);
515
551
  (0, react_1.useEffect)(() => {
516
552
  if (expandedGroupKey != null && !groupsByKey.has(expandedGroupKey)) {
517
553
  setExpandedGroupKey(null);
@@ -798,7 +798,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
798
798
  });
799
799
  }, []);
800
800
  (0, react_1.useEffect)(() => {
801
- if (!(showToolbar && toolbarPlacement === 'inline' && isFocused && editable)) {
801
+ if (!(showToolbar && toolbarPlacement === 'inline' && editable)) {
802
802
  setInlineToolbarFrame(null);
803
803
  return;
804
804
  }
@@ -806,7 +806,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
806
806
  updateToolbarFrame();
807
807
  });
808
808
  return () => cancelAnimationFrame(frame);
809
- }, [editable, isFocused, showToolbar, toolbarPlacement, updateToolbarFrame]);
809
+ }, [editable, showToolbar, toolbarPlacement, updateToolbarFrame]);
810
810
  (0, react_1.useEffect)(() => {
811
811
  if (heightBehavior !== 'autoGrow') {
812
812
  setAutoGrowHeight(null);
@@ -1287,7 +1287,7 @@ exports.NativeRichTextEditor = (0, react_1.forwardRef)(function NativeRichTextEd
1287
1287
  nativeViewStyleParts.push({ height: autoGrowHeight });
1288
1288
  }
1289
1289
  const nativeViewStyle = nativeViewStyleParts.length <= 1 ? nativeViewStyleParts[0] : nativeViewStyleParts;
1290
- const toolbarFrameJson = serializeToolbarFrames(isFocused && editable
1290
+ const toolbarFrameJson = serializeToolbarFrames(editable
1291
1291
  ? [
1292
1292
  ...(toolbarPlacement === 'inline' && inlineToolbarFrame != null
1293
1293
  ? [inlineToolbarFrame]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-prose-editor",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Native rich text editor with Rust core for React Native",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/apollohg/react-native-prose-editor",
@@ -75,6 +75,7 @@
75
75
  "LICENSE",
76
76
  "dist",
77
77
  "android/build.gradle",
78
+ "android/consumer-rules.pro",
78
79
  "android/src/main",
79
80
  "ios/*.swift",
80
81
  "ios/*.h",