@chaitrabhairappa/react-native-rich-text-editor 2.1.1 → 3.0.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 CHANGED
@@ -20,12 +20,13 @@ Unlike other rich text editor packages that rely on HTML and WebView, this libra
20
20
  - Bullet lists and Numbered lists
21
21
  - Headings
22
22
  - Quotes and Checklists
23
+ - Media attachments (images)
23
24
  - Link insertion
24
25
  - Undo/Redo
25
26
  - Text alignment (left, center, right)
26
27
  - Indent/Outdent
27
28
  - Floating toolbar with customizable options
28
- - Two variants: outlined and flat
29
+ - Three variants: outlined, flat, and plain
29
30
  - Auto-growing height
30
31
  - **`numberOfLines` support** — truncate text with ellipsis in readOnly mode
31
32
  - **Delta-based content updates** for optimized performance
@@ -143,7 +144,7 @@ export default App;
143
144
  | `maxHeight` | `number` | `undefined` | Maximum height before scrolling |
144
145
  | `showToolbar` | `boolean` | `true` | Show/hide floating toolbar |
145
146
  | `toolbarOptions` | `ToolbarOption[]` | All options | Customize toolbar buttons |
146
- | `variant` | `'outlined' \| 'flat'` | `'outlined'` | Editor style variant |
147
+ | `variant` | `'outlined' \| 'flat' \| 'plain'` | `'outlined'` | Editor style variant |
147
148
  | `onContentChange` | `(event: ContentChangeEvent) => void` | `undefined` | Called when content changes |
148
149
  | `onSelectionChange` | `(event: SelectionChangeEvent) => void` | `undefined` | Called when selection changes |
149
150
  | `onFocus` | `() => void` | `undefined` | Called when editor gains focus |
@@ -180,6 +181,9 @@ editorRef.current?.setQuote();
180
181
  editorRef.current?.setChecklist();
181
182
  editorRef.current?.setParagraph();
182
183
 
184
+ // Media
185
+ editorRef.current?.insertMediaAttachment({ kind: 'image', uri: 'https://example.com/image.png' });
186
+
183
187
  // Actions
184
188
  editorRef.current?.insertLink(url, text);
185
189
  editorRef.current?.undo();
@@ -204,9 +208,18 @@ interface Block {
204
208
  alignment?: TextAlignment;
205
209
  checked?: boolean;
206
210
  indentLevel?: number;
211
+ mediaAttachment?: MediaAttachment;
212
+ }
213
+
214
+ interface MediaAttachment {
215
+ kind: 'image';
216
+ uri: string;
217
+ width?: number;
218
+ height?: number;
219
+ alt?: string;
207
220
  }
208
221
 
209
- type BlockType = 'paragraph' | 'bullet' | 'numbered' | 'heading' | 'quote' | 'checklist';
222
+ type BlockType = 'paragraph' | 'bullet' | 'numbered' | 'heading' | 'quote' | 'checklist' | 'mediaAttachment';
210
223
  type TextAlignment = 'left' | 'center' | 'right';
211
224
 
