diff --git a/JSTokenField/JSTokenButton.h b/JSTokenField/JSTokenButton.h index 1223204..a219026 100644 --- a/JSTokenField/JSTokenButton.h +++ b/JSTokenField/JSTokenButton.h @@ -27,28 +27,26 @@ // #import + @class JSTokenField; -@interface JSTokenButton : UIButton { +@protocol JSTokenButtonCustomView +@optional +- (void)buttonStateChanged:(BOOL)selected; +@end - BOOL _toggled; - - UIImage *_normalBg; - UIImage *_highlightedBg; - +@interface JSTokenButton : UIButton { id _representedObject; - + id _value; } -@property (nonatomic, getter=isToggled) BOOL toggled; - -@property (nonatomic, retain) UIImage *normalBg; -@property (nonatomic, retain) UIImage *highlightedBg; - @property (nonatomic, retain) id representedObject; +@property (nonatomic, retain) id value; +@property (nonatomic, retain) UIView *customView; @property (nonatomic, assign) JSTokenField *parentField; + (JSTokenButton *)tokenWithString:(NSString *)string representedObject:(id)obj; ++ (JSTokenButton *)tokenWithView:(UIView *)view representedObject:(id)obj; @end diff --git a/JSTokenField/JSTokenButton.m b/JSTokenField/JSTokenButton.m index 783188a..3a115f0 100644 --- a/JSTokenField/JSTokenButton.m +++ b/JSTokenField/JSTokenButton.m @@ -32,21 +32,24 @@ @implementation JSTokenButton -@synthesize toggled = _toggled; -@synthesize normalBg = _normalBg; -@synthesize highlightedBg = _highlightedBg; +@synthesize value = _value; @synthesize representedObject = _representedObject; @synthesize parentField = _parentField; +@synthesize customView = _customView; -+ (JSTokenButton *)tokenWithString:(NSString *)string representedObject:(id)obj -{ + ++ (JSTokenButton *)tokenWithString:(NSString *)string representedObject:(id)obj { JSTokenButton *button = (JSTokenButton *)[self buttonWithType:UIButtonTypeCustom]; - [button setNormalBg:[[UIImage imageNamed:@"tokenNormal.png"] stretchableImageWithLeftCapWidth:14 topCapHeight:0]]; - [button setHighlightedBg:[[UIImage imageNamed:@"tokenHighlighted.png"] stretchableImageWithLeftCapWidth:14 topCapHeight:0]]; + + [button setBackgroundImage:[[UIImage imageNamed:@"tokenNormal.png"] stretchableImageWithLeftCapWidth:14 topCapHeight:0] + forState:UIControlStateNormal]; + [button setBackgroundImage:[[UIImage imageNamed:@"tokenHighlighted.png"] stretchableImageWithLeftCapWidth:14 topCapHeight:0] + forState:UIControlStateSelected]; + [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; + [button setTitleColor:[UIColor whiteColor] forState:UIControlStateSelected]; [button setAdjustsImageWhenHighlighted:NO]; - [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; - [[button titleLabel] setFont:[UIFont fontWithName:@"Helvetica Neue" size:15]]; - [[button titleLabel] setLineBreakMode:UILineBreakModeTailTruncation]; + [[button titleLabel] setFont:[UIFont systemFontOfSize:15]]; + [[button titleLabel] setLineBreakMode:NSLineBreakByTruncatingTail]; [button setTitleEdgeInsets:UIEdgeInsetsMake(2, 10, 0, 10)]; [button setTitle:string forState:UIControlStateNormal]; @@ -57,69 +60,76 @@ + (JSTokenButton *)tokenWithString:(NSString *)string representedObject:(id)obj frame.size.height = 25; [button setFrame:frame]; - [button setToggled:NO]; - + [button setValue:string]; [button setRepresentedObject:obj]; return button; } -- (void)setToggled:(BOOL)toggled -{ - _toggled = toggled; + ++ (JSTokenButton *)tokenWithView:(UIView *)view representedObject:(id)obj { + JSTokenButton *button = (JSTokenButton *)[self buttonWithType:UIButtonTypeCustom]; + button.frame = CGRectMake(0.f, 0.f, view.frame.size.width, view.frame.size.height); + button.customView = view; + [button addSubview:view]; + [button setValue:view]; + [button setRepresentedObject:obj]; - if (_toggled) - { - [self setBackgroundImage:self.highlightedBg forState:UIControlStateNormal]; - [self setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; - } - else - { - [self setBackgroundImage:self.normalBg forState:UIControlStateNormal]; - [self setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; - } + return button; } -- (void)dealloc -{ - self.representedObject = nil; - self.highlightedBg = nil; - self.normalBg = nil; - [super dealloc]; + +- (void)dealloc { + } + - (BOOL)becomeFirstResponder { BOOL superReturn = [super becomeFirstResponder]; if (superReturn) { - self.toggled = YES; + self.selected = YES; + if ([_customView respondsToSelector:@selector(buttonStateChanged:)]) { + [_customView buttonStateChanged:self.selected]; + } } return superReturn; } + - (BOOL)resignFirstResponder { BOOL superReturn = [super resignFirstResponder]; if (superReturn) { - self.toggled = NO; + self.selected = NO; + if ([_customView respondsToSelector:@selector(buttonStateChanged:)]) { + [_customView buttonStateChanged:self.selected]; + } } return superReturn; } + #pragma mark - UIKeyInput + + - (void)deleteBackward { + id delegate = _parentField.delegate; if ([delegate respondsToSelector:@selector(tokenField:shouldRemoveToken:representedObject:)]) { - NSString *name = [self titleForState:UIControlStateNormal]; - BOOL shouldRemove = [delegate tokenField:_parentField shouldRemoveToken:name representedObject:self.representedObject]; + BOOL shouldRemove = [delegate tokenField:_parentField shouldRemoveToken:self.value representedObject:self.representedObject]; if (!shouldRemove) { return; } } - [_parentField removeTokenForString:[self titleForState:UIControlStateNormal]]; + + [_parentField removeTokenWithRepresentedObject:self.representedObject]; } + - (BOOL)hasText { return NO; } + + - (void)insertText:(NSString *)text { return; } diff --git a/JSTokenField/JSTokenField.h b/JSTokenField/JSTokenField.h index 2aa3a83..58d796b 100644 --- a/JSTokenField/JSTokenField.h +++ b/JSTokenField/JSTokenField.h @@ -29,7 +29,7 @@ #import @class JSTokenButton; -@protocol JSTokenFieldDelegate; +@protocol JSTokenFieldDelegate, JSTokenButtonCustomView; extern NSString *const JSTokenFieldFrameDidChangeNotification; extern NSString *const JSTokenFieldNewFrameKey; @@ -38,23 +38,17 @@ extern NSString *const JSDeletedTokenKey; @interface JSTokenField : UIView { - NSMutableArray *_tokens; - - UITextField *_textField; - - id _delegate; - JSTokenButton *_deletedToken; - - UILabel *_label; } @property (nonatomic, readonly) UITextField *textField; -@property (nonatomic, retain) UILabel *label; -@property (nonatomic, readonly, copy) NSMutableArray *tokens; -@property (nonatomic, assign) id delegate; +@property (nonatomic) UILabel *label; +@property (nonatomic, readonly) NSMutableArray *tokens; +@property (nonatomic, weak) id delegate; +@property (nonatomic) BOOL editing; - (void)addTokenWithTitle:(NSString *)string representedObject:(id)obj; +- (void)addTokenWithView:(UIView *)view representedObject:(id)obj; - (void)removeTokenForString:(NSString *)string; - (void)removeTokenWithRepresentedObject:(id)representedObject; - (void)removeAllTokens; @@ -65,14 +59,17 @@ extern NSString *const JSDeletedTokenKey; @optional -- (void)tokenField:(JSTokenField *)tokenField didAddToken:(NSString *)title representedObject:(id)obj; -- (void)tokenField:(JSTokenField *)tokenField didRemoveToken:(NSString *)title representedObject:(id)obj; -- (BOOL)tokenField:(JSTokenField *)tokenField shouldRemoveToken:(NSString *)title representedObject:(id)obj; - +- (void)tokenField:(JSTokenField *)tokenField didAddToken:(id)value representedObject:(id)obj; +- (void)tokenField:(JSTokenField *)tokenField didRemoveToken:(id)value representedObject:(id)obj; +- (BOOL)tokenField:(JSTokenField *)tokenField shouldRemoveToken:(id)value representedObject:(id)obj; - (void)tokenFieldTextDidChange:(JSTokenField *)tokenField; - (BOOL)tokenFieldShouldReturn:(JSTokenField *)tokenField; - (void)tokenFieldDidEndEditing:(JSTokenField *)tokenField; +- (void)tokenFieldDidBeginEditing:(JSTokenField *)tokenField; + +- (void)didSelectTokenButton:(JSTokenButton *)tokenButton; +- (void)didUnselectTokenButton:(JSTokenButton *)tokenButton; @end diff --git a/JSTokenField/JSTokenField.m b/JSTokenField/JSTokenField.m index f607c29..9593a7b 100644 --- a/JSTokenField/JSTokenField.m +++ b/JSTokenField/JSTokenField.m @@ -52,10 +52,6 @@ - (void)commonSetup; @implementation JSTokenField -@synthesize tokens = _tokens; -@synthesize textField = _textField; -@synthesize label = _label; -@synthesize delegate = _delegate; - (id)initWithFrame:(CGRect)frame { @@ -87,13 +83,10 @@ - (void)commonSetup { _label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 0, frame.size.height)]; [_label setBackgroundColor:[UIColor clearColor]]; [_label setTextColor:[UIColor colorWithRed:0.3 green:0.3 blue:0.3 alpha:1.0]]; - [_label setFont:[UIFont fontWithName:@"Helvetica Neue" size:17.0]]; + [_label setFont:[UIFont systemFontOfSize:17.0]]; [self addSubview:_label]; - // self.layer.borderColor = [[UIColor blueColor] CGColor]; - // self.layer.borderWidth = 1.0; - _tokens = [[NSMutableArray alloc] init]; frame.origin.y += HEIGHT_PADDING; @@ -104,25 +97,12 @@ - (void)commonSetup { [_textField setBackground:nil]; [_textField setBackgroundColor:[UIColor clearColor]]; [_textField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter]; - - // [_textField.layer setBorderColor:[[UIColor redColor] CGColor]]; - // [_textField.layer setBorderWidth:1.0]; - + [self addSubview:_textField]; [self.textField addTarget:self action:@selector(textFieldWasUpdated:) forControlEvents:UIControlEventEditingChanged]; } -- (void)dealloc -{ - [_textField release], _textField = nil; - [_label release], _label = nil; - [_tokens release], _tokens = nil; - - [super dealloc]; -} - - - (void)addTokenWithTitle:(NSString *)string representedObject:(id)obj { NSString *aString = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; @@ -142,6 +122,24 @@ - (void)addTokenWithTitle:(NSString *)string representedObject:(id)obj } } + +- (void)addTokenWithView:(UIView *)view representedObject:(id)obj +{ + if (view) + { + JSTokenButton *token = [self tokenWithView:view representedObject:obj]; + token.parentField = self; + [_tokens addObject:token]; + + if ([self.delegate respondsToSelector:@selector(tokenField:didAddToken:representedObject:)]) + { + [self.delegate tokenField:self didAddToken:view representedObject:obj]; + } + + [self setNeedsLayout]; + } +} + - (void)removeTokenWithTest:(BOOL (^)(JSTokenButton *token))test { JSTokenButton *tokenToRemove = nil; for (JSTokenButton *token in [_tokens reverseObjectEnumerator]) { @@ -156,7 +154,6 @@ - (void)removeTokenWithTest:(BOOL (^)(JSTokenButton *token))test { [_textField becomeFirstResponder]; } [tokenToRemove removeFromSuperview]; - [[tokenToRemove retain] autorelease]; // removing it from the array will dealloc the object, but we want to keep it around for the delegate method below [_tokens removeObject:tokenToRemove]; if ([self.delegate respondsToSelector:@selector(tokenField:didRemoveToken:representedObject:)]) @@ -190,15 +187,14 @@ - (void)removeAllTokens { return token == button; }]; } - [tokensCopy release]; } - (void)deleteHighlightedToken { for (int i = 0; i < [_tokens count]; i++) { - _deletedToken = [[_tokens objectAtIndex:i] retain]; - if ([_deletedToken isToggled]) + _deletedToken = [_tokens objectAtIndex:i]; + if (_deletedToken.selected) { NSString *tokenName = [_deletedToken titleForState:UIControlStateNormal]; if ([self.delegate respondsToSelector:@selector(tokenField:shouldRemoveToken:representedObject:)]) { @@ -213,7 +209,7 @@ - (void)deleteHighlightedToken [_deletedToken removeFromSuperview]; [_tokens removeObject:_deletedToken]; - if ([self.delegate respondsToSelector:@selector(tokenField:didRemove:representedObject:)]) + if ([self.delegate respondsToSelector:@selector(tokenField:didRemoveToken:representedObject:)]) { [self.delegate tokenField:self didRemoveToken:tokenName representedObject:_deletedToken.representedObject]; } @@ -242,6 +238,25 @@ - (JSTokenButton *)tokenWithString:(NSString *)string representedObject:(id)obj return token; } +- (JSTokenButton *)tokenWithView:(UIView *)view representedObject:(id)obj +{ + JSTokenButton *token = [JSTokenButton tokenWithView:view representedObject:obj]; + CGRect frame = [token frame]; + + if (frame.size.width > self.frame.size.width) + { + frame.size.width = self.frame.size.width - (WIDTH_PADDING * 2); + } + + [token setFrame:frame]; + + [token addTarget:self + action:@selector(toggle:) + forControlEvents:UIControlEventTouchUpInside]; + + return token; +} + - (void)layoutSubviews { CGRect currentRect = CGRectZero; @@ -271,10 +286,6 @@ - (void)layoutSubviews [token setFrame:frame]; - if (![token superview]) - { - [self addSubview:token]; - } [lastLineTokens addObject:token]; currentRect.origin.x += frame.size.width + WIDTH_PADDING; @@ -285,7 +296,7 @@ - (void)layoutSubviews textFieldFrame.origin = currentRect.origin; - if ((self.frame.size.width - textFieldFrame.origin.x) >= 60) + if ((self.frame.size.width - textFieldFrame.origin.x) >= ((self.editing)? 60 : 0)) { textFieldFrame.size.width = self.frame.size.width - textFieldFrame.origin.x; } @@ -312,47 +323,98 @@ - (void)layoutSubviews if (self.layer.presentationLayer == nil) { [self setFrame:selfFrame]; + [self addMissingTokensAsSubview:_tokens]; } else { - [UIView animateWithDuration:0.3 - animations:^{ - [self setFrame:selfFrame]; - } - completion:nil]; + [UIView animateWithDuration:0.3 + animations:^{ + [self setFrame:selfFrame]; + } + completion:^(BOOL finished) { + [self addMissingTokensAsSubview:_tokens]; + }]; } } + +- (void)addMissingTokensAsSubview:(NSArray *)tokens +{ + for (UIButton *token in _tokens) { + if (![token superview]) + { + [self addSubview:token]; + } + } +} + + - (void)toggle:(id)sender { for (JSTokenButton *token in _tokens) { - [token setToggled:NO]; + if ([token isFirstResponder]) { + if ([self.delegate respondsToSelector:@selector(didUnselectTokenButton:)]) { + [self.delegate didUnselectTokenButton:token]; + } + } + token.selected = NO; } JSTokenButton *token = (JSTokenButton *)sender; - [token setToggled:YES]; + token.selected = YES; [token becomeFirstResponder]; + + if ([self.delegate respondsToSelector:@selector(didSelectTokenButton:)]) { + [self.delegate didSelectTokenButton:token]; + } } - (void)setFrame:(CGRect)frame { CGRect oldFrame = self.frame; + + [super setFrame:frame]; - [super setFrame:frame]; - - NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSValue valueWithCGRect:frame] forKey:JSTokenFieldNewFrameKey]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:[NSValue valueWithCGRect:frame] forKey:JSTokenFieldNewFrameKey]; [userInfo setObject:[NSValue valueWithCGRect:oldFrame] forKey:JSTokenFieldOldFrameKey]; if (_deletedToken) { - [userInfo setObject:_deletedToken forKey:JSDeletedTokenKey]; - [_deletedToken release], _deletedToken = nil; + [userInfo setObject:_deletedToken forKey:JSDeletedTokenKey]; } if (CGRectEqualToRect(oldFrame, frame) == NO) { - [[NSNotificationCenter defaultCenter] postNotificationName:JSTokenFieldFrameDidChangeNotification object:self userInfo:[[userInfo copy] autorelease]]; + [[NSNotificationCenter defaultCenter] postNotificationName:JSTokenFieldFrameDidChangeNotification object:self userInfo:[userInfo copy]]; } } + +- (BOOL)resignFirstResponder +{ + BOOL ourReturnValue = [super resignFirstResponder]; + + if ([_textField isFirstResponder]) { + return [_textField resignFirstResponder]; + }; + + for (JSTokenButton *token in _tokens) + { + if ([token isFirstResponder]) + { + token.selected = NO; + return [token resignFirstResponder]; + } + } + + return ourReturnValue; +} + + +- (void)setEditing:(BOOL)editing { + _editing = editing; + [self setNeedsLayout]; +} + + #pragma mark - #pragma mark UITextFieldDelegate @@ -372,11 +434,13 @@ - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRang return NO; } - NSString *name = [token titleForState:UIControlStateNormal]; // If we don't allow deleting the token, don't even bother letting it highlight BOOL responds = [self.delegate respondsToSelector:@selector(tokenField:shouldRemoveToken:representedObject:)]; - if (responds == NO || [self.delegate tokenField:self shouldRemoveToken:name representedObject:token.representedObject]) { + if (responds == NO || [self.delegate tokenField:self shouldRemoveToken:token.value representedObject:token.representedObject]) { [token becomeFirstResponder]; + if ([self.delegate respondsToSelector:@selector(didSelectTokenButton:)]) { + [self.delegate didSelectTokenButton:token]; + } } return NO; } @@ -397,9 +461,9 @@ - (BOOL)textFieldShouldReturn:(UITextField *)textField - (void)textFieldDidEndEditing:(UITextField *)textField { + self.editing = NO; if ([self.delegate respondsToSelector:@selector(tokenFieldDidEndEditing:)]) { [self.delegate tokenFieldDidEndEditing:self]; - return; } else if ([[textField text] length] > 1) { @@ -408,4 +472,12 @@ - (void)textFieldDidEndEditing:(UITextField *)textField } } +- (void)textFieldDidBeginEditing:(UITextField *)textField +{ + self.editing = YES; + if ([self.delegate respondsToSelector:@selector(tokenFieldDidBeginEditing:)]) { + [self.delegate tokenFieldDidBeginEditing:self]; + } +} + @end