// // TextEntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSCoreTokens import Combine /// An input field is an input wherein a customer enters information. They typically appear in forms. /// Specialized input fields capture credit card numbers, inline actions, passwords, phone numbers, /// dates and security codes in their correct formats. @objc(VDSInputField) open class InputField: EntryFieldBase { //-------------------------------------------------- // MARK: - Initializers //-------------------------------------------------- required public init() { super.init(frame: .zero) } public override init(frame: CGRect) { super.init(frame: .zero) } public required init?(coder: NSCoder) { super.init(coder: coder) } //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- internal override var containerBackgroundColor: UIColor { if showSuccess { return backgroundColorConfiguration.getColor(self) } else { return super.containerBackgroundColor } } internal override var minWidth: CGFloat { fieldType.handler().minWidth } internal override var maxWidth: CGFloat { let frameWidth = frame.size.width return helperTextPlacement == .right ? (frameWidth - horizontalStackView.spacing) / 2 : frameWidth } /// The is used for the for adding the helperLabel to the right of the containerView. internal var horizontalStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fillEqually $0.spacing = VDSLayout.space3X $0.alignment = .top } }() //-------------------------------------------------- // MARK: - Public FieldType Properties //-------------------------------------------------- /// Representing the type of input. open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - CreditCard/SecurityCode //-------------------------------------------------- open var cardType: CreditCardType = .placeholder { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Password //-------------------------------------------------- /// This is the text that will be displayed when the password is unmasked. open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } /// This is the text that will be displayed when the password is masked. open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Date //-------------------------------------------------- /// Date Format used when using the FieldType 'Date'. open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- /// Label to render the successText. open var successLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall } /// UITextField shown in the InputField. open var textField = TextField().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.textStyle = TextStyle.bodyLarge } /// Color configuration for the textField. open var textFieldTextColorConfiguration: AnyColorable = ViewColorConfiguration().with { $0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true) $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false) }.eraseToAnyColorable() open var actionTextLink = TextLink().with { $0.contentEdgeInsets = .top(-2) } open var actionTextLinkModel: TextLinkModel? { didSet { setNeedsUpdate() } } /// The text of this TextField. open var text: String? { get { textField.text } set { textField.text = newValue setNeedsUpdate() } } /// Value for the textField open override var value: String? { if let value = fieldType.handler().value { return value } else { return textField.text } } var _showError: Bool = false /// Whether not to show the error. open override var showError: Bool { get { _showError } set { if !showSuccess && _showError != newValue { _showError = newValue setNeedsUpdate() } } } var _showSuccess: Bool = false /// Whether not to show the success. open var showSuccess: Bool { get { _showSuccess } set { if !showError && _showSuccess != newValue { _showSuccess = newValue setNeedsUpdate() } } } /// Override UIControl state to add the .error state if showSuccess is true and if showError is true. open override var state: UIControl.State { get { var state = super.state if showSuccess { state.insert(.success) } if textField.isFirstResponder { state.insert(.focused) } return state } } /// If given, this will be shown if showSuccess if true. open var successText: String? { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- /// Called once when a view is initialized and is used to Setup additional UI or other constants and configurations. open override func setup() { super.setup() containerView.isAccessibilityElement = true textField.isAccessibilityElement = false textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self bottomContainerStackView.insertArrangedSubview(successLabel, at: 0) fieldStackView.addArrangedSubview(actionTextLink) successLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: [.success, .focused]) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) textField.textColorConfiguration = textFieldTextColorConfiguration } open override func getFieldContainer() -> UIView { return textField } /// Resets to default settings. open override func reset() { super.reset() textField.text = "" successLabel.reset() successLabel.textStyle = .bodySmall fieldType = .text showSuccess = false successText = nil helperTextPlacement = .bottom } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { //update fieldType first fieldType.handler().updateView(self) super.updateView() textField.surface = surface textField.isEnabled = isEnabled textField.isUserInteractionEnabled = isEnabled && !isReadOnly } open override func updateAccessibility() { super.updateAccessibility() containerView.accessibilityLabel = "Input Field, \(accessibilityLabelText)" containerView.accessibilityHint = isReadOnly || !isEnabled ? "" : "Double tap to edit." containerView.accessibilityValue = value } open override func updateErrorLabel() { super.updateErrorLabel() //show error or success if showError, let _ = errorText { successLabel.isHidden = true } else if showSuccess, let successText { successLabel.text = successText successLabel.surface = surface successLabel.isEnabled = isEnabled successLabel.isHidden = false errorLabel.isHidden = true statusIcon.name = .checkmarkAlt statusIcon.color = iconColorConfiguration.getColor(self) statusIcon.surface = surface statusIcon.isHidden = !isEnabled || state.contains(.focused) } else { successLabel.isHidden = true } } override func updateRules() { super.updateRules() fieldType.handler().appendRules(self) } open override var accessibilityElements: [Any]? { get { var elements = [Any]() elements.append(contentsOf: [titleLabel, containerView]) if showError { elements.append(statusIcon) if let errorText, !errorText.isEmpty { elements.append(errorLabel) } } else if showSuccess, let successText, !successText.isEmpty { elements.append(successLabel) } if let helperText, !helperText.isEmpty { elements.append(helperLabel) } return elements } set { super.accessibilityElements = newValue } } open override var canBecomeFirstResponder: Bool { return textField.canBecomeFirstResponder } open override func becomeFirstResponder() -> Bool { return textField.becomeFirstResponder() } open override var canResignFirstResponder: Bool { return textField.canResignFirstResponder } open override func resignFirstResponder() -> Bool { return textField.resignFirstResponder() } } extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { fieldType.handler().textFieldDidBeginEditing(self, textField: textField) updateContainerView() updateErrorLabel() } public func textFieldDidEndEditing(_ textField: UITextField) { fieldType.handler().textFieldDidEndEditing(self, textField: textField) validate() } public func textFieldDidChangeSelection(_ textField: UITextField) { fieldType.handler().textFieldDidChangeSelection(self, textField: textField) if fieldType.handler().validateOnChange { validate() } sendActions(for: .valueChanged) setNeedsUpdate() } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) } } extension String { internal static func format(_ value: String, indices: [Int], with separator: String) -> String { var formattedString = "" var currentIndex = value.startIndex for index in 0..