// // EntryField.swift // VDS // // Created by Matt Bruce on 10/3/22. // import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens import Combine open class EntryField: Control { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- private var stackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .vertical $0.distribution = .fill } }() private var containerStackView: UIStackView = { return UIStackView().with { $0.translatesAutoresizingMaskIntoConstraints = false $0.axis = .horizontal $0.distribution = .fill $0.spacing = 12 } }() private var titleLabel = Label() private var errorLabel = Label() private var helperLabel = Label() private var successLabel = Label() internal var containerView: UIView = { return UIView().with { $0.translatesAutoresizingMaskIntoConstraints = false } }() //-------------------------------------------------- // MARK: - Configuration Properties //-------------------------------------------------- // Sizes are from InVision design specs. public 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 var backgroundColorConfiguration: EntryFieldColorConfiguration = { return EntryFieldColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.enabled.darkColor = VDSFormControlsColor.backgroundOndark $0.disabled.lightColor = VDSFormControlsColor.backgroundOnlight $0.disabled.darkColor = VDSFormControlsColor.backgroundOndark //error/success doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorBackgroundOnlight $0.error.darkColor = VDSColor.feedbackErrorBackgroundOndark $0.success.lightColor = VDSColor.feedbackSuccessBackgroundOnlight $0.success.darkColor = VDSColor.feedbackSuccessBackgroundOndark } }() internal var borderColorConfiguration: EntryFieldColorConfiguration = { return EntryFieldColorConfiguration().with { $0.enabled.lightColor = VDSFormControlsColor.borderOnlight $0.enabled.darkColor = VDSFormControlsColor.borderOnlight $0.disabled.lightColor = VDSColor.interactiveDisabledOnlight $0.disabled.darkColor = VDSColor.interactiveDisabledOndark //error/success doesn't care enabled/disable $0.error.lightColor = VDSColor.feedbackErrorOnlight $0.error.darkColor = VDSColor.feedbackErrorOndark $0.success.lightColor = VDSColor.feedbackSuccessOnlight $0.success.darkColor = VDSColor.feedbackSuccessOndark } }() //-------------------------------------------------- // MARK: - Public Properties //-------------------------------------------------- @Proxy(\.model.labelText) open var labelText: String? @Proxy(\.model.helperText) open var helperText: String? @Proxy(\.model.showError) open var showError: Bool @Proxy(\.model.errorText) open var errorText: String? @Proxy(\.model.showSuccess) open var showSuccess: Bool @Proxy(\.model.successText) open var successText: String? @Proxy(\.model.helperTextPlacement) open var helperTextPlacement: HelperTextPlacement @Proxy(\.model.tooltipTitle) open var tooltipTitle: String? @Proxy(\.model.tooltipContent) open var tooltipContent: String? @Proxy(\.model.transparentBackground) open var transparentBackground: Bool @Proxy(\.model.width) open var width: CGFloat? @Proxy(\.model.maxLength) open var maxLength: Int? @Proxy(\.model.inputId) open var inputId: String? @Proxy(\.model.value) open var value: AnyHashable? @Proxy(\.model.defaultVaue) open var defaultValue: AnyHashable? @Proxy(\.model.required) open var required: Bool @Proxy(\.model.readOnly) open var readOnly: Bool //-------------------------------------------------- // MARK: - Constraints //-------------------------------------------------- internal var heightConstraint: NSLayoutConstraint? internal var widthConstraint: NSLayoutConstraint? internal var minWidthConstraint: 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) minWidthConstraint = containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 0) minWidthConstraint?.isActive = true containerStackView.addArrangedSubview(containerView) stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(containerStackView) stackView.addArrangedSubview(errorLabel) stackView.addArrangedSubview(successLabel) stackView.setCustomSpacing(4, after: titleLabel) stackView.setCustomSpacing(8, after: containerStackView) stackView.setCustomSpacing(8, after: errorLabel) stackView.setCustomSpacing(8, after: successLabel) 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 successLabel.textColorConfiguration = primaryColorConfig errorLabel.textColorConfiguration = primaryColorConfig helperLabel.textColorConfiguration = secondaryColorConfig } public override func reset() { super.reset() setAccessibilityLabel() } //-------------------------------------------------- // MARK: - State //-------------------------------------------------- open override func updateView(viewModel: ModelType) { //update the title model if the required flag is false var titleLabelModel = viewModel.labelModel .addOptional(required: viewModel.required, colorConfiguration: secondaryColorConfig) //tooltip action if let toolTipTitle = viewModel.tooltipTitle, let toolTipContent = viewModel.tooltipContent { let toolTipAction = PassthroughSubject() titleLabelModel = titleLabelModel.addToolTip(action: toolTipAction, colorConfiguration: primaryColorConfig) toolTipAction.sink { print("clicked Tool Tip: \rtitle:\(toolTipTitle)\rcontent:\(toolTipContent)") }.store(in: &subscribers) } titleLabel.set(with: titleLabelModel) //show error or success if viewModel.showError, let errorLabelModel = viewModel.errorLabelModel { errorLabel.set(with: errorLabelModel) errorLabel.isHidden = false successLabel.isHidden = true } else if viewModel.showSuccess, let successLabelModel = viewModel.successLabelModel { successLabel.set(with: successLabelModel) errorLabel.isHidden = true successLabel.isHidden = false } else { errorLabel.isHidden = true successLabel.isHidden = true } //set the helper label position if let helperLabelModel = viewModel.helperLabelModel { helperLabel.removeFromSuperview() if viewModel.helperTextPlacement == .right { containerStackView.spacing = 12 containerStackView.addArrangedSubview(helperLabel) } else { containerStackView.spacing = 0 stackView.addArrangedSubview(helperLabel) } helperLabel.set(with: helperLabelModel) helperLabel.isHidden = false } else { helperLabel.isHidden = true } setAccessibilityHint(!viewModel.disabled) backgroundColor = viewModel.surface.color setNeedsLayout() layoutIfNeeded() } //-------------------------------------------------- // MARK: - Color Class Configurations //-------------------------------------------------- internal class EntryFieldColorConfiguration: DisabledSurfaceColorConfiguration { public let error = SurfaceColorConfiguration() public let success = SurfaceColorConfiguration() override func getColor(_ viewModel: ModelType) -> UIColor { //only show error is enabled and showError == true let showErrorColor = !viewModel.disabled && viewModel.showError let showSuccessColor = !viewModel.disabled && viewModel.showSuccess if showErrorColor { return error.getColor(viewModel) } else if showSuccessColor { return success.getColor(viewModel) } else { return super.getColor(viewModel) } } } } extension DefaultLabelModel { public func addOptional(required: Bool, colorConfiguration: DisabledSurfaceColorConfiguration) -> DefaultLabelModel { guard let text = text, !required else { return self} let optionColorAttr = ColorLabelAttribute(location: text.count + 2, length: 8, color: colorConfiguration.getColor(self)) let newText = "\(text) Optional" return copyWith { $0.text = newText $0.attributes = [optionColorAttr] } } public func addToolTip(action: PassthroughSubject, colorConfiguration: DisabledSurfaceColorConfiguration) -> DefaultLabelModel { guard let text = text else { return self} var newAttributes = attributes ?? [] let newText = "\(text) " let toolTip = ToolTipLabelAttribute(action: action, location: newText.count, length: 1, tintColor: colorConfiguration.getColor(self)) newAttributes.append(toolTip) return copyWith { $0.text = newText $0.attributes = newAttributes } } }