Skip to content

Commit cfa0b3c

Browse files
fix: handle conflicts between paragraphs (#306)
# Summary Fixes: #283 This PR fixes merging paragraph styles with different styles when deleting newline between them. ## Test Plan Do reproduce steps from issue: #283 ## Screenshots / Videos https://github.com/user-attachments/assets/e56e0772-6185-450a-b89a-bc0d85046ad0 https://github.com/user-attachments/assets/1149d86a-a5de-472a-ba0d-a80ecdb41ee5 ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ✅ | ---------
1 parent f7bdf9e commit cfa0b3c

2 files changed

Lines changed: 89 additions & 7 deletions

File tree

android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ package com.swmansion.enriched.spans
22

33
import com.swmansion.enriched.styles.HtmlStyle
44

5-
data class BaseSpanConfig(val clazz: Class<*>)
6-
data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
7-
data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
5+
interface ISpanConfig {
6+
val clazz: Class<*>
7+
}
8+
9+
data class BaseSpanConfig(override val clazz: Class<*>): ISpanConfig
10+
data class ParagraphSpanConfig(override val clazz: Class<*>, val isContinuous: Boolean): ISpanConfig
11+
data class ListSpanConfig(override val clazz: Class<*>, val shortcut: String) : ISpanConfig
812

913
data class StylesMergingConfig(
1014
// styles that should be removed when we apply specific style
@@ -64,6 +68,8 @@ object EnrichedSpans {
6468
MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
6569
)
6670

71+
val allSpans: Map<String, ISpanConfig> = inlineSpans + paragraphSpans + listSpans + parametrizedStyles
72+
6773
fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
6874
return when (style) {
6975
BOLD -> {

android/src/main/java/com/swmansion/enriched/styles/ParagraphStyles.kt

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ import android.text.Spannable
55
import android.text.SpannableStringBuilder
66
import android.util.Log
77
import com.swmansion.enriched.EnrichedTextInputView
8-
import com.swmansion.enriched.spans.EnrichedBlockQuoteSpan
9-
import com.swmansion.enriched.spans.EnrichedCodeBlockSpan
108
import com.swmansion.enriched.spans.EnrichedSpans
9+
import com.swmansion.enriched.spans.interfaces.EnrichedSpan
1110
import com.swmansion.enriched.utils.getParagraphBounds
1211
import com.swmansion.enriched.utils.getSafeSpanBoundaries
1312

@@ -194,16 +193,84 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
194193
s.setSpan(span, safeStart, safeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
195194
}
196195

196+
private fun handleConflictsDuringNewlineDeletion(s: Editable, style: String, paragraphStart: Int, paragraphEnd: Int): Boolean {
197+
val spanState = view.spanState ?: return false
198+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return false
199+
var isConflicting = false
200+
val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
201+
202+
for (styleToCheck in stylesToCheck) {
203+
val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
204+
205+
val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
206+
if (spans.isEmpty()) {
207+
continue
208+
}
209+
isConflicting = true
210+
211+
val isParagraphStyle = EnrichedSpans.paragraphSpans[styleToCheck] != null
212+
if (!isParagraphStyle) {
213+
continue
214+
}
215+
216+
for (span in spans) {
217+
extendStyleOnWholeParagraph(s, span as EnrichedSpan, conflictingType, paragraphEnd)
218+
}
219+
}
220+
221+
if (isConflicting) {
222+
val styleStart = spanState.getStart(style) ?: return false
223+
spanState.setStart(style, null)
224+
removeStyle(style, styleStart, paragraphEnd)
225+
return true
226+
}
227+
228+
return false
229+
}
230+
231+
232+
private fun deleteConflictingAndBlockingStyles(s: Editable, style: String, paragraphStart: Int, paragraphEnd: Int) {
233+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(style, view.htmlStyle) ?: return
234+
val stylesToCheck = mergingConfig.blockingStyles + mergingConfig.conflictingStyles
235+
236+
for (styleToCheck in stylesToCheck) {
237+
val conflictingType = EnrichedSpans.allSpans[styleToCheck]?.clazz ?: continue
238+
239+
val spans = s.getSpans(paragraphStart, paragraphEnd, conflictingType)
240+
for (span in spans) {
241+
s.removeSpan(span)
242+
}
243+
}
244+
}
245+
246+
private fun <T>extendStyleOnWholeParagraph(s: Editable, span: EnrichedSpan, type: Class<T>, paragraphEnd: Int) {
247+
val currStyleStart = s.getSpanStart(span)
248+
s.removeSpan(span)
249+
val (safeStart, safeEnd) = s.getSafeSpanBoundaries(currStyleStart, paragraphEnd)
250+
setSpan(s, type, safeStart, safeEnd)
251+
}
252+
197253
fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) {
198254
var endCursorPosition = endPosition
199255
val isBackspace = s.length < previousTextLength
200256
val isNewLine = endCursorPosition == 0 || endCursorPosition > 0 && s[endCursorPosition - 1] == '\n'
257+
val spanState = view.spanState ?: return
201258

202259
for ((style, config) in EnrichedSpans.paragraphSpans) {
203-
val spanState = view.spanState ?: continue
204260
val styleStart = spanState.getStart(style)
205261

206262
if (styleStart == null) {
263+
if (isBackspace) {
264+
val (start, end) = s.getParagraphBounds(endCursorPosition)
265+
val spans = s.getSpans(start, end, config.clazz)
266+
267+
for (span in spans) {
268+
// handle conflicts when entering paragraph with some paragraph style applied
269+
deleteConflictingAndBlockingStyles(s, style, start, end)
270+
extendStyleOnWholeParagraph(s, span as EnrichedSpan, config.clazz, end)
271+
}
272+
}
273+
207274
if (config.isContinuous) {
208275
mergeAdjacentStyleSpans(s, endCursorPosition, config.clazz)
209276
}
@@ -218,14 +285,23 @@ class ParagraphStyles(private val view: EnrichedTextInputView) {
218285

219286
if (isBackspace) {
220287
endCursorPosition -= 1
221-
view.spanState.setStart(style, null)
288+
spanState.setStart(style, null)
222289
} else {
223290
s.insert(endCursorPosition, "\u200B")
224291
endCursorPosition += 1
225292
}
226293
}
227294

228295
var (start, end) = s.getParagraphBounds(styleStart, endCursorPosition)
296+
297+
// handle conflicts when deleting newline from paragraph style (going back to previous line)
298+
if (isBackspace && styleStart != start) {
299+
val isConflicting = handleConflictsDuringNewlineDeletion(s, style, start, end)
300+
if (isConflicting) {
301+
continue
302+
}
303+
}
304+
229305
val isNotEndLineSpan = isSpanEnabledInNextLine(s, end, config.clazz)
230306
val spans = s.getSpans(start, end, config.clazz)
231307

0 commit comments

Comments
 (0)