From acaf3bea0dc4237be1a1cb02de39399ec3191ca6 Mon Sep 17 00:00:00 2001 From: cp-megh Date: Tue, 6 Feb 2024 16:46:20 +0530 Subject: [PATCH] WIP - Removed old files and passed first 3 test scenarios --- app/src/main/assets/sample-data.json | 110 ----- .../texteditor/parser/JsonEditorParser.kt | 23 - .../texteditor/parser/RichTextStateAdapter.kt | 55 --- .../editor/ui/data/QuillTextManager.kt | 68 +-- .../canopas/editor/ui/data/RichEditorState.kt | 59 --- .../canopas/editor/ui/data/RichTextManager.kt | 441 ------------------ .../{RichTextSpan.kt => QuillTextSpan.kt} | 6 - .../com/canopas/editor/ui/model/RichText.kt | 6 - .../canopas/editor/ui/parser/EditorAdapter.kt | 18 - .../com/canopas/editor/ui/ui/RichEditor.kt | 1 - 10 files changed, 40 insertions(+), 747 deletions(-) delete mode 100644 app/src/main/assets/sample-data.json delete mode 100644 app/src/main/java/com/example/texteditor/parser/JsonEditorParser.kt delete mode 100644 app/src/main/java/com/example/texteditor/parser/RichTextStateAdapter.kt delete mode 100644 editor/src/main/java/com/canopas/editor/ui/data/RichEditorState.kt delete mode 100644 editor/src/main/java/com/canopas/editor/ui/data/RichTextManager.kt rename editor/src/main/java/com/canopas/editor/ui/model/{RichTextSpan.kt => QuillTextSpan.kt} (66%) delete mode 100644 editor/src/main/java/com/canopas/editor/ui/model/RichText.kt delete mode 100644 editor/src/main/java/com/canopas/editor/ui/parser/EditorAdapter.kt diff --git a/app/src/main/assets/sample-data.json b/app/src/main/assets/sample-data.json deleted file mode 100644 index 5052232..0000000 --- a/app/src/main/assets/sample-data.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "spans": [ - { - "from": 0, - "to": 10, - "style": "h1" - }, - { - "from": 0, - "to": 10, - "style": "bold" - }, - { - "from": 19, - "to": 27, - "style": "bold" - }, - { - "from": 44, - "to": 59, - "style": "bold" - }, - { - "from": 44, - "to": 59, - "style": "italic" - }, - { - "from": 44, - "to": 59, - "style": "underline" - }, - { - "from": 61, - "to": 69, - "style": "h3" - }, - { - "from": 62, - "to": 69, - "style": "bold" - }, - { - "from": 103, - "to": 118, - "style": "bold" - }, - { - "from": 119, - "to": 126, - "style": "italic" - }, - { - "from": 130, - "to": 138, - "style": "underline" - }, - { - "from": 160, - "to": 167, - "style": "h3" - }, - { - "from": 161, - "to": 167, - "style": "bold" - }, - { - "from": 169, - "to": 180, - "style": "bold" - }, - { - "from": 224, - "to": 235, - "style": "bold" - }, - { - "from": 224, - "to": 235, - "style": "italic" - }, - { - "from": 224, - "to": 235, - "style": "underline" - }, - { - "from": 237, - "to": 251, - "style": "h4" - }, - { - "from": 238, - "to": 250, - "style": "bold" - }, - { - "from": 238, - "to": 250, - "style": "italic" - }, - { - "from": 238, - "to": 250, - "style": "underline" - } - ], - "text": "Rich Editor\nAndroid WYSIWYG Rich editor for Jetpack compose.\n\nFeatures\nThe editor offers the following options\n\n- Bold\n- Italic\n- Underline\n- Different headers\n\nCredits\nRich Editor for compose is owned and maintained by the canopas team\n\nThanks You ☺️\n" -} \ No newline at end of file diff --git a/app/src/main/java/com/example/texteditor/parser/JsonEditorParser.kt b/app/src/main/java/com/example/texteditor/parser/JsonEditorParser.kt deleted file mode 100644 index c0dd155..0000000 --- a/app/src/main/java/com/example/texteditor/parser/JsonEditorParser.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.example.texteditor.parser - -import com.canopas.editor.ui.model.RichText -import com.canopas.editor.ui.model.RichTextSpan -import com.canopas.editor.ui.parser.EditorAdapter -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.reflect.TypeToken - -class JsonEditorParser : EditorAdapter { - - private val gson: Gson = GsonBuilder() - .registerTypeAdapter(RichTextSpan::class.java, RichTextSpanAdapter()) - .create() - - override fun encode(input: String): RichText { - return gson.fromJson(input, object : TypeToken() {}.type) - } - - override fun decode(editorValue: RichText): String { - return gson.toJson(editorValue) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/texteditor/parser/RichTextStateAdapter.kt b/app/src/main/java/com/example/texteditor/parser/RichTextStateAdapter.kt deleted file mode 100644 index 0409208..0000000 --- a/app/src/main/java/com/example/texteditor/parser/RichTextStateAdapter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.texteditor.parser - -import com.canopas.editor.ui.model.RichTextSpan -import com.canopas.editor.ui.utils.TextSpanStyle -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParseException -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import java.lang.reflect.Type - -class RichTextSpanAdapter : JsonSerializer, JsonDeserializer { - override fun serialize( - src: RichTextSpan?, - typeOfSrc: Type?, - context: JsonSerializationContext? - ): JsonElement { - val jsonObject = JsonObject() - jsonObject.addProperty("from", src?.from) - jsonObject.addProperty("to", src?.to) - jsonObject.addProperty("style", src?.style?.key ?: "") - return jsonObject - } - - override fun deserialize( - json: JsonElement?, - typeOfT: Type?, - context: JsonDeserializationContext? - ): RichTextSpan { - val jsonObject = json?.asJsonObject ?: throw JsonParseException("Invalid JSON") - val fromIndex = jsonObject.get("from").asInt - val toIndex = jsonObject.get("to").asInt - val spansString = jsonObject.get("style").asString - val spanStyle = spansString.toSpanStyle() - return RichTextSpan(fromIndex, toIndex, spanStyle) - } -} - -fun String.toSpanStyle(): TextSpanStyle { - return spanStyleParserMap[this] ?: TextSpanStyle.Default -} - -val spanStyleParserMap = mapOf( - "bold" to TextSpanStyle.BoldStyle, - "italic" to TextSpanStyle.ItalicStyle, - "underline" to TextSpanStyle.UnderlineStyle, - "h1" to TextSpanStyle.H1Style, - "h2" to TextSpanStyle.H2Style, - "h3" to TextSpanStyle.H3Style, - "h4" to TextSpanStyle.H4Style, - "h5" to TextSpanStyle.H5Style, - "h6" to TextSpanStyle.H6Style, -) \ No newline at end of file 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 3dba064..de1788f 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 @@ -93,7 +93,7 @@ class QuillTextManager(quillSpan: QuillSpan) { val quillGroupedSpans = quillTextSpans.groupBy { it.from to it.to } - val quillTextSpans = quillGroupedSpans.map {(fromTo, spanList) -> + val quillTextSpans = quillGroupedSpans.map { (fromTo, spanList) -> val (from, to) = fromTo val uniqueStyles = spanList.map { it.style }.flatten().distinct() QuillTextSpan(from, to, uniqueStyles) @@ -106,12 +106,14 @@ class QuillTextManager(quillSpan: QuillSpan) { return@forEachIndexed } val nextSpan = quillTextSpans.getOrNull(index + 1) - val nextInsert = nextSpan?.let { editableText.substring(nextSpan.from, nextSpan.to + 1) } + val nextInsert = + nextSpan?.let { editableText.substring(nextSpan.from, nextSpan.to + 1) } if (nextInsert == " " || nextInsert == "") { insert += nextInsert } val attributes = Attributes( - header = if (span.style.any { it.isHeaderStyle() }) span.style.find { it.isHeaderStyle() }?.headerLevel() else null, + 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, @@ -119,7 +121,7 @@ class QuillTextManager(quillSpan: QuillSpan) { ) // Merge consecutive spans with the same attributes into one - if (groupedSpans.isNotEmpty() && groupedSpans.last().attributes == attributes) { + if (groupedSpans.isNotEmpty() && groupedSpans.last().attributes == attributes && attributes.list == null) { groupedSpans.last().insert += insert } else { groupedSpans.add(Span(insert, attributes)) @@ -130,7 +132,7 @@ class QuillTextManager(quillSpan: QuillSpan) { } private fun TextSpanStyle.headerLevel(): Int? { - return when(this) { + return when (this) { TextSpanStyle.H1Style -> 1 TextSpanStyle.H2Style -> 2 TextSpanStyle.H3Style -> 3 @@ -398,14 +400,26 @@ class QuillTextManager(quillSpan: QuillSpan) { } if (startParts.isEmpty() && endParts.isEmpty() && selectedParts.isNotEmpty()) { - quillTextSpans.add(QuillTextSpan(from = fromIndex, to = toIndex - 1, style = listOf(style))) + quillTextSpans.add( + QuillTextSpan( + from = fromIndex, + to = toIndex - 1, + style = listOf(style) + ) + ) } else if (startParts.map { it.style }.any { it.contains(style) }) { startParts.filter { it.style == style }.forEach { updateToIndex(it, toIndex - 1) } } else if (endParts.map { it.style }.any { it.contains(style) }) { endParts.filter { it.style == style } .forEach { part -> updateFromIndex(part, fromIndex) } } else { - quillTextSpans.add(QuillTextSpan(from = fromIndex, to = toIndex - 1, style = listOf(style))) + quillTextSpans.add( + QuillTextSpan( + from = fromIndex, + to = toIndex - 1, + style = listOf(style) + ) + ) } updateText() @@ -444,7 +458,7 @@ class QuillTextManager(quillSpan: QuillSpan) { currentStyles.clear() } - val selectedStyles = currentStyles.toMutableList() + var selectedStyles = currentStyles.distinct().toMutableList() moveSpans(startTypeIndex, typedCharsCount) @@ -454,21 +468,23 @@ class QuillTextManager(quillSpan: QuillSpan) { startParts.filter { it !in commonParts } .forEach { - if (selectedStyles == it.style) { + if (selectedStyles.any { selectedStyle -> selectedStyle in it.style }) { val index = quillTextSpans.indexOf(it) quillTextSpans[index] = it.copy(to = it.to + typedCharsCount) - selectedStyles.forEach {style -> - if (style in it.style) { - selectedStyles.remove(style) - } - } + selectedStyles = + selectedStyles.filterNot { style -> style in it.style }.toMutableList() } } endParts.filter { it !in commonParts } - .forEach { processSpan(it, typedCharsCount, startTypeIndex, selectedStyles, true) } + .forEach { + selectedStyles = + processSpan(it, typedCharsCount, startTypeIndex, selectedStyles, true) + } - commonParts.forEach { processSpan(it, typedCharsCount, startTypeIndex, selectedStyles) } + commonParts.forEach { + selectedStyles = processSpan(it, typedCharsCount, startTypeIndex, selectedStyles) + } selectedStyles.forEach { quillTextSpans.add( @@ -487,19 +503,17 @@ class QuillTextManager(quillSpan: QuillSpan) { startTypeIndex: Int, selectedStyles: MutableList, forward: Boolean = false - ) { + ): MutableList { val newFromIndex = richTextSpan.from + typedChars val newToIndex = richTextSpan.to + typedChars + var updatedSelectedStyle = selectedStyles val index = quillTextSpans.indexOf(richTextSpan) - if (selectedStyles == richTextSpan.style) { + if (selectedStyles.any { it in richTextSpan.style }) { quillTextSpans[index] = richTextSpan.copy(to = newToIndex) - selectedStyles.forEach { style -> - if (style in richTextSpan.style) { - selectedStyles.remove(style) - } - } + updatedSelectedStyle = + selectedStyles.filterNot { it in richTextSpan.style }.toMutableList() } else { if (forward) { quillTextSpans[index] = richTextSpan.copy( @@ -514,13 +528,11 @@ class QuillTextManager(quillSpan: QuillSpan) { to = newToIndex ) ) - selectedStyles.forEach {style -> - if (style in richTextSpan.style) { - selectedStyles.remove(style) - } - } + updatedSelectedStyle = + selectedStyles.filterNot { it in richTextSpan.style }.toMutableList() } } + return updatedSelectedStyle } private fun moveSpans(startTypeIndex: Int, by: Int) { diff --git a/editor/src/main/java/com/canopas/editor/ui/data/RichEditorState.kt b/editor/src/main/java/com/canopas/editor/ui/data/RichEditorState.kt deleted file mode 100644 index b11dea4..0000000 --- a/editor/src/main/java/com/canopas/editor/ui/data/RichEditorState.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.canopas.editor.ui.data - -import com.canopas.editor.ui.model.RichText -import com.canopas.editor.ui.parser.DefaultAdapter -import com.canopas.editor.ui.parser.EditorAdapter -import com.canopas.editor.ui.utils.TextSpanStyle - -class RichEditorState internal constructor( - private val input: String, - private val adapter: EditorAdapter = DefaultAdapter(), -) { - - internal var manager: RichTextManager - - init { - manager = RichTextManager(getRichText()) - } - - private fun getRichText(): RichText { - return if (input.isNotEmpty()) adapter.encode(input) else RichText() - } - - fun output(): String { - return adapter.decode(manager.richText) - } - - fun reset() { - manager.reset() - } - - fun hasStyle(style: TextSpanStyle) = manager.hasStyle(style) - - fun toggleStyle(style: TextSpanStyle) { - manager.toggleStyle(style) - } - - fun updateStyle(style: TextSpanStyle) { - manager.setStyle(style) - } - - class Builder { - private var adapter: EditorAdapter = DefaultAdapter() - private var input: String = "" - - fun setInput(input: String) = apply { - this.input = input - } - - fun adapter(adapter: EditorAdapter) = apply { - this.adapter = adapter - } - - fun build(): RichEditorState { - return RichEditorState(input, adapter) - } - } -} - - diff --git a/editor/src/main/java/com/canopas/editor/ui/data/RichTextManager.kt b/editor/src/main/java/com/canopas/editor/ui/data/RichTextManager.kt deleted file mode 100644 index 91e1aa0..0000000 --- a/editor/src/main/java/com/canopas/editor/ui/data/RichTextManager.kt +++ /dev/null @@ -1,441 +0,0 @@ -package com.canopas.editor.ui.data - -import android.text.Editable -import android.text.Spannable -import android.text.style.RelativeSizeSpan -import android.text.style.StyleSpan -import android.text.style.UnderlineSpan -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.ui.text.TextRange -import com.canopas.editor.ui.model.RichText -import com.canopas.editor.ui.model.RichTextSpan -import com.canopas.editor.ui.utils.TextSpanStyle -import kotlin.math.max -import kotlin.math.min - -class RichTextManager(richText: RichText) { - - private var editable: Editable = Editable.Factory().newEditable(richText.text) - private val spans: MutableList = richText.spans - private val editableText: String get() = editable.toString() - - private var selection = TextRange(0, 0) - private val currentStyles = mutableStateListOf() - private var rawText: String = richText.text - - internal val richText: RichText - get() = RichText(editableText, spans) - - internal fun setEditable(editable: Editable) { - editable.append(editableText) - this.editable = editable - if (editableText.isNotEmpty()) updateText() - } - - private fun updateText() { - editable.removeSpans() - editable.removeSpans() - editable.removeSpans() - - spans.forEach { - editable.setSpan( - it.style.style, - it.from, - it.to + 1, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - - updateCurrentSpanStyle() - } - - private fun updateCurrentSpanStyle() { - if (this.selection.collapsed && this.selection.min == 0) return - this.currentStyles.clear() - - val currentStyles = if (selection.collapsed) { - getRichSpanByTextIndex(textIndex = selection.min - 1) - } else { - getRichSpanListByTextRange(selection).distinct() - } - - this.currentStyles.addAll(currentStyles) - } - - private fun getRichSpanByTextIndex(textIndex: Int): List { - return spans.filter { textIndex >= it.from && textIndex <= it.to } - .map { it.style } - } - - private fun getRichSpanListByTextRange(selection: TextRange): List { - val matchingSpans = mutableListOf() - - for (part in spans) { - val partRange = TextRange(part.from, part.to) - if (selection.overlaps(partRange)) { - part.style.let { - matchingSpans.add(it) - } - } - } - return matchingSpans - } - - fun toggleStyle(style: TextSpanStyle) { - if (currentStyles.contains(style)) { - removeStyle(style) - } else { - addStyle(style) - } - } - - private fun removeStyle(style: TextSpanStyle) { - if (currentStyles.contains(style)) { - currentStyles.remove(style) - } - - if (!selection.collapsed) { - val fromIndex = selection.min - val toIndex = selection.max - - val selectedParts = spans.filter { part -> - part.from < toIndex && part.to >= fromIndex && part.style == style - } - removeStylesFromSelectedPart(selectedParts, fromIndex, toIndex) - updateText() - } - } - - private fun addStyle(style: TextSpanStyle) { - if (!currentStyles.contains(style)) { - currentStyles.add(style) - } - - if ((style.isHeaderStyle() || style.isDefault()) && selection.collapsed) { - handleAddHeaderStyle(style) - } - - if (!selection.collapsed) { - applyStylesToSelectedText(style) - } - } - - private fun handleAddHeaderStyle( - style: TextSpanStyle, - text: String = rawText - ) { - if (text.isEmpty()) return - val fromIndex = selection.min - val toIndex = if (selection.collapsed) fromIndex else selection.max - - val startIndex: Int = max(0, text.lastIndexOf("\n", fromIndex - 1)) - var endIndex: Int = text.indexOf("\n", toIndex) - - if (endIndex == -1) endIndex = text.length - 1 - val selectedParts = - spans.filter { endIndex >= it.to && startIndex <= it.from && it.style.isHeaderStyle() } - - spans.removeAll(selectedParts) - spans.add( - RichTextSpan( - from = startIndex, - to = endIndex, - style = style - ) - ) - updateText() - } - - private fun handleRemoveHeaderStyle( - text: String = rawText - ) { - if (text.isEmpty()) return - - val fromIndex = selection.min - val toIndex = selection.max - - val startIndex: Int = max(0, text.lastIndexOf("\n", fromIndex - 1)) - var endIndex: Int = text.indexOf("\n", toIndex) - - if (endIndex == -1) endIndex = text.length - 1 - - val nextNewlineIndex = text.lastIndexOf("\n", startIndex) - - val parts = spans.filter { part -> - part.from < nextNewlineIndex && part.to >= startIndex - } - if (parts.isEmpty() && fromIndex - 1 == nextNewlineIndex) return - - val selectedParts = spans.filter { part -> - part.from < endIndex && part.to >= startIndex - } - - spans.removeAll(selectedParts.filter { it.style.isHeaderStyle() }) - } - - private fun removeStylesFromSelectedPart( - selectedParts: List, - fromIndex: Int, toIndex: Int - ) { - selectedParts.forEach { part -> - val index = spans.indexOf(part) - if (index !in spans.indices) return@forEach - - if (part.from < fromIndex && part.to >= toIndex) { - spans[index] = part.copy(to = fromIndex - 1) - spans.add(index + 1, part.copy(from = toIndex)) - } else if (part.from < fromIndex) { - spans[index] = part.copy(to = fromIndex - 1) - } else if (part.to > toIndex) { - spans[index] = part.copy(from = toIndex) - } else { - spans.removeAt(index) - } - } - } - - private fun applyStylesToSelectedText(style: TextSpanStyle) { - if (selection.collapsed) return - - val fromIndex = selection.min - val toIndex = selection.max - - val selectedParts = spans.filter { part -> - part.from < toIndex && part.to >= fromIndex - } - val startParts = spans.filter { fromIndex - 1 in it.from..it.to } - val endParts = spans.filter { toIndex in it.from..it.to } - - val updateToIndex: (RichTextSpan, Int) -> Unit = { part, index -> - val partIndex = spans.indexOf(part) - spans[partIndex] = part.copy(to = index) - } - - val updateFromIndex: (RichTextSpan, Int) -> Unit = { part, index -> - val partIndex = spans.indexOf(part) - spans[partIndex] = part.copy(from = index) - } - - if (startParts.isEmpty() && endParts.isEmpty() && selectedParts.isNotEmpty()) { - spans.add(RichTextSpan(from = fromIndex, to = toIndex - 1, style = style)) - } else if (style in startParts.map { it.style }) { - startParts.filter { it.style == style }.forEach { updateToIndex(it, toIndex - 1) } - } else if (style in endParts.map { it.style }) { - endParts.filter { it.style == style } - .forEach { part -> updateFromIndex(part, fromIndex) } - } else { - spans.add(RichTextSpan(from = fromIndex, to = toIndex - 1, style = style)) - } - - updateText() - } - - fun setStyle(style: TextSpanStyle) { - currentStyles.clear() - currentStyles.add(style) - - if ((style.isHeaderStyle() || style.isDefault())) { - handleAddHeaderStyle(style) - return - } - if (!selection.collapsed) { - applyStylesToSelectedText(style) - } - } - - fun onTextFieldValueChange(newText: Editable, selection: TextRange) { - this.selection = selection - if (newText.length > rawText.length) - handleAddingCharacters(newText) - else if (newText.length < rawText.length) - handleRemovingCharacters(newText) - - updateText() - this.rawText = newText.toString() - - } - - private fun handleAddingCharacters(newValue: Editable) { - val typedChars = newValue.length - rawText.length - val startTypeIndex = selection.min - typedChars - - if (newValue.getOrNull(startTypeIndex) == '\n' && currentStyles.any { it.isHeaderStyle() }) { - currentStyles.clear() - } - - val selectedStyles = currentStyles.toMutableList() - - moveSpans(startTypeIndex, typedChars) - - val startParts = spans.filter { startTypeIndex - 1 in it.from..it.to } - val endParts = spans.filter { startTypeIndex in it.from..it.to } - val commonParts = startParts.intersect(endParts.toSet()) - - startParts.filter { it !in commonParts } - .forEach { - if (selectedStyles.contains(it.style)) { - val index = spans.indexOf(it) - spans[index] = it.copy(to = it.to + typedChars) - selectedStyles.remove(it.style) - } - } - - endParts.filter { it !in commonParts } - .forEach { processSpan(it, typedChars, startTypeIndex, selectedStyles, true) } - - commonParts.forEach { processSpan(it, typedChars, startTypeIndex, selectedStyles) } - - selectedStyles.forEach { - spans.add( - RichTextSpan( - from = startTypeIndex, - to = startTypeIndex + typedChars - 1, - style = it - ) - ) - } - } - - private fun processSpan( - richTextSpan: RichTextSpan, - typedChars: Int, - startTypeIndex: Int, - selectedStyles: MutableList, - forward: Boolean = false - ) { - - val newFromIndex = richTextSpan.from + typedChars - val newToIndex = richTextSpan.to + typedChars - - val index = spans.indexOf(richTextSpan) - if (selectedStyles.contains(richTextSpan.style)) { - spans[index] = richTextSpan.copy(to = newToIndex) - selectedStyles.remove(richTextSpan.style) - } else { - if (forward) { - spans[index] = richTextSpan.copy( - from = newFromIndex, - to = newToIndex - ) - } else { - spans[index] = richTextSpan.copy(to = startTypeIndex - 1) - spans.add( - index + 1, richTextSpan.copy( - from = startTypeIndex + typedChars, - to = newToIndex - ) - ) - selectedStyles.remove(richTextSpan.style) - } - } - } - - private fun moveSpans(startTypeIndex: Int, by: Int) { - val filteredSpans = spans.filter { it.from > startTypeIndex } - - filteredSpans.forEach { - val index = spans.indexOf(it) - spans[index] = it.copy( - from = it.from + by, - to = it.to + by, - ) - } - } - - private fun handleRemovingCharacters(newText: Editable) { - if (newText.isEmpty()) { - spans.clear() - currentStyles.clear() - return - } - - val removedCharsCount = rawText.length - newText.length - val startRemoveIndex = selection.min + removedCharsCount - val endRemoveIndex = selection.min - val removeRange = endRemoveIndex until startRemoveIndex - - val newLineIndex = rawText.substring(endRemoveIndex, startRemoveIndex).indexOf("\n") - - if (newLineIndex != -1) { - handleRemoveHeaderStyle(newText.toString()) - } - - val iterator = spans.iterator() - - val partsCopy = spans.toMutableList() - - while (iterator.hasNext()) { - val part = iterator.next() - val index = partsCopy.indexOf(part) - - if (removeRange.last < part.from) { - 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) - } - } - - spans.clear() - spans.addAll(partsCopy) - } - - internal fun adjustSelection(selection: TextRange) { - if (this.selection != selection) { - this.selection = selection - updateCurrentSpanStyle() - } - } - - fun hasStyle(style: TextSpanStyle) = currentStyles.contains(style) - - fun reset() { - spans.clear() - this.rawText = "" - this.editable.clear() - updateText() - } - - companion object { - private fun TextRange.overlaps(range: TextRange): Boolean { - return end > range.start && start < range.end - } - - fun TextSpanStyle.isDefault(): Boolean { - return this == TextSpanStyle.Default - } - - fun TextSpanStyle.isHeaderStyle(): Boolean { - val headers = listOf( - TextSpanStyle.H1Style, - TextSpanStyle.H2Style, - TextSpanStyle.H3Style, - TextSpanStyle.H4Style, - TextSpanStyle.H5Style, - TextSpanStyle.H6Style, - ) - - return headers.contains(this) - } - - internal inline fun Editable.removeSpans() { - val allSpans = getSpans(0, length, T::class.java) - for (span in allSpans) { - removeSpan(span) - } - } - - } -} \ No newline at end of file diff --git a/editor/src/main/java/com/canopas/editor/ui/model/RichTextSpan.kt b/editor/src/main/java/com/canopas/editor/ui/model/QuillTextSpan.kt similarity index 66% rename from editor/src/main/java/com/canopas/editor/ui/model/RichTextSpan.kt rename to editor/src/main/java/com/canopas/editor/ui/model/QuillTextSpan.kt index 8c3fff7..610191b 100644 --- a/editor/src/main/java/com/canopas/editor/ui/model/RichTextSpan.kt +++ b/editor/src/main/java/com/canopas/editor/ui/model/QuillTextSpan.kt @@ -2,12 +2,6 @@ package com.canopas.editor.ui.model import com.canopas.editor.ui.utils.TextSpanStyle -data class RichTextSpan( - val from: Int, - val to: Int, - val style: TextSpanStyle, -) - data class QuillTextSpan( val from: Int, val to: Int, diff --git a/editor/src/main/java/com/canopas/editor/ui/model/RichText.kt b/editor/src/main/java/com/canopas/editor/ui/model/RichText.kt deleted file mode 100644 index f1fd4a8..0000000 --- a/editor/src/main/java/com/canopas/editor/ui/model/RichText.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.canopas.editor.ui.model - -data class RichText( - val text: String = "", - val spans: MutableList = mutableListOf() -) \ No newline at end of file diff --git a/editor/src/main/java/com/canopas/editor/ui/parser/EditorAdapter.kt b/editor/src/main/java/com/canopas/editor/ui/parser/EditorAdapter.kt deleted file mode 100644 index 0ed6869..0000000 --- a/editor/src/main/java/com/canopas/editor/ui/parser/EditorAdapter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.canopas.editor.ui.parser - -import com.canopas.editor.ui.model.RichText - -interface EditorAdapter { - fun encode(input: String): RichText - fun decode(editorValue: RichText): String -} - -class DefaultAdapter : EditorAdapter { - override fun encode(input: String): RichText { - return RichText("") - } - - override fun decode(editorValue: RichText): String { - return editorValue.text - } -} \ No newline at end of file diff --git a/editor/src/main/java/com/canopas/editor/ui/ui/RichEditor.kt b/editor/src/main/java/com/canopas/editor/ui/ui/RichEditor.kt index 4b4eb1a..00ddcc2 100644 --- a/editor/src/main/java/com/canopas/editor/ui/ui/RichEditor.kt +++ b/editor/src/main/java/com/canopas/editor/ui/ui/RichEditor.kt @@ -15,7 +15,6 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.viewinterop.AndroidView import androidx.core.widget.doAfterTextChanged import com.canopas.editor.ui.data.QuillEditorState -import com.canopas.editor.ui.data.RichEditorState @Composable fun RichEditor(