@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +8 -116
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  6. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  7. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  8. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  10. package/app.plugin.js +42 -39
  11. package/dist/AtomHost.js +18 -11
  12. package/dist/EditorAddon.js +4 -2
  13. package/dist/EditorAddonNormalization.js +17 -9
  14. package/dist/EditorMentionThemeNormalization.js +6 -3
  15. package/dist/EditorMentionThemeValidation.js +28 -11
  16. package/dist/EditorStyleSheetNormalization.js +85 -38
  17. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  18. package/dist/EditorTheme.d.ts +4 -0
  19. package/dist/EditorTheme.js +5 -3
  20. package/dist/EditorToolbarComponent.d.ts +1 -1
  21. package/dist/EditorToolbarComponent.js +1 -1
  22. package/dist/EditorToolbarRegistry.js +5 -5
  23. package/dist/EditorToolbarTypes.d.ts +12 -12
  24. package/dist/EditorToolbarVisuals.js +1 -1
  25. package/dist/ExternalTextComposition.js +89 -43
  26. package/dist/ImageLoadingPolicy.d.ts +1 -1
  27. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  28. package/dist/NativeEditorBoundaryError.js +9 -5
  29. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  30. package/dist/NativeEditorCollaborationTransport.js +28 -15
  31. package/dist/NativeEditorCreateConfig.js +11 -6
  32. package/dist/NativeEditorCreateJson.js +51 -25
  33. package/dist/NativeEditorCreateValidation.js +11 -6
  34. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  35. package/dist/NativeEditorDocumentBridge.js +32 -18
  36. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  37. package/dist/NativeEditorDocumentHandle.js +1 -1
  38. package/dist/NativeEditorLocalAwareness.js +25 -13
  39. package/dist/NativeEditorNativeModule.d.ts +8 -1
  40. package/dist/NativeEditorNativeModule.js +7 -5
  41. package/dist/NativeEditorRenderNormalization.js +66 -25
  42. package/dist/NativeEditorResultNormalization.js +53 -29
  43. package/dist/NativeEditorTypes.d.ts +10 -10
  44. package/dist/NativeEditorV2Decimal.js +6 -3
  45. package/dist/NativeProseViewer.js +11 -3
  46. package/dist/ResourceLimits.d.ts +2 -2
  47. package/dist/ResourceLimits.js +7 -2
  48. package/dist/RichTextEditor.js +1 -1
  49. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  50. package/dist/RichTextEditorNativeView.d.ts +1 -3
  51. package/dist/RichTextEditorSerialization.js +24 -14
  52. package/dist/RichTextEditorTypes.d.ts +1 -1
  53. package/dist/YjsCollaboration.d.ts +7 -7
  54. package/dist/YjsCollaboration.js +42 -26
  55. package/dist/addons.d.ts +4 -4
  56. package/dist/addons.js +3 -3
  57. package/dist/atomInstances.js +6 -3
  58. package/dist/atomUpdates.js +7 -4
  59. package/dist/atoms.js +20 -12
  60. package/dist/attributeValidation.js +55 -24
  61. package/dist/contentExpression.js +80 -42
  62. package/dist/listMarkers.d.ts +1 -1
  63. package/dist/listMarkers.js +1 -1
  64. package/dist/schemaDefinition.d.ts +1 -1
  65. package/dist/schemaDefinition.js +23 -14
  66. package/dist/schemaDocument.js +1 -1
  67. package/dist/schemaDocumentConstruction.js +25 -14
  68. package/dist/schemaNormalization.js +51 -29
  69. package/dist/schemaPresets.d.ts +1 -1
  70. package/dist/schemaPresets.js +9 -4
  71. package/dist/schemaResolution.js +46 -25
  72. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  73. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  74. package/dist/useEditorToolbarInteractions.js +45 -23
  75. package/dist/useEditorToolbarItems.d.ts +1 -1
  76. package/dist/useEditorToolbarItems.js +4 -3
  77. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  78. package/dist/useEditorToolbarPresentation.js +4 -4
  79. package/dist/useEditorToolbarState.d.ts +1 -1
  80. package/dist/useEditorToolbarState.js +1 -1
  81. package/dist/useFocusPreservingFrames.js +11 -8
  82. package/dist/useNativeEditor.d.ts +1 -1
  83. package/dist/useNativeEditor.js +21 -11
  84. package/dist/useRichTextEditorCommands.d.ts +2 -2
  85. package/dist/useRichTextEditorCommands.js +59 -29
  86. package/dist/useRichTextEditorEvents.d.ts +3 -3
  87. package/dist/useRichTextEditorEvents.js +75 -32
  88. package/dist/useRichTextEditorMentions.d.ts +4 -4
  89. package/dist/useRichTextEditorMentions.js +48 -29
  90. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  91. package/dist/useRichTextEditorPresentation.js +25 -23
  92. package/dist/useRichTextEditorState.d.ts +3 -3
  93. package/dist/useRichTextEditorState.js +20 -15
  94. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  95. package/dist/useRichTextEditorUpdates.js +99 -41
  96. package/dist/useViewerAtoms.js +63 -36
  97. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  98. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  99. package/ios/EditorInteractionOverlays.swift +114 -122
  100. package/ios/EditorLayoutManager.swift +13 -3
  101. package/ios/EditorStyleSheet.swift +12 -1
  102. package/ios/EditorTextView+Images.swift +32 -10
  103. package/ios/EditorTextView.swift +1 -1
  104. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  105. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  106. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  107. package/ios/RenderBridge+Atoms.swift +0 -1
  108. package/ios/RenderBridge+Attachments.swift +0 -29
  109. package/ios/RichTextEditorView.swift +4 -12
  110. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  111. package/package.json +11 -3
  112. package/react-native.config.js +1 -1
  113. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  114. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  115. package/rust/android/x86/libeditor_core.so +0 -0
  116. package/rust/android/x86_64/libeditor_core.so +0 -0
  117. package/src/AtomHost.tsx +58 -28
  118. package/src/EditorAddon.ts +18 -8
  119. package/src/EditorAddonNormalization.ts +40 -11
  120. package/src/EditorMentionThemeNormalization.ts +15 -4
  121. package/src/EditorMentionThemeValidation.ts +51 -14
  122. package/src/EditorStyleSheet.ts +1 -0
  123. package/src/EditorStyleSheetNormalization.ts +195 -64
  124. package/src/EditorStyleSheetTypes.ts +1 -1
  125. package/src/EditorTheme.ts +13 -5
  126. package/src/EditorToolbarComponent.tsx +2 -1
  127. package/src/EditorToolbarRegistry.tsx +23 -13
  128. package/src/EditorToolbarTypes.tsx +12 -12
  129. package/src/EditorToolbarVisuals.tsx +6 -3
  130. package/src/EditorUpdateRevision.ts +1 -0
  131. package/src/ExternalTextComposition.ts +259 -49
  132. package/src/ImageLoadingPolicy.ts +3 -1
  133. package/src/NativeEditorBoundaryError.ts +38 -11
  134. package/src/NativeEditorCollaborationTransport.ts +108 -27
  135. package/src/NativeEditorCreateConfig.ts +92 -19
  136. package/src/NativeEditorCreateJson.ts +163 -26
  137. package/src/NativeEditorCreateValidation.ts +33 -12
  138. package/src/NativeEditorDocumentBridge.ts +111 -19
  139. package/src/NativeEditorDocumentHandle.ts +9 -4
  140. package/src/NativeEditorLocalAwareness.ts +70 -15
  141. package/src/NativeEditorNativeModule.ts +13 -9
  142. package/src/NativeEditorRenderNormalization.ts +128 -29
  143. package/src/NativeEditorResultNormalization.ts +143 -32
  144. package/src/NativeEditorTypes.ts +12 -11
  145. package/src/NativeEditorV2Decimal.ts +12 -3
  146. package/src/NativeProseViewer.tsx +46 -16
  147. package/src/ResourceLimits.ts +15 -8
  148. package/src/RichTextEditor.tsx +3 -1
  149. package/src/RichTextEditorNativeTypes.tsx +1 -1
  150. package/src/RichTextEditorNativeView.tsx +2 -8
  151. package/src/RichTextEditorSerialization.tsx +70 -24
  152. package/src/RichTextEditorTypes.tsx +1 -1
  153. package/src/YjsCollaboration.ts +143 -47
  154. package/src/addons.ts +12 -8
  155. package/src/atomInstances.ts +25 -5
  156. package/src/atomUpdates.ts +23 -6
  157. package/src/atoms.ts +85 -18
  158. package/src/attributeValidation.ts +97 -33
  159. package/src/contentExpression.ts +218 -63
  160. package/src/listMarkers.ts +1 -1
  161. package/src/schemaDefinition.ts +82 -20
  162. package/src/schemaDocument.ts +15 -1
  163. package/src/schemaDocumentConstruction.ts +80 -16
  164. package/src/schemaNormalization.ts +155 -41
  165. package/src/schemaPresets.ts +37 -31
  166. package/src/schemaResolution.ts +126 -27
  167. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  168. package/src/useEditorToolbarInteractions.tsx +85 -36
  169. package/src/useEditorToolbarItems.tsx +50 -22
  170. package/src/useEditorToolbarPresentation.tsx +81 -58
  171. package/src/useEditorToolbarState.tsx +8 -6
  172. package/src/useFocusPreservingFrames.ts +43 -15
  173. package/src/useNativeEditor.ts +111 -51
  174. package/src/useRichTextEditorCommands.tsx +127 -54
  175. package/src/useRichTextEditorEvents.tsx +144 -42
  176. package/src/useRichTextEditorMentions.tsx +118 -37
  177. package/src/useRichTextEditorPresentation.tsx +128 -114
  178. package/src/useRichTextEditorState.tsx +76 -40
  179. package/src/useRichTextEditorUpdates.tsx +207 -56
  180. package/src/useViewerAtoms.tsx +162 -66
