// // TextEntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSTokens 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 var maxWidthConstraint: NSLayoutConstraint? internal var minWidthConstraint: NSLayoutConstraint? internal var titleLabelWidthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Public FieldType Properties //-------------------------------------------------- /// Representing the type of input. open var fieldType: FieldType = .text { didSet { setNeedsUpdate() } } //-------------------------------------------------- // MARK: - CreditCard/SecurityCode //-------------------------------------------------- open var cardType: CreditCardType = .generic { 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.font = TextStyle.bodyLarge.font } /// 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 leftImageView = UIImageView().with { $0.height(21) $0.width(32) $0.isAccessibilityElement = false $0.translatesAutoresizingMaskIntoConstraints = false $0.contentMode = .scaleAspectFill $0.clipsToBounds = true } 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() } } /// Determines the placement of the helper text. open var helperTextPlacement: HelperTextPlacement = .bottom { 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() titleLabel.setContentCompressionResistancePriority(.required, for: .horizontal) titleLabel.setContentHuggingPriority(.required, for: .horizontal) titleLabelWidthConstraint = titleLabel.width(constant: 0) maxWidthConstraint = containerView.width(constant: containerSize.width) minWidthConstraint = containerView.widthGreaterThanEqualTo(constant: 0) textField.heightAnchor.constraint(equalToConstant: 20).isActive = true textField.delegate = self textField .textPublisher .sink { [weak self] newText in guard let self else { return } print("textPublisher newText: \(newText)") if self.fieldType.handler().validateOnChange { self.validate() } self.sendActions(for: .valueChanged) }.store(in: &subscribers) stackView.addArrangedSubview(successLabel) stackView.setCustomSpacing(8, after: successLabel) fieldStackView.addArrangedSubview(actionTextLink) successLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) } open override func getFieldContainer() -> UIView { // stackview for controls in EntryFieldBase.controlContainerView let stackView = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.spacing = VDSLayout.space3X } stackView.addArrangedSubview(leftImageView) stackView.addArrangedSubview(textField) return stackView } /// 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.isEnabled = isEnabled textField.textColor = textFieldTextColorConfiguration.getColor(self) } 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 = VDSColor.paletteBlack statusIcon.surface = surface statusIcon.isHidden = !isEnabled } else { successLabel.isHidden = true } } open override func updateHelperLabel(){ //remove first helperLabel.removeFromSuperview() super.updateHelperLabel() //set the helper label position if helperText != nil { if helperTextPlacement == .right { middleStackView.spacing = VDSLayout.space3X middleStackView.distribution = .fillEqually middleStackView.addArrangedSubview(helperLabel) } else { middleStackView.spacing = 0 middleStackView.distribution = .fill bottomContainerStackView.addArrangedSubview(helperLabel) } } } override func updateRules() { super.updateRules() fieldType.handler().appendRules(self) } /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() textField.accessibilityLabel = showError ? "error" : nil } open override var accessibilityElements: [Any]? { get { var elements = [Any]() elements.append(contentsOf: [titleLabel, textField]) 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 func layoutSubviews() { super.layoutSubviews() titleLabelWidthConstraint?.constant = containerView.frame.width titleLabelWidthConstraint?.isActive = helperTextPlacement == .right } open override var canBecomeFirstResponder: Bool { true } open override func resignFirstResponder() -> Bool { if textField.isFirstResponder { textField.resignFirstResponder() } return super.resignFirstResponder() } } extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { fieldType.handler().textFieldDidBeginEditing(self, textField: textField) setNeedsUpdate() } public func textFieldDidEndEditing(_ textField: UITextField) { fieldType.handler().textFieldDidEndEditing(self, textField: textField) validate() } 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..