Skip to content

Commit 1ae05e7

Browse files
authored
android - improve conflicting styles logic (#93)
* fix: conflicting styles logic * fix: conflicting styles ranges * fix: conflicting styles * fix: conflicting styles
1 parent 79474a4 commit 1ae05e7

6 files changed

Lines changed: 118 additions & 18 deletions

File tree

android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import android.text.Spannable
1313
import android.util.AttributeSet
1414
import android.util.Log
1515
import android.util.TypedValue
16+
import android.view.Gravity
1617
import android.view.MotionEvent
1718
import android.view.inputmethod.InputMethodManager
1819
import androidx.appcompat.widget.AppCompatEditText
@@ -92,8 +93,8 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
9293
isSingleLine = false
9394
isHorizontalScrollBarEnabled = false
9495
isVerticalScrollBarEnabled = true
95-
gravity = android.view.Gravity.TOP or android.view.Gravity.START
96-
inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE
96+
gravity = Gravity.TOP or Gravity.START
97+
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
9798

9899
setPadding(0, 0, 0, 0)
99100
setBackgroundColor(Color.TRANSPARENT)
@@ -364,10 +365,56 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
364365
layoutManager.invalidateLayout(text)
365366
}
366367

368+
private fun removeStyle(name: String, start: Int, end: Int) {
369+
when (name) {
370+
EditorSpans.BOLD -> inlineStyles?.removeStyle(EditorSpans.BOLD, start, end)
371+
EditorSpans.ITALIC -> inlineStyles?.removeStyle(EditorSpans.ITALIC, start, end)
372+
EditorSpans.UNDERLINE -> inlineStyles?.removeStyle(EditorSpans.UNDERLINE, start, end)
373+
EditorSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EditorSpans.STRIKETHROUGH, start, end)
374+
EditorSpans.INLINE_CODE -> inlineStyles?.removeStyle(EditorSpans.INLINE_CODE, start, end)
375+
EditorSpans.H1 -> paragraphStyles?.removeStyle(EditorSpans.H1, start, end)
376+
EditorSpans.H2 -> paragraphStyles?.removeStyle(EditorSpans.H2, start, end)
377+
EditorSpans.H3 -> paragraphStyles?.removeStyle(EditorSpans.H3, start, end)
378+
EditorSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EditorSpans.CODE_BLOCK, start, end)
379+
EditorSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EditorSpans.BLOCK_QUOTE, start, end)
380+
EditorSpans.ORDERED_LIST -> listStyles?.removeStyle(EditorSpans.ORDERED_LIST, start, end)
381+
EditorSpans.UNORDERED_LIST -> listStyles?.removeStyle(EditorSpans.UNORDERED_LIST, start, end)
382+
EditorSpans.LINK -> parametrizedStyles?.removeStyle(EditorSpans.LINK, start, end)
383+
EditorSpans.IMAGE -> parametrizedStyles?.removeStyle(EditorSpans.IMAGE, start, end)
384+
EditorSpans.MENTION -> parametrizedStyles?.removeStyle(EditorSpans.MENTION, start, end)
385+
else -> Log.w("ReactNativeRichTextEditorView", "Unknown style: $name")
386+
}
387+
}
388+
389+
private fun getTargetRange(name: String): Pair<Int, Int> {
390+
val result = when (name) {
391+
EditorSpans.BOLD -> inlineStyles?.getStyleRange()
392+
EditorSpans.ITALIC -> inlineStyles?.getStyleRange()
393+
EditorSpans.UNDERLINE -> inlineStyles?.getStyleRange()
394+
EditorSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange()
395+
EditorSpans.INLINE_CODE -> inlineStyles?.getStyleRange()
396+
EditorSpans.H1 -> paragraphStyles?.getStyleRange()
397+
EditorSpans.H2 -> paragraphStyles?.getStyleRange()
398+
EditorSpans.H3 -> paragraphStyles?.getStyleRange()
399+
EditorSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange()
400+
EditorSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
401+
EditorSpans.ORDERED_LIST -> listStyles?.getStyleRange()
402+
EditorSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
403+
EditorSpans.LINK -> parametrizedStyles?.getStyleRange()
404+
EditorSpans.IMAGE -> parametrizedStyles?.getStyleRange()
405+
EditorSpans.MENTION -> parametrizedStyles?.getStyleRange()
406+
else -> Pair(0, 0)
407+
}
408+
409+
return result ?: Pair(0, 0)
410+
}
411+
367412
private fun verifyStyle(name: String): Boolean {
368413
val mergingConfig = EditorSpans.mergingConfig[name] ?: return true
369414
val conflictingStyles = mergingConfig.conflictingStyles
370415
val blockingStyles = mergingConfig.blockingStyles
416+
val isEnabling = spanState?.getStart(name) == null
417+
if (!isEnabling) return true
371418

372419
for (style in blockingStyles) {
373420
if (spanState?.getStart(style) != null) {
@@ -377,23 +424,22 @@ class ReactNativeRichTextEditorView : AppCompatEditText {
377424
}
378425

379426
for (style in conflictingStyles) {
380-
if (spanState?.getStart(style) != null) {
381-
val start = selection?.start ?: 0
382-
val end = selection?.end ?: 0
383-
val lengthBefore = text?.length ?: 0
384-
385-
toggleStyle(style)
386-
387-
val lengthAfter = text?.length ?: 0
388-
val charactersRemoved = lengthBefore - lengthAfter
389-
val finalEnd = if (charactersRemoved > 0 && end > start) {
390-
end - charactersRemoved
391-
} else {
392-
end
393-
}
427+
val start = selection?.start ?: 0
428+
val end = selection?.end ?: 0
429+
val lengthBefore = text?.length ?: 0
394430

395-
selection?.onSelection(start, finalEnd)
431+
val targetRange = getTargetRange(name)
432+
removeStyle(style, targetRange.first, targetRange.second)
433+
434+
val lengthAfter = text?.length ?: 0
435+
val charactersRemoved = lengthBefore - lengthAfter
436+
val finalEnd = if (charactersRemoved > 0 && end > start) {
437+
end - charactersRemoved
438+
} else {
439+
end
396440
}
441+
442+
selection?.onSelection(start, finalEnd)
397443
}
398444

399445
return true

android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorSpans.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ object EditorSpans {
9292
conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK),
9393
),
9494
CODE_BLOCK to StylesMergingConfig(
95-
conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE),
95+
conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE),
9696
),
9797
UNORDERED_LIST to StylesMergingConfig(
9898
conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),

android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/InlineStyles.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,17 @@ class InlineStyles(private val editorView: ReactNativeRichTextEditorView) {
126126
setAndMergeSpans(spannable, type, start, end)
127127
editorView.selection.validateStyles()
128128
}
129+
130+
fun removeStyle(name: String, start: Int, end: Int) {
131+
val config = EditorSpans.inlineSpans[name] ?: return
132+
val spannable = editorView.text as Spannable
133+
val spans = spannable.getSpans(start, end, config.clazz)
134+
for (span in spans) {
135+
spannable.removeSpan(span)
136+
}
137+
}
138+
139+
fun getStyleRange(): Pair<Int, Int> {
140+
return editorView.selection?.getInlineSelection() ?: Pair(0, 0)
141+
}
129142
}

