Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -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("<html>") && text.endsWith("</html>")
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("<html>") && value.endsWith("</html>")
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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <T>getPreviousParagraphSpan(spannable: Spannable, s: Int, type: Class<T>): 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()) {
Expand Down Expand Up @@ -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'
Expand All @@ -132,15 +131,15 @@ 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
}

if (!isBackspace && isNewLine && isPreviousParagraphList(s, start, config.clazz)) {
s.insert(cursorPosition, "\u200B")
setSpan(s, name, start, end + 1)
// Inform that new span has been added
editorView.selection.validateStyles()
editorView.selection?.validateStyles()
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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'
Expand All @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,36 +106,10 @@ class EditorSelection(private val editorView: ReactNativeRichTextEditorView) {
return styleStart
}

fun getParagraphBounds(spannable: Spannable, index: Int): Pair<Int, Int> {
return getParagraphBounds(spannable, index, index)
}

fun getParagraphBounds(spannable: Spannable, start: Int, end: Int): Pair<Int, Int> {
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<Int, Int> {
val (currentStart, currentEnd) = getInlineSelection()
val spannable = editorView.text as Spannable
return getParagraphBounds(spannable, currentStart, currentEnd)
return spannable.getParagraphBounds(currentStart, currentEnd)
}

private fun <T>getParagraphStyleStart(type: Class<T>): Int? {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> {
Expand All @@ -27,3 +30,57 @@ fun Spannable.getSafeSpanBoundaries(start: Int, end: Int): Pair<Int, Int> {

return Pair(safeStart, safeEnd)
}

fun Spannable.getParagraphBounds(start: Int, end: Int): Pair<Int, Int> {
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<Int, Int> {
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
}
Loading