Support filters in input

commit_hash:fbd2d5116647550b5cedb474cf123079dea8f8d0
This commit is contained in:
grechka62
2025-03-06 16:14:13 +03:00
parent 3f8881478d
commit 500d5a79d0
13 changed files with 180 additions and 44 deletions
+4
View File
@@ -1172,6 +1172,10 @@
"client/android/div/src/main/java/com/yandex/div/core/util/SynchronizedWeakHashMap.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/SynchronizedWeakHashMap.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/TypeConverter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/TypeConverter.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/Views.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/Views.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/BaseInputFilter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/BaseInputFilter.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/ExpressionInputFilter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/ExpressionInputFilter.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/InputFiltersHolder.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/InputFiltersHolder.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/RegexInputFilter.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/inputfilter/RegexInputFilter.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/mask/BaseInputMask.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/mask/BaseInputMask.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/mask/CurrencyInputMask.kt",
"client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt":"divkit/public/client/android/div/src/main/java/com/yandex/div/core/util/mask/FixedLengthInputMask.kt",
@@ -0,0 +1,5 @@
package com.yandex.div.core.util.inputfilter
internal interface BaseInputFilter {
fun checkValue(value: String): Boolean
}
@@ -0,0 +1,12 @@
package com.yandex.div.core.util.inputfilter
import com.yandex.div.json.expressions.Expression
import com.yandex.div.json.expressions.ExpressionResolver
internal class ExpressionInputFilter(
private val condition: Expression<Boolean>,
private val resolver: ExpressionResolver,
) : BaseInputFilter {
override fun checkValue(value: String) = condition.evaluate(resolver)
}
@@ -0,0 +1,9 @@
package com.yandex.div.core.util.inputfilter
internal class InputFiltersHolder(private val filters: List<BaseInputFilter>) : BaseInputFilter {
var currentValue = ""
var cursorPosition = 0
override fun checkValue(value: String) = filters.all { it.checkValue(value) }
}
@@ -0,0 +1,8 @@
package com.yandex.div.core.util.inputfilter
internal class RegexInputFilter(pattern: String) : BaseInputFilter {
private val regex = Regex(pattern)
override fun checkValue(value: String) = regex.matches(value)
}
@@ -1,6 +1,7 @@
package com.yandex.div.core.view2.divs
import android.graphics.Color
import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.method.DigitsKeyListener
@@ -16,6 +17,9 @@ import com.yandex.div.core.state.DivStatePath
import com.yandex.div.core.util.AccessibilityStateProvider
import com.yandex.div.core.util.equalsToConstant
import com.yandex.div.core.util.expressionSubscriber
import com.yandex.div.core.util.inputfilter.ExpressionInputFilter
import com.yandex.div.core.util.inputfilter.InputFiltersHolder
import com.yandex.div.core.util.inputfilter.RegexInputFilter
import com.yandex.div.core.util.isConstant
import com.yandex.div.core.util.mask.BaseInputMask
import com.yandex.div.core.util.mask.CurrencyInputMask
@@ -43,6 +47,7 @@ import com.yandex.div2.DivAlignmentVertical
import com.yandex.div2.DivCurrencyInputMask
import com.yandex.div2.DivFixedLengthInputMask
import com.yandex.div2.DivInput
import com.yandex.div2.DivInputFilter
import com.yandex.div2.DivInputValidator
import com.yandex.div2.DivPhoneInputMask
import java.util.Locale
@@ -335,16 +340,23 @@ internal class DivInputBinder @Inject constructor(
removeAfterTextChangeListener()
var inputMask: BaseInputMask? = null
observeMask(div, bindingContext.expressionResolver, divView) {
inputMask = it
inputMask?.let { mask ->
setText(mask.value)
setSelection(mask.cursorPosition)
}
}
var inputFilters: InputFiltersHolder? = null
observeFilters(div, bindingContext) { filters ->
inputFilters = filters
inputFilters?.let {
it.currentValue = editableText?.toString() ?: ""
it.cursorPosition = selectionStart
}
}
val primaryVariable: String?
var secondaryVariable: String? = null
@@ -357,50 +369,76 @@ internal class DivInputBinder @Inject constructor(
primaryVariable = div.textVariable
}
val setSecondVariable = { value: String ->
if (secondaryVariable != null) divView.setVariable(secondaryVariable, value)
}
val callbacks = object : TwoWayStringVariableBinder.Callbacks {
override fun onVariableChanged(value: String?) {
val valueToSet = inputMask?.let {
it.overrideRawValue(value ?: "")
setSecondVariable(it.value)
it.value
} ?: value
setText(valueToSet)
}
override fun setViewStateChangeListener(valueUpdater: (String) -> Unit) {
addAfterTextChangeAction { editable ->
val fieldValue = editable?.toString() ?: ""
inputMask?.apply {
if (value != fieldValue) {
applyChangeFrom(text?.toString() ?: "", selectionStart)
setText(value)
setSelection(cursorPosition)
setSecondVariable(value)
}
}
val valueToUpdate = inputMask?.rawValue?.replace(',', '.') ?: fieldValue
valueUpdater(valueToUpdate)
}
}
}
val callbacks = createCallbacks(inputMask, inputFilters, divView, secondaryVariable)
addSubscription(variableBinder.bindVariable(bindingContext, primaryVariable, callbacks, path))
observeValidators(div, bindingContext.expressionResolver, divView)
}
private fun DivInputView.createCallbacks(
inputMask: BaseInputMask?,
filters: InputFiltersHolder?,
divView: Div2View,
secondaryVariable: String?,
) = object : TwoWayStringVariableBinder.Callbacks {
override fun onVariableChanged(value: String?) {
val newValue = value ?: ""
inputMask?.let {
it.overrideRawValue(newValue)
setSecondVariable(it.value)
setText(it.value)
return
}
filters?.run {
if (!checkValue(newValue)) return
currentValue = newValue
cursorPosition = newValue.length
}
setText(newValue)
}
override fun setViewStateChangeListener(valueUpdater: (String) -> Unit) =
addAfterTextChangeAction { applyMaskOrFilters(it, valueUpdater) }
private fun applyMaskOrFilters(editable: Editable?, valueUpdater: (String) -> Unit) {
val fieldValue = editable?.toString() ?: ""
inputMask?.run {
if (value != fieldValue) {
applyChangeFrom(text?.toString() ?: "", selectionStart)
setText(value)
setSelection(cursorPosition)
setSecondVariable(value)
}
valueUpdater(rawValue.replace(',', '.'))
return
}
filters?.run {
if (currentValue == fieldValue) return
if (!checkValue(fieldValue)) {
setText(currentValue)
setSelection(cursorPosition)
return
}
currentValue = fieldValue
cursorPosition = selectionStart
}
valueUpdater(fieldValue)
}
private fun setSecondVariable(value: String) =
secondaryVariable?.let { divView.setVariable(secondaryVariable, value) }
}
private fun DivInputView.observeValidators(
div: DivInput,
resolver: ExpressionResolver,
@@ -650,6 +688,47 @@ internal class DivInputBinder @Inject constructor(
updateMaskData(Unit)
}
private fun DivInputView.observeFilters(
div: DivInput,
bindingContext: BindingContext,
onFiltersUpdate: (InputFiltersHolder?) -> Unit
) {
div.mask?.let { return }
val divFilters = div.filters
if (divFilters.isNullOrEmpty()) return
val resolver = bindingContext.expressionResolver
val updateFiltersData = { _: Any ->
val filters = divFilters.mapNotNull {
when (it) {
is DivInputFilter.Regex -> {
try {
RegexInputFilter(it.value.pattern.evaluate(resolver))
} catch (e: PatternSyntaxException) {
errorCollectors.getOrCreate(bindingContext.divView.dataTag, bindingContext.divView.divData)
.logError(IllegalArgumentException("Invalid regex pattern '${e.pattern}'.", e))
null
}
}
is DivInputFilter.Expression -> ExpressionInputFilter(it.value.condition, resolver)
}
}
onFiltersUpdate(InputFiltersHolder(filters))
}
divFilters.forEach {
when (it) {
is DivInputFilter.Regex ->
addSubscription(it.value.pattern.observe(resolver, updateFiltersData))
is DivInputFilter.Expression -> Unit
}
}
updateFiltersData(Unit)
}
private fun getCapitalization(
div: DivInput,
resolver: ExpressionResolver,
@@ -7,10 +7,8 @@ import com.yandex.divkit.demo.DummyActivity
import org.junit.Rule
import org.junit.Test
internal const val TEXT_WITH_DIFFERENT_SYMBOLS =
"https://Text_with different+symbols(123)@site.ru\nsecond_line"
private const val TEXT_BEFORE_BREAK = "https://Text_with different+symbols(123)@site.ru"
internal const val TEXT_WITH_DIFFERENT_SYMBOLS = "$TEXT_BEFORE_BREAK\nsecond_line"
class DivInputKeyboardTypeTest {
@@ -35,6 +33,14 @@ class DivInputKeyboardTypeTest {
)
}
@Test
fun checkNumber() {
checkType(
type = "number",
expectedText = "-912302."
)
}
@Test
fun checkPositiveNumber() {
checkType(
+1
View File
@@ -14,6 +14,7 @@
}
},
"platforms": [
"android",
"ios",
"web"
],
+1
View File
@@ -14,6 +14,7 @@
}
},
"platforms": [
"android",
"ios",
"web"
],
+1
View File
@@ -63,6 +63,7 @@
"type": "array",
"$description": "translations.json#/div_input_filters",
"platforms": [
"android",
"ios",
"web"
],
@@ -190,6 +190,10 @@
"condition": "@{input_enabled}"
}
]
},
{
"type": "pretty_text",
"text": "Variable value: @{text}"
}
]
}
@@ -2435,6 +2435,7 @@
"title": "Regex input filter",
"case_id": 158,
"platforms": [
"android",
"ios",
"web"
],
@@ -2463,6 +2464,7 @@
"title": "Expression input filter",
"case_id": 159,
"platforms": [
"android",
"ios",
"web"
],
@@ -120,6 +120,10 @@
"type": "pretty_input",
"variable": "regex",
"hint": "regex"
},
{
"type": "pretty_text",
"text": "Variable value: @{input}"
}
]
}