// // TextArea.swift // VDS // // Created by Matt Bruce on 1/10/23. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine /// A text area is an input wherein a customer enters long-form information. /// Use a text area when you want customers to enter text that’s longer than a single line. @objc(VDSTextArea) open class TextArea: 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 minWidthConstraint: NSLayoutConstraint? internal var textViewHeightConstraint: NSLayoutConstraint? internal var inputFieldStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.spacing = VDSLayout.Spacing.space3X.value } }() internal var bottomView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var bottomStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.alignment = .top $0.spacing = VDSLayout.Spacing.space2X.value } }() open var characterCounterLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textStyle = .bodySmall $0.textAlignment = .right $0.numberOfLines = 1 } private var _minHeight: Height = .twoX open var minHeight: Height? { get { return _minHeight } set { if let newValue { _minHeight = newValue } else { _minHeight = .twoX } textViewHeightConstraint?.constant = _minHeight.value setNeedsUpdate() } } //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- override var containerSize: CGSize { CGSize(width: 182, height: 88) } /// Enum used to describe the the height of TextArea. public enum Height: String, CaseIterable { case twoX = "2X" case fourX = "4X" case eightX = "8X" var value: CGFloat { switch self { case .twoX: 88 case .fourX: 176 case .eightX: 352 } } } /// The text of this textView private var _text: String? open override var text: String? { get { textView.text } set { if let newValue, newValue != _text { _text = newValue textView.text = newValue value = newValue } setNeedsUpdate() } } /// UITextView shown in the TextArea. open var textView = TextView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.sizeToFit() $0.isScrollEnabled = false } /// Color configuration for error icon. internal var iconColorConfiguration = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .normal) } /// Color configuration for character counter's highlight background color internal var highlightBackgroundColor = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.backgroundPrimaryDark, VDSColor.backgroundPrimaryLight, forState: .normal) } /// Color configuration for character counter's highlight text color internal var highlightTextColor = ControlColorConfiguration().with { $0.setSurfaceColors(VDSColor.elementsPrimaryOndark, VDSColor.elementsPrimaryOnlight, forState: .normal) } //-------------------------------------------------- // 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() accessibilityLabel = "TextArea" containerStackView.pinToSuperView(.uniform(VDSFormControls.spaceInset)) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: containerSize.width) minWidthConstraint?.isActive = true controlContainerView.addSubview(textView) textView .pinTop() .pinLeading() .pinTrailingLessThanOrEqualTo(nil, 0, .defaultHigh) .pinBottom(0, .defaultHigh) textView.isScrollEnabled = true textView.autocorrectionType = .no textViewHeightConstraint = textView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) textViewHeightConstraint?.isActive = true backgroundColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessBackgroundOnlight, VDSColor.feedbackSuccessBackgroundOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.feedbackSuccessOnlight, VDSColor.feedbackSuccessOndark, forState: .success) borderColorConfiguration.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forState: .focused) textView.delegate = self characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable() bottomContainerStackView.spacing = VDSLayout.Spacing.space2X.value } /// Resets to default settings. open override func reset() { super.reset() textView.text = "" characterCounterLabel.reset() characterCounterLabel.textStyle = .bodySmall setNeedsUpdate() } /// Container for the area in which the user interacts. open override func getContainer() -> UIView { inputFieldStackView.addArrangedSubview(containerView) return inputFieldStackView } /// Used to make changes to the View based off a change events or from local properties. open override func updateView() { super.updateView() countRule.maxLength = maxLength textView.isEditable = isEnabled textView.isEnabled = isEnabled textView.surface = surface //set the width constraints if let width { widthConstraint?.constant = width widthConstraint?.isActive = true minWidthConstraint?.isActive = false } else { minWidthConstraint?.constant = containerSize.width widthConstraint?.isActive = false minWidthConstraint?.isActive = true } let characterError = getCharacterCounterText() if let maxLength, maxLength > 0 { characterCounterLabel.text = characterError } else { characterCounterLabel.text = "" } showInternalError = !validator.validate() internalErrorText = validator.errorMessage icon.size = .medium icon.color = iconColorConfiguration.getColor(self) containerView.layer.borderColor = readOnly ? readOnlyBorderColorConfiguration.getColor(self).cgColor : borderColorConfiguration.getColor(self).cgColor textView.isEditable = readOnly ? false : true textView.backgroundColor = backgroundColorConfiguration.getColor(self) textView.tintColor = iconColorConfiguration.getColor(self) characterCounterLabel.surface = surface highlightCharacterOverflow() } /// Container for the area showing helper text, error text, character count, maximum length value. open override func getBottomContainer() -> UIView { bottomView.addSubview(bottomStackView) bottomStackView.pinToSuperView() bottomStackView.addArrangedSubview(bottomContainerView) bottomStackView.addArrangedSubview(characterCounterLabel) return bottomView } /// Used to update any Accessibility properties. open override func updateAccessibility() { super.updateAccessibility() if showError { setAccessibilityLabel(for: [titleLabel, textView, errorLabel, helperLabel]) } else { setAccessibilityLabel(for: [titleLabel, textView, helperLabel]) } } //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- private func getCharacterCounterText() -> String? { let count = textView.text.count let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)" if let maxLength, maxLength > 0 { if count > maxLength { return countStr } else { return ("\(countStr)" + "/" + "\(maxLength)") } } else { return nil } } open func highlightCharacterOverflow() { let count = textView.text.count guard let maxLength, maxLength > 0, count > maxLength else { textView.textAttributes = nil return } var textAttributes = [any LabelAttributeModel]() let location = maxLength let length = count - maxLength textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightBackgroundColor.getColor(self), isForegroundColor: false)) textAttributes.append(ColorLabelAttribute(location: location, length: length, color: highlightTextColor.getColor(self), isForegroundColor: true)) textView.textAttributes = textAttributes } //-------------------------------------------------- // MARK: - Validation //-------------------------------------------------- var countRule = CharacterCountRule() lazy var validator: FieldValidator