mirror of
https://github.com/divkit/divkit.git
synced 2026-05-07 20:02:32 +00:00
Support filters in input
commit_hash:fbd2d5116647550b5cedb474cf123079dea8f8d0
This commit is contained in:
@@ -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",
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.yandex.div.core.util.inputfilter
|
||||
|
||||
internal interface BaseInputFilter {
|
||||
fun checkValue(value: String): Boolean
|
||||
}
|
||||
+12
@@ -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)
|
||||
}
|
||||
+9
@@ -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) }
|
||||
}
|
||||
+8
@@ -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)
|
||||
}
|
||||
+120
-41
@@ -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,
|
||||
|
||||
+9
-3
@@ -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(
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
}
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
}
|
||||
},
|
||||
"platforms": [
|
||||
"android",
|
||||
"ios",
|
||||
"web"
|
||||
],
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user