Files
tabaina cbc0b7285c supported div-action-set-cursor-position
commit_hash:d4b02035ce9a25a385ef5a684115deef02412c8e
2026-05-22 12:19:54 +03:00

351 lines
11 KiB
Swift

import CoreFoundation
import CoreGraphics
import Foundation
import LayoutKit
import VGSL
extension DivInput: DivBlockModeling {
public func makeBlock(context: DivBlockModelingContext) throws -> Block {
let context = modifiedContextParentPath(context)
let isFocused = context.blockStateStorage.isFocused(path: context.path)
let textBinding = context.makeBinding(variableName: textVariable, defaultValue: "")
return try applyBaseProperties(
to: {
try makeBaseBlock(
context: context,
textBinding: textBinding,
isFocused: isFocused
)
},
context: context,
actionsHolder: nil,
customAccessibilityParams: CustomAccessibilityParams { [unowned self] in
accessibility?.resolveDescription(context.expressionResolver) ?? textBinding.value
},
applyPaddings: false,
isFocused: isFocused
)
}
private func makeBaseBlock(
context: DivBlockModelingContext,
textBinding: Binding<String>,
isFocused: Bool
) throws -> Block {
let expressionResolver = context.expressionResolver
let font = context.font(resolveFontParams(expressionResolver))
var typo = Typo(font: font)
let kern = CGFloat(resolveLetterSpacing(expressionResolver))
if !kern.isApproximatelyEqualTo(0) {
typo = typo.kerned(kern)
}
if let lineHeightInt = resolveLineHeight(expressionResolver) {
let lineHeight = CGFloat(lineHeightInt)
typo = typo.with(height: lineHeight)
if lineHeight > font.defaultLineHeight() {
typo = typo.with(baseline: (lineHeight - font.defaultLineHeight()) / 2)
}
}
let hintValue = resolveHintText(expressionResolver) ?? ""
let keyboardType = resolveKeyboardType(expressionResolver)
let autocapitalizationType = resolveAutocapitalization(expressionResolver)
let onFocusActions = focus?.onFocus?.uiActions(context: context) ?? []
let onBlurActions = focus?.onBlur?.uiActions(context: context) ?? []
let enterKeyActions = enterKeyActions?.uiActions(context: context) ?? []
let enterKeyType = resolveEnterKeyType(expressionResolver)
let blockStateStorage = context.blockStateStorage
if isFocused { blockStateStorage.setInputFocused() }
let shouldClearFocus = Variable<Bool> { [weak blockStateStorage] in
blockStateStorage.map { !$0.isInputFocused } ?? true
}
let viewState: TextInputViewState? = blockStateStorage.takePendingState(context.path) as? TextInputViewState
return TextInputBlock(
widthTrait: resolveWidthTrait(context),
heightTrait: resolveHeightTrait(context),
hint: hintValue.with(typo: typo.with(color: resolveHintColor(expressionResolver))),
textValue: textBinding,
rawTextValue: mask?.makeRawVariable(context),
textTypo: typo.with(color: resolveTextColor(expressionResolver)),
multiLineMode: keyboardType == .multiLineText,
inputType: keyboardType.system,
autocapitalizationType: autocapitalizationType.system,
enterKeyType: enterKeyType.system,
highlightColor: resolveHighlightColor(expressionResolver),
maxVisibleLines: resolveMaxVisibleLines(expressionResolver),
selectAllOnFocus: resolveSelectAllOnFocus(expressionResolver),
maskValidator: mask?.makeMaskValidator(expressionResolver),
path: context.path,
isFocused: isFocused,
onFocusActions: onFocusActions,
onBlurActions: onBlurActions,
enterKeyActions: enterKeyActions,
parentScrollView: context.parentScrollView,
filters: makeFilters(context),
validators: makeValidators(context),
layoutDirection: context.layoutDirection,
textAlignmentHorizontal: resolveTextAlignmentHorizontal(expressionResolver).textAlignment,
textAlignmentVertical: resolveTextAlignmentVertical(expressionResolver).textAlignment,
paddings: paddings?.resolve(context),
isEnabled: resolveIsEnabled(expressionResolver),
maxLength: resolveMaxLength(expressionResolver),
shouldClearFocus: shouldClearFocus,
autocorrection: keyboardType.autocorrection,
isSecure: keyboardType.isSecure,
viewState: viewState
)
}
private func makeValidators(_ context: DivBlockModelingContext) -> [TextInputValidator]? {
let expressionResolver = context.expressionResolver
return validators?.compactMap { validator -> TextInputValidator? in
switch validator {
case let .divInputValidatorRegex(regexValidator):
let pattern = regexValidator.resolvePattern(expressionResolver) ?? ""
guard let regex = try? NSRegularExpression(pattern: pattern) else {
DivKitLogger.error("Invalid regex pattern '\(pattern)'")
return nil
}
return TextInputValidator(
isValid: context.makeBinding(variableName: regexValidator.variable, defaultValue: false),
allowEmpty: regexValidator.resolveAllowEmpty(expressionResolver),
validator: { $0.fullMatchesRegex(regex) },
message: makeMessage(
from: regexValidator.resolveLabelId(expressionResolver),
storage: context.blockStateStorage,
cardId: context.cardId
)
)
case let .divInputValidatorExpression(expressionValidator):
return TextInputValidator(
isValid: context.makeBinding(
variableName: expressionValidator.variable,
defaultValue: false
),
allowEmpty: expressionValidator.resolveAllowEmpty(expressionResolver),
validator: { _ in expressionValidator.resolveCondition(expressionResolver) ?? true },
message: makeMessage(
from: expressionValidator.resolveLabelId(expressionResolver),
storage: context.blockStateStorage,
cardId: context.cardId
)
)
}
}
}
private func makeFilters(_ context: DivBlockModelingContext) -> [TextInputFilter]? {
let expressionResolver = context.expressionResolver
return filters?.compactMap { filter -> TextInputFilter? in
switch filter {
case let .divInputFilterExpression(expressionFilter):
return { _ in
expressionFilter.resolveCondition(context.expressionResolver) ?? true
}
case let .divInputFilterRegex(regexFilter):
let pattern = regexFilter.resolvePattern(expressionResolver) ?? ""
guard let regex = try? NSRegularExpression(pattern: pattern) else {
DivKitLogger.error("Invalid regex pattern '\(pattern)'")
return nil
}
return { text in
text.fullMatchesRegex(regex)
}
}
}
}
private func makeMessage(
from labelId: String?,
storage: DivBlockStateStorage,
cardId: DivCardID
) -> () -> String? {
{
guard let labelId else {
return nil
}
guard let state: TextBlockViewState = storage.getState(labelId, cardId: cardId) else {
DivKitLogger.error("Can't find text with id '\(labelId)'")
return nil
}
return state.text
}
}
}
extension DivInput: FontParamsProvider {}
extension DivAlignmentHorizontal {
fileprivate var textAlignment: TextInputBlock.TextAlignmentHorizontal {
switch self {
case .left:
.left
case .center:
.center
case .right:
.right
case .start:
.start
case .end:
.end
}
}
}
extension DivAlignmentVertical {
fileprivate var textAlignment: TextInputBlock.TextAlignmentVertical {
switch self {
case .top:
return .top
case .center:
return .center
case .bottom:
return .bottom
case .baseline:
DivKitLogger.warning("Baseline alignment is not supported.")
return .center
}
}
}
extension DivInput.KeyboardType {
fileprivate var system: TextInputBlock.InputType {
switch self {
case .singleLineText, .multiLineText, .password:
.default
case .phone:
.keyboard(.phonePad)
case .number:
.keyboard(.decimalPad)
case .email:
.keyboard(.emailAddress)
case .uri:
.keyboard(.URL)
}
}
fileprivate var autocorrection: Bool {
switch self {
case .singleLineText,
.multiLineText:
true
case .phone,
.number,
.email,
.uri,
.password:
false
}
}
fileprivate var isSecure: Bool {
switch self {
case .password:
true
case .singleLineText,
.multiLineText,
.phone,
.number,
.email,
.uri:
false
}
}
}
extension DivInput.Autocapitalization {
fileprivate var system: TextInputBlock.AutocapitalizationType {
switch self {
case .none: .none
case .words: .words
case .auto, .sentences: .sentences
case .allCharacters: .allCharacters
}
}
}
extension DivInput.EnterKeyType {
fileprivate var system: TextInputBlock.EnterKeyType {
switch self {
case .default: .default
case .go: .go
case .search: .search
case .send: .send
case .done: .done
}
}
}
extension DivInputMask {
fileprivate func makeMaskValidator(_ resolver: ExpressionResolver) -> MaskValidator? {
switch self {
case let .divFixedLengthInputMask(divFixedLengthInputMask):
MaskValidator(formatter: FixedLengthMaskFormatter(
pattern: divFixedLengthInputMask.resolvePattern(resolver) ?? "",
alwaysVisible: divFixedLengthInputMask.resolveAlwaysVisible(resolver),
patternElements: divFixedLengthInputMask.patternElements
.map { $0.makePatternElement(resolver) }
))
case let .divCurrencyInputMask(divCurrencyInputMask):
MaskValidator(
formatter: CurrencyMaskFormatter(
locale: divCurrencyInputMask.resolveLocale(resolver)
)
)
case .divPhoneInputMask:
MaskValidator(formatter: PhoneMaskFormatter(
masksByCountryCode: PhoneMasks().value.typedJSON(),
extraSymbols: PhoneMasks.extraNumbers
))
}
}
fileprivate func makeRawVariable(_ context: DivBlockModelingContext) -> Binding<String>? {
switch self {
case let .divFixedLengthInputMask(divFixedLengthInputMask):
context.makeBinding(
variableName: divFixedLengthInputMask.rawTextVariable,
defaultValue: ""
)
case let .divCurrencyInputMask(divCurrencyInputMask):
context.makeBinding(
variableName: divCurrencyInputMask.rawTextVariable,
defaultValue: ""
)
case let .divPhoneInputMask(divPhoneInputMask):
context.makeBinding(
variableName: divPhoneInputMask.rawTextVariable,
defaultValue: ""
)
}
}
}
extension DivFixedLengthInputMask.PatternElement {
fileprivate func makePatternElement(_ resolver: ExpressionResolver) -> PatternElement {
PatternElement(
key: (resolveKey(resolver) ?? "").first ?? " ".first!,
regex: try! NSRegularExpression(pattern: resolveRegex(resolver) ?? "a"),
placeholder: resolvePlaceholder(resolver).first ?? " ".first!
)
}
}
extension String {
fileprivate func fullMatchesRegex(_ regex: NSRegularExpression) -> Bool {
let range = stringRangeAsNSRange(wholeStringRange, inString: self)
return regex.matches(in: self, options: [], range: range)
.contains { $0.range == range }
}
}