123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700 |
- //
- // HPTextView.m
- //
- // Created by Hans Pinckaers on 29-06-10.
- //
- // MIT License
- //
- // Copyright (c) 2011 Hans Pinckaers
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- #import "HPGrowingTextView.h"
- #import "HPTextViewInternal.h"
- @interface HPGrowingTextView(private)
- -(void)commonInitialiser;
- -(void)resizeTextView:(NSInteger)newSizeH;
- -(void)growDidStop;
- @end
- @implementation HPGrowingTextView
- @synthesize internalTextView;
- @synthesize delegate;
- @synthesize maxHeight;
- @synthesize minHeight;
- @synthesize font;
- @synthesize textColor;
- @synthesize textAlignment;
- @synthesize selectedRange;
- @synthesize editable;
- @synthesize dataDetectorTypes;
- @synthesize animateHeightChange;
- @synthesize animationDuration;
- @synthesize returnKeyType;
- @dynamic placeholder;
- @dynamic placeholderColor;
- // having initwithcoder allows us to use HPGrowingTextView in a Nib. -- aob, 9/2011
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder])) {
- [self commonInitialiser];
- }
- return self;
- }
- - (id)initWithFrame:(CGRect)frame {
- if ((self = [super initWithFrame:frame])) {
- [self commonInitialiser];
- }
- return self;
- }
- #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000
- - (id)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer {
- if ((self = [super initWithFrame:frame])) {
- [self commonInitialiser:textContainer];
- }
- return self;
- }
- -(void)commonInitialiser {
- [self commonInitialiser:nil];
- }
- -(void)commonInitialiser:(NSTextContainer *)textContainer
- #else
- -(void)commonInitialiser
- #endif
- {
- // Initialization code
- CGRect r = self.frame;
- r.origin.y = 0;
- r.origin.x = 0;
- #if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000
- internalTextView = [[HPTextViewInternal alloc] initWithFrame:r textContainer:textContainer];
- #else
- internalTextView = [[HPTextViewInternal alloc] initWithFrame:r];
- #endif
- internalTextView.delegate = self;
- internalTextView.scrollEnabled = NO;
- internalTextView.font = [UIFont fontWithName:@"Helvetica" size:13];
- internalTextView.contentInset = UIEdgeInsetsZero;
- internalTextView.showsHorizontalScrollIndicator = NO;
- internalTextView.text = @"-";
- internalTextView.contentMode = UIViewContentModeRedraw;
-
- if (@available(iOS 11.0, *)) {
- internalTextView.smartDashesType = UITextSmartDashesTypeNo;
- internalTextView.smartQuotesType = UITextSmartDashesTypeNo;
- }
-
- [self addSubview:internalTextView];
-
- minHeight = internalTextView.frame.size.height;
- minNumberOfLines = 1;
-
- animateHeightChange = YES;
- animationDuration = 0.1f;
-
- internalTextView.text = @"";
-
- [self setMaxNumberOfLines:3];
-
- [self setPlaceholderColor:[UIColor lightGrayColor]];
- internalTextView.displayPlaceHolder = YES;
-
- /* make a dummy text view with the same font for use in height calculations */
- dummyTextView = [[UITextView alloc] init];
- dummyTextView.scrollEnabled = NO;
- dummyTextView.font = internalTextView.font;
- dummyTextView.contentInset = UIEdgeInsetsZero;
- dummyTextView.showsHorizontalScrollIndicator = NO;
-
- if (@available(iOS 11.0, *)) {
- dummyTextView.smartDashesType = UITextSmartDashesTypeNo;
- dummyTextView.smartQuotesType = UITextSmartDashesTypeNo;
- }
- }
- -(CGSize)sizeThatFits:(CGSize)size
- {
- if (self.text.length == 0) {
- size.height = minHeight;
- }
- return size;
- }
- -(void)layoutSubviews
- {
- [super layoutSubviews];
-
- CGRect r = self.bounds;
- r.origin.y = 0;
- r.origin.x = contentInset.left;
- r.size.width -= contentInset.left + contentInset.right;
-
- internalTextView.frame = r;
- }
- -(void)setContentInset:(UIEdgeInsets)inset
- {
- contentInset = inset;
-
- CGRect r = self.frame;
- r.origin.y = inset.top - inset.bottom;
- r.origin.x = inset.left;
- r.size.width -= inset.left + inset.right;
-
- internalTextView.frame = r;
-
- [self setMaxNumberOfLines:maxNumberOfLines];
- [self setMinNumberOfLines:minNumberOfLines];
- }
- -(UIEdgeInsets)contentInset
- {
- return contentInset;
- }
- -(void)setMaxNumberOfLines:(int)n
- {
- if(n == 0 && maxHeight > 0) return; // the user specified a maxHeight themselves.
-
- // Use internalTextView for height calculations, thanks to Gwynne <http://blog.darkrainfall.org/>
- NSString *saveText = internalTextView.text, *newText = @"-";
-
- internalTextView.delegate = nil;
- internalTextView.hidden = YES;
-
- for (int i = 1; i < n; ++i)
- newText = [newText stringByAppendingString:@"\n|W|"];
-
- internalTextView.text = newText;
-
- maxHeight = ceilf([self measureHeight]);
-
- internalTextView.text = saveText;
- internalTextView.hidden = NO;
- internalTextView.delegate = self;
-
- [self sizeToFit];
-
- maxNumberOfLines = n;
- }
- -(int)maxNumberOfLines
- {
- return maxNumberOfLines;
- }
- - (void)setMaxHeight:(int)height
- {
- maxHeight = height;
- maxNumberOfLines = 0;
- }
- -(void)setMinNumberOfLines:(int)m
- {
- if(m == 0 && minHeight > 0) return; // the user specified a minHeight themselves.
-
- // Use internalTextView for height calculations, thanks to Gwynne <http://blog.darkrainfall.org/>
- NSString *saveText = internalTextView.text, *newText = @"-";
-
- internalTextView.delegate = nil;
- internalTextView.hidden = YES;
-
- for (int i = 1; i < m; ++i)
- newText = [newText stringByAppendingString:@"\n|W|"];
-
- internalTextView.text = newText;
-
- minHeight = ceilf([self measureHeight]);
-
- internalTextView.text = saveText;
- internalTextView.hidden = NO;
- internalTextView.delegate = self;
-
- [self sizeToFit];
-
- minNumberOfLines = m;
- }
- -(int)minNumberOfLines
- {
- return minNumberOfLines;
- }
- - (void)setMinHeight:(int)height
- {
- minHeight = height;
- minNumberOfLines = 0;
- }
- - (NSString *)placeholder
- {
- return internalTextView.placeholder;
- }
- - (void)setPlaceholder:(NSString *)placeholder
- {
- [internalTextView setPlaceholder:placeholder];
- [internalTextView setNeedsDisplay];
- }
- - (UIColor *)placeholderColor
- {
- return internalTextView.placeholderColor;
- }
- - (void)setPlaceholderColor:(UIColor *)placeholderColor
- {
- [internalTextView setPlaceholderColor:placeholderColor];
- }
- - (void)textViewDidChange:(UITextView *)textView
- {
- [self refreshHeight];
- }
- - (void)refreshHeight {
- [self refreshHeightForce:NO];
- }
- - (void)refreshHeightForce:(BOOL)force
- {
- //size of content, so we can set the frame of self
- NSInteger newSizeH = ceilf([self measureHeight]);
- if (newSizeH < minHeight || !internalTextView.hasText) {
- newSizeH = minHeight; //not smalles than minHeight
- }
- else if (maxHeight && newSizeH > maxHeight) {
- newSizeH = maxHeight; // not taller than maxHeight
- }
-
- if (force || internalTextView.frame.size.height != newSizeH)
- {
- // if our new height is greater than the maxHeight
- // sets not set the height or move things
- // around and enable scrolling
- if (newSizeH >= maxHeight)
- {
- if(!internalTextView.scrollEnabled){
- internalTextView.scrollEnabled = YES;
- [internalTextView flashScrollIndicators];
- }
-
- } else {
- internalTextView.scrollEnabled = NO;
- }
-
- // [fixed] Pasting too much text into the view failed to fire the height change,
- // thanks to Gwynne <http://blog.darkrainfall.org/>
- if (newSizeH <= maxHeight)
- {
- if(animateHeightChange) {
-
- if ([UIView resolveClassMethod:@selector(animateWithDuration:animations:)]) {
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 40000
- [UIView animateWithDuration:animationDuration
- delay:0
- options:(UIViewAnimationOptionAllowUserInteraction|
- UIViewAnimationOptionBeginFromCurrentState)
- animations:^(void) {
- [self resizeTextView:newSizeH];
- }
- completion:^(BOOL finished) {
- if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) {
- [delegate growingTextView:self didChangeHeight:newSizeH];
- }
- }];
- #endif
- } else {
- [UIView beginAnimations:@"" context:nil];
- [UIView setAnimationDuration:animationDuration];
- [UIView setAnimationDelegate:self];
- [UIView setAnimationDidStopSelector:@selector(growDidStop)];
- [UIView setAnimationBeginsFromCurrentState:YES];
- [self resizeTextView:newSizeH];
- [UIView commitAnimations];
- }
- } else {
- [self resizeTextView:newSizeH];
- // [fixed] The growingTextView:didChangeHeight: delegate method was not called at all when not animating height changes.
- // thanks to Gwynne <http://blog.darkrainfall.org/>
-
- if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) {
- [delegate growingTextView:self didChangeHeight:newSizeH];
- }
- }
- }
- }
- // Display (or not) the placeholder string
-
- BOOL wasDisplayingPlaceholder = internalTextView.displayPlaceHolder;
- internalTextView.displayPlaceHolder = self.internalTextView.text.length == 0;
-
- if (wasDisplayingPlaceholder != internalTextView.displayPlaceHolder) {
- [internalTextView setNeedsDisplay];
- }
-
- // Tell the delegate that the text view changed
- if ([delegate respondsToSelector:@selector(growingTextViewDidChange:)]) {
- [delegate growingTextViewDidChange:self];
- }
- }
- - (CGFloat)measureHeight
- {
- if ([self respondsToSelector:@selector(snapshotViewAfterScreenUpdates:)])
- {
- if (internalTextView.attributedText && internalTextView.attributedText.length > 0)
- dummyTextView.attributedText = internalTextView.attributedText;
- else
- dummyTextView.text = internalTextView.text;
-
- CGSize size = [dummyTextView sizeThatFits:CGSizeMake(internalTextView.bounds.size.width, CGFLOAT_MAX)];
- return size.height;
- }
- else
- {
- return self.internalTextView.contentSize.height;
- }
- }
- -(void)resizeTextView:(NSInteger)newSizeH
- {
- if ([delegate respondsToSelector:@selector(growingTextView:willChangeHeight:)]) {
- [delegate growingTextView:self willChangeHeight:newSizeH];
- }
-
- CGRect internalTextViewFrame = self.frame;
- internalTextViewFrame.size.height = newSizeH; // + padding
- self.frame = internalTextViewFrame;
-
- internalTextViewFrame.origin.y = contentInset.top - contentInset.bottom;
- internalTextViewFrame.origin.x = contentInset.left;
- if(!CGRectEqualToRect(internalTextView.frame, internalTextViewFrame)) internalTextView.frame = internalTextViewFrame;
- }
- - (void)growDidStop
- {
- if ([delegate respondsToSelector:@selector(growingTextView:didChangeHeight:)]) {
- [delegate growingTextView:self didChangeHeight:self.frame.size.height];
- }
- }
- -(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
- {
- [internalTextView becomeFirstResponder];
- }
- - (BOOL)becomeFirstResponder
- {
- [super becomeFirstResponder];
- return [self.internalTextView becomeFirstResponder];
- }
- -(BOOL)resignFirstResponder
- {
- [super resignFirstResponder];
- return [internalTextView resignFirstResponder];
- }
- -(BOOL)isFirstResponder
- {
- return [self.internalTextView isFirstResponder];
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark UITextView properties
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setText:(NSString *)newText
- {
- internalTextView.text = newText;
- // include this line to analyze the height of the textview.
- // fix from Ankit Thakur
- [self performSelector:@selector(textViewDidChange:) withObject:internalTextView];
- }
- -(NSString*) text
- {
- return internalTextView.text;
- }
- -(void)setAttributedText:(NSAttributedString *)attributedText
- {
- internalTextView.attributedText = attributedText;
- internalTextView.typingAttributes = @{NSForegroundColorAttributeName:[UIColor blackColor], NSFontAttributeName:[UIFont systemFontOfSize:16]};
-
- // include this line to analyze the height of the textview.
- // fix from Ankit Thakur
- [self performSelector:@selector(textViewDidChange:) withObject:internalTextView];
- }
- -(NSAttributedString*)attributedText
- {
- return internalTextView.attributedText;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setFont:(UIFont *)afont
- {
- internalTextView.font = afont;
- dummyTextView.font = afont;
-
- [self setMaxNumberOfLines:maxNumberOfLines];
- [self setMinNumberOfLines:minNumberOfLines];
- }
- -(UIFont *)font
- {
- return internalTextView.font;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setTextColor:(UIColor *)color
- {
- internalTextView.textColor = color;
- }
- -(UIColor*)textColor{
- return internalTextView.textColor;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setBackgroundColor:(UIColor *)backgroundColor
- {
- [super setBackgroundColor:backgroundColor];
- internalTextView.backgroundColor = backgroundColor;
- }
- -(UIColor*)backgroundColor
- {
- return internalTextView.backgroundColor;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setTextAlignment:(NSTextAlignment)aligment
- {
- internalTextView.textAlignment = aligment;
- }
- -(NSTextAlignment)textAlignment
- {
- return internalTextView.textAlignment;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setSelectedRange:(NSRange)range
- {
- internalTextView.selectedRange = range;
- }
- -(NSRange)selectedRange
- {
- return internalTextView.selectedRange;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setIsScrollable:(BOOL)isScrollable
- {
- internalTextView.scrollEnabled = isScrollable;
- }
- - (BOOL)isScrollable
- {
- return internalTextView.scrollEnabled;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setEditable:(BOOL)beditable
- {
- internalTextView.editable = beditable;
- }
- -(BOOL)isEditable
- {
- return internalTextView.editable;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setReturnKeyType:(UIReturnKeyType)keyType
- {
- internalTextView.returnKeyType = keyType;
- }
- -(UIReturnKeyType)returnKeyType
- {
- return internalTextView.returnKeyType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setKeyboardType:(UIKeyboardType)keyType
- {
- internalTextView.keyboardType = keyType;
- }
- - (UIKeyboardType)keyboardType
- {
- return internalTextView.keyboardType;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance
- {
- internalTextView.keyboardAppearance = keyboardAppearance;
- }
- - (UIKeyboardAppearance)keyboardAppearance
- {
- return internalTextView.keyboardAppearance;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically
- {
- internalTextView.enablesReturnKeyAutomatically = enablesReturnKeyAutomatically;
- }
- - (BOOL)enablesReturnKeyAutomatically
- {
- return internalTextView.enablesReturnKeyAutomatically;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- -(void)setDataDetectorTypes:(UIDataDetectorTypes)datadetector
- {
- internalTextView.dataDetectorTypes = datadetector;
- }
- -(UIDataDetectorTypes)dataDetectorTypes
- {
- return internalTextView.dataDetectorTypes;
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)hasText{
- return [internalTextView hasText];
- }
- - (void)scrollRangeToVisible:(NSRange)range
- {
- [internalTextView scrollRangeToVisible:range];
- }
- /////////////////////////////////////////////////////////////////////////////////////////////////////
- /////////////////////////////////////////////////////////////////////////////////////////////////////
- #pragma mark -
- #pragma mark UITextViewDelegate
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)textViewShouldBeginEditing:(UITextView *)textView {
- if ([delegate respondsToSelector:@selector(growingTextViewShouldBeginEditing:)]) {
- return [delegate growingTextViewShouldBeginEditing:self];
-
- } else {
- return YES;
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)textViewShouldEndEditing:(UITextView *)textView {
- if ([delegate respondsToSelector:@selector(growingTextViewShouldEndEditing:)]) {
- return [delegate growingTextViewShouldEndEditing:self];
-
- } else {
- return YES;
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)textViewDidBeginEditing:(UITextView *)textView {
- if ([delegate respondsToSelector:@selector(growingTextViewDidBeginEditing:)]) {
- [delegate growingTextViewDidBeginEditing:self];
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)textViewDidEndEditing:(UITextView *)textView {
- if ([delegate respondsToSelector:@selector(growingTextViewDidEndEditing:)]) {
- [delegate growingTextViewDidEndEditing:self];
- }
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range
- replacementText:(NSString *)atext {
-
- //weird 1 pixel bug when clicking backspace when textView is empty
- if(![textView hasText] && [atext isEqualToString:@""]) return NO;
-
- //Added by bretdabaker: sometimes we want to handle this ourselves
- if ([delegate respondsToSelector:@selector(growingTextView:shouldChangeTextInRange:replacementText:)])
- return [delegate growingTextView:self shouldChangeTextInRange:range replacementText:atext];
-
- if ([atext isEqualToString:@"\n"]) {
- if ([delegate respondsToSelector:@selector(growingTextViewShouldReturn:)]) {
- if (![delegate performSelector:@selector(growingTextViewShouldReturn:) withObject:self]) {
- return YES;
- } else {
- [textView resignFirstResponder];
- return NO;
- }
- }
- }
-
- return YES;
-
-
- }
- ///////////////////////////////////////////////////////////////////////////////////////////////////
- - (void)textViewDidChangeSelection:(UITextView *)textView {
- if ([delegate respondsToSelector:@selector(growingTextViewDidChangeSelection:)]) {
- [delegate growingTextViewDidChangeSelection:self];
- }
- }
- @end
|