vds_ios/VDS/Components/TextFields/TextArea/TextArea.swift

254 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 thats 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 allowCharCount: Int = 0
internal var inputFieldStackView: UIStackView = {
return UIStackView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.axis = .horizontal
$0.distribution = .fill
$0.spacing = 12
}
}()
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
}
}()
open var characterCounterLabel = Label().with {
$0.setContentCompressionResistancePriority(.required, for: .vertical)
$0.textStyle = .bodySmall
$0.textAlignment = .right
$0.numberOfLines = 1
}
//--------------------------------------------------
// MARK: - Public Properties
//--------------------------------------------------
override var containerSize: CGSize { CGSize(width: 182, height: 88) }
/// UITextView shown in the TextArea.
open var textView = UITextView().with {
$0.translatesAutoresizingMaskIntoConstraints = false
$0.font = TextStyle.bodyLarge.font
$0.sizeToFit()
$0.isScrollEnabled = false
}
/// Color configuration for the textView.
open var textViewTextColorConfiguration: AnyColorable = ViewColorConfiguration().with {
$0.setSurfaceColors(VDSColor.interactiveDisabledOnlight, VDSColor.interactiveDisabledOndark, forDisabled: true)
$0.setSurfaceColors(VDSColor.elementsPrimaryOnlight, VDSColor.elementsPrimaryOndark, forDisabled: false)
}.eraseToAnyColorable() { didSet { setNeedsUpdate() } }
/// 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()
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
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)
textView.delegate = self
}
/// Resets to default settings.
open override func reset() {
super.reset()
textView.text = ""
characterCounterLabel.reset()
characterCounterLabel.textStyle = .bodySmall
}
/// 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()
textView.isEditable = isEnabled
textView.textColor = textViewTextColorConfiguration.getColor(self)
//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
}
if ((maxLength ?? 0) > 0) {
// allow - 20% of character limit
let overflowLimit = Double(maxLength ?? 0) * 0.20
allowCharCount = Int(overflowLimit) + (maxLength ?? 0)
characterCounterLabel.text = getCharacterCounterText()
} else {
characterCounterLabel.text = ""
}
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)
characterCounterLabel.textColorConfiguration = primaryColorConfiguration.eraseToAnyColorable()
textView.tintColor = iconColorConfiguration.getColor(self)
}
/// 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
}
//--------------------------------------------------
// MARK: - Private Methods
//--------------------------------------------------
private func getCharacterCounterText() -> String {
let count = textView.text.count
let countStr = (count > maxLength ?? 0) ? ("-" + "\(count-(maxLength ?? 0))") : "\(count)"
if count > maxLength ?? 0 {
showError = true
errorText = "You have exceeded the character limit."
return countStr
} else {
showError = false
errorText = ""
return ("\(countStr)" + "/" + "\(maxLength ?? 0)")
}
}
open func highlightCharacterOverflow() {
let count = textView.text.count
print("count: \(count), maxLength: \(maxLength ?? 0)")
guard let text = textView.attributedText?.mutableCopy() as? NSMutableAttributedString else { return }
text.addAttribute(NSAttributedString.Key.backgroundColor, value: highlightBackgroundColor.getColor(self), range: NSRange(location:(maxLength ?? 0 ), length: (count - (maxLength ?? 0))))
text.addAttribute(NSAttributedString.Key.foregroundColor, value: highlightTextColor.getColor(self), range: NSRange(location:(maxLength ?? 0 ), length: (count - (maxLength ?? 0))))
textView.attributedText = text.copy() as? NSAttributedString
}
}
extension TextArea: UITextViewDelegate {
//--------------------------------------------------
// MARK: - UITextViewDelegate
//--------------------------------------------------
public func textViewDidChange(_ textView: UITextView) {
//dynamic textView Height sizing based on Figma
//if you want it to work "as-is" delete this code
//since it will autogrow with the current settings
if let textViewHeightConstraint, textView.isEditable {
let height = textView.contentSize.height
if height > 88 && height < 176 {
textViewHeightConstraint.constant = 176
} else if height > 176 {
textViewHeightConstraint.constant = 352
} else {
textViewHeightConstraint.constant = 88
}
textViewHeightConstraint.isActive = true
}
//The exceeding characters will be highlighted to help users correct their entry.
if ((maxLength ?? 0) > 0) {
if textView.text.count <= allowCharCount {
//setting the value and firing control event
value = textView.text
sendActions(for: .valueChanged)
if (textView.text.count > (maxLength ?? 0)) {
highlightCharacterOverflow()
}
} else {
textView.text.removeLast()
highlightCharacterOverflow()
}
} else {
//setting the value and firing control event
value = textView.text
sendActions(for: .valueChanged)
}
}
}