311 lines
10 KiB
Swift
311 lines
10 KiB
Swift
//
|
|
// TextLinkCaret.swift
|
|
// VDS
|
|
//
|
|
// Created by Matt Bruce on 11/1/22.
|
|
//
|
|
|
|
import Foundation
|
|
import UIKit
|
|
import VDSColorTokens
|
|
import VDSFormControlsTokens
|
|
import Combine
|
|
|
|
public enum TextLinkCaretPosition: String, CaseIterable {
|
|
case left, right
|
|
}
|
|
|
|
@objc(VDSTextLinkCaret)
|
|
open class TextLinkCaret: Control {
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Private Properties
|
|
//--------------------------------------------------
|
|
private var heightConstraint: NSLayoutConstraint?
|
|
|
|
private var label = Label().with {
|
|
$0.typograpicalStyle = TypographicalStyle.BoldBodyLarge
|
|
}
|
|
|
|
private var caretView = CaretView().with {
|
|
$0.size = CaretView.CaretSize.small(.vertical)
|
|
$0.lineWidth = 2
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Properties
|
|
//--------------------------------------------------
|
|
open var text: String? { didSet { didChange() } }
|
|
|
|
open var iconPosition: TextLinkCaretPosition = .right { didSet { didChange() } }
|
|
|
|
private var height: CGFloat {
|
|
44
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Initializers
|
|
//--------------------------------------------------
|
|
required public init() {
|
|
super.init(frame: .zero)
|
|
initialSetup()
|
|
}
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: .zero)
|
|
initialSetup()
|
|
}
|
|
|
|
public required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
initialSetup()
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Public Functions
|
|
//--------------------------------------------------
|
|
open override func initialSetup() {
|
|
super.initialSetup()
|
|
}
|
|
|
|
open override func setup() {
|
|
super.setup()
|
|
//add tapGesture to self
|
|
publisher(for: UITapGestureRecognizer()).sink { [weak self] _ in
|
|
self?.sendActions(for: .touchUpInside)
|
|
}.store(in: &subscribers)
|
|
|
|
//constraints
|
|
heightAnchor.constraint(greaterThanOrEqualToConstant: height).isActive = true
|
|
|
|
let size = caretView.size!.dimensions()
|
|
caretView.frame = .init(x: 0, y: 0, width: size.width, height: size.height)
|
|
addSubview(label)
|
|
label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
|
|
label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
|
|
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
|
|
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Constraints
|
|
//--------------------------------------------------
|
|
private var caretLeadingConstraint: NSLayoutConstraint?
|
|
private var caretTrailingConstraint: NSLayoutConstraint?
|
|
private var labelConstraint: NSLayoutConstraint?
|
|
|
|
open override func reset() {
|
|
super.reset()
|
|
label.reset()
|
|
|
|
label.typograpicalStyle = TypographicalStyle.BoldBodyLarge
|
|
text = nil
|
|
iconPosition = .right
|
|
|
|
accessibilityCustomActions = []
|
|
accessibilityTraits = .staticText
|
|
}
|
|
|
|
//--------------------------------------------------
|
|
// MARK: - Overrides
|
|
//--------------------------------------------------
|
|
open override func updateView() {
|
|
|
|
let updatedText = text ?? ""
|
|
|
|
caretView.surface = surface
|
|
caretView.disabled = disabled
|
|
caretView.direction = iconPosition == .right ? CaretView.Direction.right : CaretView.Direction.left
|
|
|
|
let image = caretView.getImage()
|
|
let location = iconPosition == .right ? updatedText.count + 1 : 0
|
|
let textColor = label.textColorConfiguration.getColor(self)
|
|
let imageAttribute = ImageLabelAttribute(location: location,
|
|
length: 1,
|
|
image: image,
|
|
frame: .init(x: 0, y: 0, width: image.size.width, height: image.size.height),
|
|
tintColor: textColor)
|
|
label.surface = surface
|
|
label.disabled = disabled
|
|
label.text = iconPosition == .right ? "\(updatedText) " : " \(updatedText)"
|
|
label.attributes = [imageAttribute]
|
|
}
|
|
|
|
}
|
|
|
|
extension UIView {
|
|
public func getImage() -> UIImage {
|
|
let renderer = UIGraphicsImageRenderer(size: self.bounds.size)
|
|
let image = renderer.image { ctx in
|
|
self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
|
|
}
|
|
return image
|
|
}
|
|
}
|
|
|
|
internal class CaretView: View {
|
|
//------------------------------------------------------
|
|
// MARK: - Properties
|
|
//------------------------------------------------------
|
|
private var caretPath: UIBezierPath = UIBezierPath()
|
|
|
|
public var lineWidth: CGFloat = 1 { didSet{ didChange() } }
|
|
|
|
public var direction: Direction = .right { didSet{ didChange() } }
|
|
|
|
public var size: CaretSize? { didSet{ didChange() } }
|
|
|
|
public var colorConfiguration: AnyColorable = DisabledSurfaceColorConfiguration().with {
|
|
$0.disabled.lightColor = VDSColor.elementsSecondaryOnlight
|
|
$0.disabled.darkColor = VDSColor.elementsSecondaryOndark
|
|
$0.enabled.lightColor = VDSColor.elementsPrimaryOnlight
|
|
$0.enabled.darkColor = VDSColor.elementsPrimaryOndark
|
|
}.eraseToAnyColorable()
|
|
|
|
|
|
//------------------------------------------------------
|
|
// MARK: - Constraints
|
|
//------------------------------------------------------
|
|
|
|
/// Sizes of CaretView are derived from InVision design specs. They are provided for convenience.
|
|
public enum CaretSize {
|
|
case small(Orientation)
|
|
case medium(Orientation)
|
|
case large(Orientation)
|
|
|
|
/// Orientation based on the longest line of the view.
|
|
public enum Orientation {
|
|
case vertical
|
|
case horizontal
|
|
}
|
|
|
|
/// Dimensions of container; provided by InVision design.
|
|
func dimensions() -> CGSize {
|
|
|
|
switch self {
|
|
case .small(let o):
|
|
return o == .vertical ? CGSize(width: 6.9, height: 10.96) : CGSize(width: 10.96, height: 6.9)
|
|
|
|
case .medium(let o):
|
|
return o == .vertical ? CGSize(width: 9.9, height: 16.96) : CGSize(width: 16.96, height: 9.9)
|
|
|
|
case .large(let o):
|
|
return o == .vertical ? CGSize(width: 14.9, height: 24.96) : CGSize(width: 24.96, height: 14.9)
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
// MARK: - Initialization
|
|
//------------------------------------------------------
|
|
|
|
public override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
public convenience init(lineWidth: CGFloat) {
|
|
self.init(frame: .zero)
|
|
self.lineWidth = lineWidth
|
|
}
|
|
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
super.init(coder: aDecoder)
|
|
fatalError("CaretView xib not supported.")
|
|
}
|
|
|
|
required public convenience init() {
|
|
self.init(frame: .zero)
|
|
}
|
|
|
|
public convenience init(size: CaretSize){
|
|
let dimensions = size.dimensions()
|
|
self.init(frame: .init(x: 0, y: 0, width: dimensions.width, height: dimensions.height))
|
|
self.size = size
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
// MARK: - Setup
|
|
//------------------------------------------------------
|
|
|
|
override open func setup() {
|
|
super.setup()
|
|
defaultState()
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
// MARK: - Drawing
|
|
//------------------------------------------------------
|
|
|
|
/// The direction the caret will be pointing to.
|
|
public enum Direction: Int {
|
|
case left
|
|
case right
|
|
case down
|
|
case up
|
|
}
|
|
|
|
override func draw(_ rect: CGRect) {
|
|
super.draw(rect)
|
|
|
|
caretPath.removeAllPoints()
|
|
caretPath.lineJoinStyle = .miter
|
|
caretPath.lineWidth = lineWidth
|
|
|
|
let inset = lineWidth / 2
|
|
let halfWidth = frame.size.width / 2
|
|
let halfHeight = frame.size.height / 2
|
|
|
|
switch direction {
|
|
case .up:
|
|
caretPath.move(to: CGPoint(x: inset, y: frame.size.height - inset))
|
|
caretPath.addLine(to: CGPoint(x: halfWidth, y: inset))
|
|
caretPath.addLine(to: CGPoint(x: frame.size.width, y: frame.size.height))
|
|
|
|
case .right:
|
|
caretPath.move(to: CGPoint(x: inset, y: inset))
|
|
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: halfHeight))
|
|
caretPath.addLine(to: CGPoint(x: inset, y: frame.size.height - inset))
|
|
|
|
case .down:
|
|
caretPath.move(to: CGPoint(x: inset, y: inset))
|
|
caretPath.addLine(to: CGPoint(x: halfWidth, y: frame.size.height - inset))
|
|
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: inset))
|
|
|
|
case .left:
|
|
caretPath.move(to: CGPoint(x: frame.size.width - inset, y: inset))
|
|
caretPath.addLine(to: CGPoint(x: inset, y: halfHeight))
|
|
caretPath.addLine(to: CGPoint(x: frame.size.width - inset, y: frame.size.height - inset))
|
|
}
|
|
|
|
let color = colorConfiguration.getColor(self)
|
|
color.setStroke()
|
|
caretPath.stroke()
|
|
}
|
|
|
|
override func updateView() {
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
//------------------------------------------------------
|
|
// MARK: - Methods
|
|
//------------------------------------------------------
|
|
public func setLineColor(_ color: UIColor) {
|
|
setNeedsDisplay()
|
|
}
|
|
|
|
public func defaultState() {
|
|
isOpaque = false
|
|
isHidden = false
|
|
backgroundColor = .clear
|
|
}
|
|
|
|
/// Ensure you have defined a CaretSize with Orientation before calling.
|
|
public func setConstraints() {
|
|
|
|
guard let dimensions = size?.dimensions() else { return }
|
|
|
|
heightAnchor.constraint(equalToConstant: dimensions.height).isActive = true
|
|
widthAnchor.constraint(equalToConstant: dimensions.width).isActive = true
|
|
}
|
|
}
|