blob: f3fa2f2c82386f7f5f8924eb6d65590a4ae6d753 [file] [log] [blame]
// Copyright 2017-present the Material Components for iOS authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#import "MDCMultilineTextField.h"
#import "MDCIntrinsicHeightTextView.h"
#import "MDCTextField.h"
#import "MDCTextFieldPositioningDelegate.h"
#import "MDCTextInputBorderView.h"
#import "MDCTextInputCharacterCounter.h"
#import "MDCTextInputController.h"
#import "MDCTextInputUnderlineView.h"
#import "private/MDCTextInputCommonFundament.h"
#import "MaterialMath.h"
#import "MaterialTypography.h"
@interface MDCMultilineTextField () {
UIColor *_cursorColor;
MDCIntrinsicHeightTextView *_textView;
}
@property(nonatomic, assign, getter=isEditing) BOOL editing;
@property(nonatomic, assign) CGFloat textViewWidth;
@property(nonatomic, strong) MDCTextInputCommonFundament *fundament;
@property(nonatomic, strong) NSLayoutConstraint *textViewBottomSuperviewBottom;
@property(nonatomic, strong) NSLayoutConstraint *textViewLeading;
@property(nonatomic, strong) NSLayoutConstraint *textViewMinHeight;
@property(nonatomic, strong) NSLayoutConstraint *textViewTop;
@property(nonatomic, strong) NSLayoutConstraint *textViewTrailing;
// textViewTrailingTrailingViewLeading is a constraint from .textView's trailing edge to
// .trailingView's leading edge.
@property(nonatomic, strong) NSLayoutConstraint *textViewTrailingTrailingViewLeading;
@property(nonatomic, strong) NSLayoutConstraint *trailingViewCenterY;
@property(nonatomic, strong) NSLayoutConstraint *trailingViewTrailing;
@end
@implementation MDCMultilineTextField
@synthesize expandsOnOverflow = _expandsOnOverflow;
@synthesize minimumLines = _minimumLines;
@synthesize trailingView = _trailingView;
@synthesize trailingViewMode = _trailingViewMode;
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_textView = [[MDCIntrinsicHeightTextView alloc] initWithFrame:CGRectZero];
[self setupTextView];
_fundament = [[MDCTextInputCommonFundament alloc] initWithTextInput:self];
[self commonMDCMultilineTextFieldInitialization];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
if (!_textView) {
_textView = [[MDCIntrinsicHeightTextView alloc] initWithFrame:CGRectZero];
}
// It should be noted that setupTextView sets many properties of the textView that may have
// been encoded differently.
[self setupTextView];
_fundament = [[MDCTextInputCommonFundament alloc] initWithTextInput:self];
[self commonMDCMultilineTextFieldInitialization];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (instancetype)copyWithZone:(__unused NSZone *)zone {
MDCMultilineTextField *copy = [[[self class] alloc] initWithFrame:self.frame];
copy.expandsOnOverflow = self.expandsOnOverflow;
copy.cursorColor = self.cursorColor;
// The .fundament creates a .clearButton so setting the clearButton's .tintColor must wait for the
// final .fundament to be created.
copy.fundament = [self.fundament copy];
copy.clearButton.tintColor = self.clearButton.tintColor;
copy.layoutDelegate = self.layoutDelegate;
copy.minimumLines = self.minimumLines;
copy.multilineDelegate = self.multilineDelegate;
copy.placeholder = self.placeholder;
copy.text = self.text;
if ([self.trailingView conformsToProtocol:@protocol(NSCopying)]) {
copy.trailingView = [self.trailingView copy];
}
copy.trailingViewMode = self.trailingViewMode;
return copy;
}
- (void)commonMDCMultilineTextFieldInitialization {
self.backgroundColor = [UIColor clearColor];
self.textColor = _fundament.textColor;
// TODO: (#4331) This needs to be converted to the new text scheme.
self.font = [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleBody1];
self.clearButton.tintColor = [UIColor colorWithWhite:0 alpha:[MDCTypography captionFontOpacity]];
_cursorColor = MDCTextInputCursorColor();
[self applyCursorColor];
self.editable = YES;
self.expandsOnOverflow = YES;
_minimumLines = 1;
[self setupUnderlineConstraints];
[self setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisVertical];
}
- (void)subscribeForNotifications {
NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(textViewDidBeginEditing:)
name:UITextViewTextDidBeginEditingNotification
object:self.textView];
[defaultCenter addObserver:self
selector:@selector(textViewDidEndEditing:)
name:UITextViewTextDidEndEditingNotification
object:self.textView];
[defaultCenter addObserver:self
selector:@selector(textViewDidChange:)
name:UITextViewTextDidChangeNotification
object:self.textView];
}
#pragma mark - UIResponder Overrides
- (BOOL)becomeFirstResponder {
return [self.textView becomeFirstResponder];
}
- (BOOL)isFirstResponder {
return self.textView.isFirstResponder;
}
#pragma mark - TextView Implementation
- (void)setupTextView {
[self insertSubview:self.textView atIndex:0];
self.textView.translatesAutoresizingMaskIntoConstraints = NO;
self.textView.scrollEnabled = !_expandsOnOverflow;
[self.textView setContentHuggingPriority:UILayoutPriorityDefaultLow - 1
forAxis:UILayoutConstraintAxisHorizontal];
[self.textView setContentHuggingPriority:UILayoutPriorityDefaultLow - 1
forAxis:UILayoutConstraintAxisVertical];
[self.textView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow - 1
forAxis:UILayoutConstraintAxisHorizontal];
[self.textView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh + 1
forAxis:UILayoutConstraintAxisVertical];
self.textView.textContainer.lineBreakMode = NSLineBreakByTruncatingTail;
self.textView.textContainer.lineFragmentPadding = 0;
self.textView.textContainerInset = UIEdgeInsetsZero;
[self subscribeForNotifications];
self.textView.backgroundColor = [UIColor clearColor];
self.textView.opaque = NO;
}
#pragma mark - Underline View Implementation
- (void)setupUnderlineConstraints {
if (!self.underline) {
return;
}
NSLayoutConstraint *underlineLeading =
[NSLayoutConstraint constraintWithItem:self.underline
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeading
multiplier:1
constant:0];
underlineLeading.priority = UILayoutPriorityDefaultLow;
underlineLeading.active = YES;
NSLayoutConstraint *underlineTrailing =
[NSLayoutConstraint constraintWithItem:self.underline
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
underlineTrailing.priority = UILayoutPriorityDefaultLow;
underlineTrailing.active = YES;
NSLayoutConstraint *underlineYTextView =
[NSLayoutConstraint constraintWithItem:self.underline
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:self.textView
attribute:NSLayoutAttributeBottom
multiplier:1
constant:MDCTextInputHalfPadding];
underlineYTextView.priority = UILayoutPriorityDefaultLow;
underlineYTextView.active = YES;
}
#pragma mark - Layout (UIView)
- (CGSize)intrinsicContentSize {
CGSize boundingSize = CGSizeZero;
boundingSize.width = UIViewNoIntrinsicMetric;
[self.textView layoutIfNeeded];
CGFloat estimatedTextViewHeight =
[self.textView systemLayoutSizeFittingSize:CGSizeMake(CGRectGetWidth(self.textView.bounds), 0)
withHorizontalFittingPriority:UILayoutPriorityDefaultLow
verticalFittingPriority:UILayoutPriorityDefaultLow]
.height;
CGFloat minimumHeight = [self estimatedTextViewLineHeight] * self.minimumLines;
estimatedTextViewHeight = MAX(estimatedTextViewHeight, minimumHeight);
boundingSize.height = [self textInsets].top + estimatedTextViewHeight + [self textInsets].bottom;
return boundingSize;
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize sizeThatFits = [self intrinsicContentSize];
sizeThatFits.width = size.width;
return sizeThatFits;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (![self.textView isDescendantOfView:self]) {
[self setupTextView];
}
if (self.subviews.firstObject != self.textView) {
[self sendSubviewToBack:self.textView];
}
[self updateTrailingViewAlpha];
[self.fundament layoutSubviewsOfInput];
[self updateBorder];
[self applyCursorColor];
if ([self.positioningDelegate respondsToSelector:@selector(textInputDidLayoutSubviews)]) {
[self.positioningDelegate textInputDidLayoutSubviews];
}
[self updateIntrinsicSizeFromTextView];
}
- (void)updateConstraints {
if (!self.textViewLeading) {
self.textViewLeading = [NSLayoutConstraint constraintWithItem:self.textView
attribute:NSLayoutAttributeLeading
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeLeading
multiplier:1
constant:self.textInsets.left];
self.textViewLeading.priority = UILayoutPriorityDefaultLow;
self.textViewLeading.active = YES;
}
self.textViewLeading.constant = self.textInsets.left;
if (!self.textViewBottomSuperviewBottom) {
self.textViewBottomSuperviewBottom =
[NSLayoutConstraint constraintWithItem:self.textView
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationLessThanOrEqual
toItem:self
attribute:NSLayoutAttributeBottom
multiplier:1
constant:-1 * MDCTextInputHalfPadding];
self.textViewBottomSuperviewBottom.priority = UILayoutPriorityDefaultLow;
self.textViewBottomSuperviewBottom.active = YES;
}
if (!self.textViewTop) {
self.textViewTop = [NSLayoutConstraint constraintWithItem:self.textView
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:self
attribute:NSLayoutAttributeTop
multiplier:1
constant:self.textInsets.top];
self.textViewTop.priority = UILayoutPriorityDefaultLow + 1;
self.textViewTop.active = YES;
}
self.textViewTop.constant = self.textInsets.top;
if (!self.textViewTrailing) {
self.textViewTrailing = [NSLayoutConstraint constraintWithItem:self.textView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.clearButton
attribute:NSLayoutAttributeLeading
multiplier:1
constant:-1 * [self textInsets].right];
self.textViewTrailing.priority = UILayoutPriorityDefaultLow;
self.textViewTrailing.active = YES;
}
self.textViewTrailing.constant = -1 * [self textInsets].right;
if (!self.textViewMinHeight) {
self.textViewMinHeight = [NSLayoutConstraint
constraintWithItem:self.textView
attribute:NSLayoutAttributeHeight
relatedBy:NSLayoutRelationGreaterThanOrEqual
toItem:nil
attribute:NSLayoutAttributeNotAnAttribute
multiplier:1
constant:[self estimatedTextViewLineHeight] * self.minimumLines];
self.textViewMinHeight.priority = UILayoutPriorityDefaultLow + 1;
}
self.textViewMinHeight.active = YES;
self.textViewMinHeight.constant = [self estimatedTextViewLineHeight] * self.minimumLines;
[self.fundament updateConstraintsOfInput];
[self updateTrailingViewLayout];
[self updateIntrinsicSizeFromTextView];
// This must always be the last message in this method.
[super updateConstraints];
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (CGFloat)estimatedTextViewLineHeight {
CGFloat scale = UIScreen.mainScreen.scale;
return MDCCeil(self.textView.font.lineHeight * scale) / scale;
}
- (void)updateIntrinsicSizeFromTextView {
if ([self textViewWidthDidChange]) {
[self invalidateIntrinsicContentSize];
}
}
- (BOOL)textViewWidthDidChange {
BOOL widthDidChange = NO;
CGFloat currentTextViewWidth = CGRectGetWidth(self.textView.bounds);
if (self.textViewWidth != currentTextViewWidth) {
widthDidChange = YES;
}
self.textViewWidth = currentTextViewWidth;
return widthDidChange;
}
#pragma mark - Touch (UIView)
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *receiver = [super hitTest:point withEvent:event];
// We do this so the hit zone to become first responder is larger than just the small text view.
if (receiver == self && self.isEditing == NO) {
return self.textView;
}
return receiver;
}
#pragma mark - Trailing View Implementation
- (void)updateTrailingViewLayout {
if (!self.trailingView) {
return;
}
self.trailingView.translatesAutoresizingMaskIntoConstraints = NO;
if (![self.trailingView isDescendantOfView:self]) {
[self addSubview:self.trailingView];
}
if (!self.trailingViewTrailing) {
self.trailingViewTrailing = [NSLayoutConstraint constraintWithItem:self.trailingView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.clearButton
attribute:NSLayoutAttributeTrailing
multiplier:1
constant:0];
}
self.trailingViewTrailing.active = YES;
if (!self.trailingViewCenterY) {
self.trailingViewCenterY = [NSLayoutConstraint constraintWithItem:self.trailingView
attribute:NSLayoutAttributeCenterY
relatedBy:NSLayoutRelationEqual
toItem:self.clearButton
attribute:NSLayoutAttributeCenterY
multiplier:1
constant:0];
}
self.trailingViewCenterY.active = YES;
if (!self.textViewTrailingTrailingViewLeading) {
self.textViewTrailingTrailingViewLeading =
[NSLayoutConstraint constraintWithItem:self.textView
attribute:NSLayoutAttributeTrailing
relatedBy:NSLayoutRelationEqual
toItem:self.trailingView
attribute:NSLayoutAttributeLeading
multiplier:1
constant:self.textViewTrailing.constant];
}
self.textViewTrailingTrailingViewLeading.active = !MDCCGFloatEqual([self trailingViewAlpha], 0);
}
- (void)updateTrailingViewAlpha {
self.trailingView.alpha = [self trailingViewAlpha];
}
- (CGFloat)trailingViewAlpha {
// The trailing view has the same behavior as .rightView in UITextField: It has visual precedence
// over the clear button.
CGFloat trailingViewAlpha;
switch (self.trailingViewMode) {
case UITextFieldViewModeAlways:
trailingViewAlpha = 1;
break;
case UITextFieldViewModeWhileEditing:
trailingViewAlpha = self.isEditing ? 1 : 0;
break;
case UITextFieldViewModeUnlessEditing:
trailingViewAlpha = self.isEditing ? 0 : 1;
break;
case UITextFieldViewModeNever:
trailingViewAlpha = 0;
break;
default:
NSAssert(NO, @"Invalid enumeration value %li.", (long)self.trailingViewMode);
trailingViewAlpha = self.trailingViewAlpha;
break;
}
return trailingViewAlpha;
}
#pragma mark - Border Implementation
- (UIBezierPath *)defaultBorderPath {
CGRect borderBound = self.bounds;
borderBound.size.height = self.underline.center.y;
return [UIBezierPath
bezierPathWithRoundedRect:borderBound
byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight
cornerRadii:CGSizeMake(MDCTextInputBorderRadius, MDCTextInputBorderRadius)];
}
- (void)updateBorder {
self.borderView.borderPath = self.borderPath;
}
#pragma mark - Applying Color
- (void)applyCursorColor {
self.textView.tintColor = self.cursorColor;
}
#pragma mark - Properties Implementation
- (NSAttributedString *)attributedPlaceholder {
return self.fundament.attributedPlaceholder;
}
- (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder {
self.fundament.attributedPlaceholder = attributedPlaceholder;
}
- (NSAttributedString *)attributedText {
return self.textView.attributedText;
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
self.textView.attributedText = attributedText;
[self.fundament didSetText];
[[NSNotificationCenter defaultCenter] postNotificationName:MDCTextFieldTextDidSetTextNotification
object:self];
}
- (UIBezierPath *)borderPath {
return self.fundament.borderPath ? self.fundament.borderPath : [self defaultBorderPath];
}
- (void)setBorderPath:(UIBezierPath *)borderPath {
if (![self.fundament.borderPath isEqual:borderPath]) {
self.fundament.borderPath = borderPath;
[self updateBorder];
}
}
- (MDCTextInputBorderView *)borderView {
return self.fundament.borderView;
}
- (void)setBorderView:(MDCTextInputBorderView *)borderView {
self.fundament.borderView = borderView;
}
- (UIButton *)clearButton {
return self.fundament.clearButton;
}
- (UITextFieldViewMode)clearButtonMode {
return self.fundament.clearButtonMode;
}
- (void)setClearButtonMode:(UITextFieldViewMode)clearButtonMode {
self.fundament.clearButtonMode = clearButtonMode;
}
- (UIColor *)cursorColor {
return _cursorColor ?: MDCTextInputCursorColor();
}
- (void)setCursorColor:(UIColor *)cursorColor {
_cursorColor = cursorColor;
[self applyCursorColor];
}
- (void)setEditable:(BOOL)editable {
[self.textView setEditable:editable];
self.fundament.enabled = editable;
}
- (BOOL)isEnabled {
return self.fundament.isEnabled && self.textView.isEditable;
}
- (void)setEnabled:(BOOL)enabled {
self.fundament.enabled = enabled;
self.textView.editable = enabled;
[[NSNotificationCenter defaultCenter]
postNotificationName:MDCTextInputDidToggleEnabledNotification
object:self];
}
- (void)setExpandsOnOverflow:(BOOL)expandsOnOverflow {
if (_expandsOnOverflow != expandsOnOverflow) {
_expandsOnOverflow = expandsOnOverflow;
self.textView.scrollEnabled = !expandsOnOverflow;
[self setNeedsUpdateConstraints];
}
}
- (UIFont *)font {
if (_textView) {
return _textView.font;
}
return self.textView.font;
}
- (void)setFont:(UIFont *)font {
if (self.textView.font != font) {
[self.textView setFont:font];
[_fundament didSetFont];
}
}
- (BOOL)hidesPlaceholderOnInput {
return self.fundament.hidesPlaceholderOnInput;
}
- (void)setHidesPlaceholderOnInput:(BOOL)hidesPlaceholderOnInput {
self.fundament.hidesPlaceholderOnInput = hidesPlaceholderOnInput;
}
- (UILabel *)leadingUnderlineLabel {
return self.fundament.leadingUnderlineLabel;
}
- (NSUInteger)minimumLines {
if (_minimumLines < 1) {
_minimumLines = 1;
}
return _minimumLines;
}
- (void)setMinimumLines:(NSUInteger)minimumLines {
if (_minimumLines != minimumLines) {
_minimumLines = minimumLines > 0 ? minimumLines : 1;
[self setNeedsUpdateConstraints];
}
}
- (NSString *)placeholder {
return self.fundament.placeholder;
}
- (void)setPlaceholder:(NSString *)placeholder {
[self.fundament setPlaceholder:placeholder];
}
- (UILabel *)placeholderLabel {
return self.fundament.placeholderLabel;
}
- (id<MDCTextInputPositioningDelegate>)positioningDelegate {
return self.fundament.positioningDelegate;
}
- (void)setPositioningDelegate:(id<MDCTextInputPositioningDelegate>)positioningDelegate {
self.fundament.positioningDelegate = positioningDelegate;
}
- (NSString *)text {
return self.textView.text;
}
- (void)setText:(NSString *)text {
[self.textView setText:text];
[self.fundament didSetText];
[[NSNotificationCenter defaultCenter] postNotificationName:MDCTextFieldTextDidSetTextNotification
object:self];
}
- (UIColor *)textColor {
return self.fundament.textColor;
}
- (void)setTextColor:(UIColor *)textColor {
[self.textView setTextColor:textColor];
self.fundament.textColor = textColor;
}
- (UIEdgeInsets)textInsets {
return self.fundament.textInsets;
}
- (MDCTextInputTextInsetsMode)textInsetsMode {
return self.fundament.textInsetsMode;
}
- (void)setTextInsetsMode:(MDCTextInputTextInsetsMode)textInsetsMode {
self.fundament.textInsetsMode = textInsetsMode;
}
- (MDCIntrinsicHeightTextView *)textView {
if (!_textView) {
_textView = [[MDCIntrinsicHeightTextView alloc] initWithFrame:CGRectZero];
[self setupTextView];
[self setupUnderlineConstraints];
}
return _textView;
}
- (void)setTextView:(MDCIntrinsicHeightTextView *)textView {
if (![_textView isEqual:textView]) {
[_textView removeFromSuperview];
_textView = textView;
if (textView) {
[self setupTextView];
[self setupUnderlineConstraints];
}
}
}
- (UILabel *)trailingUnderlineLabel {
return self.fundament.trailingUnderlineLabel;
}
- (void)setTrailingView:(UIView *)trailingView {
if (_trailingView != trailingView) {
[_trailingView removeFromSuperview];
[self addSubview:trailingView];
_trailingView = trailingView;
[self setNeedsUpdateConstraints];
}
}
- (MDCTextInputUnderlineView *)underline {
return self.fundament.underline;
}
#pragma mark - UITextView Notification Observation
- (void)textViewDidBeginEditing:(__unused NSNotification *)note {
self.editing = YES;
[self.fundament didBeginEditing];
}
- (void)textViewDidChange:(__unused NSNotification *)note {
[self.fundament didChange];
CGSize currentSize = self.bounds.size;
CGSize requiredSize = [self sizeThatFits:CGSizeMake(currentSize.width, CGFLOAT_MAX)];
if (currentSize.height != requiredSize.height && self.textView.delegate &&
[self.layoutDelegate
respondsToSelector:@selector(multilineTextField:didChangeContentSize:)]) {
id<MDCMultilineTextInputLayoutDelegate> delegate =
(id<MDCMultilineTextInputLayoutDelegate>)self.layoutDelegate;
[delegate multilineTextField:self didChangeContentSize:requiredSize];
}
}
- (void)textViewDidEndEditing:(__unused NSNotification *)note {
self.editing = NO;
[self.fundament didEndEditing];
}
#pragma mark - Accessibility
- (NSString *)accessibilityValue {
NSString *value = [self.text length] ? self.text : self.placeholder;
if (self.leadingUnderlineLabel.text.length > 0) {
[value stringByAppendingFormat:@"%@ %@", [super accessibilityValue],
self.leadingUnderlineLabel.accessibilityLabel];
}
return value;
}
- (BOOL)mdc_adjustsFontForContentSizeCategory {
return _fundament.mdc_adjustsFontForContentSizeCategory;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
// Prior to iOS 9 RTL was not automatically applied, so we don't need to apply any fixes.
if ([self.textView respondsToSelector:@selector(setAdjustsFontForContentSizeCategory:)]) {
[self.textView setAdjustsFontForContentSizeCategory:adjusts];
}
[_fundament mdc_setAdjustsFontForContentSizeCategory:adjusts];
}
@end