@@ -150,6 +150,14 @@ struct EditorStyleBox {
150
150
  }
151
151
  var inset: UIEdgeInsets { padding.adding(borders) }
152
152
  var outerInsets: UIEdgeInsets { inset.adding(margin) }
153
+ var mentionInsets: UIEdgeInsets {
154
+ UIEdgeInsets(
155
+ top: number("paddingTop", fallback: number("padding", fallback: 4)),
156
+ left: number("paddingLeft", fallback: number("padding", fallback: 6)),
157
+ bottom: number("paddingBottom", fallback: number("padding", fallback: 4)),
158
+ right: number("paddingRight", fallback: number("padding", fallback: 6))
159
+ ).adding(borders)
160
+ }
153
161
  private func insets(_ key: String) -> UIEdgeInsets {
154
162
  UIEdgeInsets(top: number(key + "Top", fallback: number(key)), left: number(key + "Left", fallback: number(key)), bottom: number(key + "Bottom", fallback: number(key)), right: number(key + "Right", fallback: number(key)))
155
163
  }
@@ -410,15 +418,18 @@ final class EditorMentionRenderedBox: NSObject {
410
418
  let label: NSAttributedString?
411
419
  let size: CGSize
412
420
  let padding: UIEdgeInsets
421
+ let baselineOffset: CGFloat
413
422
 
414
423
  init(box: EditorStyleBox, label: NSAttributedString? = nil) {
415
424
  self.box = box
416
425
  self.label = label
417
- padding = UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6).adding(box.inset)
426
+ padding = box.mentionInsets
418
427
  let measured = label?.size() ?? .zero
419
428
  let lineHeight = box.number("lineHeight", fallback: measured.height)
420
429
  size = CGSize(width: ceil(measured.width) + padding.left + padding.right,
421
430
  height: ceil(max(measured.height, lineHeight)) + padding.top + padding.bottom)
431
+ let font = label.flatMap { $0.length > 0 ? $0.attribute(.font, at: 0, effectiveRange: nil) as? UIFont : nil }
432
+ baselineOffset = padding.top + (size.height - padding.top - padding.bottom - measured.height) / 2 + (font?.ascender ?? 0)
422
433
  }
