diff --git a/editor/src/main/java/com/canopas/editor/ui/data/QuillEditorState.kt b/editor/src/main/java/com/canopas/editor/ui/data/QuillEditorState.kt index 1beafe5..cf7a7d2 100644 --- a/editor/src/main/java/com/canopas/editor/ui/data/QuillEditorState.kt +++ b/editor/src/main/java/com/canopas/editor/ui/data/QuillEditorState.kt @@ -1,6 +1,12 @@ package com.canopas.editor.ui.data +import com.canopas.editor.ui.data.QuillTextManager.Companion.headerLevel +import com.canopas.editor.ui.data.QuillTextManager.Companion.isHeaderStyle +import com.canopas.editor.ui.model.Attributes +import com.canopas.editor.ui.model.ListType import com.canopas.editor.ui.model.QuillSpan +import com.canopas.editor.ui.model.QuillTextSpan +import com.canopas.editor.ui.model.Span import com.canopas.editor.ui.parser.QuillDefaultAdapter import com.canopas.editor.ui.parser.QuillEditorAdapter import com.canopas.editor.ui.utils.TextSpanStyle @@ -21,7 +27,7 @@ class QuillEditorState internal constructor( } fun output(): String { - return adapter.decode(manager.richText) + return adapter.decode(getRichText()) } fun reset() { @@ -38,6 +44,79 @@ class QuillEditorState internal constructor( manager.setStyle(style) } + private fun getRichText() : QuillSpan { + val quillGroupedSpans = manager.quillTextSpans.groupBy { it.from to it.to } + val quillTextSpans = + quillGroupedSpans.map { (fromTo, spanList) -> + val (from, to) = fromTo + val uniqueStyles = spanList.map { it.style }.flatten().distinct() + QuillTextSpan(from, to, uniqueStyles) + } + + val groupedSpans = mutableListOf() + quillTextSpans.forEachIndexed { index, span -> + var insert = manager.editableText.substring(span.from, span.to + 1) + if (insert == " " || insert == "") { + return@forEachIndexed + } + val nextSpan = quillTextSpans.getOrNull(index + 1) + val previousSpan = quillTextSpans.getOrNull(index - 1) + val nextInsert = + nextSpan?.let { manager.editableText.substring(nextSpan.from, nextSpan.to + 1) } + if (nextInsert == " " || nextInsert == "") { + insert += nextInsert + } + var attributes = + Attributes( + header = + if (span.style.any { it.isHeaderStyle() }) + span.style.find { it.isHeaderStyle() }?.headerLevel() + else null, + bold = if (span.style.contains(TextSpanStyle.BoldStyle)) true else null, + italic = if (span.style.contains(TextSpanStyle.ItalicStyle)) true else null, + underline = + if (span.style.contains(TextSpanStyle.UnderlineStyle)) true else null, + list = + if (span.style.contains(TextSpanStyle.BulletStyle)) ListType.bullet + else null + ) + + if (insert == "\n") { + attributes = Attributes() + } + + if ( + previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true && + nextInsert == "\n" && + !insert.contains("\n") + ) { + insert += "\n" + } + if ( + insert == "\n" && + span.style.contains(TextSpanStyle.BulletStyle) && + previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true && + nextSpan?.style?.contains(TextSpanStyle.BulletStyle) == true + ) { + return@forEachIndexed + } + insert = insert.replace("\u200B", "") + // Merge consecutive spans with the same attributes into one + if ( + groupedSpans.isNotEmpty() && + groupedSpans.last().attributes == attributes && + (attributes.list == null || + (groupedSpans.last().insert?.contains('\n') == false)) + ) { + groupedSpans.last().insert += insert + } else { + groupedSpans.add(Span(insert, attributes)) + } + } + + return QuillSpan(groupedSpans) + } + class Builder { private var adapter: QuillEditorAdapter = QuillDefaultAdapter() private var input: String = "" diff --git a/editor/src/main/java/com/canopas/editor/ui/data/QuillTextManager.kt b/editor/src/main/java/com/canopas/editor/ui/data/QuillTextManager.kt index c0cd822..dd09aae 100644 --- a/editor/src/main/java/com/canopas/editor/ui/data/QuillTextManager.kt +++ b/editor/src/main/java/com/canopas/editor/ui/data/QuillTextManager.kt @@ -6,14 +6,11 @@ import android.text.style.BulletSpan import android.text.style.RelativeSizeSpan import android.text.style.StyleSpan import android.text.style.UnderlineSpan -import android.util.Log import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.text.TextRange -import com.canopas.editor.ui.model.Attributes import com.canopas.editor.ui.model.ListType import com.canopas.editor.ui.model.QuillSpan import com.canopas.editor.ui.model.QuillTextSpan -import com.canopas.editor.ui.model.Span import com.canopas.editor.ui.utils.TextSpanStyle import kotlin.math.max import kotlin.math.min @@ -23,7 +20,7 @@ class QuillTextManager(quillSpan: QuillSpan) { private var editable: Editable = Editable.Factory() .newEditable(quillSpan.spans.joinToString(separator = "") { it.insert ?: "" }) - private val quillTextSpans: MutableList = mutableListOf() + internal val quillTextSpans: MutableList = mutableListOf() init { quillSpan.spans.forEachIndexed { index, span -> @@ -79,86 +76,13 @@ class QuillTextManager(quillSpan: QuillSpan) { } } - private val editableText: String + internal val editableText: String get() = editable.toString() private var selection = TextRange(0, 0) private val currentStyles = mutableStateListOf() private var rawText: String = editableText - internal val richText: QuillSpan - get() { - - val quillGroupedSpans = quillTextSpans.groupBy { it.from to it.to } - val quillTextSpans = - quillGroupedSpans.map { (fromTo, spanList) -> - val (from, to) = fromTo - val uniqueStyles = spanList.map { it.style }.flatten().distinct() - QuillTextSpan(from, to, uniqueStyles) - } - - val groupedSpans = mutableListOf() - quillTextSpans.forEachIndexed { index, span -> - var insert = editableText.substring(span.from, span.to + 1) - if (insert == " " || insert == "") { - return@forEachIndexed - } - val nextSpan = quillTextSpans.getOrNull(index + 1) - val previousSpan = quillTextSpans.getOrNull(index - 1) - val nextInsert = - nextSpan?.let { editableText.substring(nextSpan.from, nextSpan.to + 1) } - if (nextInsert == " " || nextInsert == "") { - insert += nextInsert - } - var attributes = - Attributes( - header = - if (span.style.any { it.isHeaderStyle() }) - span.style.find { it.isHeaderStyle() }?.headerLevel() - else null, - bold = if (span.style.contains(TextSpanStyle.BoldStyle)) true else null, - italic = if (span.style.contains(TextSpanStyle.ItalicStyle)) true else null, - underline = - if (span.style.contains(TextSpanStyle.UnderlineStyle)) true else null, - list = - if (span.style.contains(TextSpanStyle.BulletStyle)) ListType.bullet - else null - ) - - if (insert == "\n") { - attributes = Attributes() - } - - if ( - previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true && - nextInsert == "\n" && - !insert.contains("\n") - ) { - insert += "\n" - } - if ( - insert == "\n" && - previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true && - nextSpan?.style?.contains(TextSpanStyle.BulletStyle) == true - ) { - return@forEachIndexed - } - // Merge consecutive spans with the same attributes into one - if ( - groupedSpans.isNotEmpty() && - groupedSpans.last().attributes == attributes && - (attributes.list == null || - (groupedSpans.last().insert?.contains('\n') == false)) - ) { - groupedSpans.last().insert += insert - } else { - groupedSpans.add(Span(insert, attributes)) - } - } - - return QuillSpan(groupedSpans) - } - internal fun setEditable(editable: Editable) { editable.append(editableText) this.editable = editable @@ -240,6 +164,11 @@ class QuillTextManager(quillSpan: QuillSpan) { private fun removeStyle(style: TextSpanStyle) { if (currentStyles.contains(style)) { + if (style == TextSpanStyle.BulletStyle && editable[selection.min - 1] == '\n') { + editable.delete(selection.min - 1, selection.min) + } else if (style == TextSpanStyle.BulletStyle && editable[selection.min - 1] != '\n') { + return + } currentStyles.remove(style) } @@ -248,86 +177,63 @@ class QuillTextManager(quillSpan: QuillSpan) { val toIndex = selection.max - 1 val selectedSpan = quillTextSpans.find { it.from <= fromIndex && it.to >= toIndex } - if (selectedSpan != null) { - if (fromIndex == selectedSpan.from && toIndex == selectedSpan.to) { - val index = quillTextSpans.indexOf(selectedSpan) - quillTextSpans[index] = - selectedSpan.copy(style = selectedSpan.style.filterNot { it == style }) - } else { - if (fromIndex == selectedSpan.from && toIndex < selectedSpan.to) { - val index = quillTextSpans.indexOf(selectedSpan) - quillTextSpans.removeAt(index) - quillTextSpans.add( - index, + selectedSpan?.let { + val index = quillTextSpans.indexOf(it) + val updatedStyle = it.style.filterNot { it == style } + + val newSpans = mutableListOf() + + when { + fromIndex == it.from && toIndex == it.to -> { + quillTextSpans[index] = it.copy(style = updatedStyle) + } + + fromIndex == it.from && toIndex < it.to -> { + newSpans.add( QuillTextSpan( from = fromIndex, to = toIndex, - style = selectedSpan.style.filterNot { it == style } + style = updatedStyle ) ) - quillTextSpans.add( - index + 1, + newSpans.add( QuillTextSpan( from = toIndex + 1, - to = selectedSpan.to, - style = selectedSpan.style + to = it.to, + style = it.style ) ) - } else if (fromIndex > selectedSpan.from && toIndex < selectedSpan.to) { - val index = quillTextSpans.indexOf(selectedSpan) quillTextSpans.removeAt(index) - quillTextSpans.add( - index, + quillTextSpans.addAll(index, newSpans) + } + + fromIndex > it.from -> { + newSpans.add( QuillTextSpan( - from = selectedSpan.from, + from = it.from, to = fromIndex - 1, - style = selectedSpan.style + style = it.style ) ) - quillTextSpans.add( - index + 1, + newSpans.add( QuillTextSpan( from = fromIndex, to = toIndex, - style = selectedSpan.style.filterNot { it == style } + style = updatedStyle ) ) - quillTextSpans.add( - index + 2, + newSpans.add( QuillTextSpan( from = toIndex + 1, - to = selectedSpan.to, - style = selectedSpan.style + to = it.to, + style = it.style ) ) - } else if (fromIndex > 0) { - val index = quillTextSpans.indexOf(selectedSpan) quillTextSpans.removeAt(index) - quillTextSpans.add( - index, - QuillTextSpan( - from = selectedSpan.from, - to = fromIndex - 1, - style = selectedSpan.style - ) - ) - quillTextSpans.add( - index + 1, - QuillTextSpan( - from = fromIndex, - to = toIndex, - style = selectedSpan.style.filterNot { it == style } - ) - ) - quillTextSpans.add( - index + 2, - QuillTextSpan( - from = toIndex + 1, - to = selectedSpan.to, - style = selectedSpan.style - ) - ) + quillTextSpans.addAll(index, newSpans) } + + else -> {} } } updateText() @@ -335,15 +241,51 @@ class QuillTextManager(quillSpan: QuillSpan) { } private fun addStyle(style: TextSpanStyle) { - if (!currentStyles.contains(style)) { - currentStyles.add(style) + if (selection.min > 0) { + when { + !currentStyles.contains(style) -> { + when (style) { + TextSpanStyle.BulletStyle -> { + when { + editable[selection.min - 1] == '\n' && selection.collapsed -> currentStyles.add( + style + ) + + editable[selection.min - 1] == '\n' && !selection.collapsed -> currentStyles.add( + style + ) + } + } + + else -> currentStyles.add(style) + } + + when { + style == TextSpanStyle.BulletStyle && selection.collapsed && editable[selection.min - 1] == '\n' -> editable.insert( + selection.min, + "\u200B" + ) + } + } + } + } else { + if (!currentStyles.contains(style)) { + currentStyles.add(style) + } } - if ((style.isHeaderStyle() || style.isDefault()) && selection.collapsed) { - handleAddHeaderStyle(style) + when { + (style.isHeaderStyle() || style.isDefault()) && selection.collapsed -> handleAddHeaderStyle( + style + ) } - if (!selection.collapsed) { + if (!selection.collapsed && selection.min > 0) { + when { + editable[selection.min - 1] != '\n' && style == TextSpanStyle.BulletStyle -> return + else -> applyStylesToSelectedText(style) + } + } else { applyStylesToSelectedText(style) } } @@ -511,18 +453,13 @@ class QuillTextManager(quillSpan: QuillSpan) { currentSpan?.let { span -> val index = quillTextSpans.indexOf(span) val styles = (span.style + selectedStyles).distinct() - val previousSpan = quillTextSpans.getOrNull(index - 1) - val from = span.from val to = span.to when { span.style == selectedStyles -> { if (isBulletStyle && newValue[startTypeIndex] == '\n') { - if ( - newValue[startTypeIndex - 1] != '\n' || - previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true - ) { + if (newValue[startTypeIndex - 1] != '\n') { quillTextSpans.add( index + 1, QuillTextSpan( @@ -558,16 +495,16 @@ class QuillTextManager(quillSpan: QuillSpan) { quillTextSpans[index] = updatedSpan } } + span.style != selectedStyles -> { quillTextSpans.removeAt(index) if (startTypeIndex == from) { - Log.d("XXX", "Here") quillTextSpans.add( index, span.copy( from = startTypeIndex, to = startTypeIndex + typedCharsCount - 1, - style = selectedStyles + style = styles ) ) quillTextSpans.add( @@ -575,7 +512,7 @@ class QuillTextManager(quillSpan: QuillSpan) { span.copy( from = startTypeIndex + typedCharsCount, to = to + typedCharsCount, - style = span.style + style = styles ) ) } else { @@ -601,10 +538,12 @@ class QuillTextManager(quillSpan: QuillSpan) { ) } } + startTypeIndex == from && to == startTypeIndex -> { quillTextSpans[index] = span.copy(to = to + typedCharsCount, style = selectedStyles) } + startTypeIndex == from && to > startTypeIndex -> { quillTextSpans[index] = span.copy(to = startTypeIndex + typedCharsCount - 1, style = selectedStyles) @@ -617,9 +556,11 @@ class QuillTextManager(quillSpan: QuillSpan) { ) ) } + startTypeIndex > from && to == startTypeIndex -> { quillTextSpans[index] = span.copy(to = to + typedCharsCount, style = styles) } + startTypeIndex in (from + 1) until to -> { quillTextSpans.removeAt(index) quillTextSpans.add(index, span.copy(to = startTypeIndex - 1, style = styles)) @@ -709,10 +650,14 @@ class QuillTextManager(quillSpan: QuillSpan) { val part = iterator.next() val index = partsCopy.indexOf(part) val previousPart = partsCopy.getOrNull(index - 1) - - if (part.style.contains(TextSpanStyle.BulletStyle)) { - if (removeRange.last < part.from) { - if (previousPart?.style?.contains(TextSpanStyle.BulletStyle) == true) { + val nextPart = partsCopy.getOrNull(index + 1) + + if (removeRange.last < part.from) { + if (part.style.contains(TextSpanStyle.BulletStyle)) { + if ( + previousPart?.style?.contains(TextSpanStyle.BulletStyle) == true || + nextPart?.style?.contains(TextSpanStyle.BulletStyle) == true + ) { partsCopy[index] = part.copy( from = part.from - removedCharsCount, @@ -726,41 +671,25 @@ class QuillTextManager(quillSpan: QuillSpan) { style = part.style.filterNot { it == TextSpanStyle.BulletStyle } ) } - } else if (removeRange.first <= part.from && removeRange.last >= part.to) { - // Remove the element from the copy. - partsCopy.removeAt(index) - } else if (removeRange.first <= part.from) { - partsCopy[index] = - part.copy( - from = max(0, removeRange.first), - to = min(newText.length, part.to - removedCharsCount) - ) - } else if (removeRange.last <= part.to) { - partsCopy[index] = part.copy(to = part.to - removedCharsCount) - } else if (removeRange.first < part.to) { - partsCopy[index] = part.copy(to = removeRange.first) - } - } else { - if (removeRange.last < part.from) { + } else { partsCopy[index] = part.copy( from = part.from - removedCharsCount, to = part.to - removedCharsCount ) - } else if (removeRange.first <= part.from && removeRange.last >= part.to) { - // Remove the element from the copy. - partsCopy.removeAt(index) - } else if (removeRange.first <= part.from) { - partsCopy[index] = - part.copy( - from = max(0, removeRange.first), - to = min(newText.length, part.to - removedCharsCount) - ) - } else if (removeRange.last <= part.to) { - partsCopy[index] = part.copy(to = part.to - removedCharsCount) - } else if (removeRange.first < part.to) { - partsCopy[index] = part.copy(to = removeRange.first) } + } else if (removeRange.first <= part.from && removeRange.last >= part.to) { + partsCopy.removeAt(index) + } else if (removeRange.first <= part.from) { + partsCopy[index] = + part.copy( + from = max(0, removeRange.first), + to = min(newText.length, part.to - removedCharsCount) + ) + } else if (removeRange.last <= part.to) { + partsCopy[index] = part.copy(to = part.to - removedCharsCount) + } else if (removeRange.first < part.to) { + partsCopy[index] = part.copy(to = removeRange.first) } } @@ -785,10 +714,6 @@ class QuillTextManager(quillSpan: QuillSpan) { } companion object { - private fun TextRange.overlaps(range: TextRange): Boolean { - return end > range.start && start < range.end - } - fun TextSpanStyle.isDefault(): Boolean { return this == TextSpanStyle.Default } @@ -807,7 +732,7 @@ class QuillTextManager(quillSpan: QuillSpan) { return headers.contains(this) } - private fun TextSpanStyle.headerLevel(): Int? { + internal fun TextSpanStyle.headerLevel(): Int? { return when (this) { TextSpanStyle.H1Style -> 1 TextSpanStyle.H2Style -> 2