@chaitrabhairappa/react-native-rich-text-editor 2.1.2 → 3.1.0

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,5 +1,6 @@
1
1
  import UIKit
2
2
  import React
3
+ import PhotosUI
3
4
 
4
5
  class FloatingToolbar: UIView, UIScrollViewDelegate {
5
6
  weak var editorView: RichTextEditorView?
@@ -12,6 +13,7 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
12
13
  private var enabledOptions: [String] = [
13
14
  "bold", "italic", "underline", "strikethrough", "code", "highlight",
14
15
  "heading", "bullet", "numbered", "quote", "checklist",
16
+ "mediaAttachment",
15
17
  "link", "undo", "redo", "clearFormatting",
16
18
  "indent", "outdent",
17
19
  "alignLeft", "alignCenter", "alignRight"
@@ -20,9 +22,10 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
20
22
  private let optionToIndex: [String: Int] = [
21
23
  "bold": 0, "italic": 1, "strikethrough": 2, "underline": 3, "code": 4, "highlight": 5,
22
24
  "heading": 6, "bullet": 7, "numbered": 8, "quote": 9, "checklist": 10,
23
- "link": 11, "undo": 12, "redo": 13, "clearFormatting": 14,
24
- "indent": 15, "outdent": 16,
25
- "alignLeft": 17, "alignCenter": 18, "alignRight": 19
25
+ "mediaAttachment": 11,
26
+ "link": 12, "undo": 13, "redo": 14, "clearFormatting": 15,
27
+ "indent": 16, "outdent": 17,
28
+ "alignLeft": 18, "alignCenter": 19, "alignRight": 20
26
29
  ]
27
30
 
28
31
  private let toolbarBackgroundColor = UIColor(red: 45/255, green: 45/255, blue: 45/255, alpha: 1.0)
@@ -47,6 +50,7 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
47
50
  enabledOptions = [
48
51
  "bold", "italic", "underline", "strikethrough", "code", "highlight",
49
52
  "heading", "bullet", "numbered", "quote", "checklist",
53
+ "mediaAttachment",
50
54
  "link", "undo", "redo", "clearFormatting",
51
55
  "indent", "outdent",
52
56
  "alignLeft", "alignCenter", "alignRight"
@@ -371,15 +375,16 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
371
375
  case 8: editorView?.toggleNumberedList()
372
376
  case 9: editorView?.setQuote()
373
377
  case 10: editorView?.setChecklist()
374
- case 11: editorView?.promptInsertLink()
375
- case 12: editorView?.undo()
376
- case 13: editorView?.redo()
377
- case 14: editorView?.clearFormatting()
378
- case 15: editorView?.indent()
379
- case 16: editorView?.outdent()
380
- case 17: editorView?.setAlignment(.left)
381
- case 18: editorView?.setAlignment(.center)
382
- case 19: editorView?.setAlignment(.right)
378
+ case 11: editorView?.openImagePicker()
379
+ case 12: editorView?.promptInsertLink()
380
+ case 13: editorView?.undo()
381
+ case 14: editorView?.redo()
382
+ case 15: editorView?.clearFormatting()
383
+ case 16: editorView?.indent()
384
+ case 17: editorView?.outdent()
385
+ case 18: editorView?.setAlignment(.left)
386
+ case 19: editorView?.setAlignment(.center)
387
+ case 20: editorView?.setAlignment(.right)
383
388
  default: break
384
389
  }
385
390
  editorView?.updateToolbarButtonStates()
@@ -388,18 +393,20 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
388
393
  private let indexToOption: [Int: String] = [
389
394
  0: "bold", 1: "italic", 2: "strikethrough", 3: "underline", 4: "code", 5: "highlight",
390
395
  6: "heading", 7: "bullet", 8: "numbered", 9: "quote", 10: "checklist",
391
- 11: "link", 12: "undo", 13: "redo", 14: "clearFormatting",
392
- 15: "indent", 16: "outdent",
393
- 17: "alignLeft", 18: "alignCenter", 19: "alignRight"
396
+ 11: "mediaAttachment",
397
+ 12: "link", 13: "undo", 14: "redo", 15: "clearFormatting",
398
+ 16: "indent", 17: "outdent",
399
+ 18: "alignLeft", 19: "alignCenter", 20: "alignRight"
394
400
  ]
395
401
 
396
402
  func updateButtonStates(bold: Bool, italic: Bool, underline: Bool, strikethrough: Bool, code: Bool = false, highlight: Bool = false, heading: Bool = false, bullet: Bool, numbered: Bool, quote: Bool = false, checklist: Bool = false, alignLeft: Bool = true, alignCenter: Bool = false, alignRight: Bool = false) {
397
403
  let styleStates: [Int: Bool] = [
398
404
  0: bold, 1: italic, 2: strikethrough, 3: underline, 4: code, 5: highlight,
399
405
  6: heading, 7: bullet, 8: numbered, 9: quote, 10: checklist,
400
- 11: false, 12: false, 13: false, 14: false,
401
- 15: false, 16: false,
402
- 17: alignLeft, 18: alignCenter, 19: alignRight
406
+ 11: false,
407
+ 12: false, 13: false, 14: false, 15: false,
408
+ 16: false, 17: false,
409
+ 18: alignLeft, 19: alignCenter, 20: alignRight
403
410
  ]
404
411
 
405
412
  for button in buttons {
@@ -419,6 +426,9 @@ class FloatingToolbar: UIView, UIScrollViewDelegate {
419
426
  }
420
427
 
421
428
  class RichTextView: UITextView {
429
+ var onPasteImage: ((UIImage) -> Bool)?
430
+ var onPasteImageURL: ((URL) -> Bool)?
431
+
422
432
  override init(frame: CGRect, textContainer: NSTextContainer?) {
423
433
  super.init(frame: frame, textContainer: textContainer)
424
434
  disableAutofill()
@@ -457,13 +467,35 @@ class RichTextView: UITextView {
457
467
  }
458
468
 
459
469
  override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
470
+ if action == #selector(paste(_:)) {
471
+ return true
472
+ }
460
473
  return false
461
474
  }
475
+
476
+ override func paste(_ sender: Any?) {
477
+ let pasteboard = UIPasteboard.general
478
+
479
+ if let image = pasteboard.image,
480
+ onPasteImage?(image) == true {
481
+ return
482
+ }
483
+
484
+ if let url = pasteboard.url {
485
+ if onPasteImageURL?(url) == true {
486
+ return
487
+ }
488
+ }
489
+
490
+ super.paste(sender)
491
+ }
462
492
  }
463
493
 
464
494
  @objcMembers
465
- class RichTextEditorView: UIView, UITextViewDelegate {
495
+ class RichTextEditorView: UIView, UITextViewDelegate, PHPickerViewControllerDelegate, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
466
496
  private static let defaultLineHeightMultiple: CGFloat = 1.3
497
+ private static let mediaPlaceholderCharacter: Character = "\u{FFFC}"
498
+ private static let mediaAttachmentAttributeKey = NSAttributedString.Key("richTextMediaAttachment")
467
499
 
468
500
  private let textView: RichTextView = {
469
501
  let tv = RichTextView()
@@ -512,6 +544,7 @@ class RichTextEditorView: UIView, UITextViewDelegate {
512
544
  private var isInternalChange = false
513
545
  private var currentKeyboardHeight: CGFloat = 0
514
546
  private var savedSelectionRange: NSRange = NSRange(location: 0, length: 0)
547
+ private var isPresentingMediaPicker = false
515
548
 
516
549
  @objc var placeholder: String = "" {
517
550
  didSet { placeholderLabel.text = placeholder }
@@ -632,6 +665,19 @@ class RichTextEditorView: UIView, UITextViewDelegate {
632
665
 
633
666
  textView.delegate = self
634
667
  textView.isScrollEnabled = false
668
+ textView.onPasteImage = { [weak self] image in
669
+ guard let self = self else { return false }
670
+ self.insertPickedImage(image)
671
+ return true
672
+ }
673
+ textView.onPasteImageURL = { [weak self] url in
674
+ guard let self = self else { return false }
675
+ if self.isImageURL(url) {
676
+ self.insertMediaAttachment(uri: url.absoluteString)
677
+ return true
678
+ }
679
+ return false
680
+ }
635
681
 
636
682
  NotificationCenter.default.addObserver(self, selector: #selector(textDidChange), name: UITextView.textDidChangeNotification, object: textView)
637
683
  NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
@@ -1856,6 +1902,254 @@ class RichTextEditorView: UIView, UITextViewDelegate {
1856
1902
  viewController.present(alert, animated: true)
1857
1903
  }
1858
1904
 
1905
+ func openImagePicker() {
1906
+ guard !isPresentingMediaPicker else { return }
1907
+ guard let presenter = topViewController() else { return }
1908
+
1909
+ isPresentingMediaPicker = true
1910
+
1911
+ if #available(iOS 14.0, *) {
1912
+ var config = PHPickerConfiguration(photoLibrary: .shared())
1913
+ config.selectionLimit = 1
1914
+ config.filter = .images
1915
+
1916
+ let picker = PHPickerViewController(configuration: config)
1917
+ picker.delegate = self
1918
+ presenter.present(picker, animated: true)
1919
+ return
1920
+ }
1921
+
1922
+ guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else {
1923
+ isPresentingMediaPicker = false
1924
+ return
1925
+ }
1926
+
1927
+ let picker = UIImagePickerController()
1928
+ picker.sourceType = .photoLibrary
1929
+ picker.mediaTypes = ["public.image"]
1930
+ picker.delegate = self
1931
+ presenter.present(picker, animated: true)
1932
+ }
1933
+
1934
+ @available(iOS 14.0, *)
1935
+ func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
1936
+ picker.dismiss(animated: true) { [weak self] in
1937
+ guard let self = self else { return }
1938
+ defer { self.isPresentingMediaPicker = false }
1939
+
1940
+ guard let result = results.first else { return }
1941
+ let provider = result.itemProvider
1942
+ guard provider.canLoadObject(ofClass: UIImage.self) else { return }
1943
+
1944
+ provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
1945
+ guard let self = self, let image = object as? UIImage else { return }
1946
+ DispatchQueue.main.async {
1947
+ self.insertPickedImage(image)
1948
+ }
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
1954
+ picker.dismiss(animated: true) { [weak self] in
1955
+ self?.isPresentingMediaPicker = false
1956
+ }
1957
+ }
1958
+
1959
+ func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
1960
+ picker.dismiss(animated: true) { [weak self] in
1961
+ guard let self = self else { return }
1962
+ defer { self.isPresentingMediaPicker = false }
1963
+
1964
+ guard let image = info[.originalImage] as? UIImage else { return }
1965
+ self.insertPickedImage(image)
1966
+ }
1967
+ }
1968
+
1969
+ private func topViewController() -> UIViewController? {
1970
+ guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
1971
+ let root = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController ?? windowScene.windows.first?.rootViewController else {
1972
+ return nil
1973
+ }
1974
+
1975
+ var current = root
1976
+ while let presented = current.presentedViewController {
1977
+ current = presented
1978
+ }
1979
+ return current
1980
+ }
1981
+
1982
+ private func insertPickedImage(_ image: UIImage) {
1983
+ guard let imageUrl = writeImageToTemporaryURL(image) else { return }
1984
+ insertMediaAttachmentBlock(uri: imageUrl.absoluteString, image: image)
1985
+ }
1986
+
1987
+ private func writeImageToTemporaryURL(_ image: UIImage) -> URL? {
1988
+ guard let data = image.jpegData(compressionQuality: 0.9) else { return nil }
1989
+ let fileName = "richtext-\(UUID().uuidString).jpg"
1990
+ let fileUrl = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
1991
+
1992
+ do {
1993
+ try data.write(to: fileUrl, options: .atomic)
1994
+ return fileUrl
1995
+ } catch {
1996
+ return nil
1997
+ }
1998
+ }
1999
+
2000
+ private func insertMediaAttachmentBlock(uri: String, image: UIImage?) {
2001
+ let mutable = NSMutableAttributedString(attributedString: textView.attributedText)
2002
+ var insertPos = textView.selectedRange.location
2003
+ insertPos = max(0, min(insertPos, mutable.length))
2004
+
2005
+ let defaultAttrs: [NSAttributedString.Key: Any] = textView.typingAttributes
2006
+
2007
+ if insertPos > 0 {
2008
+ let prevChar = (mutable.string as NSString).character(at: insertPos - 1)
2009
+ if prevChar != 10 {
2010
+ mutable.insert(NSAttributedString(string: "\n", attributes: defaultAttrs), at: insertPos)
2011
+ insertPos += 1
2012
+ }
2013
+ }
2014
+
2015
+ let attachmentString = createMediaAttachmentAttributedString(uri: uri, image: image, width: nil, height: nil, alt: "Selected image")
2016
+ mutable.insert(attachmentString, at: insertPos)
2017
+
2018
+ var nextPos = insertPos + attachmentString.length
2019
+ let hasFollowingText = nextPos < mutable.length
2020
+ if hasFollowingText {
2021
+ let nextChar = (mutable.string as NSString).character(at: nextPos)
2022
+ if nextChar != 10 {
2023
+ mutable.insert(NSAttributedString(string: "\n", attributes: defaultAttrs), at: nextPos)
2024
+ nextPos += 1
2025
+ }
2026
+ }
2027
+
2028
+ isInternalChange = true
2029
+ textView.attributedText = mutable
2030
+ textView.selectedRange = NSRange(location: min(nextPos, mutable.length), length: 0)
2031
+ placeholderLabel.isHidden = !textView.text.isEmpty
2032
+ isInternalChange = false
2033
+
2034
+ sendContentChange()
2035
+ saveToUndoStack()
2036
+ updateContentSize()
2037
+ }
2038
+
2039
+ private func createMediaAttachmentAttributedString(uri: String, image: UIImage?, width: CGFloat?, height: CGFloat?, alt: String) -> NSAttributedString {
2040
+ let textContainerWidth = max(
2041
+ 120,
2042
+ textView.bounds.width - textView.textContainerInset.left - textView.textContainerInset.right - textView.textContainer.lineFragmentPadding * 2
2043
+ )
2044
+
2045
+ let fallbackWidth = width ?? textContainerWidth
2046
+ let normalizedWidth = max(1, min(textContainerWidth, fallbackWidth))
2047
+
2048
+ let sourceImage = image ?? loadImageFromUri(uri)
2049
+ let normalizedHeight: CGFloat
2050
+ if let sourceImage = sourceImage, sourceImage.size.width > 0 {
2051
+ normalizedHeight = max(1, normalizedWidth * (sourceImage.size.height / sourceImage.size.width))
2052
+ } else {
2053
+ normalizedHeight = max(1, height ?? normalizedWidth)
2054
+ }
2055
+
2056
+ let targetSize = CGSize(width: normalizedWidth, height: normalizedHeight)
2057
+ let attachment = NSTextAttachment()
2058
+ attachment.bounds = CGRect(origin: .zero, size: targetSize)
2059
+ attachment.image = renderMediaImage(sourceImage, targetSize: targetSize)
2060
+
2061
+ let attributed = NSMutableAttributedString(attachment: attachment)
2062
+ attributed.addAttribute(
2063
+ RichTextEditorView.mediaAttachmentAttributeKey,
2064
+ value: [
2065
+ "kind": "image",
2066
+ "uri": uri,
2067
+ "width": Int(normalizedWidth.rounded()),
2068
+ "height": Int(normalizedHeight.rounded()),
2069
+ "alt": alt
2070
+ ],
2071
+ range: NSRange(location: 0, length: attributed.length)
2072
+ )
2073
+
2074
+ let paragraphStyle = NSMutableParagraphStyle()
2075
+ paragraphStyle.alignment = .left
2076
+ paragraphStyle.lineHeightMultiple = RichTextEditorView.defaultLineHeightMultiple
2077
+ attributed.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributed.length))
2078
+
2079
+ return attributed
2080
+ }
2081
+
2082
+ private func renderMediaImage(_ image: UIImage?, targetSize: CGSize) -> UIImage {
2083
+ guard let image = image else {
2084
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
2085
+ return renderer.image { context in
2086
+ UIColor.systemGray5.setFill()
2087
+ context.fill(CGRect(origin: .zero, size: targetSize))
2088
+ let iconSize = min(targetSize.width, targetSize.height) * 0.35
2089
+ let iconRect = CGRect(
2090
+ x: (targetSize.width - iconSize) / 2,
2091
+ y: (targetSize.height - iconSize) / 2,
2092
+ width: iconSize,
2093
+ height: iconSize
2094
+ )
2095
+ if let symbol = UIImage(systemName: "photo")?.withTintColor(.systemGray2, renderingMode: .alwaysOriginal) {
2096
+ symbol.draw(in: iconRect)
2097
+ }
2098
+ }
2099
+ }
2100
+
2101
+ let renderer = UIGraphicsImageRenderer(size: targetSize)
2102
+ return renderer.image { _ in
2103
+ image.draw(in: CGRect(origin: .zero, size: targetSize))
2104
+ }
2105
+ }
2106
+
2107
+ private func loadImageFromUri(_ uri: String) -> UIImage? {
2108
+ guard !uri.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
2109
+ let url = URL(string: uri) else {
2110
+ return nil
2111
+ }
2112
+
2113
+ if url.isFileURL {
2114
+ return UIImage(contentsOfFile: url.path)
2115
+ }
2116
+
2117
+ guard let data = try? Data(contentsOf: url) else { return nil }
2118
+ return UIImage(data: data)
2119
+ }
2120
+
2121
+ private func isImageURL(_ url: URL) -> Bool {
2122
+ let lowercasedPath = url.path.lowercased()
2123
+ return lowercasedPath.hasSuffix(".png") ||
2124
+ lowercasedPath.hasSuffix(".jpg") ||
2125
+ lowercasedPath.hasSuffix(".jpeg") ||
2126
+ lowercasedPath.hasSuffix(".webp") ||
2127
+ lowercasedPath.hasSuffix(".gif") ||
2128
+ lowercasedPath.hasSuffix(".bmp") ||
2129
+ lowercasedPath.hasSuffix(".heic") ||
2130
+ lowercasedPath.hasSuffix(".heif")
2131
+ }
2132
+
2133
+ private func extractMediaAttachment(from attributedText: NSAttributedString, in lineRange: NSRange) -> [String: Any]? {
2134
+ guard lineRange.location < attributedText.length else { return nil }
2135
+ let safeLength = min(lineRange.length, attributedText.length - lineRange.location)
2136
+ guard safeLength > 0 else { return nil }
2137
+
2138
+ var mediaData: [String: Any]?
2139
+ attributedText.enumerateAttribute(
2140
+ RichTextEditorView.mediaAttachmentAttributeKey,
2141
+ in: NSRange(location: lineRange.location, length: safeLength),
2142
+ options: []
2143
+ ) { value, _, stop in
2144
+ if let data = value as? [String: Any] {
2145
+ mediaData = data
2146
+ stop.pointee = true
2147
+ }
2148
+ }
2149
+
2150
+ return mediaData
2151
+ }
2152
+
1859
2153
  private func toggleStyle(key: NSAttributedString.Key, trait: UIFontDescriptor.SymbolicTraits) {
1860
2154
  let range = textView.selectedRange
1861
2155
  guard range.length > 0 else { return }
@@ -2010,9 +2304,30 @@ class RichTextEditorView: UIView, UITextViewDelegate {
2010
2304
 
2011
2305
  var numberedIndex = 1
2012
2306
  for (blockIndex, block) in blocks.enumerated() {
2013
- guard let text = block["text"] as? String else { continue }
2014
2307
  let blockType = block["type"] as? String ?? "paragraph"
2015
2308
 
2309
+ if blockType == "mediaAttachment",
2310
+ let mediaAttachment = block["mediaAttachment"] as? [String: Any],
2311
+ let uri = mediaAttachment["uri"] as? String {
2312
+ let width = (mediaAttachment["width"] as? NSNumber).map { CGFloat(truncating: $0) }
2313
+ let height = (mediaAttachment["height"] as? NSNumber).map { CGFloat(truncating: $0) }
2314
+ let alt = mediaAttachment["alt"] as? String ?? "Selected image"
2315
+
2316
+ attributedString.append(createMediaAttachmentAttributedString(uri: uri, image: nil, width: width, height: height, alt: alt))
2317
+
2318
+ if blockIndex < blocks.count - 1 {
2319
+ attributedString.append(NSAttributedString(string: "\n", attributes: [
2320
+ .font: font,
2321
+ .foregroundColor: UIColor.label
2322
+ ]))
2323
+ }
2324
+
2325
+ numberedIndex = 1
2326
+ continue
2327
+ }
2328
+
2329
+ guard let text = block["text"] as? String else { continue }
2330
+
2016
2331
  var displayText = text
2017
2332
  var prefixLength = 0
2018
2333
  let paragraphStyle = NSMutableParagraphStyle()
@@ -2113,6 +2428,20 @@ class RichTextEditorView: UIView, UITextViewDelegate {
2113
2428
  let regex = try? NSRegularExpression(pattern: numberedPattern, options: [])
2114
2429
 
2115
2430
  for line in lines {
2431
+ let lineRange = NSRange(location: currentIndex, length: line.count)
2432
+ let mediaLineText = line.replacingOccurrences(of: String(RichTextEditorView.mediaPlaceholderCharacter), with: "")
2433
+ if mediaLineText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
2434
+ let mediaAttachment = extractMediaAttachment(from: attributedText, in: lineRange) {
2435
+ blocks.append([
2436
+ "type": "mediaAttachment",
2437
+ "text": "",
2438
+ "styles": [],
2439
+ "mediaAttachment": mediaAttachment
2440
+ ])
2441
+ currentIndex += line.count + 1
2442
+ continue
2443
+ }
2444
+
2116
2445
  var blockType = "paragraph"
2117
2446
  var displayText = line
2118
2447
 
@@ -2205,6 +2534,13 @@ class RichTextEditorView: UIView, UITextViewDelegate {
2205
2534
  sendContentChange()
2206
2535
  }
2207
2536
 
2537
+ func insertMediaAttachment(uri: String) {
2538
+ let safeUri = uri.trimmingCharacters(in: .whitespacesAndNewlines)
2539
+ guard !safeUri.isEmpty else { return }
2540
+ let image = loadImageFromUri(safeUri)
2541
+ insertMediaAttachmentBlock(uri: safeUri, image: image)
2542
+ }
2543
+
2208
2544
  func undo() {
2209
2545
  guard undoStack.count > 1 else { return }
2210
2546
 
@@ -24,6 +24,7 @@ RCT_EXTERN_METHOD(clear:(nonnull NSNumber *)node)
24
24
  RCT_EXTERN_METHOD(focus:(nonnull NSNumber *)node)
25
25
  RCT_EXTERN_METHOD(blur:(nonnull NSNumber *)node)
26
26
  RCT_EXTERN_METHOD(insertLink:(nonnull NSNumber *)node url:(nonnull NSString *)url text:(nonnull NSString *)text)
27
+ RCT_EXTERN_METHOD(insertMediaAttachment:(nonnull NSNumber *)node uri:(nonnull NSString *)uri)
27
28
  RCT_EXTERN_METHOD(undo:(nonnull NSNumber *)node)
28
29
  RCT_EXTERN_METHOD(redo:(nonnull NSNumber *)node)
29
30
  RCT_EXTERN_METHOD(toggleBold:(nonnull NSNumber *)node)
@@ -72,6 +72,14 @@ class RichTextEditorViewManager: RCTViewManager {
72
72
  }
73
73
  }
74
74
 
75
+ @objc func insertMediaAttachment(_ node: NSNumber, uri: NSString) {
76
+ DispatchQueue.main.async {
77
+ if let view = self.bridge?.uiManager.view(forReactTag: node) as? RichTextEditorView {
78
+ view.insertMediaAttachment(uri: uri as String)
79
+ }
80
+ }
81
+ }
82
+
75
83
  @objc func undo(_ node: NSNumber) {
76
84
  DispatchQueue.main.async {
77
85
  if let view = self.bridge?.uiManager.view(forReactTag: node) as? RichTextEditorView {
@@ -17,6 +17,7 @@ class ToolbarIcons {
17
17
  case "numbered": return drawNumberedListIcon(color: color, size: size)
18
18
  case "quote": return drawQuoteIcon(color: color, size: size)
19
19
  case "checklist": return drawChecklistIcon(color: color, size: size)
20
+ case "mediaAttachment": return drawMediaAttachmentIcon(color: color, size: size)
20
21
  case "link": return drawLinkIcon(color: color, size: size)
21
22
  case "undo": return drawUndoIcon(color: color, size: size)
22
23
  case "redo": return drawRedoIcon(color: color, size: size)
@@ -382,6 +383,29 @@ class ToolbarIcons {
382
383
  }
383
384
  }
384
385
 
386
+ private static func drawMediaAttachmentIcon(color: UIColor, size: CGSize) -> UIImage {
387
+ return drawIcon(size: size, color: color) { ctx, scale in
388
+ let frame = CGRect(x: 3 * scale, y: 5 * scale, width: 18 * scale, height: 14 * scale)
389
+ let framePath = UIBezierPath(roundedRect: frame, cornerRadius: 1.8 * scale)
390
+ ctx.setLineWidth(1.8 * scale)
391
+ ctx.addPath(framePath.cgPath)
392
+ ctx.strokePath()
393
+
394
+ let sunPath = UIBezierPath(arcCenter: CGPoint(x: 8 * scale, y: 10 * scale), radius: 1.5 * scale, startAngle: 0, endAngle: .pi * 2, clockwise: true)
395
+ sunPath.fill()
396
+
397
+ let mountain = UIBezierPath()
398
+ mountain.move(to: CGPoint(x: 6 * scale, y: 16 * scale))
399
+ mountain.addLine(to: CGPoint(x: 10.5 * scale, y: 11.5 * scale))
400
+ mountain.addLine(to: CGPoint(x: 13 * scale, y: 14 * scale))
401
+ mountain.addLine(to: CGPoint(x: 16 * scale, y: 11 * scale))
402
+ mountain.addLine(to: CGPoint(x: 18 * scale, y: 13 * scale))
403
+ mountain.addLine(to: CGPoint(x: 18 * scale, y: 16 * scale))
404
+ mountain.close()
405
+ mountain.fill()
406
+ }
407
+ }
408
+
385
409
  private static func drawLinkIcon(color: UIColor, size: CGSize) -> UIImage {
386
410
  return drawIcon(size: size, color: color) { ctx, scale in
387
411
  let path = UIBezierPath()
@@ -10,28 +10,51 @@ Object.defineProperty(exports, "DEFAULT_TOOLBAR_OPTIONS", {
10
10
  }
11
11
  });
12
12
  exports.default = void 0;
13
- var _react = _interopRequireWildcard(require("react"));
13
+ var _react = _interopRequireDefault(require("react"));
14
14
  var _reactNative = require("react-native");
15
15
  var _RichTextEditorViewNativeComponent = _interopRequireDefault(require("./RichTextEditorViewNativeComponent"));
16
16
  var _types = require("./types");
17
17
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
- function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
19
- const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
20
- const nativeRef = (0, _react.useRef)(null);
21
- const [height, setHeight] = (0, _react.useState)(44);
22
- const handleSizeChange = (0, _react.useCallback)(event => {
18
+ const RichTextEditor = /*#__PURE__*/_react.default.forwardRef((props, ref) => {
19
+ const nativeRef = _react.default.useRef(null);
20
+ const [height, setHeight] = _react.default.useState(44);
21
+ const handleSizeChange = _react.default.useCallback(event => {
23
22
  const newHeight = event.nativeEvent?.height;
24
23
  if (newHeight && newHeight > 0) {
25
24
  setHeight(newHeight);
26
25
  }
27
26
  }, []);
27
+ const dispatchAndroidCommand = _react.default.useCallback((commandName, args = []) => {
28
+ if (_reactNative.Platform.OS !== "android") return;
29
+ const nativeTag = (0, _reactNative.findNodeHandle)(nativeRef.current);
30
+ if (nativeTag == null) return;
31
+ const commandConfig = _reactNative.UIManager.getViewManagerConfig("RichTextEditorView")?.Commands;
32
+ const commandId = commandConfig?.[commandName];
33
+ if (commandId == null) return;
34
+ _reactNative.UIManager.dispatchViewManagerCommand(nativeTag, commandId, args);
35
+ }, []);
36
+ const dispatchInsertMediaAttachment = _react.default.useCallback(uri => {
37
+ if (typeof uri !== "string" || uri.trim().length === 0) return;
38
+ if (_reactNative.Platform.OS === "android") {
39
+ dispatchAndroidCommand("insertMediaAttachment", [uri]);
40
+ return;
41
+ }
42
+ if (_reactNative.Platform.OS === "ios") {
43
+ const nativeTag = (0, _reactNative.findNodeHandle)(nativeRef.current);
44
+ if (nativeTag == null) return;
45
+ const manager = _reactNative.NativeModules ? _reactNative.NativeModules["RichTextEditorViewManager"] : null;
46
+ if (manager && typeof manager === "object" && typeof manager.insertMediaAttachment === "function") {
47
+ manager.insertMediaAttachment(nativeTag, uri);
48
+ }
49
+ }
50
+ }, [dispatchAndroidCommand]);
28
51
 
29
52
  // These are placeholder methods for potential future UIManager.dispatchViewManagerCommand usage
30
- (0, _react.useImperativeHandle)(ref, () => ({
53
+ _react.default.useImperativeHandle(ref, () => ({
31
54
  setContent: _blocks => {
32
55
  /* Native toolbar handles this */
33
56
  },
34
- getText: async () => '',
57
+ getText: async () => "",
35
58
  getBlocks: async () => [],
36
59
  clear: () => {
37
60
  /* Native toolbar handles this */
@@ -81,6 +104,11 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
81
104
  insertLink: (_url, _text) => {
82
105
  /* Native toolbar handles this */
83
106
  },
107
+ insertMediaAttachment: mediaAttachment => {
108
+ if (mediaAttachment && typeof mediaAttachment === "object" && typeof mediaAttachment.uri === "string") {
109
+ dispatchInsertMediaAttachment(mediaAttachment.uri);
110
+ }
111
+ },
84
112
  undo: () => {
85
113
  /* Native toolbar handles this */
86
114
  },
@@ -102,10 +130,10 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
102
130
  toggleChecklistItem: () => {
103
131
  /* Native toolbar handles this */
104
132
  }
105
- }));
133
+ }), [dispatchInsertMediaAttachment]);
106
134
 
107
135
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
- const handleContentChange = (0, _react.useCallback)(event => {
136
+ const handleContentChange = _react.default.useCallback(event => {
109
137
  // Parse blocksJson string (codegen doesn't support nested ReadonlyArray<Object>)
110
138
  let blocks = [];
111
139
  try {
@@ -131,7 +159,7 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
131
159
  }, [props.onContentChange]);
132
160
 
133
161
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
- const handleSelectionChange = (0, _react.useCallback)(event => {
162
+ const handleSelectionChange = _react.default.useCallback(event => {
135
163
  const selectionEvent = {
136
164
  nativeEvent: {
137
165
  start: event.nativeEvent.start,
@@ -140,13 +168,13 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
140
168
  };
141
169
  props.onSelectionChange?.(selectionEvent);
142
170
  }, [props.onSelectionChange]);
143
- const handleFocus = (0, _react.useCallback)(() => {
171
+ const handleFocus = _react.default.useCallback(() => {
144
172
  props.onFocus?.();
145
173
  }, [props.onFocus]);
146
- const handleBlur = (0, _react.useCallback)(() => {
174
+ const handleBlur = _react.default.useCallback(() => {
147
175
  props.onBlur?.();
148
176
  }, [props.onBlur]);
149
- const handleActiveStylesChange = (0, _react.useCallback)(event => {
177
+ const handleActiveStylesChange = _react.default.useCallback(event => {
150
178
  props.onActiveStylesChange?.(event.nativeEvent);
151
179
  }, [props.onActiveStylesChange]);
152
180
  const combinedStyle = _reactNative.StyleSheet.flatten([props.style, {
@@ -162,7 +190,7 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
162
190
  numberOfLines: props.numberOfLines,
163
191
  showToolbar: props.readOnly ? false : props.showToolbar ?? true,
164
192
  toolbarOptions: props.toolbarOptions,
165
- variant: props.variant ?? 'outlined',
193
+ variant: props.variant ?? "outlined",
166
194
  onContentChange: handleContentChange,
167
195
  onSelectionChange: handleSelectionChange,
168
196
  onEditorFocus: handleFocus,
@@ -171,6 +199,6 @@ const RichTextEditor = /*#__PURE__*/(0, _react.forwardRef)((props, ref) => {
171
199
  onActiveStylesChange: handleActiveStylesChange
172
200
  });
173
201
  });
174
- RichTextEditor.displayName = 'RichTextEditor';
202
+ RichTextEditor.displayName = "RichTextEditor";
175
203
  var _default = exports.default = RichTextEditor;
176
204
  //# sourceMappingURL=index.js.map