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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ Supported styles:
- blockquote
- ordered list
- unordered list
- checkbox list

Each of the styles can be toggled the same way as in the example from [usage section](#usage); call a proper `toggle` function on the component ref.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import com.swmansion.enriched.textinput.utils.EnrichedParser
import com.swmansion.enriched.textinput.utils.EnrichedSelection
import com.swmansion.enriched.textinput.utils.EnrichedSpanState
import com.swmansion.enriched.textinput.utils.mergeSpannables
import com.swmansion.enriched.textinput.utils.setCheckboxClickListener
import com.swmansion.enriched.textinput.watchers.EnrichedSpanWatcher
import com.swmansion.enriched.textinput.watchers.EnrichedTextWatcher
import java.util.regex.Pattern
Expand Down Expand Up @@ -155,9 +156,12 @@ class EnrichedTextInputView : AppCompatEditText {
// Ensure that every time new editable is created, it has EnrichedSpanWatcher attached
val spanWatcher = EnrichedSpanWatcher(this)
this.spanWatcher = spanWatcher
setEditableFactory(EnrichedEditableFactory(spanWatcher))

setEditableFactory(EnrichedEditableFactory(spanWatcher))
addTextChangedListener(EnrichedTextWatcher(this))

// Handle checkbox list item clicks
this.setCheckboxClickListener()
}

// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
Expand Down Expand Up @@ -546,6 +550,7 @@ class EnrichedTextInputView : AppCompatEditText {
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.toggleStyle(EnrichedSpans.BLOCK_QUOTE)
EnrichedSpans.ORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.ORDERED_LIST)
EnrichedSpans.UNORDERED_LIST -> listStyles?.toggleStyle(EnrichedSpans.UNORDERED_LIST)
EnrichedSpans.CHECKBOX_LIST -> listStyles?.toggleStyle(EnrichedSpans.CHECKBOX_LIST)
else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
}

