diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt index c4563fe8f..f2056aa9e 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/ReactNativeRichTextEditorView.kt @@ -37,6 +37,7 @@ import com.swmansion.reactnativerichtexteditor.styles.RichTextStyle import com.swmansion.reactnativerichtexteditor.utils.EditorParser import com.swmansion.reactnativerichtexteditor.utils.EditorSelection import com.swmansion.reactnativerichtexteditor.utils.EditorSpanState +import com.swmansion.reactnativerichtexteditor.utils.mergeSpannables import com.swmansion.reactnativerichtexteditor.watchers.EditorSpanWatcher import com.swmansion.reactnativerichtexteditor.watchers.EditorTextWatcher import kotlin.math.ceil @@ -188,13 +189,19 @@ class ReactNativeRichTextEditorView : AppCompatEditText { val clip = clipboard.primaryClip val item = clip?.getItemAt(0) val htmlText = item?.htmlText + val currentText = text as Spannable + val start = selection?.start ?: 0 + val end = selection?.end ?: 0 if (htmlText != null) { - setValue(htmlText) + val parsedText = parseText(htmlText) as Spannable + val finalText = currentText.mergeSpannables(start, end, parsedText) + setValue(finalText) return } - setValue(item?.text.toString()) + val finalText = currentText.replaceRange(start, end, item?.text.toString()) + setValue(finalText) } fun requestFocusProgrammatically() { @@ -203,18 +210,21 @@ class ReactNativeRichTextEditorView : AppCompatEditText { setSelection(selection?.start ?: text?.length ?: 0) } - fun setValue(value: String?) { + private fun parseText(text: CharSequence): CharSequence { + val isHtml = text.startsWith("") && text.endsWith("") + if (!isHtml) return text + + val parsed = EditorParser.fromHtml(text.toString(), richTextStyle, null) + val withoutLastNewLine = parsed.trimEnd('\n') + return withoutLastNewLine + } + + fun setValue(value: CharSequence?) { if (value == null) return isSettingValue = true - val isHtml = value.startsWith("") && value.endsWith("") - if (isHtml) { - val parsed = EditorParser.fromHtml(value, richTextStyle, null) - val withoutLastNewLine = parsed.trimEnd('\n') - setText(withoutLastNewLine) - } else { - setText(value) - } + val newText = parseText(value) + setText(newText) // Assign SpanWatcher one more time as our previous spannable has been replaced addSpanWatcher(EditorSpanWatcher(this)) 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 0db8a08fe..710ba114c 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ListStyles.kt @@ -8,14 +8,14 @@ import com.swmansion.reactnativerichtexteditor.ReactNativeRichTextEditorView import com.swmansion.reactnativerichtexteditor.spans.EditorOrderedListSpan import com.swmansion.reactnativerichtexteditor.spans.EditorSpans import com.swmansion.reactnativerichtexteditor.spans.EditorUnorderedListSpan +import com.swmansion.reactnativerichtexteditor.utils.getParagraphBounds import com.swmansion.reactnativerichtexteditor.utils.getSafeSpanBoundaries class ListStyles(private val editorView: ReactNativeRichTextEditorView) { private fun getPreviousParagraphSpan(spannable: Spannable, s: Int, type: Class): T? { if (s <= 0) return null - val selection = editorView.selection ?: return null - val (previousParagraphStart, previousParagraphEnd) = selection.getParagraphBounds(spannable, s - 1) + val (previousParagraphStart, previousParagraphEnd) = spannable.getParagraphBounds(s - 1) val spans = spannable.getSpans(previousParagraphStart, previousParagraphEnd, type) if (spans.isNotEmpty()) { @@ -112,10 +112,9 @@ class ListStyles(private val editorView: ReactNativeRichTextEditorView) { } private fun handleAfterTextChanged(s: Editable, name: String, endCursorPosition: Int, previousTextLength: Int) { - val selection = editorView.selection ?: return val config = EditorSpans.listSpans[name] ?: return val cursorPosition = endCursorPosition.coerceAtMost(s.length) - val (start, end) = selection.getParagraphBounds(s, cursorPosition) + val (start, end) = s.getParagraphBounds(cursorPosition) val isBackspace = previousTextLength > s.length val isNewLine = cursorPosition > 0 && s[cursorPosition - 1] == '\n' @@ -132,7 +131,7 @@ class ListStyles(private val editorView: ReactNativeRichTextEditorView) { s.replace(start, cursorPosition, "\u200B") setSpan(s, name, start, start + 1) // Inform that new span has been added - editorView.selection.validateStyles() + editorView.selection?.validateStyles() return } @@ -140,7 +139,7 @@ class ListStyles(private val editorView: ReactNativeRichTextEditorView) { s.insert(cursorPosition, "\u200B") setSpan(s, name, start, end + 1) // Inform that new span has been added - editorView.selection.validateStyles() + editorView.selection?.validateStyles() return } 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 9f3f05ac9..f7201a8e9 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/styles/ParagraphStyles.kt @@ -5,6 +5,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import com.swmansion.reactnativerichtexteditor.ReactNativeRichTextEditorView import com.swmansion.reactnativerichtexteditor.spans.EditorSpans +import com.swmansion.reactnativerichtexteditor.utils.getParagraphBounds import com.swmansion.reactnativerichtexteditor.utils.getSafeSpanBoundaries class ParagraphStyles(private val editorView: ReactNativeRichTextEditorView) { @@ -90,7 +91,6 @@ class ParagraphStyles(private val editorView: ReactNativeRichTextEditorView) { } fun afterTextChanged(s: Editable, endPosition: Int, previousTextLength: Int) { - val selection = editorView.selection ?: return var endCursorPosition = endPosition val isBackspace = s.length < previousTextLength val isNewLine = endCursorPosition == 0 || endCursorPosition > 0 && s[endCursorPosition - 1] == '\n' @@ -114,7 +114,7 @@ class ParagraphStyles(private val editorView: ReactNativeRichTextEditorView) { } } - var (start, end) = selection.getParagraphBounds(s, styleStart, endCursorPosition) + var (start, end) = s.getParagraphBounds(styleStart, endCursorPosition) val isNotEndLineSpan = isSpanEnabledInNextLine(s, end, config.clazz) val spans = s.getSpans(start, end, config.clazz) diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/EditorSelection.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/EditorSelection.kt index 84d2b97af..d9325e574 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/EditorSelection.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/EditorSelection.kt @@ -106,36 +106,10 @@ class EditorSelection(private val editorView: ReactNativeRichTextEditorView) { return styleStart } - fun getParagraphBounds(spannable: Spannable, index: Int): Pair { - return getParagraphBounds(spannable, index, index) - } - - fun getParagraphBounds(spannable: Spannable, start: Int, end: Int): Pair { - var startPosition = start.coerceAtLeast(0).coerceAtMost(spannable.length) - var endPosition = end.coerceAtLeast(0).coerceAtMost(spannable.length) - - // Find the start of the paragraph - while (startPosition > 0 && spannable[startPosition - 1] != '\n') { - startPosition-- - } - - // Find the end of the paragraph - while (endPosition < spannable.length && spannable[endPosition] != '\n') { - endPosition++ - } - - if (startPosition >= endPosition) { - // If the start position is equal or greater than the end position, return the same position - startPosition = endPosition - } - - return Pair(startPosition, endPosition) - } - fun getParagraphSelection(): Pair { val (currentStart, currentEnd) = getInlineSelection() val spannable = editorView.text as Spannable - return getParagraphBounds(spannable, currentStart, currentEnd) + return spannable.getParagraphBounds(currentStart, currentEnd) } private fun getParagraphStyleStart(type: Class): Int? { diff --git a/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/Utils.kt b/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/Utils.kt index 7f1a14ffc..a26c6e259 100644 --- a/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/Utils.kt +++ b/android/src/main/java/com/swmansion/reactnativerichtexteditor/utils/Utils.kt @@ -1,7 +1,10 @@ package com.swmansion.reactnativerichtexteditor.utils import android.text.Spannable +import android.text.SpannableStringBuilder import android.util.Log +import com.swmansion.reactnativerichtexteditor.spans.interfaces.EditorBlockSpan +import com.swmansion.reactnativerichtexteditor.spans.interfaces.EditorParagraphSpan import org.json.JSONObject fun jsonStringToStringMap(json: String): Map { @@ -27,3 +30,57 @@ fun Spannable.getSafeSpanBoundaries(start: Int, end: Int): Pair { return Pair(safeStart, safeEnd) } + +fun Spannable.getParagraphBounds(start: Int, end: Int): Pair { + var startPosition = start.coerceAtLeast(0).coerceAtMost(this.length) + var endPosition = end.coerceAtLeast(0).coerceAtMost(this.length) + + // Find the start of the paragraph + while (startPosition > 0 && this[startPosition - 1] != '\n') { + startPosition-- + } + + // Find the end of the paragraph + while (endPosition < this.length && this[endPosition] != '\n') { + endPosition++ + } + + if (startPosition >= endPosition) { + // If the start position is equal or greater than the end position, return the same position + startPosition = endPosition + } + + return Pair(startPosition, endPosition) +} + +fun Spannable.getParagraphBounds(index: Int): Pair { + return this.getParagraphBounds(index, index) +} + +fun Spannable.mergeSpannables(start: Int, end: Int, spannable: Spannable): Spannable { + var finalStart = start + var finalEnd = end + + val builder = SpannableStringBuilder(this) + val startBlockSpans = spannable.getSpans(0, 0, EditorBlockSpan::class.java) + val startParagraphSpans = spannable.getSpans(0, 0, EditorParagraphSpan::class.java) + val endBlockSpans = spannable.getSpans(this.length, this.length, EditorBlockSpan::class.java) + val endParagraphSpans = spannable.getSpans(this.length, this.length, EditorParagraphSpan::class.java) + val (paragraphStart, paragraphEnd) = this.getParagraphBounds(start, end) + val isNewLineStart = startBlockSpans.isNotEmpty() || startParagraphSpans.isNotEmpty() + val isNewLineEnd = endBlockSpans.isNotEmpty() || endParagraphSpans.isNotEmpty() + + if (isNewLineStart && start != paragraphStart) { + builder.insert(start, "\n") + finalStart = start + 1 + finalEnd = end + 1 + } + + if (isNewLineEnd && end != paragraphEnd) { + builder.insert(finalEnd, "\n") + } + + builder.replace(finalStart, finalEnd, spannable) + + return builder +}