Skip to content

Commit 2475695

Browse files
fix: extending paragraph style on paste (#472)
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please follow the template so that the reviewers can easily understand what the code changes affect --> # Summary This PR fixes applying styles for pasted text at the end of paragraph. It was mentioned here: #409 ## Test Plan 1. Create Heading with text "Hello" 2. Copy some text 3. paste it at the end of "Hello" 4. Heading styles should extend on pasted text ## Screenshots / Videos Before: https://github.com/user-attachments/assets/cad248b8-1674-477f-af45-fae8c0a63538 After: https://github.com/user-attachments/assets/e67b861e-8394-41da-9305-71799ccca072 ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ✅ | ---------
1 parent 84378d2 commit 2475695

7 files changed

Lines changed: 137 additions & 16 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
appId: swmansion.enriched.example
2+
tags:
3+
- android-only
4+
---
5+
- launchApp
6+
- tapOn:
7+
id: "toggle-screen-button"
8+
9+
- tapOn:
10+
id: "focus-button"
11+
12+
- inputText: "EADING"
13+
- doubleTapOn:
14+
id: "editor-input"
15+
point: "10%, 50%"
16+
17+
- tapOn:
18+
id: "android:id/floating_toolbar_menu_item_text"
19+
text: "Copy"
20+
21+
# Dismiss toolbar with Copy/Cut etc. options
22+
- tapOn:
23+
id: "editor-input"
24+
- tapOn:
25+
id: "clear-button"
26+
- tapOn:
27+
id: "focus-button"
28+
29+
- tapOn:
30+
id: "toolbar-heading-1"
31+
32+
- inputText: "H"
33+
34+
- longPressOn:
35+
id: "editor-input"
36+
point: "50%, 50%"
37+
38+
- tapOn:
39+
id: "android:id/floating_toolbar_menu_item_text"
40+
text: "Paste"
41+
42+
- runFlow:
43+
file: "../subflows/capture_or_assert_screenshot.yaml"
44+
env:
45+
SCREENSHOT_NAME: "extending_paragraph_style_on_paste_after_copy"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
appId: swmansion.enriched.example
2+
tags:
3+
- android-only
4+
---
5+
- launchApp
6+
- tapOn:
7+
id: "toggle-screen-button"
8+
9+
- tapOn:
10+
id: "focus-button"
11+
12+
- inputText: "EADING"
13+
- doubleTapOn:
14+
id: "editor-input"
15+
point: "10%, 50%"
16+
17+
- tapOn:
18+
id: "android:id/floating_toolbar_menu_item_text"
19+
text: "Cut"
20+
21+
- tapOn:
22+
id: "toolbar-heading-1"
23+
24+
- inputText: "H"
25+
26+
- longPressOn:
27+
id: "editor-input"
28+
point: "50%, 50%"
29+
30+
- tapOn:
31+
id: "android:id/floating_toolbar_menu_item_text"
32+
text: "Paste"
33+
34+
- runFlow:
35+
file: "../subflows/capture_or_assert_screenshot.yaml"
36+
env:
37+
SCREENSHOT_NAME: "extending_paragraph_style_on_paste_after_cut"
18.4 KB
Loading
18.4 KB
Loading

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import android.os.Build
1212
import android.text.Editable
1313
import android.text.InputType
1414
import android.text.Spannable
15+
import android.text.SpannableString
1516
import android.util.AttributeSet
1617
import android.util.Log
1718
import android.util.Patterns
@@ -338,28 +339,37 @@ class EnrichedTextInputView :
338339
}
339340