Expand Down Expand Up @@ -574,6 +579,7 @@ class EnrichedTextInputView : AppCompatEditText {
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.removeStyle(EnrichedSpans.BLOCK_QUOTE, start, end)
EnrichedSpans.ORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.ORDERED_LIST, start, end)
EnrichedSpans.UNORDERED_LIST -> listStyles?.removeStyle(EnrichedSpans.UNORDERED_LIST, start, end)
EnrichedSpans.CHECKBOX_LIST -> listStyles?.removeStyle(EnrichedSpans.CHECKBOX_LIST, start, end)
EnrichedSpans.LINK -> parametrizedStyles?.removeStyle(EnrichedSpans.LINK, start, end)
EnrichedSpans.IMAGE -> parametrizedStyles?.removeStyle(EnrichedSpans.IMAGE, start, end)
EnrichedSpans.MENTION -> parametrizedStyles?.removeStyle(EnrichedSpans.MENTION, start, end)
Expand Down Expand Up @@ -601,6 +607,7 @@ class EnrichedTextInputView : AppCompatEditText {
EnrichedSpans.BLOCK_QUOTE -> paragraphStyles?.getStyleRange()
EnrichedSpans.ORDERED_LIST -> listStyles?.getStyleRange()
EnrichedSpans.UNORDERED_LIST -> listStyles?.getStyleRange()
EnrichedSpans.CHECKBOX_LIST -> listStyles?.getStyleRange()
EnrichedSpans.LINK -> parametrizedStyles?.getStyleRange()
EnrichedSpans.IMAGE -> parametrizedStyles?.getStyleRange()
EnrichedSpans.MENTION -> parametrizedStyles?.getStyleRange()
Expand Down Expand Up @@ -660,6 +667,13 @@ class EnrichedTextInputView : AppCompatEditText {
toggleStyle(name)
}

fun toggleCheckboxListItem(checked: Boolean) {
val isValid = verifyStyle(EnrichedSpans.CHECKBOX_LIST)
if (!isValid) return

listStyles?.toggleCheckboxListStyle(checked)
}

fun addLink(
start: Int,
end: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,13 @@ class EnrichedTextInputViewManager :
view?.verifyAndToggleStyle(EnrichedSpans.UNORDERED_LIST)
}

override fun toggleCheckboxList(
view: EnrichedTextInputView?,
isChecked: Boolean,
) {
view?.toggleCheckboxListItem(isChecked)
}

override fun addLink(
view: EnrichedTextInputView?,
start: Int,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.swmansion.enriched.textinput.spans

import android.graphics.Canvas
import android.graphics.Paint
import android.text.Layout
import android.text.Spanned
import android.text.TextPaint
import android.text.style.LeadingMarginSpan
import android.text.style.LineHeightSpan
import android.text.style.MetricAffectingSpan
import androidx.core.graphics.withTranslation
import com.swmansion.enriched.textinput.spans.interfaces.EnrichedParagraphSpan
import com.swmansion.enriched.textinput.styles.HtmlStyle
import com.swmansion.enriched.utils.CheckboxDrawable

class EnrichedCheckboxListSpan(
var isChecked: Boolean,
private val htmlStyle: HtmlStyle,
) : MetricAffectingSpan(),
LineHeightSpan,
LeadingMarginSpan,
EnrichedParagraphSpan {
override val dependsOnHtmlStyle: Boolean = true

private val checkboxDrawable =
CheckboxDrawable(htmlStyle.ulCheckboxBoxSize, htmlStyle.ulCheckboxBoxColor, isChecked).apply {
Comment thread
exploIF marked this conversation as resolved.
setBounds(0, 0, htmlStyle.ulCheckboxBoxSize, htmlStyle.ulCheckboxBoxSize)
}

override fun updateMeasureState(tp: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}

override fun updateDrawState(tp: TextPaint) {
// Do nothing, but inform layout that this span affects text metrics
}

// Include checkbox size in text measurements to avoid clipping
override fun chooseHeight(
text: CharSequence,
start: Int,
end: Int,
spanstartv: Int,
v: Int,
fm: Paint.FontMetricsInt,
) {
val checkboxSize = htmlStyle.ulCheckboxBoxSize
val currentLineHeight = fm.descent - fm.ascent

if (checkboxSize > currentLineHeight) {
val extraSpace = checkboxSize - currentLineHeight
val halfExtra = extraSpace / 2

fm.ascent -= halfExtra
fm.descent += (extraSpace - halfExtra)

fm.top -= halfExtra
fm.bottom += (extraSpace - halfExtra)
}
}

override fun getLeadingMargin(first: Boolean): Int =
htmlStyle.ulCheckboxBoxSize + htmlStyle.ulCheckboxMarginLeft + htmlStyle.ulCheckboxGapWidth

override fun drawLeadingMargin(
canvas: Canvas,
paint: Paint,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
first: Boolean,
layout: Layout?,
) {
val spannedText = text as Spanned

if (spannedText.getSpanStart(this) == start) {
checkboxDrawable.update(isChecked)

val lineCenter = (top + bottom) / 2f
val drawableTop = lineCenter - (htmlStyle.ulCheckboxBoxSize / 2f)

canvas.withTranslation(x.toFloat() + htmlStyle.ulCheckboxMarginLeft, drawableTop) {
checkboxDrawable.draw(this)
}
}
}

override fun rebuildWithStyle(htmlStyle: HtmlStyle): EnrichedCheckboxListSpan = EnrichedCheckboxListSpan(isChecked, htmlStyle)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ data class ParagraphSpanConfig(

data class ListSpanConfig(
override val clazz: Class<*>,
val shortcut: String,
val shortcut: String?,
) : ISpanConfig

data class StylesMergingConfig(
Expand Down Expand Up @@ -48,6 +48,7 @@ object EnrichedSpans {
// list styles
const val UNORDERED_LIST = "unordered_list"
const val ORDERED_LIST = "ordered_list"
const val CHECKBOX_LIST = "checkbox_list"

// parametrized styles
const val LINK = "link"
Expand Down Expand Up @@ -79,6 +80,7 @@ object EnrichedSpans {
mapOf(
UNORDERED_LIST to ListSpanConfig(EnrichedUnorderedListSpan::class.java, "- "),
ORDERED_LIST to ListSpanConfig(EnrichedOrderedListSpan::class.java, "1. "),
CHECKBOX_LIST to ListSpanConfig(EnrichedCheckboxListSpan::class.java, null),
)

val parametrizedStyles: Map<String, BaseSpanConfig> =
Expand Down Expand Up @@ -132,44 +134,44 @@ object EnrichedSpans {
}

H1 -> {
val conflictingStyles = mutableListOf(H2, H3, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H2, H3, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

H2 -> {
val conflictingStyles = mutableListOf(H1, H3, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H1, H3, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

H3 -> {
val conflictingStyles = mutableListOf(H1, H2, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H1, H2, H4, H5, H6, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

H4 -> {
val conflictingStyles = mutableListOf(H1, H2, H3, H5, H6, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H1, H2, H3, H5, H6, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h4Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

H5 -> {
val conflictingStyles = mutableListOf(H1, H2, H3, H4, H6, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H1, H2, H3, H4, H6, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h5Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

H6 -> {
val conflictingStyles = mutableListOf(H1, H2, H3, H4, H5, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
val conflictingStyles = mutableListOf(H1, H2, H3, H4, H5, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST, BLOCK_QUOTE, CODE_BLOCK)
if (htmlStyle.h6Bold) conflictingStyles.add(BOLD)
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
}

BLOCK_QUOTE -> {
StylesMergingConfig(
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST),
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST, CHECKBOX_LIST),
)
}

Expand All @@ -189,6 +191,7 @@ object EnrichedSpans {
STRIKETHROUGH,
UNORDERED_LIST,
ORDERED_LIST,
CHECKBOX_LIST,
BLOCK_QUOTE,
INLINE_CODE,
),
Expand All @@ -197,13 +200,19 @@ object EnrichedSpans {

UNORDERED_LIST -> {
StylesMergingConfig(
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, ORDERED_LIST, CHECKBOX_LIST, CODE_BLOCK, BLOCK_QUOTE),
)
}

ORDERED_LIST -> {
StylesMergingConfig(
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, UNORDERED_LIST, CHECKBOX_LIST, CODE_BLOCK, BLOCK_QUOTE),
)
}

CHECKBOX_LIST -> {
StylesMergingConfig(
conflictingStyles = arrayOf(H1, H2, H3, H4, H5, H6, UNORDERED_LIST, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ class HtmlStyle {
var ulBulletSize: Int = 8
var ulBulletColor: Int = Color.BLACK

var ulCheckboxBoxSize: Int = 50
var ulCheckboxGapWidth: Int = 16
var ulCheckboxMarginLeft: Int = 24
var ulCheckboxBoxColor: Int = Color.BLACK

var aColor: Int = Color.BLACK
var aUnderline: Boolean = true

Expand Down Expand Up @@ -118,6 +123,12 @@ class HtmlStyle {
ulMarginLeft = parseFloat(ulStyle, "marginLeft").toInt()
ulBulletSize = parseFloat(ulStyle, "bulletSize").toInt()

val ulCheckboxStyle = style.getMap("ulCheckbox")
ulCheckboxBoxSize = parseFloat(ulCheckboxStyle, "boxSize").toInt()
ulCheckboxGapWidth = parseFloat(ulCheckboxStyle, "gapWidth").toInt()
ulCheckboxMarginLeft = parseFloat(ulCheckboxStyle, "marginLeft").toInt()
ulCheckboxBoxColor = parseColor(ulCheckboxStyle, "boxColor")

val aStyle = style.getMap("a")
aColor = parseColor(aStyle, "color")
aUnderline = parseIsUnderline(aStyle)
Expand Down Expand Up @@ -290,6 +301,11 @@ class HtmlStyle {
ulBulletSize == other.ulBulletSize &&
ulBulletColor == other.ulBulletColor &&

ulCheckboxBoxSize == other.ulCheckboxBoxSize &&
ulCheckboxGapWidth == other.ulCheckboxGapWidth &&
ulCheckboxMarginLeft == other.ulCheckboxMarginLeft &&
ulCheckboxBoxColor == other.ulCheckboxBoxColor &&

aColor == other.aColor &&
aUnderline == other.aUnderline &&

Expand Down Expand Up @@ -332,6 +348,11 @@ class HtmlStyle {
result = 31 * result + ulBulletSize.hashCode()
result = 31 * result + ulBulletColor.hashCode()

result = 31 * result + ulCheckboxBoxSize.hashCode()
result = 31 * result + ulCheckboxGapWidth.hashCode()
result = 31 * result + ulCheckboxMarginLeft.hashCode()
result = 31 * result + ulCheckboxBoxColor.hashCode()

result = 31 * result + aColor.hashCode()
result = 31 * result + aUnderline.hashCode()

Expand Down
Loading
Loading