@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,6 +1,7 @@
|
|
|
1
1
|
package com.richtext.editor
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.content.ClipboardManager
|
|
4
5
|
import android.graphics.Canvas
|
|
5
6
|
import android.graphics.Color
|
|
6
7
|
import android.graphics.Paint
|
|
@@ -20,11 +21,22 @@ import android.view.View
|
|
|
20
21
|
import android.view.ViewGroup
|
|
21
22
|
import android.view.WindowManager
|
|
22
23
|
import android.view.inputmethod.EditorInfo
|
|
24
|
+
import android.view.inputmethod.InputConnection
|
|
23
25
|
import android.widget.PopupWindow
|
|
24
26
|
import android.widget.FrameLayout
|
|
25
27
|
import android.app.AlertDialog
|
|
28
|
+
import android.net.Uri
|
|
26
29
|
import android.widget.EditText
|
|
27
30
|
import android.widget.LinearLayout
|
|
31
|
+
import android.webkit.MimeTypeMap
|
|
32
|
+
import androidx.activity.ComponentActivity
|
|
33
|
+
import androidx.activity.result.ActivityResultLauncher
|
|
34
|
+
import androidx.activity.result.contract.ActivityResultContracts
|
|
35
|
+
import androidx.core.view.ContentInfoCompat
|
|
36
|
+
import androidx.core.view.ViewCompat
|
|
37
|
+
import androidx.core.view.inputmethod.EditorInfoCompat
|
|
38
|
+
import androidx.core.view.inputmethod.InputConnectionCompat
|
|
39
|
+
import androidx.core.view.inputmethod.InputContentInfoCompat
|
|
28
40
|
import com.facebook.react.bridge.Arguments
|
|
29
41
|
import com.facebook.react.bridge.ReactContext
|
|
30
42
|
import com.facebook.react.bridge.WritableArray
|
|
@@ -34,6 +46,10 @@ import com.facebook.react.uimanager.events.RCTEventEmitter
|
|
|
34
46
|
class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompatEditText(context),
|
|
35
47
|
FloatingToolbar.ToolbarActionListener {
|
|
36
48
|
|
|
49
|
+
companion object {
|
|
50
|
+
private const val MEDIA_PLACEHOLDER_CHAR = '\uFFFC'
|
|
51
|
+
}
|
|
52
|
+
|
|
37
53
|
private var placeholder: String = ""
|
|
38
54
|
private var maxHeightValue: Int = 0
|
|
39
55
|
private var numberOfLinesValue: Int = 0
|
|
@@ -49,6 +65,32 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
49
65
|
private var previousText: String = ""
|
|
50
66
|
private var pendingDelta: Map<String, Any>? = null
|
|
51
67
|
private var pendingPrefixDeletion: Pair<Int, Int>? = null // (lineStart, prefixLength) for backspace-in-prefix
|
|
68
|
+
private var imagePickerLauncher: ActivityResultLauncher<String>? = null
|
|
69
|
+
private val imagePickerLauncherKey = "richtext_image_picker_${hashCode()}"
|
|
70
|
+
private val mediaAttachmentSupport by lazy {
|
|
71
|
+
MediaAttachmentSupport(
|
|
72
|
+
context = context,
|
|
73
|
+
density = density,
|
|
74
|
+
placeholderChar = MEDIA_PLACEHOLDER_CHAR,
|
|
75
|
+
getLineSpacingMultiplier = { lineSpacingMultiplier },
|
|
76
|
+
getTargetWidthPx = {
|
|
77
|
+
val contentWidth = width - totalPaddingLeft - totalPaddingRight
|
|
78
|
+
if (contentWidth > 0) {
|
|
79
|
+
contentWidth
|
|
80
|
+
} else {
|
|
81
|
+
(context.resources.displayMetrics.widthPixels - totalPaddingLeft - totalPaddingRight)
|
|
82
|
+
.coerceAtLeast((120 * density).toInt())
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
editableProvider = { text },
|
|
86
|
+
runOnUiThread = { action -> post(action) },
|
|
87
|
+
onMediaSpansUpdated = {
|
|
88
|
+
invalidate()
|
|
89
|
+
requestLayout()
|
|
90
|
+
post { updateContentSize() }
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
}
|
|
52
94
|
|
|
53
95
|
// For flat variant bottom border
|
|
54
96
|
private val bottomBorderPaint = Paint().apply {
|
|
@@ -103,6 +145,7 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
103
145
|
isFocusable = true
|
|
104
146
|
isFocusableInTouchMode = true
|
|
105
147
|
inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
|
|
148
|
+
setupImageReceiveContentHandler()
|
|
106
149
|
|
|
107
150
|
// Disable vertical scrolling by default
|
|
108
151
|
isVerticalScrollBarEnabled = false
|
|
@@ -168,6 +211,9 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
168
211
|
if (!handled) {
|
|
169
212
|
handled = autoContinueListOnEnter(s)
|
|
170
213
|
}
|
|
214
|
+
if (!handled) {
|
|
215
|
+
handled = applyInlineStyleShortcut(s)
|
|
216
|
+
}
|
|
171
217
|
if (!handled) {
|
|
172
218
|
isInternalChange = true
|
|
173
219
|
renumberNumberedLists()
|
|
@@ -194,6 +240,51 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
194
240
|
isInitialized = true
|
|
195
241
|
}
|
|
196
242
|
|
|
243
|
+
// Handle image pasting from clipboard and input method
|
|
244
|
+
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
|
|
245
|
+
val inputConnection = super.onCreateInputConnection(outAttrs) ?: return null
|
|
246
|
+
EditorInfoCompat.setContentMimeTypes(outAttrs, arrayOf("image/*"))
|
|
247
|
+
|
|
248
|
+
return InputConnectionCompat.createWrapper(
|
|
249
|
+
inputConnection,
|
|
250
|
+
outAttrs
|
|
251
|
+
) { inputContentInfo: InputContentInfoCompat, flags: Int, _ ->
|
|
252
|
+
if ((flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
|
|
253
|
+
runCatching { inputContentInfo.requestPermission() }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
val uri = inputContentInfo.contentUri
|
|
257
|
+
if (isImageUri(uri)) {
|
|
258
|
+
post { insertMediaAttachmentBlock(uri.toString()) }
|
|
259
|
+
true
|
|
260
|
+
} else {
|
|
261
|
+
false
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Handle receiving images via drag-and-drop or clipboard paste (Android 13+)
|
|
267
|
+
private fun setupImageReceiveContentHandler() {
|
|
268
|
+
ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*")) { _, payload: ContentInfoCompat ->
|
|
269
|
+
val clip = payload.clip
|
|
270
|
+
if (clip == null || clip.itemCount == 0) {
|
|
271
|
+
return@setOnReceiveContentListener payload
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
var handled = false
|
|
275
|
+
for (index in 0 until clip.itemCount) {
|
|
276
|
+
val item = clip.getItemAt(index)
|
|
277
|
+
val uri = item.uri
|
|
278
|
+
if (uri != null && isImageUri(uri)) {
|
|
279
|
+
insertMediaAttachmentBlock(uri.toString())
|
|
280
|
+
handled = true
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (handled) null else payload
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
197
288
|
private fun setupToolbar() {
|
|
198
289
|
floatingToolbar = FloatingToolbar(context).apply {
|
|
199
290
|
listener = this@RichTextEditorView
|
|
@@ -571,6 +662,49 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
571
662
|
return super.onTouchEvent(event)
|
|
572
663
|
}
|
|
573
664
|
|
|
665
|
+
// Handle image pasting from clipboard (Android 13+ also sends via onReceiveContent)
|
|
666
|
+
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
667
|
+
val isPasteAction = id == android.R.id.paste || id == android.R.id.pasteAsPlainText
|
|
668
|
+
if (!isPasteAction) {
|
|
669
|
+
return super.onTextContextMenuItem(id)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
|
|
673
|
+
?: return super.onTextContextMenuItem(id)
|
|
674
|
+
val primaryClip = clipboard.primaryClip ?: return super.onTextContextMenuItem(id)
|
|
675
|
+
|
|
676
|
+
val imageUris = mutableListOf<Uri>()
|
|
677
|
+
for (index in 0 until primaryClip.itemCount) {
|
|
678
|
+
val item = primaryClip.getItemAt(index)
|
|
679
|
+
item.uri?.let { uri ->
|
|
680
|
+
if (isImageUri(uri)) {
|
|
681
|
+
imageUris.add(uri)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
val maybeUriText = item.text?.toString()?.trim().orEmpty()
|
|
686
|
+
if (maybeUriText.isNotEmpty()) {
|
|
687
|
+
runCatching { Uri.parse(maybeUriText) }
|
|
688
|
+
.getOrNull()
|
|
689
|
+
?.let { parsed ->
|
|
690
|
+
if (parsed.scheme != null && isImageUri(parsed)) {
|
|
691
|
+
imageUris.add(parsed)
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (imageUris.isEmpty()) {
|
|
698
|
+
return super.onTextContextMenuItem(id)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
imageUris
|
|
702
|
+
.distinctBy { it.toString() }
|
|
703
|
+
.forEach { insertMediaAttachmentBlock(it.toString()) }
|
|
704
|
+
|
|
705
|
+
return true
|
|
706
|
+
}
|
|
707
|
+
|
|
574
708
|
private fun selectWordAtPosition(x: Float, y: Float) {
|
|
575
709
|
val layout = layout ?: return
|
|
576
710
|
val textContent = text?.toString() ?: return
|
|
@@ -601,6 +735,38 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
601
735
|
}
|
|
602
736
|
}
|
|
603
737
|
|
|
738
|
+
private fun isImageUri(uri: Uri): Boolean {
|
|
739
|
+
val scheme = uri.scheme?.lowercase()
|
|
740
|
+
|
|
741
|
+
if (scheme == "http" || scheme == "https") {
|
|
742
|
+
val path = uri.path.orEmpty()
|
|
743
|
+
return path.endsWith(".png", true) ||
|
|
744
|
+
path.endsWith(".jpg", true) ||
|
|
745
|
+
path.endsWith(".jpeg", true) ||
|
|
746
|
+
path.endsWith(".webp", true) ||
|
|
747
|
+
path.endsWith(".gif", true) ||
|
|
748
|
+
path.endsWith(".bmp", true)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
val mimeType = runCatching {
|
|
752
|
+
context.contentResolver.getType(uri)
|
|
753
|
+
}.getOrNull()
|
|
754
|
+
|
|
755
|
+
if (mimeType?.startsWith("image/") == true) {
|
|
756
|
+
return true
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
val extension = MimeTypeMap.getFileExtensionFromUrl(uri.toString())
|
|
760
|
+
if (!extension.isNullOrEmpty()) {
|
|
761
|
+
val guessedMime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.lowercase())
|
|
762
|
+
if (guessedMime?.startsWith("image/") == true) {
|
|
763
|
+
return true
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return false
|
|
768
|
+
}
|
|
769
|
+
|
|
604
770
|
override fun onDraw(canvas: Canvas) {
|
|
605
771
|
super.onDraw(canvas)
|
|
606
772
|
|
|
@@ -878,6 +1044,18 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
878
1044
|
val textContent = block["text"] as? String ?: ""
|
|
879
1045
|
val blockType = block["type"] as? String ?: "paragraph"
|
|
880
1046
|
|
|
1047
|
+
if (blockType == "mediaAttachment") {
|
|
1048
|
+
numberedListCounter = 1
|
|
1049
|
+
val mediaData = mediaAttachmentSupport.parseMediaData(block) ?: return@forEachIndexed
|
|
1050
|
+
currentOffset = mediaAttachmentSupport.appendMediaBlock(
|
|
1051
|
+
spannable = spannable,
|
|
1052
|
+
currentOffset = currentOffset,
|
|
1053
|
+
mediaData = mediaData,
|
|
1054
|
+
appendTrailingNewline = index < blocks.size - 1
|
|
1055
|
+
)
|
|
1056
|
+
return@forEachIndexed
|
|
1057
|
+
}
|
|
1058
|
+
|
|
881
1059
|
// Add list prefix based on block type
|
|
882
1060
|
val prefix = when (blockType) {
|
|
883
1061
|
"bullet", "bulletList" -> "• "
|
|
@@ -1042,6 +1220,19 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1042
1220
|
val lines = textContent.split("\n")
|
|
1043
1221
|
var currentIndex = 0
|
|
1044
1222
|
lines.forEach { line ->
|
|
1223
|
+
val lineStart = currentIndex
|
|
1224
|
+
val lineEnd = currentIndex + line.length
|
|
1225
|
+
|
|
1226
|
+
val mediaSpan = mediaAttachmentSupport.findMediaAttachmentSpan(spannable, lineStart, lineEnd)
|
|
1227
|
+
if (mediaSpan != null && mediaAttachmentSupport.isMediaLine(line)) {
|
|
1228
|
+
val mediaData = mediaSpan.toMediaAttachmentData()
|
|
1229
|
+
val block = mediaAttachmentSupport.createWritableMediaBlock(mediaData)
|
|
1230
|
+
|
|
1231
|
+
blocks.pushMap(block)
|
|
1232
|
+
currentIndex += line.length + 1
|
|
1233
|
+
return@forEach
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1045
1236
|
val (blockType, displayText) = detectBlockType(line)
|
|
1046
1237
|
val prefixLen = line.length - displayText.length
|
|
1047
1238
|
|
|
@@ -1050,9 +1241,8 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1050
1241
|
block.putString("text", displayText)
|
|
1051
1242
|
|
|
1052
1243
|
val stylesArray = Arguments.createArray()
|
|
1053
|
-
val
|
|
1054
|
-
|
|
1055
|
-
extractStylesForRange(spannable, lineStart, lineEnd).forEach { style ->
|
|
1244
|
+
val styleStart = currentIndex + prefixLen
|
|
1245
|
+
extractStylesForRange(spannable, styleStart, lineEnd).forEach { style ->
|
|
1056
1246
|
val styleMap = Arguments.createMap()
|
|
1057
1247
|
styleMap.putString("style", style["style"] as String)
|
|
1058
1248
|
styleMap.putInt("start", style["start"] as Int)
|
|
@@ -1076,6 +1266,19 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1076
1266
|
val lines = textContent.split("\n")
|
|
1077
1267
|
var currentIndex = 0
|
|
1078
1268
|
lines.forEach { line ->
|
|
1269
|
+
val lineStart = currentIndex
|
|
1270
|
+
val lineEnd = currentIndex + line.length
|
|
1271
|
+
|
|
1272
|
+
val mediaSpan = mediaAttachmentSupport.findMediaAttachmentSpan(spannable, lineStart, lineEnd)
|
|
1273
|
+
if (mediaSpan != null && mediaAttachmentSupport.isMediaLine(line)) {
|
|
1274
|
+
val mediaData = mediaSpan.toMediaAttachmentData()
|
|
1275
|
+
val block = mediaAttachmentSupport.createJsonMediaBlock(mediaData)
|
|
1276
|
+
|
|
1277
|
+
jsonArray.put(block)
|
|
1278
|
+
currentIndex += line.length + 1
|
|
1279
|
+
return@forEach
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1079
1282
|
val (blockType, displayText) = detectBlockType(line)
|
|
1080
1283
|
val prefixLen = line.length - displayText.length
|
|
1081
1284
|
|
|
@@ -1084,9 +1287,8 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1084
1287
|
block.put("text", displayText)
|
|
1085
1288
|
|
|
1086
1289
|
val stylesJson = org.json.JSONArray()
|
|
1087
|
-
val
|
|
1088
|
-
|
|
1089
|
-
extractStylesForRange(spannable, lineStart, lineEnd).forEach { style ->
|
|
1290
|
+
val styleStart = currentIndex + prefixLen
|
|
1291
|
+
extractStylesForRange(spannable, styleStart, lineEnd).forEach { style ->
|
|
1090
1292
|
val styleObj = org.json.JSONObject()
|
|
1091
1293
|
styleObj.put("style", style["style"])
|
|
1092
1294
|
styleObj.put("start", style["start"])
|
|
@@ -1244,6 +1446,10 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1244
1446
|
toggleChecklistPrefix()
|
|
1245
1447
|
}
|
|
1246
1448
|
|
|
1449
|
+
override fun onMediaAttachmentClick() {
|
|
1450
|
+
openImagePicker()
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1247
1453
|
override fun onLinkClick() {
|
|
1248
1454
|
promptInsertLink()
|
|
1249
1455
|
}
|
|
@@ -1533,6 +1739,64 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1533
1739
|
return false
|
|
1534
1740
|
}
|
|
1535
1741
|
|
|
1742
|
+
private fun applyInlineStyleShortcut(s: Editable?): Boolean {
|
|
1743
|
+
if (s == null) return false
|
|
1744
|
+
|
|
1745
|
+
val start = selectionStart
|
|
1746
|
+
val end = selectionEnd
|
|
1747
|
+
if (start != end || start <= 0 || start > s.length) return false
|
|
1748
|
+
|
|
1749
|
+
var lineStart = start - 1
|
|
1750
|
+
while (lineStart > 0 && s[lineStart - 1] != '\n') {
|
|
1751
|
+
lineStart--
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
val textBeforeCursor = s.subSequence(lineStart, start).toString()
|
|
1755
|
+
if (textBeforeCursor.length < 3) return false
|
|
1756
|
+
|
|
1757
|
+
data class ShortcutPattern(
|
|
1758
|
+
val regex: Regex,
|
|
1759
|
+
val apply: (Editable, Int, Int) -> Unit,
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
val patterns = listOf(
|
|
1763
|
+
ShortcutPattern(Regex("(^|\\s)\\*([^*\\n]+)\\*$")) { editable, spanStart, spanEnd ->
|
|
1764
|
+
editable.setSpan(StyleSpan(Typeface.BOLD), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
1765
|
+
},
|
|
1766
|
+
ShortcutPattern(Regex("(^|\\s)_([^_\\n]+)_$")) { editable, spanStart, spanEnd ->
|
|
1767
|
+
editable.setSpan(StyleSpan(Typeface.ITALIC), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
1768
|
+
},
|
|
1769
|
+
ShortcutPattern(Regex("(^|\\s)~([^~\\n]+)~$")) { editable, spanStart, spanEnd ->
|
|
1770
|
+
editable.setSpan(StrikethroughSpan(), spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
1771
|
+
}
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
for (pattern in patterns) {
|
|
1775
|
+
val match = pattern.regex.find(textBeforeCursor) ?: continue
|
|
1776
|
+
val prefixWhitespaceLen = match.groupValues[1].length
|
|
1777
|
+
val styledText = match.groupValues[2]
|
|
1778
|
+
if (styledText.isEmpty()) continue
|
|
1779
|
+
|
|
1780
|
+
val markerStartInLine = match.range.first + prefixWhitespaceLen
|
|
1781
|
+
val markerStart = lineStart + markerStartInLine
|
|
1782
|
+
if (markerStart < 0 || markerStart > start) continue
|
|
1783
|
+
|
|
1784
|
+
isInternalChange = true
|
|
1785
|
+
s.replace(markerStart, start, styledText)
|
|
1786
|
+
|
|
1787
|
+
val styleStart = markerStart
|
|
1788
|
+
val styleEnd = markerStart + styledText.length
|
|
1789
|
+
if (styleStart < styleEnd && styleEnd <= s.length) {
|
|
1790
|
+
pattern.apply(s, styleStart, styleEnd)
|
|
1791
|
+
setSelection(styleEnd)
|
|
1792
|
+
}
|
|
1793
|
+
isInternalChange = false
|
|
1794
|
+
return true
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
return false
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1536
1800
|
private fun renumberNumberedLists() {
|
|
1537
1801
|
val editable = text ?: return
|
|
1538
1802
|
val fullText = editable.toString()
|
|
@@ -1621,6 +1885,47 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1621
1885
|
}
|
|
1622
1886
|
}
|
|
1623
1887
|
|
|
1888
|
+
private fun openImagePicker() {
|
|
1889
|
+
val reactContext = context as? ReactContext ?: return
|
|
1890
|
+
val activity = reactContext.currentActivity ?: return
|
|
1891
|
+
|
|
1892
|
+
if (activity !is ComponentActivity) {
|
|
1893
|
+
return
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
if (imagePickerLauncher == null) {
|
|
1897
|
+
imagePickerLauncher = activity.activityResultRegistry.register(
|
|
1898
|
+
imagePickerLauncherKey,
|
|
1899
|
+
ActivityResultContracts.GetContent()
|
|
1900
|
+
) { uri: Uri? ->
|
|
1901
|
+
if (uri == null) return@register
|
|
1902
|
+
insertMediaAttachmentBlock(uri.toString())
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
imagePickerLauncher?.launch("image/*")
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
private fun insertMediaAttachmentBlock(uri: String) {
|
|
1910
|
+
val editable = text ?: return
|
|
1911
|
+
var insertPos = selectionStart.coerceIn(0, editable.length)
|
|
1912
|
+
|
|
1913
|
+
isInternalChange = true
|
|
1914
|
+
val nextPos = mediaAttachmentSupport.insertMediaAttachmentBlock(editable, insertPos, uri)
|
|
1915
|
+
setSelection(nextPos.coerceAtMost(editable.length))
|
|
1916
|
+
|
|
1917
|
+
isInternalChange = false
|
|
1918
|
+
sendContentChange()
|
|
1919
|
+
saveToUndoStack()
|
|
1920
|
+
post { updateContentSize() }
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
fun insertMediaAttachment(uri: String?) {
|
|
1924
|
+
val safeUri = uri?.trim().orEmpty()
|
|
1925
|
+
if (safeUri.isEmpty()) return
|
|
1926
|
+
insertMediaAttachmentBlock(safeUri)
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1624
1929
|
private fun promptInsertLink() {
|
|
1625
1930
|
val context = context
|
|
1626
1931
|
val builder = AlertDialog.Builder(context)
|
|
@@ -1828,6 +2133,8 @@ class RichTextEditorView(context: Context) : androidx.appcompat.widget.AppCompat
|
|
|
1828
2133
|
|
|
1829
2134
|
override fun onDetachedFromWindow() {
|
|
1830
2135
|
super.onDetachedFromWindow()
|
|
2136
|
+
imagePickerLauncher?.unregister()
|
|
2137
|
+
imagePickerLauncher = null
|
|
1831
2138
|
hideToolbar()
|
|
1832
2139
|
toolbarPopup = null
|
|
1833
2140
|
floatingToolbar = null
|
|
@@ -11,14 +11,39 @@ class RichTextEditorViewManager : SimpleViewManager<RichTextEditorView>() {
|
|
|
11
11
|
|
|
12
12
|
companion object {
|
|
13
13
|
const val NAME = "RichTextEditorView"
|
|
14
|
+
private const val COMMAND_INSERT_MEDIA_ATTACHMENT = 1
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
override fun getName(): String = NAME
|
|
17
18
|
|
|
19
|
+
override fun getCommandsMap(): MutableMap<String, Int> {
|
|
20
|
+
return mutableMapOf(
|
|
21
|
+
"insertMediaAttachment" to COMMAND_INSERT_MEDIA_ATTACHMENT
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
override fun createViewInstance(reactContext: ThemedReactContext): RichTextEditorView {
|
|
19
26
|
return RichTextEditorView(reactContext)
|
|
20
27
|
}
|
|
21
28
|
|
|
29
|
+
override fun receiveCommand(view: RichTextEditorView, commandId: String?, args: ReadableArray?) {
|
|
30
|
+
when (commandId) {
|
|
31
|
+
"insertMediaAttachment" -> {
|
|
32
|
+
val uri = args?.getString(0)
|
|
33
|
+
view.insertMediaAttachment(uri)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
override fun receiveCommand(view: RichTextEditorView, commandId: Int, args: ReadableArray?) {
|
|
39
|
+
when (commandId) {
|
|
40
|
+
COMMAND_INSERT_MEDIA_ATTACHMENT -> {
|
|
41
|
+
val uri = args?.getString(0)
|
|
42
|
+
view.insertMediaAttachment(uri)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
22
47
|
@ReactProp(name = "placeholder")
|
|
23
48
|
fun setPlaceholder(view: RichTextEditorView, placeholder: String?) {
|
|
24
49
|
try {
|
|
@@ -116,6 +141,18 @@ class RichTextEditorViewManager : SimpleViewManager<RichTextEditorView>() {
|
|
|
116
141
|
}
|
|
117
142
|
}
|
|
118
143
|
blockMap["styles"] = stylesList
|
|
144
|
+
|
|
145
|
+
val mediaAttachment = block.optJSONObject("mediaAttachment")
|
|
146
|
+
if (mediaAttachment != null) {
|
|
147
|
+
val mediaMap = mutableMapOf<String, Any>()
|
|
148
|
+
mediaMap["kind"] = mediaAttachment.optString("kind", "image")
|
|
149
|
+
mediaMap["uri"] = mediaAttachment.optString("uri", "")
|
|
150
|
+
mediaMap["width"] = mediaAttachment.optInt("width", 100)
|
|
151
|
+
mediaMap["height"] = mediaAttachment.optInt("height", 100)
|
|
152
|
+
mediaMap["alt"] = mediaAttachment.optString("alt", "")
|
|
153
|
+
blockMap["mediaAttachment"] = mediaMap
|
|
154
|
+
}
|
|
155
|
+
|
|
119
156
|
blocksList.add(blockMap)
|
|
120
157
|
}
|
|
121
158
|
view.post {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<path
|
|
7
|
+
android:fillColor="#FFFFFF"
|
|
8
|
+
android:pathData="M19,3H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM19,19H5V5h14v14zM8.5,11A1.5,1.5 0 1,0 8.5,8A1.5,1.5 0 1,0 8.5,11zM6,17h12l-3.75,-5 -2.75,3.54 -1.75,-2.04L6,17z" />
|
|
9
|
+
</vector>
|