423
434
  }
424
435
 
@@ -289,7 +289,7 @@ extension EditorTextView {
289
289
  return (selectionState.docPos, rect)
290
290
  }
291
291
 
292
- private func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
292
+ func blockImageAttachment(docPos: UInt32) -> (range: NSRange, attachment: BlockImageAttachment)? {
293
293
  let fullRange = NSRange(location: 0, length: textStorage.length)
294
294
  var resolved: (range: NSRange, attachment: BlockImageAttachment)?
295
295
  textStorage.enumerateAttribute(
@@ -309,10 +309,6 @@ extension EditorTextView {
309
309
  return resolved
310
310
  }
311
311
 
312
- func imagePreviewForDocPos(_ docPos: UInt32) -> UIImage? {
313
- blockImageAttachment(docPos: docPos)?.attachment.previewImage()
314
- }
315
-
316
312
  func maximumRenderableImageWidth() -> CGFloat {
317
313
  let containerWidth: CGFloat
318
314
  if bounds.width > 0 {
@@ -337,14 +333,40 @@ extension EditorTextView {
337
333
  }
338
334
  }
339
335
 
340
- func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat, height: CGFloat) {
336
+ func previewResizeImageAtDocPos(_ docPos: UInt32, width: CGFloat?, height: CGFloat?) {
341
337
  guard let attachmentState = blockImageAttachment(docPos: docPos) else { return }
342
- attachmentState.attachment.setPreferredSize(width: width, height: height)
343
- layoutManager.invalidateLayout(forCharacterRange: attachmentState.range, actualCharacterRange: nil)
344
- layoutManager.invalidateDisplay(forCharacterRange: attachmentState.range)
338
+ updateImageResizePreview(attachmentState.attachment, range: attachmentState.range, width: width, height: height)
339
+ }
340
+
341
+ func restoreImageResizePreview(_ attachment: BlockImageAttachment, width: CGFloat?, height: CGFloat?) {
342
+ var attachmentRange: NSRange?
343
+ textStorage.enumerateAttribute(.attachment, in: NSRange(location: 0, length: textStorage.length)) { value, range, stop in
344
+ if value as? BlockImageAttachment === attachment {
345
+ attachmentRange = range
346
+ stop.pointee = true
347
+ }
348
+ }
349
+ guard let attachmentRange else { return }
350
+ updateImageResizePreview(attachment, range: attachmentRange, width: width, height: height)
351
+ }
352
+
353
+ private func updateImageResizePreview(_ attachment: BlockImageAttachment, range: NSRange, width: CGFloat?, height: CGFloat?) {
354
+ guard attachment.preferredWidth != width || attachment.preferredHeight != height else { return }
355
+ let previousOffset = contentOffset
356
+ attachment.preferredWidth = width
357
+ attachment.preferredHeight = height
358
+ layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
359
+ layoutManager.invalidateDisplay(forCharacterRange: range)
345
360
  textStorage.beginEditing()
346
- textStorage.edited(.editedAttributes, range: attachmentState.range, changeInLength: 0)
361
+ textStorage.edited(.editedAttributes, range: range, changeInLength: 0)
347
362
  textStorage.endEditing()
363
+ invalidateAutoGrowHeightMeasurement()
364
+ layoutManager.ensureLayout(forCharacterRange: range)
365
+ setNeedsLayout()
366
+ layoutIfNeeded()
367
+ setContentOffset(previousOffset, animated: false)
368
+ notifyHeightChangeIfNeeded()
369
+ onViewportMayChange?()
348
370
  }
349
371
 
350
372
  func setImageResizePreviewActive(_ active: Bool) {
@@ -415,7 +415,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
415
415
  height: min(maxPlaceholderHeight, ceil(fittedHeight))
416
416
  )
417
417
  }
418
- if heightBehavior == .autoGrow, !isPreviewingImageResize {
418
+ if heightBehavior == .autoGrow {
419
419
  let currentWidth = ceil(bounds.width)
420
420
  if abs(currentWidth - lastAutoGrowMeasuredWidth) > 0.5 {
421
421
  autoGrowHeightCheckIsDirty = true
@@ -150,7 +150,7 @@ extension EditorV2Adapter {
150
150
  private static func isValidMentionStyle(_ value: Any) -> Bool {
151
151
  guard let style = value as? [String: Any] else { return false }
152
152
  let colors: Set<String> = ["color", "backgroundColor", "textDecorationColor", "borderColor", "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
153
- let dimensions: Set<String> = ["fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
153
+ let dimensions: Set<String> = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
154
154
  let strings: Set<String> = ["fontFamily", "fontWeight", "fontStyle", "textDecorationLine", "textDecorationStyle", "borderStyle"]
155
155
  guard hasOnlyKeys(style, colors.union(dimensions).union(strings)) else { return false }
156
156
  for key in colors where style[key] != nil {
@@ -159,7 +159,7 @@ extension EditorV2Adapter {
159
159
  for key in dimensions where style[key] != nil {
160
160
  guard let number = finiteNumber(style[key])?.doubleValue else { return false }
161
161
  if ["fontSize", "lineHeight"].contains(key), number <= 0 { return false }
162
- if key.hasPrefix("border"), number < 0 { return false }
162
+ if key.hasPrefix("border") || key.hasPrefix("padding"), number < 0 { return false }
163
163
  }
164
164
  for key in strings where style[key] != nil {
165
165
  guard let value = style[key] as? String else { return false }
@@ -108,9 +108,7 @@ extension EditorAccessoryToolbarView {
108
108
  ?? (resolvedAppearance == .native ? self.tintColor : .secondaryLabel)
109
109
  }
110
110
 
111
- button.tintColor = tintColor
112
- button.setTitleColor(tintColor, for: .normal)
113
- button.setTitleColor(tintColor, for: .disabled)
111
+ applyButtonForeground(to: button, color: tintColor)
114
112
  button.tintAdjustmentMode = enabled ? .automatic : .normal
115
113
  button.alpha = enabled || resolvedAppearance == .native ? 1 : 0.7
116
114
  let inactiveBackgroundColor = buttonStyle?.backgroundColor
@@ -139,7 +137,20 @@ extension EditorAccessoryToolbarView {
139
137
  color: backgroundColor,
140
138
  cornerRadius: cornerRadius
141
139
  )
142
- applyButtonIconStyle(to: button, item: item)
140
+ applyButtonIconStyle(to: button, item: item, color: tintColor)
141
+ }
142
+
143
+ /// A configured `UIButton` paints disabled content in its own system gray,
144
+ /// ignoring `tintColor`, so the colour must be forced through the
145
+ /// configuration's transformers as well.
146
+ private func applyButtonForeground(to button: UIButton, color: UIColor) {
147
+ button.tintColor = color
148
+ button.setTitleColor(color, for: .normal)
149
+ button.setTitleColor(color, for: .disabled)
150
+ guard #available(iOS 15.0, *), var configuration = button.configuration else { return }
151
+ configuration.baseForegroundColor = color
152
+ configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in color }
153
+ button.configuration = configuration
143
154
  }
144
155
 
145
156
  /// Own the configured background to avoid stacking it with UIButton state fills.
@@ -175,13 +186,14 @@ extension EditorAccessoryToolbarView {
175
186
  return min(requestedSize, resolvedButtonSize)
176
187
  }
177
188
 
178
- private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem) {
189
+ private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem, color: UIColor) {
179
190
  let iconSize = resolvedButtonIconSize(for: item)
180
191
  let font = UIFont.systemFont(ofSize: iconSize, weight: .semibold)
181
192
  if #available(iOS 15.0, *), var configuration = button.configuration {
182
193
  configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
183
194
  var outgoing = incoming
184
195
  outgoing.font = font
196
+ outgoing.foregroundColor = color
185
197
  return outgoing
186
198
  }
187
199
  button.configuration = configuration
@@ -25,6 +25,21 @@ extension EditorAccessoryToolbarView {
25
25
  buttonBindings.indices.contains(index) ? buttonBindings[index].button.tintColor : nil
26
26
  }
27
27
 
28
+ /// The foreground colour a configured button will actually paint for its
29
+ /// current state, resolved through the configuration transformers rather
30
+ /// than `tintColor`, which UIKit ignores for disabled configured buttons.
31
+ func buttonPaintedForegroundColorForTesting(_ index: Int) -> UIColor? {
32
+ guard buttonBindings.indices.contains(index) else { return nil }
33
+ let button = buttonBindings[index].button
34
+ guard #available(iOS 15.0, *), let configuration = button.configuration else {
35
+ return button.titleColor(for: button.state)
36
+ }
37
+ if button.image(for: .normal) != nil {
38
+ return configuration.imageColorTransformer?(.black)
39
+ }
40
+ return configuration.titleTextAttributesTransformer?(AttributeContainer()).foregroundColor
41
+ }
42
+
28
43
  func buttonFontSizeForTesting(_ index: Int) -> CGFloat? {
29
44
  buttonBindings.indices.contains(index) ? buttonBindings[index].button.titleLabel?.font.pointSize : nil
30
45
  }
@@ -192,7 +192,6 @@ extension RenderBridge {
192
192
  }
193
193
  let attachment = BlockImageAttachment(
194
194
  source: source,
195
- placeholderTint: textColor,
196
195
  preferredWidth: jsonCGFloat(elementAttrs["width"]),
197
196
  preferredHeight: jsonCGFloat(elementAttrs["height"])
198
197
  )
@@ -101,7 +101,6 @@ final class AtomBlockAttachment: NSTextAttachment {
101
101
  final class BlockImageAttachment: NSTextAttachment {
102
102
  var styleBox: EditorStyleBox?
103
103
  let source: String
104
- let placeholderTint: UIColor
105
104
  private weak var loadOwner: RenderImageLoadOwner?
106
105
  private var loadReceipt: RenderImageLoadOwner.ImageLoadReceipt?
107
106
  var preferredWidth: CGFloat?
@@ -110,12 +109,10 @@ final class BlockImageAttachment: NSTextAttachment {
110
109
 
111
110
  init(
112
111
  source: String,
113
- placeholderTint: UIColor,
114
112
  preferredWidth: CGFloat?,
115
113
  preferredHeight: CGFloat?
116
114
  ) {
117
115
  self.source = source
118
- self.placeholderTint = placeholderTint
119
116
  self.preferredWidth = preferredWidth
120
117
  self.preferredHeight = preferredHeight
121
118
  self.loadOwner = RenderImageLoadOwner.current
@@ -131,15 +128,6 @@ final class BlockImageAttachment: NSTextAttachment {
131
128
  loadReceipt?.cancel()
132
129
  }
133
130
 
134
- func setPreferredSize(width: CGFloat, height: CGFloat) {
135
- preferredWidth = width
136
- preferredHeight = height
137
- }
138
-
139
- func previewImage() -> UIImage? {
140
- loadedImage ?? image
141
- }
142
-
143
131
  override func attachmentBounds(
144
132
  for textContainer: NSTextContainer?,
145
133
  proposedLineFragment lineFrag: CGRect,
@@ -193,9 +181,6 @@ final class BlockImageAttachment: NSTextAttachment {
193
181
  renderer.cgContext.clip(to: imageBounds.inset(by: styleBox.inset))
194
182
  if let loadedImage {
195
183
  loadedImage.draw(in: styleBox.imageRect(loadedImage.size, in: imageBounds))
196
- } else {
197
- let iconBounds = imageBounds.insetBy(dx: imageBounds.width * 0.35, dy: imageBounds.height * 0.35)
198
- UIImage(systemName: "photo")?.withTintColor(placeholderTint, renderingMode: .alwaysOriginal).draw(in: iconBounds)
199
184
  }
200
185
  }
201
186
  }
@@ -206,20 +191,6 @@ final class BlockImageAttachment: NSTextAttachment {
206
191
  let path = UIBezierPath(roundedRect: imageBounds, cornerRadius: 12)
207
192
  UIColor.secondarySystemFill.setFill()
208
193
  path.fill()
209
-
210
- let iconSize = min(imageBounds.width, imageBounds.height) * 0.28
211
- let iconOrigin = CGPoint(
212
- x: imageBounds.midX - (iconSize / 2),
213
- y: imageBounds.midY - (iconSize / 2)
214
- )
215
- let iconRect = CGRect(origin: iconOrigin, size: CGSize(width: iconSize, height: iconSize))
216
-
217
- if #available(iOS 13.0, *) {
218
- let config = UIImage.SymbolConfiguration(pointSize: iconSize, weight: .medium)
219
- let icon = UIImage(systemName: "photo", withConfiguration: config)?
220
- .withTintColor(placeholderTint.withAlphaComponent(0.7), renderingMode: .alwaysOriginal)
221
- icon?.draw(in: iconRect)
222
- }
223
194
  }
224
195
  }
225
196
 
@@ -684,10 +684,6 @@ final class RichTextEditorView: UIView {
684
684
  imageTapOverlayView.handleTapForTesting(convert(point, to: imageTapOverlayView))
685
685
  }
686
686
 
687
- func imageResizePreviewHasImageForTesting() -> Bool {
688
- imageResizeOverlayView.previewHasImageForTesting
689
- }
690
-
691
687
  func refreshSelectionVisualStateForTesting() {
692
688
  textView.refreshSelectionVisualState()
693
689
  }
@@ -708,6 +704,10 @@ final class RichTextEditorView: UIView {
708
704
  imageResizeOverlayView.simulatePreviewResizeForTesting(width: width, height: height)
709
705
  }
710
706
 
707
+ func previewImageResizeDragForTesting(corner: ImageResizeOverlayView.Corner, translation: CGPoint) {
708
+ imageResizeOverlayView.simulatePreviewDragForTesting(corner: corner, translation: translation)
709
+ }
710
+
711
711
  func commitPreviewResizeForTesting() {
712
712
  imageResizeOverlayView.commitPreviewResizeForTesting()
713
713
  }
@@ -803,14 +803,6 @@ final class RichTextEditorView: UIView {
803
803
  textView.setImageResizePreviewActive(active)
804
804
  }
805
805
 
806
- func imagePreviewForResize(docPos: UInt32) -> UIImage? {
807
- textView.imagePreviewForDocPos(docPos)
808
- }
809
-
810
- func imageResizePreviewBackgroundColor() -> UIColor {
811
- textView.backgroundColor ?? .systemBackground
812
- }
813
-
814
806
  func maximumImageWidthForResizeGesture() -> CGFloat {
815
807
  textView.maximumRenderableImageWidth()
816
808
  }
@@ -79,8 +79,9 @@ extension CoreTextProseLayoutEngine {
79
79
  let labelWidth = CGFloat(CTLineGetTypographicBounds(labelLine, &labelAscent, &labelDescent, &labelLeading))
80
80
  let requestedHeight = EditorTheme.cgFloat(appearance.attributes[editorInlineLineHeightAttribute]) ?? 0
81
81
  let extra = max(0, requestedHeight - labelAscent - max(labelDescent, 2)) / 2
82
+ let paddedWidth = labelWidth + appearance.padding.left + appearance.padding.right
82
83
  let metrics = PreparedAtomMetrics(
83
- width: max(paint.font.lineHeight, labelWidth + appearance.padding.left + appearance.padding.right),
84
+ width: nodeType == "mention" ? paddedWidth : max(paint.font.lineHeight, paddedWidth),
84
85
  ascent: labelAscent + extra + appearance.padding.top,
85
86
  descent: max(labelDescent, 2) + extra + appearance.padding.bottom
86
87
  )
@@ -388,7 +389,7 @@ extension CoreTextProseLayoutEngine {
388
389
  var boxValues = values
389
390
  boxValues["backgroundColor"] = values["backgroundColor"] ?? "#007aff1f"
390
391
  let box = EditorStyleBox(boxValues)
391
- return PreparedAtomAppearance(styleBox: box, attributes: coreTextAttributes(styled), background: box.color("backgroundColor") ?? .clear, borderColor: nil, borderWidth: 0, radius: 0, padding: UIEdgeInsets(top: 4, left: 6, bottom: 4, right: 6).adding(box.inset))
392
+ return PreparedAtomAppearance(styleBox: box, attributes: coreTextAttributes(styled), background: box.color("backgroundColor") ?? .clear, borderColor: nil, borderWidth: 0, radius: 0, padding: box.mentionInsets)
392
393
  }
393
394
  return PreparedAtomAppearance(
394
395
  attributes: attributes,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apollohg/react-native-rich-text-editor",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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-rich-text-editor",
@@ -104,21 +104,29 @@
104
104
  },
105
105
  "devDependencies": {
106
106
  "@babel/runtime": "^7.29.7",
107
+ "@eslint/js": "^10.0.1",
107
108
  "@expo/vector-icons": "^15.0.3",
108
109
  "@react-native/jest-preset": "0.86.2",
110
+ "@stylistic/eslint-plugin": "^5.10.0",
109
111
  "@testing-library/react-native": "^13.3.3",
110
112
  "@types/jest": "29.5.14",
111
113
  "@types/react": "^19.2.14",
112
114
  "babel-jest": "^29.7.0",
113
115
  "babel-preset-expo": "~57.0.8",
116
+ "eslint": "^10.10.0",
117
+ "eslint-plugin-react": "^7.37.5",
118
+ "eslint-plugin-react-hooks": "^7.1.1",
119
+ "eslint-plugin-react-native": "^5.0.0",
120
+ "eslint-plugin-slop": "^0.1.3",
114
121
  "expo": "^57.0.9",
115
122
  "expo-modules-core": "~57.0.8",
116
123
  "jest": "^29.7.0",
117
- "prettier": "^3.8.1",
124
+ "jiti": "^2.7.0",
118
125
  "react": "19.2.3",
119
126
  "react-native": "0.86.2",
120
127
  "react-test-renderer": "19.2.3",
121
- "typescript": "^5.9.3"
128
+ "typescript": "^5.9.3",
129
+ "typescript-eslint": "^8.70.0"
122
130
  },
123
131
  "files": [
124
132
  "README.md",
@@ -7,7 +7,7 @@ module.exports = {
7
7
  // metadata; no nested compatibility podspec exists.
8
8
  ios: {},
9
9
  android: {
10
- componentDescriptors: ['PreparedProseViewerComponentDescriptor'],
10
+ componentDescriptors: [ 'PreparedProseViewerComponentDescriptor' ],
11
11
  cmakeListsPath: '../android/src/main/jni/CMakeLists.txt',
12
12
  packageImportPath: 'import com.apollohg.editor.viewer.PreparedProseViewerPackage;',
13
13
  packageInstance: 'new PreparedProseViewerPackage()',
Binary file
package/src/AtomHost.tsx CHANGED
@@ -11,9 +11,12 @@ export interface AtomViewport {
11
11
  }
12
12
 
13
13
  export function atomIsVisible(y: number, height: number, viewport?: AtomViewport): boolean {
14
- if (!viewport) return true;
14
+ if (!viewport) {
15
+ return true;
16
+ }
17
+
15
18
  if (
16
- ![viewport.y, viewport.height, viewport.overscan ?? 200].every(Number.isFinite) ||
19
+ ![ viewport.y, viewport.height, viewport.overscan ?? 200 ].every(Number.isFinite) ||
17
20
  viewport.height < 0 ||
18
21
  (viewport.overscan ?? 200) < 0
19
22
  ) {
@@ -21,7 +24,9 @@ export function atomIsVisible(y: number, height: number, viewport?: AtomViewport
21
24
  'Atom viewport must have finite coordinates and non-negative height and overscan.'
22
25
  );
23
26
  }
27
+
24
28
  const overscan = viewport.overscan ?? 200;
29
+
25
30
  return y + height >= viewport.y - overscan && y <= viewport.y + viewport.height + overscan;
26
31
  }
27
32
 
@@ -49,13 +54,17 @@ class AtomErrorBoundary extends React.Component<
49
54
  }
50
55
 
51
56
  render() {
52
- if (!this.state.error) return this.props.children;
57
+ if (!this.state.error) {
58
+ return this.props.children;
59
+ }
60
+
53
61
  return (
54
- <View accessibilityRole='alert' style={{ minHeight: 32, padding: 8 }}>
62
+ <View accessibilityRole={'alert'} style={{ minHeight: 32, padding: 8 }}>
55
63
  <Text>Unable to display {this.props.nodeType}</Text>
56
64
  <Pressable
57
- accessibilityRole='button'
58
- onPress={() => this.setState({ error: false })}>
65
+ accessibilityRole={'button'}
66
+ onPress={() => this.setState({ error: false })}
67
+ >
59
68
  <Text>Retry</Text>
60
69
  </Pressable>
61
70
  </View>
@@ -80,58 +89,77 @@ export function AtomHost({
80
89
  onMeasure?: (event: import('react-native').LayoutChangeEvent) => void;
81
90
  nativeID?: string;
82
91
  }) {
83
- const [measurement, setMeasurement] = useState({ width, height: estimatedHeight });
84
- const [focused, setFocused] = useState(false);
85
- const [active, setActive] = useState(false);
86
- const [pending, setPending] = useState(0);
87
- const [updateError, setUpdateError] = useState<Error | null>(null);
92
+ const [ measurement, setMeasurement ] = useState({ width, height: estimatedHeight });
93
+ const [ focused, setFocused ] = useState(false);
94
+ const [ active, setActive ] = useState(false);
95
+ const [ pending, setPending ] = useState(0);
96
+ const [ updateError, setUpdateError ] = useState<Error | null>(null);
88
97
  const latestRequest = useRef(0);
89
98
  const applyUpdate = atomProps.updateAttrs;
90
99
  const mounted = useRef(true);
100
+
91
101
  useEffect(() => {
92
102
  mounted.current = true;
103
+
93
104
  return () => {
94
105
  mounted.current = false;
95
106
  };
96
107
  }, []);
108
+
97
109
  const updateAttrs = useCallback(
98
- async (update: AtomAttrsUpdate) => {
99
- if (!mounted.current)
110
+ async(update: AtomAttrsUpdate) => {
111
+ if (!mounted.current) {
100
112
  throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
113
+ }
114
+
101
115
  const request = ++latestRequest.current;
102
- setPending((count) => count + 1);
116
+ setPending(count => count + 1);
103
117
  setUpdateError(null);
118
+
104
119
  try {
105
120
  await applyUpdate(update);
106
121
  } catch (error) {
107
- if (mounted.current && latestRequest.current === request)
122
+ if (mounted.current && latestRequest.current === request) {
108
123
  setUpdateError(error instanceof Error ? error : new Error(String(error)));
124
+ }
125
+
109
126
  throw error;
110
127
  } finally {
111
- if (mounted.current) setPending((count) => count - 1);
128
+ if (mounted.current) {
129
+ setPending(count => count - 1);
130
+ }
112
131
  }
113
132
  },
114
- [applyUpdate]
133
+ [ applyUpdate ]
115
134
  );
135
+
116
136
  const editor = useMemo(() => {
117
- if (!atomProps.editor) return undefined;
118
- const guard = (action: () => Promise<void>) => async () => {
119
- if (!mounted.current)
137
+ if (!atomProps.editor) {
138
+ return undefined;
139
+ }
140
+
141
+ const guard = (action: () => Promise<void>) => async() => {
142
+ if (!mounted.current) {
120
143
  throw new AtomUpdateAttrsError('not-ready', 'The atom is unmounted.');
144
+ }
145
+
121
146
  return action();
122
147
  };
148
+
123
149
  return {
124
150
  select: guard(atomProps.editor.select),
125
151
  delete: guard(atomProps.editor.delete),
126
152
  focusBefore: guard(atomProps.editor.focusBefore),
127
153
  focusAfter: guard(atomProps.editor.focusAfter),
128
154
  };
129
- }, [atomProps.editor]);
155
+ }, [ atomProps.editor ]);
156
+
130
157
  const retainedHeight = measurement.width === width ? measurement.height : estimatedHeight;
131
158
  const show = visible || focused || active || pending > 0 || atomProps.selected;
159
+
132
160
  return (
133
161
  <View
134
- testID='atom-host'
162
+ testID={'atom-host'}
135
163
  nativeID={nativeID}
136
164
  collapsable={false}
137
165
  pointerEvents={atomProps.interactive === false ? 'none' : 'box-none'}
@@ -139,23 +167,24 @@ export function AtomHost({
139
167
  importantForAccessibility={!show ? 'no-hide-descendants' : 'auto'}
140
168
  onFocus={() => setFocused(true)}
141
169
  onBlur={() => setFocused(false)}
142
- onLayout={(event) => {
170
+ onLayout={event => {
143
171
  onMeasure?.(event);
144
172
  const size = event.nativeEvent.layout;
173
+
145
174
  if (
146
175
  show &&
147
176
  Number.isFinite(size.height) &&
148
177
  size.height >= 0 &&
149
178
  Math.abs(size.width - width) <= 1
150
179
  ) {
151
- setMeasurement((previous) =>
180
+ setMeasurement(previous =>
152
181
  previous.width === width && previous.height === size.height
153
182
  ? previous
154
- : { width, height: size.height }
155
- );
183
+ : { width, height: size.height });
156
184
  }
157
185
  }}
158
- style={show ? undefined : { height: retainedHeight }}>
186
+ style={show ? undefined : { height: retainedHeight }}
187
+ >
159
188
  {show && (
160
189
  <AtomErrorBoundary component={Component} nodeType={atomProps.nodeType}>
161
190
  <Suspense
@@ -163,7 +192,8 @@ export function AtomHost({
163
192
  <View style={{ height: retainedHeight }}>
164
193
  <Text>Loading {atomProps.nodeType}…</Text>
165
194
  </View>
166
- }>
195
+ }
196
+ >
167
197
  <Component
168
198
  {...atomProps}
169
199
  editor={editor}
@@ -37,21 +37,31 @@ export type EditorAddonEntry = EditorAddon | false | null | undefined;
37
37
  export type EditorAddons = readonly EditorAddonEntry[];
38
38
 
39
39
  function copyConfiguration<T>(value: T, ancestors = new Set<object>()): T {
40
- if (value === null || typeof value !== 'object') return value;
41
- if (ancestors.has(value)) throw new Error('Addon configuration must not contain cycles.');
40
+ if (value === null || typeof value !== 'object') {
41
+ return value;
42
+ }
43
+
44
+ if (ancestors.has(value)) {
45
+ throw new Error('Addon configuration must not contain cycles.');
46
+ }
47
+
42
48
  if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype) {
43
49
  throw new Error('Addon configuration must contain only plain objects and arrays.');
44
50
  }
51
+
45
52
  ancestors.add(value);
53
+
46
54
  const copy = Array.isArray(value)
47
- ? value.map((entry) => copyConfiguration(entry, ancestors))
55
+ ? value.map((entry: unknown) => copyConfiguration(entry, ancestors))
48
56
  : Object.fromEntries(
49
- Object.entries(value).map(([key, entry]) => [
50
- key,
51
- copyConfiguration(entry, ancestors),
52
- ])
53
- );
57
+ Object.entries(value).map(([ key, entry ]) => [
58
+ key,
59
+ copyConfiguration(entry, ancestors),
60
+ ])
61
+ );
62
+
54
63
  ancestors.delete(value);
64
+
55
65
  return Object.freeze(copy) as T;
56
66
  }
57
67