@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.
- package/README.md +59 -3
- package/android/src/main/java/com/richtext/editor/FloatingToolbar.kt +7 -0
- package/android/src/main/java/com/richtext/editor/MediaAttachmentSpan.kt +85 -0
- package/android/src/main/java/com/richtext/editor/MediaAttachmentSupport.kt +296 -0
- package/android/src/main/java/com/richtext/editor/RichTextEditorView.kt +313 -6
- package/android/src/main/java/com/richtext/editor/RichTextEditorViewManager.kt +37 -0
- package/android/src/main/res/drawable/ic_format_media_attachment.xml +9 -0
- package/ios/RichTextEditorView.swift +356 -20
- package/ios/RichTextEditorViewManager.m +1 -0
- package/ios/RichTextEditorViewManager.swift +8 -0
- package/ios/ToolbarIcons.swift +24 -0
- package/lib/commonjs/index.js +44 -16
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/types.js +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/index.js +47 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/types.js +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +16 -7
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +175 -94
- package/src/types.ts +72 -46
|
@@ -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
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
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?.
|
|
375
|
-
case 12: editorView?.
|
|
376
|
-
case 13: editorView?.
|
|
377
|
-
case 14: editorView?.
|
|
378
|
-
case 15: editorView?.
|
|
379
|
-
case 16: editorView?.
|
|
380
|
-
case 17: editorView?.
|
|
381
|
-
case 18: editorView?.setAlignment(.
|
|
382
|
-
case 19: editorView?.setAlignment(.
|
|
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: "
|
|
392
|
-
|
|
393
|
-
|
|
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,
|
|
401
|
-
|
|
402
|
-
|
|
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 {
|
package/ios/ToolbarIcons.swift
CHANGED
|
@@ -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()
|
package/lib/commonjs/index.js
CHANGED
|
@@ -10,28 +10,51 @@ Object.defineProperty(exports, "DEFAULT_TOOLBAR_OPTIONS", {
|
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
12
|
exports.default = void 0;
|
|
13
|
-
var _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
|
-
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
171
|
+
const handleFocus = _react.default.useCallback(() => {
|
|
144
172
|
props.onFocus?.();
|
|
145
173
|
}, [props.onFocus]);
|
|
146
|
-
const handleBlur =
|
|
174
|
+
const handleBlur = _react.default.useCallback(() => {
|
|
147
175
|
props.onBlur?.();
|
|
148
176
|
}, [props.onBlur]);
|
|
149
|
-
const handleActiveStylesChange =
|
|
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 ??
|
|
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 =
|
|
202
|
+
RichTextEditor.displayName = "RichTextEditor";
|
|
175
203
|
var _default = exports.default = RichTextEditor;
|
|
176
204
|
//# sourceMappingURL=index.js.map
|