blob: 6453aeabba4857158422ef7d57af90d06c30f320 [file] [log] [blame] [edit]
// 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 "MDCChipField.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MaterialMath.h"
#import "MaterialTextFields.h"
NSString *const MDCEmptyTextString = @"";
NSString *const MDCChipDelimiterSpace = @" ";
static const CGFloat MDCChipFieldHorizontalInset = 15;
static const CGFloat MDCChipFieldVerticalInset = 8;
static const CGFloat MDCChipFieldIndent = 4;
static const CGFloat MDCChipFieldHorizontalMargin = 4;
static const CGFloat MDCChipFieldVerticalMargin = 5;
static const CGFloat MDCChipFieldClearButtonSquareWidthHeight = 24;
static const CGFloat MDCChipFieldClearImageSquareWidthHeight = 18;
static const UIKeyboardType MDCChipFieldDefaultKeyboardType = UIKeyboardTypeEmailAddress;
const CGFloat MDCChipFieldDefaultMinTextFieldWidth = 60;
const UIEdgeInsets MDCChipFieldDefaultContentEdgeInsets = {
MDCChipFieldVerticalInset, MDCChipFieldHorizontalInset, MDCChipFieldVerticalInset,
MDCChipFieldHorizontalInset};
@protocol MDCChipFieldTextFieldDelegate <NSObject>
- (void)textFieldShouldRespondToDeleteBackward:(UITextField *)textField;
@end
@interface MDCChipFieldTextField : MDCTextField
@property(nonatomic, weak) id<MDCChipFieldTextFieldDelegate> deletionDelegate;
@end
@implementation MDCChipFieldTextField
- (CGRect)textRectForBounds:(CGRect)bounds {
CGRect textRect = [super textRectForBounds:bounds];
if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
textRect = MDFRectFlippedHorizontally(textRect, CGRectGetWidth(self.bounds));
textRect.origin.x += 5;
}
return textRect;
}
#pragma mark UIKeyInput
- (void)deleteBackward {
[super deleteBackward];
if (self.text.length == 0) {
[self.deletionDelegate textFieldShouldRespondToDeleteBackward:self];
}
}
#if MDC_CHIPFIELD_PRIVATE_API_BUG_FIX && \
!(defined(__IPHONE_8_3) && (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_3))
// WARNING: This is a private method, see the warning in MDCChipField.h.
// This is only compiled if you explicitly defined MDC_CHIPFIELD_PRIVATE_API_BUG_FIX yourself, and
// you are targeting an iOS version less than 8.3.
- (BOOL)keyboardInputShouldDelete:(UITextField *)textField {
BOOL shouldDelete = YES;
if ([UITextField instancesRespondToSelector:_cmd]) {
// clang-format off
BOOL (*keyboardInputShouldDelete)(id, SEL, UITextField *) =
(BOOL(*)(id, SEL, UITextField *))[UITextField instanceMethodForSelector:_cmd];
// clang-format on
if (keyboardInputShouldDelete) {
shouldDelete = keyboardInputShouldDelete(self, _cmd, textField);
NSOperatingSystemVersion minimumVersion = {8, 0, 0};
NSOperatingSystemVersion maximumVersion = {8, 3, 0};
NSProcessInfo *processInfo = [NSProcessInfo processInfo];
BOOL isIos8 = [processInfo isOperatingSystemAtLeastVersion:minimumVersion];
BOOL isLessThanIos8_3 = ![processInfo isOperatingSystemAtLeastVersion:maximumVersion];
if (![textField.text length] && isIos8 && isLessThanIos8_3) {
[self deleteBackward];
}
}
}
return shouldDelete;
}
#endif
#pragma mark - UIAccessibility
- (CGRect)accessibilityFrame {
CGRect frame = [super accessibilityFrame];
return CGRectMake(frame.origin.x + self.textInsets.left, frame.origin.y,
frame.size.width - self.textInsets.left, frame.size.height);
}
@end
@interface MDCChipField () <MDCChipFieldTextFieldDelegate,
MDCTextInputPositioningDelegate,
UITextFieldDelegate>
@end
@implementation MDCChipField {
NSMutableArray<MDCChipView *> *_chips;
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonMDCChipFieldInit];
_chips = [NSMutableArray array];
MDCChipFieldTextField *chipFieldTextField =
[[MDCChipFieldTextField alloc] initWithFrame:self.bounds];
chipFieldTextField.underline.hidden = YES;
chipFieldTextField.delegate = self;
chipFieldTextField.deletionDelegate = self;
chipFieldTextField.positioningDelegate = self;
chipFieldTextField.accessibilityTraits = UIAccessibilityTraitNone;
chipFieldTextField.autocorrectionType = UITextAutocorrectionTypeNo;
chipFieldTextField.autocapitalizationType = UITextAutocapitalizationTypeNone;
chipFieldTextField.keyboardType = MDCChipFieldDefaultKeyboardType;
// Listen for notifications posted when the text field is the first responder.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(textFieldDidChange)
name:UITextFieldTextDidChangeNotification
object:chipFieldTextField];
// Also listen for notifications posted when the text field is not the first responder.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(textFieldDidChange)
name:MDCTextFieldTextDidSetTextNotification
object:chipFieldTextField];
[self addSubview:chipFieldTextField];
_textField = chipFieldTextField;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonMDCChipFieldInit];
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)commonMDCChipFieldInit {
_chips = [NSMutableArray array];
_delimiter = MDCChipFieldDelimiterDefault;
_minTextFieldWidth = MDCChipFieldDefaultMinTextFieldWidth;
_contentEdgeInsets = MDCChipFieldDefaultContentEdgeInsets;
_showPlaceholderWithChips = YES;
_chipHeight = 32;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect standardizedBounds = CGRectStandardize(self.bounds);
BOOL isRTL =
self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
// Calculate the frames for all the chips and set them.
NSArray *chipFrames = [self chipFramesForSize:standardizedBounds.size];
for (NSUInteger index = 0; index < _chips.count; index++) {
MDCChipView *chip = _chips[index];
CGRect chipFrame = [chipFrames[index] CGRectValue];
if (isRTL) {
chipFrame = MDFRectFlippedHorizontally(chipFrame, CGRectGetWidth(self.bounds));
}
chip.frame = chipFrame;
}
// Get the last chip frame and calculate the text field frame from that.
CGRect lastChipFrame = [chipFrames.lastObject CGRectValue];
CGRect textFieldFrame = [self frameForTextFieldForLastChipFrame:lastChipFrame
chipFieldSize:standardizedBounds.size];
if (isRTL) {
textFieldFrame = MDFRectFlippedHorizontally(textFieldFrame, CGRectGetWidth(self.bounds));
}
self.textField.frame = textFieldFrame;
[self updateTextFieldPlaceholderText];
}
- (void)updateTextFieldPlaceholderText {
// Place holder label should be hidden if showPlaceholderWithChips is NO and there are chips.
// MDCTextField sets the placeholderLabel opacity to 0 if the text field has no text.
self.textField.placeholderLabel.hidden =
(!self.showPlaceholderWithChips && self.chips.count > 0) || ![self isTextFieldEmpty];
}
+ (UIFont *)textFieldFont {
return [UIFont systemFontOfSize:[UIFont systemFontSize]];
}
- (CGSize)intrinsicContentSize {
CGFloat minWidth =
MAX(self.minTextFieldWidth + self.contentEdgeInsets.left + self.contentEdgeInsets.right,
CGRectGetWidth(self.bounds));
return [self sizeThatFits:CGSizeMake(minWidth, CGFLOAT_MAX)];
}
- (CGSize)sizeThatFits:(CGSize)size {
NSArray *chipFrames = [self chipFramesForSize:size];
CGRect lastChipFrame = [chipFrames.lastObject CGRectValue];
CGRect textFieldFrame = [self frameForTextFieldForLastChipFrame:lastChipFrame chipFieldSize:size];
// Calculate the required size off the text field.
// To properly apply bottom inset: Calculate what would be the height if there were a chip
// instead of the text field. Then add the bottom inset.
CGFloat height = CGRectGetMaxY(textFieldFrame) + self.contentEdgeInsets.bottom +
(self.chipHeight - textFieldFrame.size.height) / 2;
CGFloat width = MAX(size.width, self.minTextFieldWidth);
return CGSizeMake(width, height);
}
- (void)clearTextInput {
self.textField.text = MDCEmptyTextString;
[self updateTextFieldPlaceholderText];
}
- (void)setChips:(NSArray<MDCChipView *> *)chips {
if ([_chips isEqual:chips]) {
return;
}
for (MDCChipView *chip in _chips) {
[self removeChipSubview:chip];
}
_chips = [chips mutableCopy];
for (MDCChipView *chip in _chips) {
[self addChipSubview:chip];
}
[self notifyDelegateIfSizeNeedsToBeUpdated];
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (NSArray<MDCChipView *> *)chips {
return [NSArray arrayWithArray:_chips];
}
- (BOOL)becomeFirstResponder {
return [self.textField becomeFirstResponder];
}
- (BOOL)resignFirstResponder {
[super resignFirstResponder];
return [self.textField resignFirstResponder];
}
- (void)addChip:(MDCChipView *)chip {
// Note that |chipField:shouldAddChip| is only called in |createNewChipFromInput| when it is
// necessary to restrict chip creation based on input text generated in the user interface.
// Clients calling |addChip| directly programmatically are expected to handle such restrictions
// themselves rather than using |chipField:shouldAddChip| to prevent chips from being added.
[_chips addObject:chip];
[self addChipSubview:chip];
if ([self.delegate respondsToSelector:@selector(chipField:didAddChip:)]) {
[self.delegate chipField:self didAddChip:chip];
}
[self.textField setNeedsLayout];
[self notifyDelegateIfSizeNeedsToBeUpdated];
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)removeChip:(MDCChipView *)chip {
[_chips removeObject:chip];
[self removeChipSubview:chip];
if ([self.delegate respondsToSelector:@selector(chipField:didRemoveChip:)]) {
[self.delegate chipField:self didRemoveChip:chip];
}
[self notifyDelegateIfSizeNeedsToBeUpdated];
[self.textField setNeedsLayout];
[self invalidateIntrinsicContentSize];
[self setNeedsLayout];
}
- (void)removeSelectedChips {
NSMutableArray *chipsToRemove = [NSMutableArray array];
for (MDCChipView *chip in self.chips) {
if (chip.isSelected) {
[chipsToRemove addObject:chip];
}
}
for (MDCChipView *chip in chipsToRemove) {
[self removeChip:chip];
}
}
- (void)selectChip:(MDCChipView *)chip {
[self deselectAllChipsExceptChip:chip];
chip.selected = YES;
}
- (void)selectLastChip {
MDCChipView *lastChip = self.chips.lastObject;
[self deselectAllChipsExceptChip:lastChip];
lastChip.selected = YES;
UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
[lastChip accessibilityLabel]);
}
- (void)deselectAllChips {
[self deselectAllChipsExceptChip:nil];
}
- (void)deselectAllChipsExceptChip:(MDCChipView *)chip {
for (MDCChipView *otherChip in self.chips) {
if (chip != otherChip) {
otherChip.selected = NO;
}
}
}
- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
if (!UIEdgeInsetsEqualToEdgeInsets(_contentEdgeInsets, contentEdgeInsets)) {
_contentEdgeInsets = contentEdgeInsets;
[self setNeedsLayout];
[self invalidateIntrinsicContentSize];
}
}
- (void)setMinTextFieldWidth:(CGFloat)minTextFieldWidth {
if (_minTextFieldWidth != minTextFieldWidth) {
_minTextFieldWidth = minTextFieldWidth;
[self setNeedsLayout];
[self invalidateIntrinsicContentSize];
}
}
- (void)commitInput {
if (![self isTextFieldEmpty]) {
[self createNewChipFromInput];
}
}
- (void)createNewChipFromInput {
NSString *strippedTitle = [self.textField.text
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (strippedTitle.length > 0) {
MDCChipView *chip = [[MDCChipView alloc] init];
chip.titleLabel.text = strippedTitle;
if (self.showChipsDeleteButton) {
[self addClearButtonToChip:chip];
}
BOOL shouldAddChip = YES;
if ([self.delegate respondsToSelector:@selector(chipField:shouldAddChip:)]) {
shouldAddChip = [self.delegate chipField:self shouldAddChip:chip];
}
if (shouldAddChip) {
[self addChip:chip];
[self clearTextInput];
}
} else {
[self clearTextInput];
}
}
- (void)addClearButtonToChip:(MDCChipView *)chip {
UIControl *clearButton = [[UIControl alloc] init];
CGFloat clearButtonWidthAndHeight = MDCChipFieldClearButtonSquareWidthHeight;
clearButton.frame = CGRectMake(0, 0, clearButtonWidthAndHeight, clearButtonWidthAndHeight);
clearButton.layer.cornerRadius = clearButtonWidthAndHeight / 2;
UIImageView *clearImageView = [[UIImageView alloc] initWithImage:[self drawClearButton]];
CGFloat widthAndHeight = MDCChipFieldClearImageSquareWidthHeight;
CGFloat padding =
(MDCChipFieldClearButtonSquareWidthHeight - MDCChipFieldClearImageSquareWidthHeight) / 2;
clearImageView.frame = CGRectMake(padding, padding, widthAndHeight, widthAndHeight);
clearButton.tintColor = [UIColor.blackColor colorWithAlphaComponent:(CGFloat)0.6];
[clearButton addSubview:clearImageView];
chip.accessoryView = clearButton;
[clearButton addTarget:self
action:@selector(deleteChip:)
forControlEvents:UIControlEventTouchUpInside];
}
- (UIImage *)drawClearButton {
CGSize clearButtonSize =
CGSizeMake(MDCChipFieldClearImageSquareWidthHeight, MDCChipFieldClearImageSquareWidthHeight);
CGRect bounds = CGRectMake(0, 0, clearButtonSize.width, clearButtonSize.height);
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0);
[UIColor.grayColor setFill];
[MDCPathForClearButtonImageFrame(bounds) fill];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
return image;
}
static inline UIBezierPath *MDCPathForClearButtonImageFrame(CGRect frame) {
// GENERATED CODE
CGRect innerBounds =
CGRectMake(CGRectGetMinX(frame) + 2, CGRectGetMinY(frame) + 2,
MDCFloor((frame.size.width - 2) * (CGFloat)0.90909 + (CGFloat)0.5),
MDCFloor((frame.size.height - 2) * (CGFloat)0.90909 + (CGFloat)0.5));
UIBezierPath *ic_clear_path = [UIBezierPath bezierPath];
[ic_clear_path moveToPoint:CGPointMake(CGRectGetMinX(innerBounds) +
(CGFloat)0.50000 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 0 * innerBounds.size.height)];
[ic_clear_path
addCurveToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + 1 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.50000 * innerBounds.size.height)
controlPoint1:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.77600 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 0 * innerBounds.size.height)
controlPoint2:CGPointMake(
CGRectGetMinX(innerBounds) + 1 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.22400 * innerBounds.size.height)];
[ic_clear_path
addCurveToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.50000 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 1 * innerBounds.size.height)
controlPoint1:CGPointMake(
CGRectGetMinX(innerBounds) + 1 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.77600 * innerBounds.size.height)
controlPoint2:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.77600 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 1 * innerBounds.size.height)];
[ic_clear_path
addCurveToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + 0 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.50000 * innerBounds.size.height)
controlPoint1:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.22400 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 1 * innerBounds.size.height)
controlPoint2:CGPointMake(
CGRectGetMinX(innerBounds) + 0 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.77600 * innerBounds.size.height)];
[ic_clear_path
addCurveToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.50000 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 0 * innerBounds.size.height)
controlPoint1:CGPointMake(
CGRectGetMinX(innerBounds) + 0 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.22400 * innerBounds.size.height)
controlPoint2:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.22400 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + 0 * innerBounds.size.height)];
[ic_clear_path closePath];
[ic_clear_path
moveToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.73417 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.31467 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.68700 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.26750 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.50083 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.45367 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.31467 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.26750 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.26750 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.31467 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.45367 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.50083 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.26750 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.68700 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.31467 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.73417 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.50083 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.54800 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.68700 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.73417 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.73417 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.68700 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.54800 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.50083 * innerBounds.size.height)];
[ic_clear_path
addLineToPoint:CGPointMake(
CGRectGetMinX(innerBounds) + (CGFloat)0.73417 * innerBounds.size.width,
CGRectGetMinY(innerBounds) + (CGFloat)0.31467 * innerBounds.size.height)];
[ic_clear_path closePath];
return ic_clear_path;
}
- (void)deleteChip:(id)sender {
UIControl *deleteButton = (UIControl *)sender;
MDCChipView *chip = (MDCChipView *)deleteButton.superview;
[self removeChip:chip];
[self clearTextInput];
}
- (void)notifyDelegateIfSizeNeedsToBeUpdated {
if ([self.delegate respondsToSelector:@selector(chipFieldHeightDidChange:)]) {
CGSize currentSize = CGRectStandardize(self.bounds).size;
CGSize requiredSize = [self sizeThatFits:CGSizeMake(currentSize.width, CGFLOAT_MAX)];
if (currentSize.height != requiredSize.height) {
[self.delegate chipFieldHeightDidChange:self];
}
}
}
- (void)chipTapped:(id)sender {
BOOL shouldBecomeFirstResponder = YES;
if ([self.delegate respondsToSelector:@selector(chipFieldShouldBecomeFirstResponder:)]) {
shouldBecomeFirstResponder = [self.delegate chipFieldShouldBecomeFirstResponder:self];
}
if (shouldBecomeFirstResponder) {
[self becomeFirstResponder];
}
MDCChipView *chip = (MDCChipView *)sender;
if ([self.delegate respondsToSelector:@selector(chipField:didTapChip:)]) {
[self.delegate chipField:self didTapChip:chip];
}
}
#pragma mark - MDCChipFieldTextFieldDelegate
- (void)textFieldShouldRespondToDeleteBackward:(UITextField *)textField {
if ([self isAnyChipSelected]) {
[self removeSelectedChips];
[self deselectAllChips];
} else {
[self selectLastChip];
}
}
#pragma mark - UITextFieldDelegate
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
BOOL shouldBeginEditing = YES;
if ([self.delegate respondsToSelector:@selector(chipFieldShouldBeginEditing:)]) {
shouldBeginEditing = [self.delegate chipFieldShouldBeginEditing:self];
}
if (shouldBeginEditing) {
if (textField == self.textField) {
[self deselectAllChips];
}
if ([self.delegate respondsToSelector:@selector(chipFieldDidBeginEditing:)]) {
[self.delegate chipFieldDidBeginEditing:self];
}
}
return shouldBeginEditing;
}
- (BOOL)textFieldShouldEndEditing:(UITextField *)textField {
if ((self.delimiter & MDCChipFieldDelimiterDidEndEditing) == MDCChipFieldDelimiterDidEndEditing) {
if (textField == self.textField) {
[self commitInput];
}
}
if ([self.delegate respondsToSelector:@selector(chipFieldDidEndEditing:)]) {
[self.delegate chipFieldDidEndEditing:self];
}
return YES;
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
BOOL shouldReturn = YES;
// Chip field content view will handle |chipFieldShouldReturn| if the client is not using chip
// field directly. If the client uses chip field directly without the content view and has not
// implemented |chipFieldShouldReturn|, then a chip should always be created.
if ([self.delegate respondsToSelector:@selector(chipFieldShouldReturn:)]) {
shouldReturn = [self.delegate chipFieldShouldReturn:self];
}
if (shouldReturn) {
[self createNewChipWithTextField:textField delimiter:MDCChipFieldDelimiterReturn];
}
return shouldReturn;
}
- (void)textFieldDidChange {
[self deselectAllChips];
[self createNewChipWithTextField:self.textField delimiter:MDCChipFieldDelimiterSpace];
if ([self.delegate respondsToSelector:@selector(chipField:didChangeInput:)]) {
[self.delegate chipField:self didChangeInput:[self.textField.text copy]];
}
}
#pragma mark - Private
- (void)removeChipSubview:(MDCChipView *)chip {
[chip removeFromSuperview];
[chip removeTarget:chip.superview
action:@selector(chipTapped:)
forControlEvents:UIControlEventTouchUpInside];
}
- (void)addChipSubview:(MDCChipView *)chip {
if (chip.superview != self) {
[chip addTarget:self
action:@selector(chipTapped:)
forControlEvents:UIControlEventTouchUpInside];
[self addSubview:chip];
}
}
- (void)createNewChipWithTextField:(UITextField *)textField
delimiter:(MDCChipFieldDelimiter)delimiter {
if ((self.delimiter & delimiter) == delimiter && textField.text.length > 0) {
if (delimiter == MDCChipFieldDelimiterReturn) {
[self createNewChipFromInput];
} else if (delimiter == MDCChipFieldDelimiterSpace) {
NSString *lastChar = [textField.text substringFromIndex:textField.text.length - 1];
if ([lastChar isEqualToString:MDCChipDelimiterSpace]) {
[self createNewChipFromInput];
}
}
}
}
- (BOOL)isAnyChipSelected {
for (MDCChipView *chip in self.chips) {
if (chip.isSelected) {
return YES;
}
}
return NO;
}
- (BOOL)isTextFieldEmpty {
return self.textField.text.length == 0;
}
#pragma mark - Sizing
- (NSArray<NSValue *> *)chipFramesForSize:(CGSize)size {
NSMutableArray *chipFrames = [NSMutableArray arrayWithCapacity:self.chips.count];
CGFloat chipFieldMaxX = size.width - self.contentEdgeInsets.right;
CGFloat maxWidth = size.width - self.contentEdgeInsets.left - self.contentEdgeInsets.right;
NSUInteger row = 0;
CGFloat currentOriginX = self.contentEdgeInsets.left;
for (MDCChipView *chip in self.chips) {
CGSize chipSize = [chip sizeThatFits:CGSizeMake(maxWidth, self.chipHeight)];
chipSize.width = MIN(chipSize.width, maxWidth);
CGFloat availableWidth = chipFieldMaxX - currentOriginX;
// Check if the chip will fit on the current line. If it won't fit and the available width
// is the maximum width, it won't fit on any line. Put it on the current one and move on.
if (chipSize.width > availableWidth &&
availableWidth < (chipFieldMaxX - self.contentEdgeInsets.right)) {
row++;
currentOriginX = self.contentEdgeInsets.left;
}
CGFloat currentOriginY =
self.contentEdgeInsets.top + (row * (self.chipHeight + MDCChipFieldVerticalMargin));
CGRect chipFrame = CGRectMake(currentOriginX, currentOriginY, chipSize.width, chipSize.height);
[chipFrames addObject:[NSValue valueWithCGRect:chipFrame]];
currentOriginX = CGRectGetMaxX(chipFrame) + MDCChipFieldHorizontalMargin;
}
return [chipFrames copy];
}
- (CGRect)frameForTextFieldForLastChipFrame:(CGRect)lastChipFrame
chipFieldSize:(CGSize)chipFieldSize {
CGFloat textFieldWidth =
chipFieldSize.width - self.contentEdgeInsets.left - self.contentEdgeInsets.right;
CGFloat textFieldHeight = [self.textField sizeThatFits:chipFieldSize].height;
CGFloat originY = lastChipFrame.origin.y + (self.chipHeight - textFieldHeight) / 2;
// If no chip exists, make the text field the full width minus padding.
if (CGRectIsEmpty(lastChipFrame)) {
// Adjust for the top inset
originY += self.contentEdgeInsets.top;
return CGRectMake(self.contentEdgeInsets.left, originY, textFieldWidth, textFieldHeight);
}
CGFloat availableWidth = chipFieldSize.width - self.contentEdgeInsets.right -
CGRectGetMaxX(lastChipFrame) - MDCChipFieldHorizontalMargin;
CGFloat placeholderDesiredWidth = [self placeholderDesiredWidth];
if (availableWidth < placeholderDesiredWidth) {
// The text field doesn't fit on the line with the last chip.
originY += self.chipHeight + MDCChipFieldVerticalMargin;
return CGRectMake(self.contentEdgeInsets.left, originY, textFieldWidth, textFieldHeight);
}
return CGRectMake(self.contentEdgeInsets.left, originY, textFieldWidth, textFieldHeight);
}
- (CGFloat)placeholderDesiredWidth {
NSString *placeholder = self.textField.placeholder;
if (!self.showPlaceholderWithChips && self.chips.count > 0) {
placeholder = nil;
}
UIFont *placeholderFont = self.textField.placeholderLabel.font;
CGRect placeholderDesiredRect =
[placeholder boundingRectWithSize:CGRectStandardize(self.bounds).size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{
NSFontAttributeName : placeholderFont,
}
context:nil];
return MAX(CGRectGetWidth(placeholderDesiredRect), self.minTextFieldWidth);
}
#pragma mark - MDCTextInputPositioningDelegate
- (UIEdgeInsets)textInsets:(UIEdgeInsets)defaultInsets
withSizeThatFitsWidthHint:(CGFloat)widthHint {
CGRect lastChipFrame = self.chips.lastObject.frame;
if (self.mdf_effectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft) {
lastChipFrame = MDFRectFlippedHorizontally(lastChipFrame, CGRectGetWidth(self.bounds));
}
CGFloat availableWidth = CGRectGetWidth(self.bounds) - self.contentEdgeInsets.right -
CGRectGetMaxX(lastChipFrame) - MDCChipFieldHorizontalMargin;
CGFloat leftInset = MDCChipFieldIndent;
if (!CGRectIsEmpty(lastChipFrame) && availableWidth >= [self placeholderDesiredWidth]) {
leftInset +=
CGRectGetMaxX(lastChipFrame) + MDCChipFieldHorizontalMargin - self.contentEdgeInsets.left;
}
defaultInsets.left = leftInset;
return defaultInsets;
}
#pragma mark - UIAccessibilityContainer
- (BOOL)isAccessibilityElement {
return NO;
}
- (id)accessibilityElementAtIndex:(NSInteger)index {
if (index < (NSInteger)self.chips.count) {
return self.chips[index];
} else if (index == (NSInteger)self.chips.count) {
return self.textField;
}
return nil;
}
- (NSInteger)accessibilityElementCount {
return self.chips.count + 1;
}
- (NSInteger)indexOfAccessibilityElement:(id)element {
if (element == self.textField) {
return self.chips.count;
}
return [self.chips indexOfObject:element];
}
#pragma mark - Accessibility
- (void)focusTextFieldForAccessibility {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, self.textField);
}
@end