340341
fun handleTextPaste(item: ClipData.Item) {
341-
val htmlText = item.htmlText
342342
val currentText = text as Spannable
343343
val start = selectionStart.coerceAtLeast(0)
344344
val end = selectionEnd.coerceAtLeast(0)
345+
val lengthBefore = currentText.length
346+
347+
val pastedSpannable: Spannable =
348+
when {
349+
item.htmlText != null -> {
350+
val parsed = parseText(item.htmlText)
351+
(parsed as? Spannable) ?: return
352+
}
353+
354+
item.text != null -> {
355+
SpannableString(item.text.toString())
356+
}
345357

346-
if (htmlText != null) {
347-
val parsedText = parseText(htmlText)
348-
if (parsedText is Spannable) {
349-
val finalText = currentText.mergeSpannables(start, end, parsedText)
350-
setValue(finalText, false)
351-
return
358+
else -> {
359+
return
360+
}
352361
}
353-
}
354362

355-
if (item.text == null) return
356-
val lengthBefore = currentText.length
357-
val finalText = currentText.mergeSpannables(start, end, item.text.toString())
358-
setValue(finalText)
363+
val finalText = currentText.mergeSpannables(start, end, pastedSpannable)
364+
setValue(finalText, false)
365+
366+
// replacement-safe: oldLength - removed + inserted
367+
val insertedLength = finalText.length - (lengthBefore - (end - start))
368+
val pasteEnd = (start + insertedLength).coerceIn(0, finalText.length)
369+
setSelection(pasteEnd)
359370

360371
// Detect links in the newly pasted range
361-
val finalEndIndex = start + finalText.length - lengthBefore
362-
parametrizedStyles?.detectLinksInRange(finalText, start, finalEndIndex)
372+
parametrizedStyles?.detectLinksInRange(finalText, start.coerceAtMost(pasteEnd), pasteEnd)
363373
}
364374

365375
fun requestFocusProgrammatically() {

android/src/main/java/com/swmansion/enriched/textinput/styles/ParametrizedStyles.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ class ParametrizedStyles(
100100
end: Int,
101101
) {
102102
val regex = view.linkRegex ?: return
103-
val contextText = spannable.subSequence(start, end).toString()
103+
val textLength = spannable.length
104+
val safeStart = minOf(start, end).coerceIn(0, textLength)
105+
val safeEnd = maxOf(start, end).coerceIn(0, textLength)
106+
if (safeStart >= safeEnd) return
104107

105-
val spans = spannable.getSpans(start, end, EnrichedInputLinkSpan::class.java)
108+
val contextText = spannable.subSequence(safeStart, safeEnd).toString()
109+
110+
val spans = spannable.getSpans(safeStart, safeEnd, EnrichedInputLinkSpan::class.java)
106111
for (span in spans) {
107112
spannable.removeSpan(span)
108113
}

android/src/main/java/com/swmansion/enriched/textinput/utils/EnrichedSpannable.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ fun Spannable.mergeSpannables(
6666
val isNewLineStart = startBlockSpans.isNotEmpty() || startParagraphSpans.isNotEmpty()
6767
val isNewLineEnd = endBlockSpans.isNotEmpty() || endParagraphSpans.isNotEmpty()
6868

69+
val pastedHasOwnStyles =
70+
spannable.getSpans(0, spannable.length, EnrichedBlockSpan::class.java).isNotEmpty() ||
71+
spannable.getSpans(0, spannable.length, EnrichedParagraphSpan::class.java).isNotEmpty()
72+
6973
if (isNewLineStart && start != paragraphStart) {
7074
builder.insert(start, "\n")
7175
finalStart = start + 1
@@ -78,5 +82,25 @@ fun Spannable.mergeSpannables(
7882

7983
builder.replace(finalStart, finalEnd, spannable)
8084

85+
// Manually extend existing paragraph/block spans to cover the pasted text.
86+
if (!pastedHasOwnStyles) {
87+
val pasteEnd = finalStart + spannable.length
88+
89+
val affectedParagraphSpans = builder.getSpans(finalStart, finalStart, EnrichedParagraphSpan::class.java)
90+
val affectedBlockSpans = builder.getSpans(finalStart, finalStart, EnrichedBlockSpan::class.java)
91+
val affectedSpans = affectedBlockSpans.toList() + affectedParagraphSpans.toList()
92+
93+
for (span in affectedSpans) {
94+
val spanStart = builder.getSpanStart(span)
95+
val spanEnd = builder.getSpanEnd(span)
96+
if (spanStart == -1 || spanEnd >= pasteEnd) continue
97+
98+
val (_, newParagraphEnd) = builder.getParagraphBounds(spanStart, pasteEnd)
99+
val flags = builder.getSpanFlags(span)
100+
builder.removeSpan(span)
101+
builder.setSpan(span, spanStart, newParagraphEnd, flags)
102+
}
103+
}
104+
81105
return builder
82106
}

0 commit comments

Comments
 (0)