diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt index 71e85d3dd..c4563fe8f 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt @@ -13,6 +13,7 @@ import android.text.Spannable import android.util.AttributeSet import android.util.Log import android.util.TypedValue +import android.view.Gravity import android.view.MotionEvent import android.view.inputmethod.InputMethodManager import androidx.appcompat.widget.AppCompatEditText @@ -92,8 +93,8 @@ class ReactNativeRichTextEditorView : AppCompatEditText { isSingleLine = false isHorizontalScrollBarEnabled = false isVerticalScrollBarEnabled = true - gravity = android.view.Gravity.TOP or android.view.Gravity.START - inputType = android.text.InputType.TYPE_CLASS_TEXT or android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE + gravity = Gravity.TOP or Gravity.START + inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE setPadding(0, 0, 0, 0) setBackgroundColor(Color.TRANSPARENT) @@ -364,10 +365,56 @@ class ReactNativeRichTextEditorView : AppCompatEditText { layoutManager.invalidateLayout(text) } + private fun removeStyle(name: String, start: Int, end: Int) { + when (name) { + EditorSpans.BOLD -> inlineStyles?.removeStyle(EditorSpans.BOLD, start, end) + EditorSpans.ITALIC -> inlineStyles?.removeStyle(EditorSpans.ITALIC, start, end) + EditorSpans.UNDERLINE -> inlineStyles?.removeStyle(EditorSpans.UNDERLINE, start, end) + EditorSpans.STRIKETHROUGH -> inlineStyles?.removeStyle(EditorSpans.STRIKETHROUGH, start, end) + EditorSpans.INLINE_CODE -> inlineStyles?.removeStyle(EditorSpans.INLINE_CODE, start, end) + EditorSpans.H1 -> paragraphStyles?.removeStyle(EditorSpans.H1, start, end) + EditorSpans.H2 -> paragraphStyles?.removeStyle(EditorSpans.H2, start, end) + EditorSpans.H3 -> paragraphStyles?.removeStyle(EditorSpans.H3, start, end) + EditorSpans.CODE_BLOCK -> paragraphStyles?.removeStyle(EditorSpans.CODE_BLOCK, start, end) + EditorSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EditorSpans.BLOCK_QUOTE, start, end) + EditorSpans.ORDERED_LIST -> listStyles?.removeStyle(EditorSpans.ORDERED_LIST, start, end) + EditorSpans.UNORDERED_LIST -> listStyles?.removeStyle(EditorSpans.UNORDERED_LIST, start, end) + EditorSpans.LINK -> parametrizedStyles?.removeStyle(EditorSpans.LINK, start, end) + EditorSpans.IMAGE -> parametrizedStyles?.removeStyle(EditorSpans.IMAGE, start, end) + EditorSpans.MENTION -> parametrizedStyles?.removeStyle(EditorSpans.MENTION, start, end) + else -> Log.w("ReactNativeRichTextEditorView", "Unknown style: $name") + } + } + + private fun getTargetRange(name: String): Pair { + val result = when (name) { + EditorSpans.BOLD -> inlineStyles?.getStyleRange() + EditorSpans.ITALIC -> inlineStyles?.getStyleRange() + EditorSpans.UNDERLINE -> inlineStyles?.getStyleRange() + EditorSpans.STRIKETHROUGH -> inlineStyles?.getStyleRange() + EditorSpans.INLINE_CODE -> inlineStyles?.getStyleRange() + EditorSpans.H1 -> paragraphStyles?.getStyleRange() + EditorSpans.H2 -> paragraphStyles?.getStyleRange() + EditorSpans.H3 -> paragraphStyles?.getStyleRange() + EditorSpans.CODE_BLOCK -> paragraphStyles?.getStyleRange() + EditorSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange() + EditorSpans.ORDERED_LIST -> listStyles?.getStyleRange() + EditorSpans.UNORDERED_LIST -> listStyles?.getStyleRange() + EditorSpans.LINK -> parametrizedStyles?.getStyleRange() + EditorSpans.IMAGE -> parametrizedStyles?.getStyleRange() + EditorSpans.MENTION -> parametrizedStyles?.getStyleRange() + else -> Pair(0, 0) + } + + return result ?: Pair(0, 0) + } + private fun verifyStyle(name: String): Boolean { val mergingConfig = EditorSpans.mergingConfig[name] ?: return true val conflictingStyles = mergingConfig.conflictingStyles val blockingStyles = mergingConfig.blockingStyles + val isEnabling = spanState?.getStart(name) == null + if (!isEnabling) return true for (style in blockingStyles) { if (spanState?.getStart(style) != null) { @@ -377,23 +424,22 @@ class ReactNativeRichTextEditorView : AppCompatEditText { } for (style in conflictingStyles) { - if (spanState?.getStart(style) != null) { - val start = selection?.start ?: 0 - val end = selection?.end ?: 0 - val lengthBefore = text?.length ?: 0 - - toggleStyle(style) - - val lengthAfter = text?.length ?: 0 - val charactersRemoved = lengthBefore - lengthAfter - val finalEnd = if (charactersRemoved > 0 && end > start) { - end - charactersRemoved - } else { - end - } + val start = selection?.start ?: 0 + val end = selection?.end ?: 0 + val lengthBefore = text?.length ?: 0 - selection?.onSelection(start, finalEnd) + val targetRange = getTargetRange(name) + removeStyle(style, targetRange.first, targetRange.second) + + val lengthAfter = text?.length ?: 0 + val charactersRemoved = lengthBefore - lengthAfter + val finalEnd = if (charactersRemoved > 0 && end > start) { + end - charactersRemoved + } else { + end } + + selection?.onSelection(start, finalEnd) } return true diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorSpans.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorSpans.kt index ba365c9a4..9d934a730 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorSpans.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/spans/EditorSpans.kt @@ -92,7 +92,7 @@ object EditorSpans { conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK), ), CODE_BLOCK to StylesMergingConfig( - conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE), + conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE), ), UNORDERED_LIST to StylesMergingConfig( conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE), diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/InlineStyles.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/InlineStyles.kt index 1390516f5..37f2702e4 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/InlineStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/InlineStyles.kt @@ -126,4 +126,17 @@ class InlineStyles(private val editorView: ReactNativeRichTextEditorView) { setAndMergeSpans(spannable, type, start, end) editorView.selection.validateStyles() } + + fun removeStyle(name: String, start: Int, end: Int) { + val config = EditorSpans.inlineSpans[name] ?: return + val spannable = editorView.text as Spannable + val spans = spannable.getSpans(start, end, config.clazz) + for (span in spans) { + spannable.removeSpan(span) + } + } + + fun getStyleRange(): Pair { + return editorView.selection?.getInlineSelection() ?: Pair(0, 0) + } } diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt index bdd6fc0da..0db8a08fe 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt @@ -157,4 +157,14 @@ class ListStyles(private val editorView: ReactNativeRichTextEditorView) { handleAfterTextChanged(s, EditorSpans.ORDERED_LIST, endCursorPosition, previousTextLength) handleAfterTextChanged(s, EditorSpans.UNORDERED_LIST, endCursorPosition, previousTextLength) } + + fun getStyleRange(): Pair { + return editorView.selection?.getParagraphSelection() ?: Pair(0, 0) + } + + fun removeStyle(name: String, start: Int, end: Int) { + val config = EditorSpans.listSpans[name] ?: return + val spannable = editorView.text as Spannable + removeSpansForRange(spannable, start, end, config.clazz) + } } diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt index 2ec90014d..9f3f05ac9 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt @@ -169,4 +169,14 @@ class ParagraphStyles(private val editorView: ReactNativeRichTextEditorView) { editorView.spanState?.setStart(name, start) setAndMergeSpans(spannable, type, start, currentEnd) } + + fun getStyleRange(): Pair { + return editorView.selection?.getParagraphSelection() ?: Pair(0, 0) + } + + fun removeStyle(name: String, start: Int, end: Int) { + val config = EditorSpans.paragraphSpans[name] ?: return + val spannable = editorView.text as Spannable + removeSpansForRange(spannable, start, end, config.clazz) + } } diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt index 74752b14d..4f4d2358b 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParametrizedStyles.kt @@ -9,6 +9,7 @@ import com.swmansion.reactnativerichtexteditor.ReactNativeRichTextEditorView import com.swmansion.reactnativerichtexteditor.spans.EditorImageSpan import com.swmansion.reactnativerichtexteditor.spans.EditorLinkSpan import com.swmansion.reactnativerichtexteditor.spans.EditorMentionSpan +import com.swmansion.reactnativerichtexteditor.spans.EditorSpans import com.swmansion.reactnativerichtexteditor.utils.getSafeSpanBoundaries import java.io.File @@ -16,6 +17,16 @@ class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView) private var mentionStart: Int? = null var mentionIndicators: Array = emptyArray() + fun removeSpansForRange(spannable: Spannable, start: Int, end: Int, clazz: Class) { + val ssb = spannable as SpannableStringBuilder + ssb.replace(start, end, ssb.substring(start, end).replace("\u200B", "")) + + val spans = ssb.getSpans(start, end, clazz) + for (span in spans) { + ssb.removeSpan(span) + } + } + fun setLinkSpan(start: Int, end: Int, text: String, url: String) { val spannable = editorView.text as SpannableStringBuilder val spans = spannable.getSpans(start, end, EditorLinkSpan::class.java) @@ -167,4 +178,14 @@ class ParametrizedStyles(private val editorView: ReactNativeRichTextEditorView) editorView.selection.validateStyles() } + + fun getStyleRange(): Pair { + return editorView.selection?.getInlineSelection() ?: Pair(0, 0) + } + + fun removeStyle(name: String, start: Int, end: Int) { + val config = EditorSpans.parametrizedStyles[name] ?: return + val spannable = editorView.text as Spannable + removeSpansForRange(spannable, start, end, config.clazz) + } }