212
225
  interface StyleRange {
@@ -229,6 +242,7 @@ type ToolbarOption =
229
242
  | 'numbered'
230
243
  | 'quote'
231
244
  | 'checklist'
245
+ | 'mediaAttachment'
232
246
  | 'link'
233
247
  | 'undo'
234
248
  | 'redo'
@@ -250,6 +264,41 @@ Use the `numberOfLines` prop along with `readOnly` to truncate content with an e
250
264
 
251
265
  This works like React Native's `Text` component — content is truncated at the specified number of lines with a trailing ellipsis.
252
266
 
267
+ ## Media Attachments
268
+
269
+ You can insert images into the editor using the `insertMediaAttachment` ref method. The toolbar also includes a `mediaAttachment` button that triggers the native media attachment flow.
270
+
271
+ ```tsx
272
+ import RichTextEditor, {
273
+ RichTextEditorRef,
274
+ MediaAttachment,
275
+ } from '@chaitrabhairappa/react-native-rich-text-editor';
276
+
277
+ const editorRef = useRef<RichTextEditorRef>(null);
278
+
279
+ // Insert an image programmatically
280
+ const insertImage = () => {
281
+ editorRef.current?.insertMediaAttachment({
282
+ kind: 'image',
283
+ uri: 'https://example.com/photo.png',
284
+ });
285
+ };
286
+ ```
287
+
288
+ Media attachment blocks are included in the content output:
289
+
290
+ ```typescript
291
+ {
292
+ type: 'mediaAttachment',
293
+ text: '',
294
+ styles: [],
295
+ mediaAttachment: {
296
+ kind: 'image',
297
+ uri: 'https://example.com/photo.png',
298
+ }
299
+ }
300
+ ```
301
+
253
302
  ## Customizing Toolbar
254
303
 
255
304
  ```tsx
@@ -265,7 +314,14 @@ const toolbarOptions: ToolbarOption[] = ['bold', 'italic', 'underline', 'bullet'
265
314
 
266
315
  ## Changelog
267
316
 
268
- ### 2.0.1
317
+ ### 3.0.0
318
+
319
+ - Add media attachment support — insert images into the editor (iOS & Android)
320
+ - New `insertMediaAttachment` ref method for programmatic image insertion
321
+ - New `mediaAttachment` toolbar option
322
+ - New `MediaAttachment` type and `mediaAttachment` block type
323
+
324
+ ### 2.1.2
269
325
 
270
326
  - Add `numberOfLines` prop to truncate text with ellipsis in readOnly mode (iOS & Android)
271
327
  - Fix Android `onContentChange` to correctly extract text styles (bold, italic, underline, strikethrough, code, highlight)
@@ -34,6 +34,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
34
34
  fun onNumberedListClick()
35
35
  fun onQuoteClick()
36
36
  fun onChecklistClick()
37
+ fun onMediaAttachmentClick()
37
38
  fun onLinkClick()
38
39
  fun onUndoClick()
39
40
  fun onRedoClick()
@@ -63,6 +64,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
63
64
  private var enabledOptions: List<String> = listOf(
64
65
  "bold", "italic", "underline", "strikethrough", "code", "highlight",
65
66
  "heading", "bullet", "numbered", "quote", "checklist",
67
+ "mediaAttachment",
66
68
  "link", "undo", "redo", "clearFormatting",
67
69
  "indent", "outdent",
68
70
  "alignLeft", "alignCenter", "alignRight"
@@ -143,6 +145,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
143
145
  enabledOptions = options ?: listOf(
144
146
  "bold", "italic", "underline", "strikethrough", "code", "highlight",
145
147
  "heading", "bullet", "numbered", "quote", "checklist",
148
+ "mediaAttachment",
146
149
  "link", "undo", "redo", "clearFormatting",
147
150
  "indent", "outdent",
148
151
  "alignLeft", "alignCenter", "alignRight"
@@ -176,6 +179,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
176
179
  "numbered" -> R.drawable.ic_format_list_numbered
177
180
  "quote" -> R.drawable.ic_format_quote
178
181
  "checklist" -> R.drawable.ic_format_checklist
182
+ "mediaAttachment" -> R.drawable.ic_format_media_attachment
179
183
  "link" -> R.drawable.ic_format_link
180
184
  "undo" -> R.drawable.ic_format_undo
181
185
  "redo" -> R.drawable.ic_format_redo
@@ -227,6 +231,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
227
231
  "numbered" -> button.setOnClickListener { listener?.onNumberedListClick() }
228
232
  "quote" -> button.setOnClickListener { listener?.onQuoteClick() }
229
233
  "checklist" -> button.setOnClickListener { listener?.onChecklistClick() }
234
+ "mediaAttachment" -> button.setOnClickListener { listener?.onMediaAttachmentClick() }
230
235
  "link" -> button.setOnClickListener { listener?.onLinkClick() }
231
236
  "undo" -> button.setOnClickListener { listener?.onUndoClick() }
232
237
  "redo" -> button.setOnClickListener { listener?.onRedoClick() }
@@ -253,6 +258,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
253
258
  numbered: Boolean = false,
254
259
  quote: Boolean = false,
255
260
  checklist: Boolean = false,
261
+ mediaAttachment: Boolean = false,
256
262
  alignLeft: Boolean = true,
257
263
  alignCenter: Boolean = false,
258
264
  alignRight: Boolean = false
@@ -269,6 +275,7 @@ class FloatingToolbar(context: Context) : LinearLayout(context) {
269
275
  "numbered" to numbered,
270
276
  "quote" to quote,
271
277
  "checklist" to checklist,
278
+ "mediaAttachment" to mediaAttachment,
272
279
  "alignLeft" to alignLeft,
273
280
  "alignCenter" to alignCenter,
274
281
  "alignRight" to alignRight
@@ -0,0 +1,85 @@
1
+ package com.richtext.editor
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Color
6
+ import android.graphics.Paint
7
+ import android.graphics.RectF
8
+ import android.text.style.ReplacementSpan
9
+ import kotlin.math.ceil
10
+
11
+ data class MediaAttachmentData(
12
+ val kind: String,
13
+ val uri: String,
14
+ val widthDp: Int,
15
+ val heightDp: Int,
16
+ val alt: String
17
+ )
18
+
19
+ class MediaAttachmentSpan(
20
+ private val data: MediaAttachmentData,
21
+ private val density: Float,
22
+ private val bitmap: Bitmap? = null,
23
+ private val lineSpacingMultiplier: Float = 1f
24
+ ) : ReplacementSpan() {
25
+
26
+ fun toMediaAttachmentData(): MediaAttachmentData = data
27
+
28
+ private fun getWidthPx(): Int {
29
+ return bitmap?.width ?: (data.widthDp * density).toInt().coerceAtLeast(1)
30
+ }
31
+
32
+ private fun getHeightPx(): Int {
33
+ return bitmap?.height ?: (data.heightDp * density).toInt().coerceAtLeast(1)
34
+ }
35
+
36
+ override fun getSize(
37
+ paint: Paint,
38
+ text: CharSequence?,
39
+ start: Int,
40
+ end: Int,
41
+ fm: Paint.FontMetricsInt?
42
+ ): Int {
43
+ val widthPx = getWidthPx()
44
+ if (fm != null) {
45
+ val imageHeightPx = getHeightPx()
46
+ val metricsHeightPx = if (lineSpacingMultiplier > 1f) {
47
+ ceil(imageHeightPx / lineSpacingMultiplier).toInt().coerceAtLeast(1)
48
+ } else {
49
+ imageHeightPx
50
+ }
51
+ fm.ascent = -metricsHeightPx
52
+ fm.descent = 0
53
+ fm.top = fm.ascent
54
+ fm.bottom = 0
55
+ }
56
+ return widthPx
57
+ }
58
+
59
+ override fun draw(
60
+ canvas: Canvas,
61
+ text: CharSequence?,
62
+ start: Int,
63
+ end: Int,
64
+ x: Float,
65
+ top: Int,
66
+ y: Int,
67
+ bottom: Int,
68
+ paint: Paint
69
+ ) {
70
+ val widthPx = getWidthPx().toFloat()
71
+ val heightPx = getHeightPx().toFloat()
72
+ val oldColor = paint.color
73
+ val oldStyle = paint.style
74
+ val lineHeight = (bottom - top).toFloat().coerceAtLeast(heightPx)
75
+ val rectTop = top + ((lineHeight - heightPx) / 2f)
76
+
77
+ if (bitmap != null) {
78
+ val destRect = RectF(x, rectTop, x + widthPx, rectTop + heightPx)
79
+ canvas.drawBitmap(bitmap, null, destRect, null)
80
+ }
81
+
82
+ paint.color = oldColor
83
+ paint.style = oldStyle
84
+ }
85
+ }
@@ -0,0 +1,296 @@
1
+ package com.richtext.editor
2
+
3
+ import android.content.Context
4
+ import android.graphics.Bitmap
5
+ import android.graphics.BitmapFactory
6
+ import android.net.Uri
7
+ import android.text.Editable
8
+ import android.text.SpannableStringBuilder
9
+ import android.text.Spanned
10
+ import com.facebook.react.bridge.Arguments
11
+ import com.facebook.react.bridge.WritableMap
12
+ import org.json.JSONObject
13
+ import java.net.URL
14
+
15
+ class MediaAttachmentSupport(
16
+ private val context: Context,
17
+ private val density: Float,
18
+ private val placeholderChar: Char,
19
+ private val getLineSpacingMultiplier: () -> Float,
20
+ private val getTargetWidthPx: () -> Int,
21
+ private val editableProvider: () -> Editable?,
22
+ private val runOnUiThread: ((() -> Unit) -> Unit),
23
+ private val onMediaSpansUpdated: () -> Unit
24
+ ) {
25
+
26
+ private val mediaBitmapCache = mutableMapOf<String, Bitmap>()
27
+ private val loadingRemoteUris = mutableSetOf<String>()
28
+
29
+ // Block Data to MediaAttachmentData parsing
30
+ fun parseMediaData(block: Map<String, Any>): MediaAttachmentData? {
31
+ val blockType = block["type"] as? String ?: return null
32
+ if (blockType != "mediaAttachment") return null
33
+
34
+ val mediaInfo = block["mediaAttachment"] as? Map<*, *>
35
+ return MediaAttachmentData(
36
+ kind = mediaInfo?.get("kind") as? String ?: "image",
37
+ uri = mediaInfo?.get("uri") as? String ?: "",
38
+ widthDp = (mediaInfo?.get("width") as? Number)?.toInt() ?: 100,
39
+ heightDp = (mediaInfo?.get("height") as? Number)?.toInt() ?: 100,
40
+ alt = mediaInfo?.get("alt") as? String ?: ""
41
+ )
42
+ }
43
+
44
+ fun createMediaAttachmentSpan(mediaData: MediaAttachmentData): MediaAttachmentSpan {
45
+ val targetWidthPx = getTargetWidthPx().coerceAtLeast(1)
46
+ val loaded = loadBitmapForMedia(mediaData, targetWidthPx)
47
+ return if (loaded != null) {
48
+ val (bitmap, updatedData) = loaded
49
+ MediaAttachmentSpan(updatedData, density, bitmap, getLineSpacingMultiplier())
50
+ } else {
51
+ val fallbackData = normalizeMediaDimensions(mediaData, targetWidthPx)
52
+ MediaAttachmentSpan(fallbackData, density, null, getLineSpacingMultiplier())
53
+ }
54
+ }
55
+
56
+ fun findMediaAttachmentSpan(spannable: Spanned, lineStart: Int, lineEnd: Int): MediaAttachmentSpan? {
57
+ val safeEnd = lineEnd.coerceAtLeast(lineStart + 1)
58
+ return spannable.getSpans(lineStart, safeEnd, MediaAttachmentSpan::class.java)
59
+ .firstOrNull { span ->
60
+ val spanStart = spannable.getSpanStart(span)
61
+ val spanEnd = spannable.getSpanEnd(span)
62
+ spanStart < lineEnd && spanEnd > lineStart
63
+ }
64
+ }
65
+
66
+ fun isMediaLine(line: String): Boolean {
67
+ return line.replace(placeholderChar.toString(), "").trim().isEmpty()
68
+ }
69
+
70
+ fun createWritableMediaBlock(mediaData: MediaAttachmentData): WritableMap {
71
+ val block = Arguments.createMap()
72
+ block.putString("type", "mediaAttachment")
73
+ block.putString("text", "")
74
+ block.putArray("styles", Arguments.createArray())
75
+
76
+ val mediaMap = Arguments.createMap()
77
+ mediaMap.putString("kind", mediaData.kind)
78
+ mediaMap.putString("uri", mediaData.uri)
79
+ mediaMap.putInt("width", mediaData.widthDp)
80
+ mediaMap.putInt("height", mediaData.heightDp)
81
+ mediaMap.putString("alt", mediaData.alt)
82
+ block.putMap("mediaAttachment", mediaMap)
83
+
84
+ return block
85
+ }
86
+
87
+ fun createJsonMediaBlock(mediaData: MediaAttachmentData): JSONObject {
88
+ val block = JSONObject()
89
+ block.put("type", "mediaAttachment")
90
+ block.put("text", "")
91
+ block.put("styles", org.json.JSONArray())
92
+
93
+ val mediaObj = JSONObject()
94
+ mediaObj.put("kind", mediaData.kind)
95
+ mediaObj.put("uri", mediaData.uri)
96
+ mediaObj.put("width", mediaData.widthDp)
97
+ mediaObj.put("height", mediaData.heightDp)
98
+ mediaObj.put("alt", mediaData.alt)
99
+ block.put("mediaAttachment", mediaObj)
100
+
101
+ return block
102
+ }
103
+
104
+ fun appendMediaBlock(
105
+ spannable: SpannableStringBuilder,
106
+ currentOffset: Int,
107
+ mediaData: MediaAttachmentData,
108
+ appendTrailingNewline: Boolean
109
+ ): Int {
110
+ var offset = currentOffset
111
+ val spanStart = offset
112
+ spannable.append(placeholderChar)
113
+ offset += 1
114
+ spannable.setSpan(
115
+ createMediaAttachmentSpan(mediaData),
116
+ spanStart,
117
+ spanStart + 1,
118
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
119
+ )
120
+
121
+ if (appendTrailingNewline) {
122
+ spannable.append("\n")
123
+ offset += 1
124
+ }
125
+
126
+ return offset
127
+ }
128
+
129
+ fun insertMediaAttachmentBlock(editable: Editable, insertPos: Int, uri: String): Int {
130
+ var mutableInsertPos = insertPos.coerceIn(0, editable.length)
131
+ val mediaData = MediaAttachmentData(
132
+ kind = "image",
133
+ uri = uri,
134
+ widthDp = 100,
135
+ heightDp = 100,
136
+ alt = "Selected image"
137
+ )
138
+
139
+ if (mutableInsertPos > 0 && editable[mutableInsertPos - 1] != '\n') {
140
+ editable.insert(mutableInsertPos, "\n")
141
+ mutableInsertPos += 1
142
+ }
143
+
144
+ val spanStart = mutableInsertPos
145
+ editable.insert(spanStart, placeholderChar.toString())
146
+ editable.setSpan(
147
+ createMediaAttachmentSpan(mediaData),
148
+ spanStart,
149
+ spanStart + 1,
150
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
151
+ )
152
+
153
+ var nextPos = spanStart + 1
154
+ val hasFollowingText = nextPos < editable.length
155
+ if (hasFollowingText && editable[nextPos] != '\n') {
156
+ editable.insert(nextPos, "\n")
157
+ nextPos += 1
158
+ }
159
+
160
+ return nextPos.coerceAtMost(editable.length)
161
+ }
162
+
163
+ private fun normalizeMediaDimensions(mediaData: MediaAttachmentData, targetWidthPx: Int): MediaAttachmentData {
164
+ val targetWidthDp = (targetWidthPx / density).toInt().coerceAtLeast(1)
165
+ val fallbackHeightDp = if (mediaData.widthDp > 0 && mediaData.heightDp > 0) {
166
+ ((mediaData.heightDp.toFloat() / mediaData.widthDp.toFloat()) * targetWidthDp).toInt().coerceAtLeast(1)
167
+ } else {
168
+ targetWidthDp
169
+ }
170
+
171
+ return mediaData.copy(
172
+ widthDp = targetWidthDp,
173
+ heightDp = fallbackHeightDp
174
+ )
175
+ }
176
+
177
+ private fun buildScaledBitmapWithAspect(bitmap: Bitmap, targetWidthPx: Int): Pair<Bitmap, Int> {
178
+ val safeTargetWidthPx = targetWidthPx.coerceAtLeast(1)
179
+ val aspectRatio = if (bitmap.width > 0) {
180
+ bitmap.height.toFloat() / bitmap.width.toFloat()
181
+ } else {
182
+ 1f
183
+ }
184
+ val targetHeightPx = (safeTargetWidthPx * aspectRatio).toInt().coerceAtLeast(1)
185
+
186
+ val scaled = if (bitmap.width == safeTargetWidthPx && bitmap.height == targetHeightPx) {
187
+ bitmap
188
+ } else {
189
+ Bitmap.createScaledBitmap(bitmap, safeTargetWidthPx, targetHeightPx, true)
190
+ }
191
+
192
+ return Pair(scaled, targetHeightPx)
193
+ }
194
+
195
+ private fun loadBitmapForMedia(mediaData: MediaAttachmentData, targetWidthPx: Int): Pair<Bitmap, MediaAttachmentData>? {
196
+ if (mediaData.uri.isBlank() || mediaData.uri == "red-box-placeholder") return null
197
+
198
+ mediaBitmapCache[mediaData.uri]?.let { cached ->
199
+ val (scaledBitmap, targetHeightPx) = buildScaledBitmapWithAspect(cached, targetWidthPx)
200
+ val updatedData = mediaData.copy(
201
+ widthDp = (targetWidthPx / density).toInt().coerceAtLeast(1),
202
+ heightDp = (targetHeightPx / density).toInt().coerceAtLeast(1)
203
+ )
204
+ return Pair(scaledBitmap, updatedData)
205
+ }
206
+
207
+ return try {
208
+ val uri = Uri.parse(mediaData.uri)
209
+
210
+ when (uri.scheme?.lowercase()) {
211
+ "http", "https" -> {
212
+ loadRemoteBitmapAsync(mediaData, targetWidthPx)
213
+ null
214
+ }
215
+ else -> {
216
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
217
+ BitmapFactory.decodeStream(inputStream)
218
+ }?.let { original ->
219
+ mediaBitmapCache[mediaData.uri] = original
220
+ val (scaledBitmap, targetHeightPx) = buildScaledBitmapWithAspect(original, targetWidthPx)
221
+ val updatedData = mediaData.copy(
222
+ widthDp = (targetWidthPx / density).toInt().coerceAtLeast(1),
223
+ heightDp = (targetHeightPx / density).toInt().coerceAtLeast(1)
224
+ )
225
+ Pair(scaledBitmap, updatedData)
226
+ }
227
+ }
228
+ }
229
+ } catch (_: Exception) {
230
+ null
231
+ }
232
+ }
233
+
234
+ private fun loadRemoteBitmapAsync(mediaData: MediaAttachmentData, targetWidthPx: Int) {
235
+ val uriString = mediaData.uri
236
+ synchronized(loadingRemoteUris) {
237
+ if (loadingRemoteUris.contains(uriString)) return
238
+ loadingRemoteUris.add(uriString)
239
+ }
240
+
241
+ Thread {
242
+ try {
243
+ val loaded = URL(uriString).openStream().use { inputStream ->
244
+ BitmapFactory.decodeStream(inputStream)
245
+ } ?: return@Thread
246
+
247
+ mediaBitmapCache[uriString] = loaded
248
+ val (scaledBitmap, targetHeightPx) = buildScaledBitmapWithAspect(loaded, targetWidthPx)
249
+ val updatedData = mediaData.copy(
250
+ widthDp = (targetWidthPx / density).toInt().coerceAtLeast(1),
251
+ heightDp = (targetHeightPx / density).toInt().coerceAtLeast(1)
252
+ )
253
+
254
+ runOnUiThread {
255
+ synchronized(loadingRemoteUris) {
256
+ loadingRemoteUris.remove(uriString)
257
+ }
258
+ refreshMediaSpansForUri(uriString, scaledBitmap, updatedData)
259
+ }
260
+ } catch (_: Exception) {
261
+ runOnUiThread {
262
+ synchronized(loadingRemoteUris) {
263
+ loadingRemoteUris.remove(uriString)
264
+ }
265
+ }
266
+ }
267
+ }.start()
268
+ }
269
+
270
+ private fun refreshMediaSpansForUri(uriString: String, bitmap: Bitmap, updatedData: MediaAttachmentData) {
271
+ val editable = editableProvider() ?: return
272
+ val spans = editable.getSpans(0, editable.length, MediaAttachmentSpan::class.java)
273
+ var updated = false
274
+
275
+ spans.forEach { span ->
276
+ val data = span.toMediaAttachmentData()
277
+ if (data.uri == uriString) {
278
+ val start = editable.getSpanStart(span)
279
+ val end = editable.getSpanEnd(span)
280
+ val flags = editable.getSpanFlags(span)
281
+ editable.removeSpan(span)
282
+ editable.setSpan(
283
+ MediaAttachmentSpan(updatedData, density, bitmap, getLineSpacingMultiplier()),
284
+ start,
285
+ end,
286
+ flags
287
+ )
288
+ updated = true
289
+ }
290
+ }
291
+
292
+ if (updated) {
293
+ onMediaSpansUpdated()
294
+ }
295
+ }
296
+ }