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

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.
@@ -362,6 +362,10 @@ private fun parseColor(raw: String?): Int? {
362
362
  val value = raw?.trim()?.lowercase() ?: return null
363
363
  if (value.isEmpty()) return null
364
364
 
365
+ when (value) {
366
+ "clear", "transparent" -> return Color.TRANSPARENT
367
+ }
368
+
365
369
  parseCssHexColor(value)?.let { return it }
366
370
 
367
371
  try {
@@ -54,6 +54,7 @@ class RichTextEditorView @JvmOverloads constructor(
54
54
  private var baseBackgroundColor: Int = Color.WHITE
55
55
  private var viewportBottomInsetPx: Int = 0
56
56
  internal var appliedCornerRadiusPx: Float = 0f
57
+ internal var appliedBackgroundColorForTesting: Int = Color.WHITE
57
58
 
58
59
  /** Binds or unbinds the Rust editor instance. */
59
60
  var editorId: Long = 0
@@ -251,13 +252,15 @@ class RichTextEditorView @JvmOverloads constructor(
251
252
 
252
253
  private fun updateScrollContainerAppearance() {
253
254
  val cornerRadiusPx = (theme?.borderRadius ?: 0f) * resources.displayMetrics.density
255
+ val backgroundColor = theme?.backgroundColor ?: baseBackgroundColor
254
256
  editorViewport.background = GradientDrawable().apply {
255
257
  cornerRadius = cornerRadiusPx
256
- setColor(theme?.backgroundColor ?: baseBackgroundColor)
258
+ setColor(backgroundColor)
257
259
  }
258
260
  editorViewport.clipToOutline = cornerRadiusPx > 0f
259
261
  editorScrollView.setBackgroundColor(Color.TRANSPARENT)
260
262
  appliedCornerRadiusPx = cornerRadiusPx
263
+ appliedBackgroundColorForTesting = backgroundColor
261
264
  }
262
265
 
263
266
  private fun updateScrollContainerInsets() {
@@ -1576,6 +1576,46 @@ final class EditorAccessoryToolbarView: UIInputView {
1576
1576
  }
1577
1577
  }
1578
1578
 
1579
+ /// Keeps iOS keyboard integrations on the inputAccessoryView path when the
1580
+ /// visible toolbar is rendered outside the native keyboard accessory.
1581
+ final class EditorAccessoryPlaceholderView: UIView {
1582
+ override init(frame: CGRect) {
1583
+ super.init(
1584
+ frame: CGRect(
1585
+ x: frame.origin.x,
1586
+ y: frame.origin.y,
1587
+ width: frame.width,
1588
+ height: 0
1589
+ )
1590
+ )
1591
+ commonInit()
1592
+ }
1593
+
1594
+ required init?(coder: NSCoder) {
1595
+ return nil
1596
+ }
1597
+
1598
+ override var intrinsicContentSize: CGSize {
1599
+ CGSize(width: UIView.noIntrinsicMetric, height: 0)
1600
+ }
1601
+
1602
+ override func sizeThatFits(_ size: CGSize) -> CGSize {
1603
+ CGSize(width: size.width, height: 0)
1604
+ }
1605
+
1606
+ override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
1607
+ false
1608
+ }
1609
+
1610
+ private func commonInit() {
1611
+ frame.size.height = 0
1612
+ backgroundColor = .clear
1613
+ isOpaque = false
1614
+ isUserInteractionEnabled = false
1615
+ autoresizingMask = [.flexibleWidth]
1616
+ }
1617
+ }
1618
+
1579
1619
  class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognizerDelegate {
1580
1620
 
1581
1621
  // MARK: - Subviews
@@ -1585,6 +1625,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
1585
1625
  frame: .zero,
1586
1626
  inputViewStyle: .keyboard
1587
1627
  )
1628
+ private let accessoryPlaceholder = EditorAccessoryPlaceholderView(frame: .zero)
1588
1629
  private var toolbarFrameInWindow: CGRect?
1589
1630
  private var didApplyAutoFocus = false
1590
1631
  private var toolbarState = NativeToolbarState.empty
@@ -2269,14 +2310,33 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
2269
2310
  func triggerMentionSuggestionTapForTesting(at index: Int) {
2270
2311
  accessoryToolbar.triggerMentionSuggestionTapForTesting(at: index)
2271
2312
  }
2313
+
2314
+ func inputAccessoryViewForTesting() -> UIView? {
2315
+ richTextView.textView.inputAccessoryView
2316
+ }
2317
+
2318
+ func isUsingAccessoryToolbarForTesting() -> Bool {
2319
+ richTextView.textView.inputAccessoryView === accessoryToolbar
2320
+ }
2321
+
2322
+ func isUsingAccessoryPlaceholderForTesting() -> Bool {
2323
+ richTextView.textView.inputAccessoryView === accessoryPlaceholder
2324
+ }
2325
+
2272
2326
  private func updateAccessoryToolbarVisibility() {
2273
2327
  refreshSystemAssistantToolbarIfNeeded()
2274
- let nextAccessoryView: UIView? = showsToolbar &&
2328
+ let nextAccessoryView: UIView?
2329
+ if showsToolbar &&
2275
2330
  toolbarPlacement == "keyboard" &&
2276
2331
  richTextView.textView.isEditable &&
2277
2332
  !shouldUseSystemAssistantToolbar
2278
- ? accessoryToolbar
2279
- : nil
2333
+ {
2334
+ nextAccessoryView = accessoryToolbar
2335
+ } else if richTextView.textView.isEditable && !shouldUseSystemAssistantToolbar {
2336
+ nextAccessoryView = accessoryPlaceholder
2337
+ } else {
2338
+ nextAccessoryView = nil
2339
+ }
2280
2340
  if richTextView.textView.inputAccessoryView !== nextAccessoryView {
2281
2341
  richTextView.textView.inputAccessoryView = nextAccessoryView
2282
2342
  if richTextView.textView.isFirstResponder {
@@ -1050,7 +1050,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1050
1050
  // Register as the text storage delegate so we can detect unauthorized
1051
1051
  // mutations (reconciliation fallback).
1052
1052
  textStorage.delegate = self
1053
- delegate = self
1053
+ ensureInternalTextViewDelegate()
1054
1054
  addGestureRecognizer(imageSelectionTapRecognizer)
1055
1055
  installImageSelectionTapDependencies()
1056
1056
 
@@ -1161,6 +1161,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1161
1161
  override func becomeFirstResponder() -> Bool {
1162
1162
  let didBecomeFirstResponder = super.becomeFirstResponder()
1163
1163
  if didBecomeFirstResponder {
1164
+ ensureInternalTextViewDelegate()
1165
+ DispatchQueue.main.async { [weak self] in
1166
+ self?.ensureInternalTextViewDelegate()
1167
+ }
1164
1168
  _ = normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded()
1165
1169
  refreshTypingAttributesForSelection()
1166
1170
  }
@@ -1340,6 +1344,10 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1340
1344
  lastApplyUpdateTraceForTesting
1341
1345
  }
1342
1346
 
1347
+ func isUsingInternalTextViewDelegateForTesting() -> Bool {
1348
+ (delegate as AnyObject?) === (self as AnyObject)
1349
+ }
1350
+
1343
1351
  func blockquoteStripeRectsForTesting() -> [CGRect] {
1344
1352
  editorLayoutManager.blockquoteStripeRectsForTesting(in: textStorage)
1345
1353
  }
@@ -1533,6 +1541,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1533
1541
  /// - id: The editor ID from `editor_create()`.
1534
1542
  /// - initialHTML: Optional HTML to set as initial content.
1535
1543
  func bindEditor(id: UInt64, initialHTML: String? = nil) {
1544
+ ensureInternalTextViewDelegate()
1536
1545
  editorId = id
1537
1546
 
1538
1547
  if let html = initialHTML, !html.isEmpty {
@@ -1561,6 +1570,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1561
1570
  /// Instead of calling `super.insertText()` (which would modify the
1562
1571
  /// underlying text storage directly), we route through Rust.
1563
1572
  override func insertText(_ text: String) {
1573
+ ensureInternalTextViewDelegate()
1564
1574
  guard !isApplyingRustState else {
1565
1575
  super.insertText(text)
1566
1576
  return
@@ -1644,6 +1654,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1644
1654
  /// If there's a range selection, delete the range. If it's a cursor,
1645
1655
  /// delete the character (grapheme cluster) before the cursor.
1646
1656
  override func deleteBackward() {
1657
+ ensureInternalTextViewDelegate()
1647
1658
  guard !isApplyingRustState else {
1648
1659
  super.deleteBackward()
1649
1660
  return
@@ -1837,6 +1848,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1837
1848
  ///
1838
1849
  /// We route the replacement through Rust to keep the document model in sync.
1839
1850
  override func replace(_ range: UITextRange, withText text: String) {
1851
+ ensureInternalTextViewDelegate()
1840
1852
  guard !isApplyingRustState else {
1841
1853
  super.replace(range, withText: text)
1842
1854
  return
@@ -1871,6 +1883,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1871
1883
  /// UITextView display the composing text normally (with its underline
1872
1884
  /// decoration). The text is NOT sent to Rust during composition.
1873
1885
  override func setMarkedText(_ markedText: String?, selectedRange: NSRange) {
1886
+ ensureInternalTextViewDelegate()
1874
1887
  isComposing = true
1875
1888
  Self.inputLog.debug(
1876
1889
  "[setMarkedText] marked=\(self.preview(markedText ?? ""), privacy: .public) nsRange=\(selectedRange.location),\(selectedRange.length) selection=\(self.selectionSummary(), privacy: .public)"
@@ -1886,6 +1899,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1886
1899
  /// replace the marked text with the final text in the text storage,
1887
1900
  /// but we intercept at a higher level.
1888
1901
  override func unmarkText() {
1902
+ ensureInternalTextViewDelegate()
1889
1903
  // Capture the finalized composed text before UIKit clears it.
1890
1904
  composedText = markedTextRange.flatMap { text(in: $0) }
1891
1905
 
@@ -1919,6 +1933,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1919
1933
  /// Attempts to extract HTML from the pasteboard first (for rich text paste),
1920
1934
  /// falling back to plain text.
1921
1935
  override func paste(_ sender: Any?) {
1936
+ ensureInternalTextViewDelegate()
1922
1937
  guard editorId != 0 else {
1923
1938
  super.paste(sender)
1924
1939
  return
@@ -1977,6 +1992,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
1977
1992
  /// internally during tap handling and word-boundary resolution.
1978
1993
  func textViewDidChangeSelection(_ textView: UITextView) {
1979
1994
  guard textView === self else { return }
1995
+ ensureInternalTextViewDelegate()
1980
1996
  guard !isApplyingRustState else { return }
1981
1997
  if normalizeSelectionForEmptyBlockAutocapitalizationIfNeeded() {
1982
1998
  return
@@ -2029,6 +2045,14 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
2029
2045
  interceptedInputDepth > 0
2030
2046
  }
2031
2047
 
2048
+ private func ensureInternalTextViewDelegate() {
2049
+ // Some keyboard integrations replace UITextView's private delegate ivar
2050
+ // directly. The editor must own delegate callbacks so external observers
2051
+ // cannot inspect transient TextKit state during Rust-driven edits.
2052
+ guard (delegate as AnyObject?) !== (self as AnyObject) else { return }
2053
+ delegate = self
2054
+ }
2055
+
2032
2056
  private func performInterceptedInput(_ action: () -> Void) {
2033
2057
  interceptedInputDepth += 1
2034
2058
  Self.inputLog.debug(
@@ -3236,12 +3260,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3236
3260
  var stringMutationNanos: UInt64 = 0
3237
3261
  var attributeMutationNanos: UInt64 = 0
3238
3262
  let previousTextStorageDelegate = textStorage.delegate
3239
- let previousTextViewDelegate = delegate
3240
3263
  textStorage.delegate = nil
3241
3264
  delegate = nil
3242
3265
  defer {
3243
3266
  textStorage.delegate = previousTextStorageDelegate
3244
- delegate = previousTextViewDelegate
3267
+ ensureInternalTextViewDelegate()
3245
3268
  }
3246
3269
  if let replaceRange {
3247
3270
  if shouldUseSmallPatchTextMutation {
@@ -3810,6 +3833,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3810
3833
  ///
3811
3834
  /// - Parameter updateJSON: The JSON string from editor_insert_text, etc.
3812
3835
  func applyUpdateJSON(_ updateJSON: String, notifyDelegate: Bool = true) {
3836
+ ensureInternalTextViewDelegate()
3813
3837
  let totalStartedAt = DispatchTime.now().uptimeNanoseconds
3814
3838
  let parseStartedAt = totalStartedAt
3815
3839
  guard let data = updateJSON.data(using: .utf8),
@@ -3996,6 +4020,7 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
3996
4020
  /// Used for initial content loading (set_html / set_json return render
3997
4021
  /// elements directly, not wrapped in an EditorUpdate).
3998
4022
  func applyRenderJSON(_ renderJSON: String) {
4023
+ ensureInternalTextViewDelegate()
3999
4024
  Self.updateLog.debug(
4000
4025
  "[applyRenderJSON.begin] before=\(self.textSnapshotSummary(), privacy: .public)"
4001
4026
  )
@@ -4031,7 +4056,11 @@ final class EditorTextView: UITextView, UITextViewDelegate, UIGestureRecognizerD
4031
4056
 
4032
4057
  let totalStartedAt = DispatchTime.now().uptimeNanoseconds
4033
4058
  isApplyingRustState = true
4034
- defer { isApplyingRustState = false }
4059
+ delegate = nil
4060
+ defer {
4061
+ ensureInternalTextViewDelegate()
4062
+ isApplyingRustState = false
4063
+ }
4035
4064
 
4036
4065
  switch type {
4037
4066
  case "text":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-prose-editor",
3
- "version": "0.5.8",
3
+ "version": "0.5.9",
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",