// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine public enum HelperTextPlacement: String, CaseIterable { case bottom, right } open class EntryField: Control, Accessable { //-------------------------------------------------- // 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 stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() internal var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.attributes = [] } internal var errorLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } internal var helperLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) } internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() internal var tooltipView: UIView? //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. internal let containerSize = CGSize(width: 45, height: 45) internal let primaryColorConfig = DisabledSurfaceColorConfiguration().with { $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.enabled.lightColor = VDSColor.elementsPrimaryOnlight $0.enabled.darkColor = VDSColor.elementsPrimaryOndark } internal let secondaryColorConfig = DisabledSurfaceColorConfiguration().with { $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark $0.enabled.lightColor = VDSColor.elementsSecondaryOnlight $0.enabled.darkColor = VDSColor.elementsSecondaryOndark } internal lazy var backgroundColorConfiguration: AnyColorable = { return getBackgroundConfig() }() internal lazy var borderColorConfiguration: AnyColorable = { return getBorderConfig() }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- open var labelText: String? { didSet { didChange() }} open var helperText: String? { didSet { didChange() }} open var showError: Bool = false { didSet { didChange() }} open var errorText: String? { didSet { didChange() }} open var tooltipTitle: String? { didSet { didChange() }} open var tooltipContent: String? { didSet { didChange() }} open var transparentBackground: Bool = false { didSet { didChange() }} open var width: CGFloat? { didSet { didChange() }} open var maxLength: Int? { didSet { didChange() }} open var inputId: String? { didSet { didChange() }} open var value: AnyHashable? { didSet { didChange() }} open var defaultValue: AnyHashable? { didSet { didChange() }} open var required: Bool = false { didSet { didChange() }} open var readOnly: Bool = false { didSet { didChange() }} open var accessibilityHintEnabled: String? { didSet { didChange() }} open var accessibilityHintDisabled: String? { didSet { didChange() }} open var accessibilityValueEnabled: String? { didSet { didChange() }} open var accessibilityValueDisabled: String? { didSet { didChange() }} open var accessibilityLabelEnabled: String? { didSet { didChange() }} open var accessibilityLabelDisabled: String? { didSet { didChange() }} //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? //-------------------------------------------------- // MARK: - Lifecycle //-------------------------------------------------- open override func setup() { super.setup() //add tapGesture to self publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in self?.sendActions(for: .touchUpInside) }.store(in: &subscribers) isAccessibilityElement = true accessibilityTraits = .button addSubview(stackView) //create the wrapping view heightConstraint = containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: containerSize.height) heightConstraint?.isActive = true widthConstraint = containerView.widthAnchor.constraint(equalToConstant: 0) let container = getContainer() stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(container) stackView.addArrangedSubview(errorLabel) stackView.addArrangedSubview(helperLabel) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: container) stackView.setCustomSpacing(8, after: errorLabel) stackView.topAnchor.constraint(equalTo: topAnchor).isActive = true stackView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true stackView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true stackView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true titleLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() errorLabel.textColorConfiguration = primaryColorConfig.eraseToAnyColorable() helperLabel.textColorConfiguration = secondaryColorConfig.eraseToAnyColorable() } open func getContainer() -> UIView { return containerView } open func getBackgroundConfig() -> AnyColorable { return ErrorDisabledSurfaceColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark //error doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark }.eraseToAnyColorable() } open func getBorderConfig() -> AnyColorable { return ErrorDisabledSurfaceColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.borderOnlight $0.enabled.darkColor = VDSFormControlsColor.borderOnlight $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark //error doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorOnlight $0.error.darkColor = VDSColor.feedbackErrorOndark }.eraseToAnyColorable() } open func getToolTipView() -> UIView? { guard let tooltipTitle, let tooltipContent else { return nil } let stack = UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill $0.spacing = 4 } let title = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.typograpicalStyle = .BoldBodySmall $0.text = tooltipTitle $0.surface = surface $0.disabled = disabled } let content = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) $0.textPosition = .left $0.typograpicalStyle = .BoldBodySmall $0.text = tooltipContent $0.surface = surface $0.disabled = disabled } stack.addArrangedSubview(title) stack.addArrangedSubview(content) stack.backgroundColor = backgroundColorConfiguration.getColor(self) return stack } open func showToolTipView(){ print("toolTip clicked: showToolTipView() called") } public override func reset() { super.reset() setAccessibilityLabel() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView() { containerView.backgroundColor = backgroundColorConfiguration.getColor(self) containerView.layer.borderColor = borderColorConfiguration.getColor(self).cgColor containerView.layer.borderWidth = 1 containerView.layer.cornerRadius = 4 updateTitleLabel() updateErrorLabel() updateHelperLabel() setAccessibilityHint() backgroundColor = surface.color } open func updateTitleLabel() { //update the local vars for the label since we no //long have a model var attributes: [any LabelAttributeModel] = [] var updatedLabelText = labelText //dealing with the "Optional" addition to the text if let oldText = updatedLabelText, !required, !oldText.hasSuffix("Optional") { let optionColorAttr = ColorLabelAttribute(location: oldText.count + 2, length: 8, color: secondaryColorConfig.getColor(self)) updatedLabelText = "\(oldText) Optional" attributes.append(optionColorAttr) } //add the tool tip if let view = getToolTipView(), let oldText = updatedLabelText { tooltipView = view let toolTipAction = PassthroughSubject() let toolTipUpdateText = "\(oldText) " //create a little space between the final character and tooltip image let toolTipAttribute = ToolTipLabelAttribute(action: toolTipAction, location: toolTipUpdateText.count, length: 1, tintColor: primaryColorConfig.getColor(self)) updatedLabelText = toolTipUpdateText attributes.append(toolTipAttribute) toolTipAction.sink { [weak self] in self?.showToolTipView() }.store(in: &subscribers) } else { tooltipView = nil } //set the titleLabel titleLabel.textPosition = .left titleLabel.typograpicalStyle = .BodySmall titleLabel.text = updatedLabelText titleLabel.attributes = attributes titleLabel.surface = surface titleLabel.disabled = disabled } open func updateErrorLabel(){ if showError, let errorText { errorLabel.textPosition = .left errorLabel.typograpicalStyle = .BodySmall errorLabel.text = errorText errorLabel.surface = surface errorLabel.disabled = disabled errorLabel.isHidden = false } else { errorLabel.isHidden = true } } open func updateHelperLabel(){ //set the helper label position if let helperText { helperLabel.textPosition = .left helperLabel.typograpicalStyle = .BodySmall helperLabel.text = helperText helperLabel.surface = surface helperLabel.disabled = disabled helperLabel.isHidden = false } else { helperLabel.isHidden = true } } } //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- internal class ErrorDisabledSurfaceColorConfiguration: DisabledSurfaceColorable { typealias ModelType = Errorable & Surfaceable & Disabling var error = SurfaceColorConfiguration() var disabled = SurfaceColorConfiguration() var enabled = SurfaceColorConfiguration() required public init(){} func getColor(_ viewModel: any ModelType) -> UIColor { //only show error is enabled and showError == true let showErrorColor = !viewModel.disabled && viewModel.showError if showErrorColor { return error.getColor(viewModel) } else { return getDisabledColor(viewModel) } } }