android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,14 @@ class ListStyles(private val editorView: ReactNativeRichTextEditorView) {
157157
handleAfterTextChanged(s, EditorSpans.ORDERED_LIST, endCursorPosition, previousTextLength)
158158
handleAfterTextChanged(s, EditorSpans.UNORDERED_LIST, endCursorPosition, previousTextLength)
159159
}
160+
161+
fun getStyleRange(): Pair<Int, Int> {
162+
return editorView.selection?.getParagraphSelection() ?: Pair(0, 0)
163+
}
164+
165+
fun removeStyle(name: String, start: Int, end: Int) {
166+
val config = EditorSpans.listSpans[name] ?: return
167+
val spannable = editorView.text as Spannable
168+
removeSpansForRange(spannable, start, end, config.clazz)
169+
}
160170
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,14 @@ class ParagraphStyles(private val editorView: ReactNativeRichTextEditorView) {
169169
editorView.spanState?.setStart(name, start)
170170
setAndMergeSpans(spannable, type, start, currentEnd)
171171
}
172+
173+
fun getStyleRange(): Pair<Int, Int> {
174+
return editorView.selection?.getParagraphSelection() ?: Pair(0, 0)
175+
}
176+
177+
fun removeStyle(name: String, start: Int, end: Int) {
178+
val config = EditorSpans.paragraphSpans[name] ?: return
179+
val spannable = editorView.text as Spannable
180+
removeSpansForRange(spannable, start, end, config.clazz)
181+
}
172182
}

android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,24 @@ import com.swmansion.reactnativerichtexteditor.ReactNativeRichTextEditorView
99
import com.swmansion.reactnativerichtexteditor.spans.EditorImageSpan
1010
import com.swmansion.reactnativerichtexteditor.spans.EditorLinkSpan
1111
import com.swmansion.reactnativerichtexteditor.spans.EditorMentionSpan
12+
import com.swmansion.reactnativerichtexteditor.spans.EditorSpans
1213
import com.swmansion.reactnativerichtexteditor.utils.getSafeSpanBoundaries
1314
import java.io.File
1415

1516
class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView) {
1617
private var mentionStart: Int? = null
1718
var mentionIndicators: Array<String> = emptyArray<String>()
1819

20+
fun <T>removeSpansForRange(spannable: Spannable, start: Int, end: Int, clazz: Class<T>) {
21+
val ssb = spannable as SpannableStringBuilder
22+
ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", ""))
23+
24+
val spans = ssb.getSpans(start, end, clazz)
25+
for (span in spans) {
26+
ssb.removeSpan(span)
27+
}
28+
}
29+
1930
fun setLinkSpan(start: Int, end: Int, text: String, url: String) {
2031
val spannable = editorView.text as SpannableStringBuilder
2132
val spans = spannable.getSpans(start, end, EditorLinkSpan::class.java)
@@ -167,4 +178,14 @@ class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView)
167178

168179
editorView.selection.validateStyles()
169180
}
181+
182+
fun getStyleRange(): Pair<Int, Int> {
183+
return editorView.selection?.getInlineSelection() ?: Pair(0, 0)
184+
}
185+
186+
fun removeStyle(name: String, start: Int, end: Int) {
187+
val config = EditorSpans.parametrizedStyles[name] ?: return
188+
val spannable = editorView.text as Spannable
189+
removeSpansForRange(spannable, start, end, config.clazz)
190+
}
170191
}

0 commit comments

Comments
 (0)