From 781f86501fcead6eb2b82a3b830f47e2f467636c Mon Sep 17 00:00:00 2001 From: cp-megh Date: Tue, 20 Feb 2024 17:06:18 +0530 Subject: [PATCH] Add bullets support - stage 1 --- app/src/main/assets/android-quill-sample.json | 24 +- .../editor/ui/data/QuillTextManager.kt | 350 +++++++++++------- 2 files changed, 220 insertions(+), 154 deletions(-) diff --git a/app/src/main/assets/android-quill-sample.json b/app/src/main/assets/android-quill-sample.json index 5710b3a..2bcd356 100644 --- a/app/src/main/assets/android-quill-sample.json +++ b/app/src/main/assets/android-quill-sample.json @@ -89,37 +89,37 @@ } }, { - "insert": "Item 1", + "insert": "Item 1\n", "attributes": { "list": "bullet" } }, { - "insert": "\n", - "attributes": {} - }, - { - "insert": "Item 2", + "insert": "Item 2\n", "attributes": { "list": "bullet" } }, { - "insert": "\n", - "attributes": {} + "insert": "Item 3\n", + "attributes": { + "list": "bullet" + } }, { - "insert": "Item 3", + "insert": "Item 4\n", "attributes": { "list": "bullet" } }, { - "insert": "\n", - "attributes": {} + "insert": "Item 5\n", + "attributes": { + "list": "bullet" + } }, { - "insert": "Item 4\n", + "insert": "Item 6\n", "attributes": { "list": "bullet" } 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 941df6c..c0cd822 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,6 +6,7 @@ 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 @@ -63,25 +64,23 @@ class QuillTextManager(quillSpan: QuillSpan) { } quillTextSpans.add( - QuillTextSpan( - from = fromIndex, - to = endIndex, - style = textSpanStyles - ) - ) - } ?: run { - quillTextSpans.add( - QuillTextSpan( - from = fromIndex, - to = endIndex, - style = listOf(TextSpanStyle.Default) - ) + QuillTextSpan(from = fromIndex, to = endIndex, style = textSpanStyles) ) } + ?: run { + quillTextSpans.add( + QuillTextSpan( + from = fromIndex, + to = endIndex, + style = listOf(TextSpanStyle.Default) + ) + ) + } } } - private val editableText: String get() = editable.toString() + private val editableText: String + get() = editable.toString() private var selection = TextRange(0, 0) private val currentStyles = mutableStateListOf() @@ -90,14 +89,13 @@ class QuillTextManager(quillSpan: QuillSpan) { 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 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 -> @@ -106,26 +104,52 @@ class QuillTextManager(quillSpan: QuillSpan) { 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 - ) + 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) { + 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)) @@ -153,7 +177,7 @@ class QuillTextManager(quillSpan: QuillSpan) { style.style, it.from, it.to + 1, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + Spannable.SPAN_INCLUSIVE_EXCLUSIVE ) } } @@ -165,23 +189,22 @@ class QuillTextManager(quillSpan: QuillSpan) { 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() - } + val currentStyles = + if (selection.collapsed) { + getRichSpanByTextIndex(textIndex = selection.min - 1) + } else { + getRichSpanListByTextRange(selection).distinct() + } val currentSpan = quillTextSpans.findLast { - it.from <= selection.min - 2 && it.to >= selection.min - 2 && it.style.contains( - TextSpanStyle.BulletStyle - ) + it.from <= selection.min - 2 && + it.to >= selection.min - 2 && + it.style.contains(TextSpanStyle.BulletStyle) } if (currentSpan != null && this.selection.collapsed) { - if (editable[selection.min - 1] == '\n' && editable[selection.min - 2] != '\n') { - addStyle(TextSpanStyle.BulletStyle) - } else if (editable[selection.min - 1] == '\n' && editable[selection.min - 2] == '\n') { + if (editable[selection.min - 1] == '\n' && editable[selection.min - 2] == '\n') { removeStyle(TextSpanStyle.BulletStyle) } else { this.currentStyles.addAll(currentStyles) @@ -192,15 +215,15 @@ class QuillTextManager(quillSpan: QuillSpan) { } private fun getRichSpanByTextIndex(textIndex: Int): List { - return quillTextSpans.filter { textIndex >= it.from && textIndex <= it.to } - .map { it.style }.flatten() + return quillTextSpans + .filter { textIndex >= it.from && textIndex <= it.to } + .map { it.style } + .flatten() } private fun getRichSpanListByTextRange(selection: TextRange): List { val matchingSpans = mutableListOf() - val currentSpan = quillTextSpans.find { - it.from <= selection.min && it.to >= selection.min - } + val currentSpan = quillTextSpans.find { it.from <= selection.min && it.to >= selection.min } if (currentSpan != null) { matchingSpans.addAll(currentSpan.style) } @@ -224,9 +247,7 @@ class QuillTextManager(quillSpan: QuillSpan) { val fromIndex = selection.min val toIndex = selection.max - 1 - val selectedSpan = quillTextSpans.find { - it.from <= fromIndex && it.to >= toIndex - } + 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) @@ -327,28 +348,21 @@ class QuillTextManager(quillSpan: QuillSpan) { } } - private fun handleAddHeaderStyle( - style: TextSpanStyle, - text: String = rawText - ) { + 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 currentSpan = quillTextSpans.find { - it.from <= fromIndex && it.to >= toIndex - } + val currentSpan = quillTextSpans.find { it.from <= fromIndex && it.to >= toIndex } val index = quillTextSpans.indexOf(currentSpan) quillTextSpans[index] = - currentSpan?.copy(style = currentSpan.style.filterNot { it.isHeaderStyle() } + listOf( - style - )) ?: return + currentSpan?.copy( + style = currentSpan.style.filterNot { it.isHeaderStyle() } + listOf(style) + ) ?: return updateText() } - private fun handleRemoveHeaderStyle( - text: String = rawText - ) { + private fun handleRemoveHeaderStyle(text: String = rawText) { if (text.isEmpty()) return val fromIndex = selection.min @@ -361,18 +375,16 @@ class QuillTextManager(quillSpan: QuillSpan) { val nextNewlineIndex = text.lastIndexOf("\n", startIndex) - val parts = quillTextSpans.filter { part -> - part.from < nextNewlineIndex && part.to >= startIndex - } + val parts = + quillTextSpans.filter { part -> part.from < nextNewlineIndex && part.to >= startIndex } if (parts.isEmpty() && fromIndex - 1 == nextNewlineIndex) return - val selectedParts = quillTextSpans.filter { part -> - part.from < endIndex && part.to >= startIndex - } + val selectedParts = + quillTextSpans.filter { part -> part.from < endIndex && part.to >= startIndex } - quillTextSpans.removeAll(selectedParts.filter { - it.style.size == 1 && it.style.first().isHeaderStyle() - }) + quillTextSpans.removeAll( + selectedParts.filter { it.style.size == 1 && it.style.first().isHeaderStyle() } + ) } private fun applyStylesToSelectedText(style: TextSpanStyle) { @@ -381,9 +393,7 @@ class QuillTextManager(quillSpan: QuillSpan) { val fromIndex = selection.min val toIndex = selection.max - val selectedSpan = quillTextSpans.find { - it.from <= fromIndex && (it.to + 1) >= toIndex - } + val selectedSpan = quillTextSpans.find { it.from <= fromIndex && (it.to + 1) >= toIndex } if (selectedSpan != null) { if (fromIndex == selectedSpan.from && toIndex < selectedSpan.to) { val index = quillTextSpans.indexOf(selectedSpan) @@ -398,11 +408,7 @@ class QuillTextManager(quillSpan: QuillSpan) { ) quillTextSpans.add( index + 1, - QuillTextSpan( - from = toIndex, - to = selectedSpan.to, - style = selectedSpan.style - ) + QuillTextSpan(from = toIndex, to = selectedSpan.to, style = selectedSpan.style) ) } else if (fromIndex > selectedSpan.from && toIndex < selectedSpan.to) { val index = quillTextSpans.indexOf(selectedSpan) @@ -425,13 +431,10 @@ class QuillTextManager(quillSpan: QuillSpan) { ) quillTextSpans.add( index + 2, - QuillTextSpan( - from = toIndex, - to = selectedSpan.to, - style = selectedSpan.style - ) + QuillTextSpan(from = toIndex, to = selectedSpan.to, style = selectedSpan.style) ) - } else if (fromIndex > selectedSpan.from && + } else if ( + fromIndex > selectedSpan.from && (toIndex == selectedSpan.to || toIndex == (selectedSpan.to + 1)) ) { val index = quillTextSpans.indexOf(selectedSpan) @@ -459,11 +462,7 @@ class QuillTextManager(quillSpan: QuillSpan) { } } else { quillTextSpans.add( - QuillTextSpan( - from = fromIndex, - to = toIndex - 1, - style = listOf(style) - ) + QuillTextSpan(from = fromIndex, to = toIndex - 1, style = listOf(style)) ) } updateText() @@ -484,21 +483,20 @@ class QuillTextManager(quillSpan: QuillSpan) { fun onTextFieldValueChange(newText: Editable, selection: TextRange) { this.selection = selection - if (newText.length > rawText.length) - handleAddingCharacters(newText) - else if (newText.length < rawText.length) - handleRemovingCharacters(newText) + 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 typedCharsCount = newValue.length - rawText.length val startTypeIndex = selection.min - typedCharsCount - if (newValue.getOrNull(startTypeIndex) == '\n' && currentStyles.any { it.isHeaderStyle() || it == TextSpanStyle.BulletStyle }) { + if ( + newValue.getOrNull(startTypeIndex) == '\n' && currentStyles.any { it.isHeaderStyle() } + ) { currentStyles.clear() } @@ -506,28 +504,64 @@ class QuillTextManager(quillSpan: QuillSpan) { moveSpans(startTypeIndex, typedCharsCount) - val currentSpan = quillTextSpans.find { - it.from <= startTypeIndex && it.to >= startTypeIndex - } + val currentSpan = + quillTextSpans.find { it.from <= startTypeIndex && it.to >= startTypeIndex } + val isBulletStyle = selectedStyles.contains(TextSpanStyle.BulletStyle) 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 - // TODO: Add support for bullet style - val isBulletStyle = TextSpanStyle.BulletStyle in styles when { span.style == selectedStyles -> { - val updatedSpan = span.copy(to = to + typedCharsCount, style = styles) - quillTextSpans[index] = updatedSpan + if (isBulletStyle && newValue[startTypeIndex] == '\n') { + if ( + newValue[startTypeIndex - 1] != '\n' || + previousSpan?.style?.contains(TextSpanStyle.BulletStyle) == true + ) { + quillTextSpans.add( + index + 1, + QuillTextSpan( + from = startTypeIndex, + to = startTypeIndex + typedCharsCount - 1, + style = selectedStyles + ) + ) + quillTextSpans.add( + index + 2, + QuillTextSpan( + from = startTypeIndex + typedCharsCount, + to = startTypeIndex + typedCharsCount, + style = selectedStyles + ) + ) + } else { + quillTextSpans[index - 1] = + span.copy( + to = to + typedCharsCount, + style = + selectedStyles.filterNot { it == TextSpanStyle.BulletStyle } + ) + quillTextSpans[index] = + span.copy( + to = to + typedCharsCount, + style = + selectedStyles.filterNot { it == TextSpanStyle.BulletStyle } + ) + } + } else { + val updatedSpan = span.copy(to = to + typedCharsCount, style = styles) + quillTextSpans[index] = updatedSpan + } } - span.style != selectedStyles -> { quillTextSpans.removeAt(index) if (startTypeIndex == from) { + Log.d("XXX", "Here") quillTextSpans.add( index, span.copy( @@ -567,12 +601,10 @@ 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) @@ -585,11 +617,9 @@ 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)) @@ -645,10 +675,11 @@ class QuillTextManager(quillSpan: QuillSpan) { filteredSpans.forEach { val index = quillTextSpans.indexOf(it) - quillTextSpans[index] = it.copy( - from = it.from + by, - to = it.to + by, - ) + quillTextSpans[index] = + it.copy( + from = it.from + by, + to = it.to + by, + ) } } @@ -677,24 +708,59 @@ class QuillTextManager(quillSpan: QuillSpan) { 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) + val previousPart = partsCopy.getOrNull(index - 1) + + if (part.style.contains(TextSpanStyle.BulletStyle)) { + if (removeRange.last < part.from) { + if (previousPart?.style?.contains(TextSpanStyle.BulletStyle) == true) { + partsCopy[index] = + part.copy( + from = part.from - removedCharsCount, + to = part.to - removedCharsCount + ) + } else { + partsCopy[index] = + part.copy( + from = part.from - removedCharsCount, + to = part.to - removedCharsCount, + 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) { + 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) + } } } @@ -728,14 +794,15 @@ class QuillTextManager(quillSpan: QuillSpan) { } fun TextSpanStyle.isHeaderStyle(): Boolean { - val headers = listOf( - TextSpanStyle.H1Style, - TextSpanStyle.H2Style, - TextSpanStyle.H3Style, - TextSpanStyle.H4Style, - TextSpanStyle.H5Style, - TextSpanStyle.H6Style, - ) + val headers = + listOf( + TextSpanStyle.H1Style, + TextSpanStyle.H2Style, + TextSpanStyle.H3Style, + TextSpanStyle.H4Style, + TextSpanStyle.H5Style, + TextSpanStyle.H6Style, + ) return headers.contains(this) } @@ -758,6 +825,5 @@ class QuillTextManager(quillSpan: QuillSpan) { removeSpan(span) } } - } -} \ No newline at end of file +}