From 7b02207537d35bfa1714bf8beafc921f717d100a Mon Sep 17 00:00:00 2001
From: 单军华
Date: Wed, 11 Jul 2018 10:47:42 +0800
Subject: [PATCH] 首次上传

---
 screendisplay/Pods/YYText/YYText/YYTextView.m | 3830 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 3,830 insertions(+), 0 deletions(-)

diff --git a/screendisplay/Pods/YYText/YYText/YYTextView.m b/screendisplay/Pods/YYText/YYText/YYTextView.m
new file mode 100755
index 0000000..01b1629
--- /dev/null
+++ b/screendisplay/Pods/YYText/YYText/YYTextView.m
@@ -0,0 +1,3830 @@
+//
+//  YYTextView.m
+//  YYText <https://github.com/ibireme/YYText>
+//
+//  Created by ibireme on 15/2/25.
+//  Copyright (c) 2015 ibireme.
+//
+//  This source code is licensed under the MIT-style license found in the
+//  LICENSE file in the root directory of this source tree.
+//
+
+#import "YYTextView.h"
+#import "YYTextInput.h"
+#import "YYTextContainerView.h"
+#import "YYTextSelectionView.h"
+#import "YYTextMagnifier.h"
+#import "YYTextEffectWindow.h"
+#import "YYTextKeyboardManager.h"
+#import "YYTextUtilities.h"
+#import "YYTextTransaction.h"
+#import "YYTextWeakProxy.h"
+#import "NSAttributedString+YYText.h"
+#import "UIPasteboard+YYText.h"
+#import "UIView+YYText.h"
+
+
+static double _YYDeviceSystemVersion() {
+    static double version;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        version = [UIDevice currentDevice].systemVersion.doubleValue;
+    });
+    return version;
+}
+
+#ifndef kSystemVersion
+#define kSystemVersion _YYDeviceSystemVersion()
+#endif
+
+#ifndef kiOS6Later
+#define kiOS6Later (kSystemVersion >= 6)
+#endif
+
+#ifndef kiOS7Later
+#define kiOS7Later (kSystemVersion >= 7)
+#endif
+
+#ifndef kiOS8Later
+#define kiOS8Later (kSystemVersion >= 8)
+#endif
+
+#ifndef kiOS9Later
+#define kiOS9Later (kSystemVersion >= 9)
+#endif
+
+
+
+#define kDefaultUndoLevelMax 20 // Default maximum undo level
+
+#define kAutoScrollMinimumDuration 0.1 // Time in seconds to tick auto-scroll.
+#define kLongPressMinimumDuration 0.5 // Time in seconds the fingers must be held down for long press gesture.
+#define kLongPressAllowableMovement 10.0 // Maximum movement in points allowed before the long press fails.
+
+#define kMagnifierRangedTrackFix -6.0 // Magnifier ranged offset fix.
+#define kMagnifierRangedPopoverOffset 4.0 // Magnifier ranged popover offset.
+#define kMagnifierRangedCaptureOffset -6.0 // Magnifier ranged capture center offset.
+
+#define kHighlightFadeDuration 0.15 // Time in seconds for highlight fadeout animation.
+
+#define kDefaultInset UIEdgeInsetsMake(6, 4, 6, 4)
+#define kDefaultVerticalInset UIEdgeInsetsMake(4, 6, 4, 6)
+
+
+NSString *const YYTextViewTextDidBeginEditingNotification = @"YYTextViewTextDidBeginEditing";
+NSString *const YYTextViewTextDidChangeNotification = @"YYTextViewTextDidChange";
+NSString *const YYTextViewTextDidEndEditingNotification = @"YYTextViewTextDidEndEditing";
+
+
+typedef NS_ENUM (NSUInteger, YYTextGrabberDirection) {
+    kStart = 1,
+    kEnd   = 2,
+};
+
+typedef NS_ENUM(NSUInteger, YYTextMoveDirection) {
+    kLeft   = 1,
+    kTop    = 2,
+    kRight  = 3,
+    kBottom = 4,
+};
+
+
+/// An object that captures the state of the text view. Used for undo and redo.
+@interface _YYTextViewUndoObject : NSObject
+@property (nonatomic, strong) NSAttributedString *text;
+@property (nonatomic, assign) NSRange selectedRange;
+@end
+@implementation _YYTextViewUndoObject
++ (instancetype)objectWithText:(NSAttributedString *)text range:(NSRange)range {
+    _YYTextViewUndoObject *obj = [self new];
+    obj.text = text ? text : [NSAttributedString new];
+    obj.selectedRange = range;
+    return obj;
+}
+@end
+
+
+@interface YYTextView () <UIScrollViewDelegate, UIAlertViewDelegate, YYTextDebugTarget, YYTextKeyboardObserver> {
+    
+    YYTextRange *_selectedTextRange; /// nonnull
+    YYTextRange *_markedTextRange;
+    
+    __weak id<YYTextViewDelegate> _outerDelegate;
+    
+    UIImageView *_placeHolderView;
+    
+    NSMutableAttributedString *_innerText; ///< nonnull, inner attributed text
+    NSMutableAttributedString *_delectedText; ///< detected text for display
+    YYTextContainer *_innerContainer; ///< nonnull, inner text container
+    YYTextLayout *_innerLayout; ///< inner text layout, the text in this layout is longer than `_innerText` by appending '\n'
+    
+    YYTextContainerView *_containerView; ///< nonnull
+    YYTextSelectionView *_selectionView; ///< nonnull
+    YYTextMagnifier *_magnifierCaret; ///< nonnull
+    YYTextMagnifier *_magnifierRanged; ///< nonnull
+    
+    NSMutableAttributedString *_typingAttributesHolder; ///< nonnull, typing attributes
+    NSDataDetector *_dataDetector;
+    CGFloat _magnifierRangedOffset;
+    
+    NSRange _highlightRange; ///< current highlight range
+    YYTextHighlight *_highlight; ///< highlight attribute in `_highlightRange`
+    YYTextLayout *_highlightLayout; ///< when _state.showingHighlight=YES, this layout should be displayed
+    YYTextRange *_trackingRange; ///< the range in _innerLayout, may out of _innerText.
+    
+    BOOL _insetModifiedByKeyboard; ///< text is covered by keyboard, and the contentInset is modified
+    UIEdgeInsets _originalContentInset; ///< the original contentInset before modified
+    UIEdgeInsets _originalScrollIndicatorInsets; ///< the original scrollIndicatorInsets before modified
+    
+    NSTimer *_longPressTimer;
+    NSTimer *_autoScrollTimer;
+    CGFloat _autoScrollOffset; ///< current auto scroll offset which shoud add to scroll view
+    NSInteger _autoScrollAcceleration; ///< an acceleration coefficient for auto scroll
+    NSTimer *_selectionDotFixTimer; ///< fix the selection dot in window if the view is moved by parents
+    CGPoint _previousOriginInWindow;
+    
+    CGPoint _touchBeganPoint;
+    CGPoint _trackingPoint;
+    NSTimeInterval _touchBeganTime;
+    NSTimeInterval _trackingTime;
+    
+    NSMutableArray *_undoStack;
+    NSMutableArray *_redoStack;
+    NSRange _lastTypeRange;
+    
+    struct {
+        unsigned int trackingGrabber : 2;       ///< YYTextGrabberDirection, current tracking grabber
+        unsigned int trackingCaret : 1;         ///< track the caret
+        unsigned int trackingPreSelect : 1;     ///< track pre-select
+        unsigned int trackingTouch : 1;         ///< is in touch phase
+        unsigned int swallowTouch : 1;          ///< don't forward event to next responder
+        unsigned int touchMoved : 3;            ///< YYTextMoveDirection, move direction after touch began
+        unsigned int selectedWithoutEdit : 1;   ///< show selected range but not first responder
+        unsigned int deleteConfirm : 1;         ///< delete a binding text range
+        unsigned int ignoreFirstResponder : 1;  ///< ignore become first responder temporary
+        unsigned int ignoreTouchBegan : 1;      ///< ignore begin tracking touch temporary
+        
+        unsigned int showingMagnifierCaret : 1;
+        unsigned int showingMagnifierRanged : 1;
+        unsigned int showingMenu : 1;
+        unsigned int showingHighlight : 1;
+        
+        unsigned int typingAttributesOnce : 1;  ///< apply the typing attributes once
+        unsigned int clearsOnInsertionOnce : 1; ///< select all once when become first responder
+        unsigned int autoScrollTicked : 1;      ///< auto scroll did tick scroll at this timer period
+        unsigned int firstShowDot : 1;          ///< the selection grabber dot has displayed at least once
+        unsigned int needUpdate : 1;            ///< the layout or selection view is 'dirty' and need update
+        unsigned int placeholderNeedUpdate : 1; ///< the placeholder need update it's contents
+        
+        unsigned int insideUndoBlock : 1;
+        unsigned int firstResponderBeforeUndoAlert : 1;
+    } _state;
+}
+
+@end
+
+
+@implementation YYTextView
+
+#pragma mark - @protocol UITextInputTraits
+@synthesize autocapitalizationType = _autocapitalizationType;
+@synthesize autocorrectionType = _autocorrectionType;
+@synthesize spellCheckingType = _spellCheckingType;
+@synthesize keyboardType = _keyboardType;
+@synthesize keyboardAppearance = _keyboardAppearance;
+@synthesize returnKeyType = _returnKeyType;
+@synthesize enablesReturnKeyAutomatically = _enablesReturnKeyAutomatically;
+@synthesize secureTextEntry = _secureTextEntry;
+
+#pragma mark - @protocol UITextInput
+@synthesize selectedTextRange = _selectedTextRange;  //copy nonnull (YYTextRange*)
+@synthesize markedTextRange = _markedTextRange;      //readonly     (YYTextRange*)
+@synthesize markedTextStyle = _markedTextStyle;      //copy
+@synthesize inputDelegate = _inputDelegate;         //assign
+@synthesize tokenizer = _tokenizer;                 //readonly
+
+#pragma mark - @protocol UITextInput optional
+@synthesize selectionAffinity = _selectionAffinity;
+
+
+#pragma mark - Private
+
+/// Update layout and selection before runloop sleep/end.
+- (void)_commitUpdate {
+#if !TARGET_INTERFACE_BUILDER
+    _state.needUpdate = YES;
+    [[YYTextTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];
+#else
+    [self _update];
+#endif
+}
+
+/// Update layout and selection view if needed.
+- (void)_updateIfNeeded {
+    if (_state.needUpdate) {
+        [self _update];
+    }
+}
+
+/// Update layout and selection view immediately.
+- (void)_update {
+    _state.needUpdate = NO;
+    [self _updateLayout];
+    [self _updateSelectionView];
+}
+
+/// Update layout immediately.
+- (void)_updateLayout {
+    NSMutableAttributedString *text = _innerText.mutableCopy;
+    _placeHolderView.hidden = text.length > 0;
+    if ([self _detectText:text]) {
+        _delectedText = text;
+    } else {
+        _delectedText = nil;
+    }
+    [text replaceCharactersInRange:NSMakeRange(text.length, 0) withString:@"\r"]; // add for nextline caret
+    [text yy_removeDiscontinuousAttributesInRange:NSMakeRange(_innerText.length, 1)];
+    [text removeAttribute:YYTextBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
+    [text removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(_innerText.length, 1)];
+    if (_innerText.length == 0) {
+        [text yy_setAttributes:_typingAttributesHolder.yy_attributes]; // add for empty text caret
+    }
+    if (_selectedTextRange.end.offset == _innerText.length) {
+        [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+            [text yy_setAttribute:key value:value range:NSMakeRange(_innerText.length, 1)];
+        }];
+    }
+    [self willChangeValueForKey:@"textLayout"];
+    _innerLayout = [YYTextLayout layoutWithContainer:_innerContainer text:text];
+    [self didChangeValueForKey:@"textLayout"];
+    CGSize size = [_innerLayout textBoundingSize];
+    CGSize visibleSize = [self _getVisibleSize];
+    if (_innerContainer.isVerticalForm) {
+        size.height = visibleSize.height;
+        if (size.width < visibleSize.width) size.width = visibleSize.width;
+    } else {
+        size.width = visibleSize.width;
+    }
+    
+    [_containerView setLayout:_innerLayout withFadeDuration:0];
+    _containerView.frame = (CGRect){.size = size};
+    _state.showingHighlight = NO;
+    self.contentSize = size;
+}
+
+/// Update selection view immediately.
+/// This method should be called after "layout update" finished.
+- (void)_updateSelectionView {
+    _selectionView.frame = _containerView.frame;
+    _selectionView.caretBlinks = NO;
+    _selectionView.caretVisible = NO;
+    _selectionView.selectionRects = nil;
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    if (!_innerLayout) return;
+    
+    NSMutableArray *allRects = [NSMutableArray new];
+    BOOL containsDot = NO;
+    
+    YYTextRange *selectedRange = _selectedTextRange;
+    if (_state.trackingTouch && _trackingRange) {
+        selectedRange = _trackingRange;
+    }
+    
+    if (_markedTextRange) {
+        NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:_markedTextRange];
+        if (rects) [allRects addObjectsFromArray:rects];
+        if (selectedRange.asRange.length > 0) {
+            rects = [_innerLayout selectionRectsWithOnlyStartAndEndForRange:selectedRange];
+            if (rects) [allRects addObjectsFromArray:rects];
+            containsDot = rects.count > 0;
+        } else {
+            CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
+            _selectionView.caretRect = [self _convertRectFromLayout:rect];
+            _selectionView.caretVisible = YES;
+            _selectionView.caretBlinks = YES;
+        }
+    } else {
+        if (selectedRange.asRange.length == 0) { // only caret
+            if (self.isFirstResponder || _state.trackingPreSelect) {
+                CGRect rect = [_innerLayout caretRectForPosition:selectedRange.end];
+                _selectionView.caretRect = [self _convertRectFromLayout:rect];
+                _selectionView.caretVisible = YES;
+                if (!_state.trackingCaret && !_state.trackingPreSelect) {
+                    _selectionView.caretBlinks = YES;
+                }
+            }
+        } else { // range selected
+            if ((self.isFirstResponder && !_state.deleteConfirm) ||
+                (!self.isFirstResponder && _state.selectedWithoutEdit)) {
+                NSArray *rects = [_innerLayout selectionRectsForRange:selectedRange];
+                if (rects) [allRects addObjectsFromArray:rects];
+                containsDot = rects.count > 0;
+            } else if ((!self.isFirstResponder && _state.trackingPreSelect) ||
+                       (self.isFirstResponder && _state.deleteConfirm)){
+                NSArray *rects = [_innerLayout selectionRectsWithoutStartAndEndForRange:selectedRange];
+                if (rects) [allRects addObjectsFromArray:rects];
+            }
+        }
+    }
+    [allRects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
+        rect.rect = [self _convertRectFromLayout:rect.rect];
+    }];
+    _selectionView.selectionRects = allRects;
+    if (!_state.firstShowDot && containsDot) {
+        _state.firstShowDot = YES;
+        /*
+         The dot position may be wrong at the first time displayed.
+         I can't find the reason. Here's a workaround.
+         */
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.02 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+        });
+    }
+    [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    
+    if (containsDot) {
+        [self _startSelectionDotFixTimer];
+    } else {
+        [self _endSelectionDotFixTimer];
+    }
+}
+
+/// Update inner contains's size.
+- (void)_updateInnerContainerSize {
+    CGSize size = [self _getVisibleSize];
+    if (_innerContainer.isVerticalForm) size.width = CGFLOAT_MAX;
+    else size.height = CGFLOAT_MAX;
+    _innerContainer.size = size;
+}
+
+/// Update placeholder before runloop sleep/end.
+- (void)_commitPlaceholderUpdate {
+#if !TARGET_INTERFACE_BUILDER
+    _state.placeholderNeedUpdate = YES;
+    [[YYTextTransaction transactionWithTarget:self selector:@selector(_updatePlaceholderIfNeeded)] commit];
+#else
+    [self _updatePlaceholder];
+#endif
+}
+
+/// Update placeholder if needed.
+- (void)_updatePlaceholderIfNeeded {
+    if (_state.placeholderNeedUpdate) {
+        _state.placeholderNeedUpdate = NO;
+        [self _updatePlaceholder];
+    }
+}
+
+/// Update placeholder immediately.
+- (void)_updatePlaceholder {
+    CGRect frame = CGRectZero;
+    _placeHolderView.image = nil;
+    _placeHolderView.frame = frame;
+    if (_placeholderAttributedText.length > 0) {
+        YYTextContainer *container = _innerContainer.copy;
+        container.size = self.bounds.size;
+        container.truncationType = YYTextTruncationTypeEnd;
+        container.truncationToken = nil;
+        YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_placeholderAttributedText];
+        CGSize size = [layout textBoundingSize];
+        BOOL needDraw = size.width > 1 && size.height > 1;
+        if (needDraw) {
+            UIGraphicsBeginImageContextWithOptions(size, NO, 0);
+            CGContextRef context = UIGraphicsGetCurrentContext();
+            [layout drawInContext:context size:size debug:self.debugOption];
+            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
+            UIGraphicsEndImageContext();
+            _placeHolderView.image = image;
+            frame.size = image.size;
+            if (container.isVerticalForm) {
+                frame.origin.x = self.bounds.size.width - image.size.width;
+            } else {
+                frame.origin = CGPointZero;
+            }
+            _placeHolderView.frame = frame;
+        }
+    }
+}
+
+/// Update the `_selectedTextRange` to a single position by `_trackingPoint`.
+- (void)_updateTextRangeByTrackingCaret {
+    if (!_state.trackingTouch) return;
+    
+    CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
+    YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
+    if (newPos) {
+        newPos = [self _correctedTextPosition:newPos];
+        if (_markedTextRange) {
+            if ([newPos compare:_markedTextRange.start] == NSOrderedAscending) {
+                newPos = _markedTextRange.start;
+            } else if ([newPos compare:_markedTextRange.end] == NSOrderedDescending) {
+                newPos = _markedTextRange.end;
+            }
+        }
+        YYTextRange *newRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
+        _trackingRange = newRange;
+    }
+}
+
+/// Update the `_selectedTextRange` to a new range by `_trackingPoint` and `_state.trackingGrabber`.
+- (void)_updateTextRangeByTrackingGrabber {
+    if (!_state.trackingTouch || !_state.trackingGrabber) return;
+    
+    BOOL isStart = _state.trackingGrabber == kStart;
+    CGPoint magPoint = _trackingPoint;
+    magPoint.y += kMagnifierRangedTrackFix;
+    magPoint = [self _convertPointToLayout:magPoint];
+    YYTextPosition *position = [_innerLayout positionForPoint:magPoint
+                                                  oldPosition:(isStart ? _selectedTextRange.start : _selectedTextRange.end)
+                                                otherPosition:(isStart ? _selectedTextRange.end : _selectedTextRange.start)];
+    if (position) {
+        position = [self _correctedTextPosition:position];
+        if ((NSUInteger)position.offset > _innerText.length) {
+            position = [YYTextPosition positionWithOffset:_innerText.length];
+        }
+        YYTextRange *newRange = [YYTextRange rangeWithStart:(isStart ? position : _selectedTextRange.start)
+                                                        end:(isStart ? _selectedTextRange.end : position)];
+        _trackingRange = newRange;
+    }
+}
+
+/// Update the `_selectedTextRange` to a new range/position by `_trackingPoint`.
+- (void)_updateTextRangeByTrackingPreSelect {
+    if (!_state.trackingTouch) return;
+    YYTextRange *newRange = [self _getClosestTokenRangeAtPoint:_trackingPoint];
+    _trackingRange = newRange;
+}
+
+/// Show or update `_magnifierCaret` based on `_trackingPoint`, and hide `_magnifierRange`.
+- (void)_showMagnifierCaret {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierRanged) {
+        _state.showingMagnifierRanged = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    }
+    
+    _magnifierCaret.hostPopoverCenter = _trackingPoint;
+    _magnifierCaret.hostCaptureCenter = _trackingPoint;
+    if (!_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = YES;
+        [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierCaret];
+    } else {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
+    }
+}
+
+/// Show or update `_magnifierRanged` based on `_trackingPoint`, and hide `_magnifierCaret`.
+- (void)_showMagnifierRanged {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_verticalForm) { // hack for vertical form...
+        [self _showMagnifierCaret];
+        return;
+    }
+    
+    if (_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    }
+    
+    CGPoint magPoint = _trackingPoint;
+    if (_verticalForm) {
+        magPoint.x += kMagnifierRangedTrackFix;
+    } else {
+        magPoint.y += kMagnifierRangedTrackFix;
+    }
+    
+    YYTextRange *selectedRange = _selectedTextRange;
+    if (_state.trackingTouch && _trackingRange) {
+        selectedRange = _trackingRange;
+    }
+    
+    YYTextPosition *position;
+    if (_markedTextRange) {
+        position = selectedRange.end;
+    } else {
+        position = [_innerLayout positionForPoint:[self _convertPointToLayout:magPoint]
+                                      oldPosition:(_state.trackingGrabber == kStart ? selectedRange.start : selectedRange.end)
+                                    otherPosition:(_state.trackingGrabber == kStart ? selectedRange.end : selectedRange.start)];
+    }
+    
+    NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
+    if (lineIndex < _innerLayout.lines.count) {
+        YYTextLine *line = _innerLayout.lines[lineIndex];
+        CGRect lineRect = [self _convertRectFromLayout:line.bounds];
+        if (_verticalForm) {
+            magPoint.x = YYTEXT_CLAMP(magPoint.x, CGRectGetMinX(lineRect), CGRectGetMaxX(lineRect));
+        } else {
+            magPoint.y = YYTEXT_CLAMP(magPoint.y, CGRectGetMinY(lineRect), CGRectGetMaxY(lineRect));
+        }
+        CGPoint linePoint = [_innerLayout linePositionForPosition:position];
+        linePoint = [self _convertPointFromLayout:linePoint];
+        
+        CGPoint popoverPoint = linePoint;
+        if (_verticalForm) {
+            popoverPoint.x = linePoint.x + _magnifierRangedOffset;
+        } else {
+            popoverPoint.y = linePoint.y + _magnifierRangedOffset;
+        }
+        
+        CGPoint capturePoint;
+        if (_verticalForm) {
+            capturePoint.x = linePoint.x + kMagnifierRangedCaptureOffset;
+            capturePoint.y = linePoint.y;
+        } else {
+            capturePoint.x = linePoint.x;
+            capturePoint.y = linePoint.y + kMagnifierRangedCaptureOffset;
+        }
+        
+        _magnifierRanged.hostPopoverCenter = popoverPoint;
+        _magnifierRanged.hostCaptureCenter = capturePoint;
+        if (!_state.showingMagnifierRanged) {
+            _state.showingMagnifierRanged = YES;
+            [[YYTextEffectWindow sharedWindow] showMagnifier:_magnifierRanged];
+        } else {
+            [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
+        }
+    }
+}
+
+/// Update the showing magnifier.
+- (void)_updateMagnifier {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierCaret) {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierCaret];
+    }
+    if (_state.showingMagnifierRanged) {
+        [[YYTextEffectWindow sharedWindow] moveMagnifier:_magnifierRanged];
+    }
+}
+
+/// Hide the `_magnifierCaret` and `_magnifierRanged`.
+- (void)_hideMagnifier {
+    if (YYTextIsAppExtension()) return;
+    
+    if (_state.showingMagnifierCaret || _state.showingMagnifierRanged) {
+        // disable touch began temporary to ignore caret animation overlap
+        _state.ignoreTouchBegan = YES;
+        __weak typeof(self) _self = self;
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            __strong typeof(_self) self = _self;
+            if (self) self->_state.ignoreTouchBegan = NO;
+        });
+    }
+    
+    if (_state.showingMagnifierCaret) {
+        _state.showingMagnifierCaret = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    }
+    if (_state.showingMagnifierRanged) {
+        _state.showingMagnifierRanged = NO;
+        [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    }
+}
+
+/// Show and update the UIMenuController.
+- (void)_showMenu {
+    CGRect rect;
+    if (_selectionView.caretVisible) {
+        rect = _selectionView.caretView.frame;
+    } else if (_selectionView.selectionRects.count > 0) {
+        YYTextSelectionRect *sRect = _selectionView.selectionRects.firstObject;
+        rect = sRect.rect;
+        for (NSUInteger i = 1; i < _selectionView.selectionRects.count; i++) {
+            sRect = _selectionView.selectionRects[i];
+            rect = CGRectUnion(rect, sRect.rect);
+        }
+        
+        CGRect inter = CGRectIntersection(rect, self.bounds);
+        if (!CGRectIsNull(inter) && inter.size.height > 1) {
+            rect = inter; //clip to bounds
+        } else {
+            if (CGRectGetMinY(rect) < CGRectGetMinY(self.bounds)) {
+                rect.size.height = 1;
+                rect.origin.y = CGRectGetMinY(self.bounds);
+            } else {
+                rect.size.height = 1;
+                rect.origin.y = CGRectGetMaxY(self.bounds);
+            }
+        }
+        
+        YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+        if (mgr.keyboardVisible) {
+            CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+            CGRect kbInter = CGRectIntersection(rect, kbRect);
+            if (!CGRectIsNull(kbInter) && kbInter.size.height > 1 && kbInter.size.width > 1) {
+                // self is covered by keyboard
+                if (CGRectGetMinY(kbInter) > CGRectGetMinY(rect)) { // keyboard at bottom
+                    rect.size.height -= kbInter.size.height;
+                } else if (CGRectGetMaxY(kbInter) < CGRectGetMaxY(rect)) { // keyboard at top
+                    rect.origin.y += kbInter.size.height;
+                    rect.size.height -= kbInter.size.height;
+                }
+            }
+        }
+    } else {
+        rect = _selectionView.bounds;
+    }
+    
+    if (!self.isFirstResponder) {
+        if (!_containerView.isFirstResponder) {
+            [_containerView becomeFirstResponder];
+        }
+    }
+    
+    if (self.isFirstResponder || _containerView.isFirstResponder) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            UIMenuController *menu = [UIMenuController sharedMenuController];
+            [menu setTargetRect:CGRectStandardize(rect) inView:_selectionView];
+            [menu update];
+            if (!_state.showingMenu || !menu.menuVisible) {
+                _state.showingMenu = YES;
+                [menu setMenuVisible:YES animated:YES];
+            }
+        });
+    }
+}
+
+/// Hide the UIMenuController.
+- (void)_hideMenu {
+    if (_state.showingMenu) {
+        _state.showingMenu = NO;
+        UIMenuController *menu = [UIMenuController sharedMenuController];
+        [menu setMenuVisible:NO animated:YES];
+    }
+    if (_containerView.isFirstResponder) {
+        _state.ignoreFirstResponder = YES;
+        [_containerView resignFirstResponder]; // it will call [self becomeFirstResponder], ignore it temporary.
+        _state.ignoreFirstResponder = NO;
+    }
+}
+
+/// Show highlight layout based on `_highlight` and `_highlightRange`
+/// If the `_highlightLayout` is nil, try to create.
+- (void)_showHighlightAnimated:(BOOL)animated {
+    NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
+    if (!_highlight) return;
+    if (!_highlightLayout) {
+        NSMutableAttributedString *hiText = (_delectedText ? _delectedText : _innerText).mutableCopy;
+        NSDictionary *newAttrs = _highlight.attributes;
+        [newAttrs enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) {
+            [hiText yy_setAttribute:key value:value range:_highlightRange];
+        }];
+        _highlightLayout = [YYTextLayout layoutWithContainer:_innerContainer text:hiText];
+        if (!_highlightLayout) _highlight = nil;
+    }
+    
+    if (_highlightLayout && !_state.showingHighlight) {
+        _state.showingHighlight = YES;
+        [_containerView setLayout:_highlightLayout withFadeDuration:fadeDuration];
+    }
+}
+
+/// Show `_innerLayout` instead of `_highlightLayout`.
+/// It does not destory the `_highlightLayout`.
+- (void)_hideHighlightAnimated:(BOOL)animated {
+    NSTimeInterval fadeDuration = animated ? kHighlightFadeDuration : 0;
+    if (_state.showingHighlight) {
+        _state.showingHighlight = NO;
+        [_containerView setLayout:_innerLayout withFadeDuration:fadeDuration];
+    }
+}
+
+/// Show `_innerLayout` and destory the `_highlight` and `_highlightLayout`.
+- (void)_removeHighlightAnimated:(BOOL)animated {
+    [self _hideHighlightAnimated:animated];
+    _highlight = nil;
+    _highlightLayout = nil;
+}
+
+/// Scroll current selected range to visible.
+- (void)_scrollSelectedRangeToVisible {
+    [self _scrollRangeToVisible:_selectedTextRange];
+}
+
+/// Scroll range to visible, take account into keyboard and insets.
+- (void)_scrollRangeToVisible:(YYTextRange *)range {
+    if (!range) return;
+    CGRect rect = [_innerLayout rectForRange:range];
+    if (CGRectIsNull(rect)) return;
+    rect = [self _convertRectFromLayout:rect];
+    rect = [_containerView convertRect:rect toView:self];
+    
+    if (rect.size.width < 1) rect.size.width = 1;
+    if (rect.size.height < 1) rect.size.height = 1;
+    CGFloat extend = 3;
+    
+    BOOL insetModified = NO;
+    YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+    
+    if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
+        CGRect bounds = self.bounds;
+        bounds.origin = CGPointZero;
+        CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+        kbRect.origin.y -= _extraAccessoryViewHeight;
+        kbRect.size.height += _extraAccessoryViewHeight;
+        
+        kbRect.origin.x -= self.contentOffset.x;
+        kbRect.origin.y -= self.contentOffset.y;
+        CGRect inter = CGRectIntersection(bounds, kbRect);
+        if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > extend) { // self is covered by keyboard
+            if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) { // keyboard below self.top
+                
+                UIEdgeInsets originalContentInset = self.contentInset;
+                UIEdgeInsets originalScrollIndicatorInsets = self.scrollIndicatorInsets;
+                if (_insetModifiedByKeyboard) {
+                    originalContentInset = _originalContentInset;
+                    originalScrollIndicatorInsets = _originalScrollIndicatorInsets;
+                }
+                
+                if (originalContentInset.bottom < inter.size.height + extend) {
+                    insetModified = YES;
+                    if (!_insetModifiedByKeyboard) {
+                        _insetModifiedByKeyboard = YES;
+                        _originalContentInset = self.contentInset;
+                        _originalScrollIndicatorInsets = self.scrollIndicatorInsets;
+                    }
+                    UIEdgeInsets newInset = originalContentInset;
+                    UIEdgeInsets newIndicatorInsets = originalScrollIndicatorInsets;
+                    newInset.bottom = inter.size.height + extend;
+                    newIndicatorInsets.bottom = newInset.bottom;
+                    UIViewAnimationOptions curve;
+                    if (kiOS7Later) {
+                        curve = 7 << 16;
+                    } else {
+                        curve = UIViewAnimationOptionCurveEaseInOut;
+                    }
+                    [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | curve animations:^{
+                        [super setContentInset:newInset];
+                        [super setScrollIndicatorInsets:newIndicatorInsets];
+                        [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
+                    } completion:NULL];
+                }
+            }
+        }
+    }
+    if (!insetModified) {
+        [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut animations:^{
+            [self _restoreInsetsAnimated:NO];
+            [self scrollRectToVisible:CGRectInset(rect, -extend, -extend) animated:NO];
+        } completion:NULL];
+    }
+}
+
+/// Restore contents insets if modified by keyboard.
+- (void)_restoreInsetsAnimated:(BOOL)animated {
+    if (_insetModifiedByKeyboard) {
+        _insetModifiedByKeyboard = NO;
+        if (animated) {
+            [UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut  animations:^{
+                [super setContentInset:_originalContentInset];
+                [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
+            } completion:NULL];
+        } else {
+            [super setContentInset:_originalContentInset];
+            [super setScrollIndicatorInsets:_originalScrollIndicatorInsets];
+        }
+    }
+}
+
+/// Keyboard frame changed, scroll the caret to visible range, or modify the content insets.
+- (void)_keyboardChanged {
+    if (!self.isFirstResponder) return;
+    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+        if ([YYTextKeyboardManager defaultManager].keyboardVisible) {
+            [self _scrollRangeToVisible:_selectedTextRange];
+        } else {
+            [self _restoreInsetsAnimated:YES];
+        }
+        [self _updateMagnifier];
+        if (_state.showingMenu) {
+            [self _showMenu];
+        }
+    });
+}
+
+/// Start long press timer, used for 'highlight' range text action.
+- (void)_startLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = [NSTimer timerWithTimeInterval:kLongPressMinimumDuration
+                                              target:[YYTextWeakProxy proxyWithTarget:self]
+                                            selector:@selector(_trackDidLongPress)
+                                            userInfo:nil
+                                             repeats:NO];
+    [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
+}
+
+/// Invalidate the long press timer.
+- (void)_endLongPressTimer {
+    [_longPressTimer invalidate];
+    _longPressTimer = nil;
+}
+
+/// Long press detected.
+- (void)_trackDidLongPress {
+    [self _endLongPressTimer];
+    
+    BOOL dealLongPressAction = NO;
+    if (_state.showingHighlight) {
+        [self _hideMenu];
+        
+        if (_highlight.longPressAction) {
+            dealLongPressAction = YES;
+            CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+            rect = [self _convertRectFromLayout:rect];
+            _highlight.longPressAction(self, _innerText, _highlightRange, rect);
+            [self _endTouchTracking];
+        } else {
+            BOOL shouldHighlight = YES;
+            if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
+                shouldHighlight = [self.delegate textView:self shouldLongPressHighlight:_highlight inRange:_highlightRange];
+            }
+            if (shouldHighlight && [self.delegate respondsToSelector:@selector(textView:didLongPressHighlight:inRange:rect:)]) {
+                dealLongPressAction = YES;
+                CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                rect = [self _convertRectFromLayout:rect];
+                [self.delegate textView:self didLongPressHighlight:_highlight inRange:_highlightRange rect:rect];
+                [self _endTouchTracking];
+            }
+        }
+    }
+    
+    if (!dealLongPressAction){
+        [self _removeHighlightAnimated:NO];
+        if (_state.trackingTouch) {
+            if (_state.trackingGrabber) {
+                self.panGestureRecognizer.enabled = NO;
+                [self _hideMenu];
+                [self _showMagnifierRanged];
+            } else if (self.isFirstResponder){
+                self.panGestureRecognizer.enabled = NO;
+                _selectionView.caretBlinks = NO;
+                _state.trackingCaret = YES;
+                CGPoint trackingPoint = [self _convertPointToLayout:_trackingPoint];
+                YYTextPosition *newPos = [_innerLayout closestPositionToPoint:trackingPoint];
+                newPos = [self _correctedTextPosition:newPos];
+                if (newPos) {
+                    if (_markedTextRange) {
+                        if ([newPos compare:_markedTextRange.start] != NSOrderedDescending) {
+                            newPos = _markedTextRange.start;
+                        } else if ([newPos compare:_markedTextRange.end] != NSOrderedAscending) {
+                            newPos = _markedTextRange.end;
+                        }
+                    }
+                    _trackingRange = [YYTextRange rangeWithRange:NSMakeRange(newPos.offset, 0) affinity:newPos.affinity];
+                    [self _updateSelectionView];
+                }
+                [self _hideMenu];
+                
+                if (_markedTextRange) {
+                    [self _showMagnifierRanged];
+                } else {
+                    [self _showMagnifierCaret];
+                }
+            } else if (self.selectable) {
+                self.panGestureRecognizer.enabled = NO;
+                _state.trackingPreSelect = YES;
+                _state.selectedWithoutEdit = NO;
+                [self _updateTextRangeByTrackingPreSelect];
+                [self _updateSelectionView];
+                [self _showMagnifierCaret];
+            }
+        }
+    }
+}
+
+/// Start auto scroll timer, used for auto scroll tick.
+- (void)_startAutoScrollTimer {
+    if (!_autoScrollTimer) {
+        [_autoScrollTimer invalidate];
+        _autoScrollTimer = [NSTimer timerWithTimeInterval:kAutoScrollMinimumDuration
+                                                   target:[YYTextWeakProxy proxyWithTarget:self]
+                                                 selector:@selector(_trackDidTickAutoScroll)
+                                                 userInfo:nil
+                                                  repeats:YES];
+        [[NSRunLoop currentRunLoop] addTimer:_autoScrollTimer forMode:NSRunLoopCommonModes];
+    }
+}
+
+/// Invalidate the auto scroll, and restore the text view state.
+- (void)_endAutoScrollTimer {
+    if (_state.autoScrollTicked) [self flashScrollIndicators];
+    [_autoScrollTimer invalidate];
+    _autoScrollTimer = nil;
+    _autoScrollOffset = 0;
+    _autoScrollAcceleration = 0;
+    _state.autoScrollTicked = NO;
+    
+    if (_magnifierCaret.captureDisabled) {
+        _magnifierCaret.captureDisabled = NO;
+        if (_state.showingMagnifierCaret) {
+            [self _showMagnifierCaret];
+        }
+    }
+    if (_magnifierRanged.captureDisabled) {
+        _magnifierRanged.captureDisabled = NO;
+        if (_state.showingMagnifierRanged) {
+            [self _showMagnifierRanged];
+        }
+    }
+}
+
+/// Auto scroll ticked by timer.
+- (void)_trackDidTickAutoScroll {
+    if (_autoScrollOffset != 0) {
+        _magnifierCaret.captureDisabled = YES;
+        _magnifierRanged.captureDisabled = YES;
+        
+        CGPoint offset = self.contentOffset;
+        if (_verticalForm) {
+            offset.x += _autoScrollOffset;
+            
+            if (_autoScrollAcceleration > 0) {
+                offset.x += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
+            }
+            _autoScrollAcceleration++;
+            offset.x = round(offset.x);
+            if (_autoScrollOffset < 0) {
+                if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
+            } else {
+                CGFloat maxOffsetX = self.contentSize.width - self.bounds.size.width + self.contentInset.right;
+                if (offset.x > maxOffsetX) offset.x = maxOffsetX;
+            }
+            if (offset.x < -self.contentInset.left) offset.x = -self.contentInset.left;
+        } else {
+            offset.y += _autoScrollOffset;
+            if (_autoScrollAcceleration > 0) {
+                offset.y += ((_autoScrollOffset > 0 ? 1 : -1) * _autoScrollAcceleration * _autoScrollAcceleration * 0.5);
+            }
+            _autoScrollAcceleration++;
+            offset.y = round(offset.y);
+            if (_autoScrollOffset < 0) {
+                if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
+            } else {
+                CGFloat maxOffsetY = self.contentSize.height - self.bounds.size.height + self.contentInset.bottom;
+                if (offset.y > maxOffsetY) offset.y = maxOffsetY;
+            }
+            if (offset.y < -self.contentInset.top) offset.y = -self.contentInset.top;
+        }
+        
+        BOOL shouldScroll;
+        if (_verticalForm) {
+            shouldScroll = fabs(offset.x -self.contentOffset.x) > 0.5;
+        } else {
+            shouldScroll = fabs(offset.y -self.contentOffset.y) > 0.5;
+        }
+        
+        if (shouldScroll) {
+            _state.autoScrollTicked = YES;
+            _trackingPoint.x += offset.x - self.contentOffset.x;
+            _trackingPoint.y += offset.y - self.contentOffset.y;
+            [UIView animateWithDuration:kAutoScrollMinimumDuration delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionCurveLinear animations:^{
+                [self setContentOffset:offset];
+            } completion:^(BOOL finished) {
+                if (_state.trackingTouch) {
+                    if (_state.trackingGrabber) {
+                        [self _showMagnifierRanged];
+                        [self _updateTextRangeByTrackingGrabber];
+                    } else if (_state.trackingPreSelect) {
+                        [self _showMagnifierCaret];
+                        [self _updateTextRangeByTrackingPreSelect];
+                    } else if (_state.trackingCaret) {
+                        if (_markedTextRange) {
+                            [self _showMagnifierRanged];
+                        } else {
+                            [self _showMagnifierCaret];
+                        }
+                        [self _updateTextRangeByTrackingCaret];
+                    }
+                    [self _updateSelectionView];
+                }
+            }];
+        } else {
+            [self _endAutoScrollTimer];
+        }
+    } else {
+        [self _endAutoScrollTimer];
+    }
+}
+
+/// End current touch tracking (if is tracking now), and update the state.
+- (void)_endTouchTracking {
+    if (!_state.trackingTouch) return;
+    
+    _state.trackingTouch = NO;
+    _state.trackingGrabber = NO;
+    _state.trackingCaret = NO;
+    _state.trackingPreSelect = NO;
+    _state.touchMoved = NO;
+    _state.deleteConfirm = NO;
+    _state.clearsOnInsertionOnce = NO;
+    _trackingRange = nil;
+    _selectionView.caretBlinks = YES;
+    
+    [self _removeHighlightAnimated:YES];
+    [self _hideMagnifier];
+    [self _endLongPressTimer];
+    [self _endAutoScrollTimer];
+    [self _updateSelectionView];
+    
+    self.panGestureRecognizer.enabled = self.scrollEnabled;
+}
+
+/// Start a timer to fix the selection dot.
+- (void)_startSelectionDotFixTimer {
+    [_selectionDotFixTimer invalidate];
+    _longPressTimer = [NSTimer timerWithTimeInterval:1/15.0
+                                              target:[YYTextWeakProxy proxyWithTarget:self]
+                                            selector:@selector(_fixSelectionDot)
+                                            userInfo:nil
+                                             repeats:NO];
+    [[NSRunLoop currentRunLoop] addTimer:_longPressTimer forMode:NSRunLoopCommonModes];
+}
+
+/// End the timer.
+- (void)_endSelectionDotFixTimer {
+    [_selectionDotFixTimer invalidate];
+    _selectionDotFixTimer = nil;
+}
+
+/// If it shows selection grabber and this view was moved by super view,
+/// update the selection dot in window.
+- (void)_fixSelectionDot {
+    if (YYTextIsAppExtension()) return;
+    CGPoint origin = [self yy_convertPoint:CGPointZero toViewOrWindow:[YYTextEffectWindow sharedWindow]];
+    if (!CGPointEqualToPoint(origin, _previousOriginInWindow)) {
+        _previousOriginInWindow = origin;
+        [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+        [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    }
+}
+
+/// Try to get the character range/position with word granularity from the tokenizer.
+- (YYTextRange *)_getClosestTokenRangeAtPosition:(YYTextPosition *)position {
+    position = [self _correctedTextPosition:position];
+    if (!position) return nil;
+    YYTextRange *range = nil;
+    if (_tokenizer) {
+        range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
+        if (range.asRange.length == 0) {
+            range = (id)[_tokenizer rangeEnclosingPosition:position withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
+        }
+    }
+    
+    if (!range || range.asRange.length == 0) {
+        range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionRight offset:1];
+        range = [self _correctedTextRange:range];
+        if (range.asRange.length == 0) {
+            range = [_innerLayout textRangeByExtendingPosition:position inDirection:UITextLayoutDirectionLeft offset:1];
+            range = [self _correctedTextRange:range];
+        }
+    } else {
+        YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:range.start];
+        YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:range.end];
+        if (extStart && extEnd) {
+            NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
+            range = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    range = [self _correctedTextRange:range];
+    if (range.asRange.length == 0) {
+        range = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    }
+    
+    return [self _correctedTextRange:range];
+}
+
+/// Try to get the character range/position with word granularity from the tokenizer.
+- (YYTextRange *)_getClosestTokenRangeAtPoint:(CGPoint)point {
+    point = [self _convertPointToLayout:point];
+    YYTextRange *touchRange = [_innerLayout closestTextRangeAtPoint:point];
+    touchRange = [self _correctedTextRange:touchRange];
+    
+    if (_tokenizer && touchRange) {
+        YYTextRange *encEnd = (id)[_tokenizer rangeEnclosingPosition:touchRange.end withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionBackward];
+        YYTextRange *encStart = (id)[_tokenizer rangeEnclosingPosition:touchRange.start withGranularity:UITextGranularityWord inDirection:UITextStorageDirectionForward];
+        if (encEnd && encStart) {
+            NSArray *arr = [@[encEnd.start, encEnd.end, encStart.start, encStart.end] sortedArrayUsingSelector:@selector(compare:)];
+            touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    if (touchRange) {
+        YYTextRange *extStart = [_innerLayout textRangeByExtendingPosition:touchRange.start];
+        YYTextRange *extEnd = [_innerLayout textRangeByExtendingPosition:touchRange.end];
+        if (extStart && extEnd) {
+            NSArray *arr = [@[extStart.start, extStart.end, extEnd.start, extEnd.end] sortedArrayUsingSelector:@selector(compare:)];
+            touchRange = [YYTextRange rangeWithStart:arr.firstObject end:arr.lastObject];
+        }
+    }
+    
+    if (!touchRange) touchRange = [YYTextRange defaultRange];
+    
+    if (_innerText.length && touchRange.asRange.length == 0) {
+        touchRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    }
+    
+    return touchRange;
+}
+
+/// Try to get the highlight property. If exist, the range will be returnd by the range pointer.
+/// If the delegate ignore the highlight, returns nil.
+- (YYTextHighlight *)_getHighlightAtPoint:(CGPoint)point range:(NSRangePointer)range {
+    if (!_highlightable || !_innerLayout.containsHighlight) return nil;
+    point = [self _convertPointToLayout:point];
+    YYTextRange *textRange = [_innerLayout textRangeAtPoint:point];
+    textRange = [self _correctedTextRange:textRange];
+    if (!textRange) return nil;
+    NSUInteger startIndex = textRange.start.offset;
+    if (startIndex == _innerText.length) {
+        if (startIndex == 0) return nil;
+        else startIndex--;
+    }
+    NSRange highlightRange = {0};
+    NSAttributedString *text = _delectedText ? _delectedText : _innerText;
+    YYTextHighlight *highlight = [text attribute:YYTextHighlightAttributeName
+                                         atIndex:startIndex
+                           longestEffectiveRange:&highlightRange
+                                         inRange:NSMakeRange(0, _innerText.length)];
+    
+    if (!highlight) return nil;
+    
+    BOOL shouldTap = YES, shouldLongPress = YES;
+    if (!highlight.tapAction && !highlight.longPressAction) {
+        if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
+            shouldTap = [self.delegate textView:self shouldTapHighlight:highlight inRange:highlightRange];
+        }
+        if ([self.delegate respondsToSelector:@selector(textView:shouldLongPressHighlight:inRange:)]) {
+            shouldLongPress = [self.delegate textView:self shouldLongPressHighlight:highlight inRange:highlightRange];
+        }
+    }
+    if (!shouldTap && !shouldLongPress) return nil;
+    if (range) *range = highlightRange;
+    return highlight;
+}
+
+/// Return the ranged magnifier popover offset from the baseline, base on `_trackingPoint`.
+- (CGFloat)_getMagnifierRangedOffset {
+    CGPoint magPoint = _trackingPoint;
+    magPoint = [self _convertPointToLayout:magPoint];
+    if (_verticalForm) {
+        magPoint.x += kMagnifierRangedTrackFix;
+    } else {
+        magPoint.y += kMagnifierRangedTrackFix;
+    }
+    YYTextPosition *position = [_innerLayout closestPositionToPoint:magPoint];
+    NSUInteger lineIndex = [_innerLayout lineIndexForPosition:position];
+    if (lineIndex < _innerLayout.lines.count) {
+        YYTextLine *line = _innerLayout.lines[lineIndex];
+        if (_verticalForm) {
+            magPoint.x = YYTEXT_CLAMP(magPoint.x, line.left, line.right);
+            return magPoint.x - line.position.x + kMagnifierRangedPopoverOffset;
+        } else {
+            magPoint.y = YYTEXT_CLAMP(magPoint.y, line.top, line.bottom);
+            return magPoint.y - line.position.y + kMagnifierRangedPopoverOffset;
+        }
+    } else {
+        return 0;
+    }
+}
+
+/// Return a YYTextMoveDirection from `_touchBeganPoint` to `_trackingPoint`.
+- (unsigned int)_getMoveDirection {
+    CGFloat moveH = _trackingPoint.x - _touchBeganPoint.x;
+    CGFloat moveV = _trackingPoint.y - _touchBeganPoint.y;
+    if (fabs(moveH) > fabs(moveV)) {
+        if (fabs(moveH) > kLongPressAllowableMovement) {
+            return moveH > 0 ? kRight : kLeft;
+        }
+    } else {
+        if (fabs(moveV) > kLongPressAllowableMovement) {
+            return moveV > 0 ? kBottom : kTop;
+        }
+    }
+    return 0;
+}
+
+/// Get the auto scroll offset in one tick time.
+- (CGFloat)_getAutoscrollOffset {
+    if (!_state.trackingTouch) return 0;
+    
+    CGRect bounds = self.bounds;
+    bounds.origin = CGPointZero;
+    YYTextKeyboardManager *mgr = [YYTextKeyboardManager defaultManager];
+    if (mgr.keyboardVisible && self.window && self.superview && self.isFirstResponder && !_verticalForm) {
+        CGRect kbRect = [mgr convertRect:mgr.keyboardFrame toView:self];
+        kbRect.origin.y -= _extraAccessoryViewHeight;
+        kbRect.size.height += _extraAccessoryViewHeight;
+        
+        kbRect.origin.x -= self.contentOffset.x;
+        kbRect.origin.y -= self.contentOffset.y;
+        CGRect inter = CGRectIntersection(bounds, kbRect);
+        if (!CGRectIsNull(inter) && inter.size.height > 1 && inter.size.width > 1) {
+            if (CGRectGetMinY(inter) > CGRectGetMinY(bounds)) {
+                bounds.size.height -= inter.size.height;
+            }
+        }
+    }
+    
+    CGPoint point = _trackingPoint;
+    point.x -= self.contentOffset.x;
+    point.y -= self.contentOffset.y;
+    
+    CGFloat maxOfs = 32; // a good value ~
+    CGFloat ofs = 0;
+    if (_verticalForm) {
+        if (point.x < self.contentInset.left) {
+            ofs = (point.x - self.contentInset.left - 5) * 0.5;
+            if (ofs < -maxOfs) ofs = -maxOfs;
+        } else if (point.x > bounds.size.width) {
+            ofs = ((point.x - bounds.size.width) + 5) * 0.5;
+            if (ofs > maxOfs) ofs = maxOfs;
+        }
+    } else {
+        if (point.y < self.contentInset.top) {
+            ofs = (point.y - self.contentInset.top - 5) * 0.5;
+            if (ofs < -maxOfs) ofs = -maxOfs;
+        } else if (point.y > bounds.size.height) {
+            ofs = ((point.y - bounds.size.height) + 5) * 0.5;
+            if (ofs > maxOfs) ofs = maxOfs;
+        }
+    }
+    return ofs;
+}
+
+/// Visible size based on bounds and insets
+- (CGSize)_getVisibleSize {
+    CGSize visibleSize = self.bounds.size;
+    visibleSize.width -= self.contentInset.left - self.contentInset.right;
+    visibleSize.height -= self.contentInset.top - self.contentInset.bottom;
+    if (visibleSize.width < 0) visibleSize.width = 0;
+    if (visibleSize.height < 0) visibleSize.height = 0;
+    return visibleSize;
+}
+
+/// Returns whether the text view can paste data from pastboard.
+- (BOOL)_isPasteboardContainsValidValue {
+    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
+    if (pasteboard.string.length > 0) {
+        return YES;
+    }
+    if (pasteboard.yy_AttributedString.length > 0) {
+        if (_allowsPasteAttributedString) {
+            return YES;
+        }
+    }
+    if (pasteboard.image || pasteboard.yy_ImageData.length > 0) {
+        if (_allowsPasteImage) {
+            return YES;
+        }
+    }
+    return NO;
+}
+
+/// Save current selected attributed text to pasteboard.
+- (void)_copySelectedTextToPasteboard {
+    if (_allowsCopyAttributedString) {
+        NSAttributedString *text = [_innerText attributedSubstringFromRange:_selectedTextRange.asRange];
+        if (text.length) {
+            [UIPasteboard generalPasteboard].yy_AttributedString = text;
+        }
+    } else {
+        NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
+        if (string.length) {
+            [UIPasteboard generalPasteboard].string = string;
+        }
+    }
+}
+
+/// Update the text view state when pasteboard changed.
+- (void)_pasteboardChanged {
+    if (_state.showingMenu) {
+        UIMenuController *menu = [UIMenuController sharedMenuController];
+        [menu update];
+    }
+}
+
+/// Whether the position is valid (not out of bounds).
+- (BOOL)_isTextPositionValid:(YYTextPosition *)position {
+    if (!position) return NO;
+    if (position.offset < 0) return NO;
+    if (position.offset > _innerText.length) return NO;
+    if (position.offset == 0 && position.affinity == YYTextAffinityBackward) return NO;
+    if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) return NO;
+    return YES;
+}
+
+/// Whether the range is valid (not out of bounds).
+- (BOOL)_isTextRangeValid:(YYTextRange *)range {
+    if (![self _isTextPositionValid:range.start]) return NO;
+    if (![self _isTextPositionValid:range.end]) return NO;
+    return YES;
+}
+
+/// Correct the position if it out of bounds.
+- (YYTextPosition *)_correctedTextPosition:(YYTextPosition *)position {
+    if (!position) return nil;
+    if ([self _isTextPositionValid:position]) return position;
+    if (position.offset < 0) {
+        return [YYTextPosition positionWithOffset:0];
+    }
+    if (position.offset > _innerText.length) {
+        return [YYTextPosition positionWithOffset:_innerText.length];
+    }
+    if (position.offset == 0 && position.affinity == YYTextAffinityBackward) {
+        return [YYTextPosition positionWithOffset:position.offset];
+    }
+    if (position.offset == _innerText.length && position.affinity == YYTextAffinityBackward) {
+        return [YYTextPosition positionWithOffset:position.offset];
+    }
+    return position;
+}
+
+/// Correct the range if it out of bounds.
+- (YYTextRange *)_correctedTextRange:(YYTextRange *)range {
+    if (!range) return nil;
+    if ([self _isTextRangeValid:range]) return range;
+    YYTextPosition *start = [self _correctedTextPosition:range.start];
+    YYTextPosition *end = [self _correctedTextPosition:range.end];
+    return [YYTextRange rangeWithStart:start end:end];
+}
+
+/// Convert the point from this view to text layout.
+- (CGPoint)_convertPointToLayout:(CGPoint)point {
+    CGSize boundingSize = _innerLayout.textBoundingSize;
+    if (_innerLayout.container.isVerticalForm) {
+        CGFloat w = _innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x += _innerLayout.container.size.width - w;
+        if (boundingSize.width < self.bounds.size.width) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.x += (self.bounds.size.width - boundingSize.width) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.x += (self.bounds.size.width - boundingSize.width);
+            }
+        }
+        return point;
+    } else {
+        if (boundingSize.height < self.bounds.size.height) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.y -= (self.bounds.size.height - boundingSize.height) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.y -= (self.bounds.size.height - boundingSize.height);
+            }
+        }
+        return point;
+    }
+}
+
+/// Convert the point from text layout to this view.
+- (CGPoint)_convertPointFromLayout:(CGPoint)point {
+    CGSize boundingSize = _innerLayout.textBoundingSize;
+    if (_innerLayout.container.isVerticalForm) {
+        CGFloat w = _innerLayout.textBoundingSize.width;
+        if (w < self.bounds.size.width) w = self.bounds.size.width;
+        point.x -= _innerLayout.container.size.width - w;
+        if (boundingSize.width < self.bounds.size.width) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.x -= (self.bounds.size.width - boundingSize.width) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.x -= (self.bounds.size.width - boundingSize.width);
+            }
+        }
+        return point;
+    } else {
+        if (boundingSize.height < self.bounds.size.height) {
+            if (_textVerticalAlignment == YYTextVerticalAlignmentCenter) {
+                point.y += (self.bounds.size.height - boundingSize.height) * 0.5;
+            } else if (_textVerticalAlignment == YYTextVerticalAlignmentBottom) {
+                point.y += (self.bounds.size.height - boundingSize.height);
+            }
+        }
+        return point;
+    }
+}
+
+/// Convert the rect from this view to text layout.
+- (CGRect)_convertRectToLayout:(CGRect)rect {
+    rect.origin = [self _convertPointToLayout:rect.origin];
+    return rect;
+}
+
+/// Convert the rect from text layout to this view.
+- (CGRect)_convertRectFromLayout:(CGRect)rect {
+    rect.origin = [self _convertPointFromLayout:rect.origin];
+    return rect;
+}
+
+/// Replace the range with the text, and change the `_selectTextRange`.
+/// The caller should make sure the `range` and `text` are valid before call this method.
+- (void)_replaceRange:(YYTextRange *)range withText:(NSString *)text notifyToDelegate:(BOOL)notify{
+    if (NSEqualRanges(range.asRange, _selectedTextRange.asRange)) {
+        if (notify) [_inputDelegate selectionWillChange:self];
+        NSRange newRange = NSMakeRange(0, 0);
+        newRange.location = _selectedTextRange.start.offset + text.length;
+        _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+        if (notify) [_inputDelegate selectionDidChange:self];
+    } else {
+        if (range.asRange.length != text.length) {
+            if (notify) [_inputDelegate selectionWillChange:self];
+            NSRange unionRange = NSIntersectionRange(_selectedTextRange.asRange, range.asRange);
+            if (unionRange.length == 0) {
+                // no intersection
+                if (range.end.offset <= _selectedTextRange.start.offset) {
+                    NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.location += ofs;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                }
+            } else if (unionRange.length == _selectedTextRange.asRange.length) {
+                // target range contains selected range
+                _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(range.start.offset + text.length, 0)];
+            } else if (range.start.offset >= _selectedTextRange.start.offset &&
+                       range.end.offset <= _selectedTextRange.end.offset) {
+                // target range inside selected range
+                NSInteger ofs = (NSInteger)text.length - (NSInteger)range.asRange.length;
+                NSRange newRange = _selectedTextRange.asRange;
+                newRange.length += ofs;
+                _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+            } else {
+                // interleaving
+                if (range.start.offset < _selectedTextRange.start.offset) {
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.location = range.start.offset + text.length;
+                    newRange.length -= unionRange.length;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                } else {
+                    NSRange newRange = _selectedTextRange.asRange;
+                    newRange.length -= unionRange.length;
+                    _selectedTextRange = [YYTextRange rangeWithRange:newRange];
+                }
+            }
+            _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+            if (notify) [_inputDelegate selectionDidChange:self];
+        }
+    }
+    if (notify) [_inputDelegate textWillChange:self];
+    NSRange newRange = NSMakeRange(range.asRange.location, text.length);
+    [_innerText replaceCharactersInRange:range.asRange withString:text];
+    [_innerText yy_removeDiscontinuousAttributesInRange:newRange];
+    if (notify) [_inputDelegate textDidChange:self];
+}
+
+/// Save current typing attributes to the attributes holder.
+- (void)_updateAttributesHolder {
+    if (_innerText.length > 0) {
+        NSUInteger index = _selectedTextRange.end.offset == 0 ? 0 : _selectedTextRange.end.offset - 1;
+        NSDictionary *attributes = [_innerText yy_attributesAtIndex:index];
+        if (!attributes) attributes = @{};
+        _typingAttributesHolder.yy_attributes = attributes;
+        [_typingAttributesHolder yy_removeDiscontinuousAttributesInRange:NSMakeRange(0, _typingAttributesHolder.length)];
+        [_typingAttributesHolder removeAttribute:YYTextBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
+        [_typingAttributesHolder removeAttribute:YYTextBackgroundBorderAttributeName range:NSMakeRange(0, _typingAttributesHolder.length)];
+    }
+}
+
+/// Update outer properties from current inner data.
+- (void)_updateOuterProperties {
+    [self _updateAttributesHolder];
+    NSParagraphStyle *style = _innerText.yy_paragraphStyle;
+    if (!style) style = _typingAttributesHolder.yy_paragraphStyle;
+    if (!style) style = [NSParagraphStyle defaultParagraphStyle];
+    
+    UIFont *font = _innerText.yy_font;
+    if (!font) font = _typingAttributesHolder.yy_font;
+    if (!font) font = [self _defaultFont];
+    
+    UIColor *color = _innerText.yy_color;
+    if (!color) color = _typingAttributesHolder.yy_color;
+    if (!color) color = [UIColor blackColor];
+    
+    [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
+    [self _setFont:font];
+    [self _setTextColor:color];
+    [self _setTextAlignment:style.alignment];
+    [self _setSelectedRange:_selectedTextRange.asRange];
+    [self _setTypingAttributes:_typingAttributesHolder.yy_attributes];
+    [self _setAttributedText:_innerText];
+}
+
+/// Parse text with `textParser` and update the _selectedTextRange.
+/// @return Whether changed (text or selection)
+- (BOOL)_parseText {
+    if (self.textParser) {
+        YYTextRange *oldTextRange = _selectedTextRange;
+        NSRange newRange = _selectedTextRange.asRange;
+        
+        [_inputDelegate textWillChange:self];
+        BOOL textChanged = [self.textParser parseText:_innerText selectedRange:&newRange];
+        [_inputDelegate textDidChange:self];
+        
+        YYTextRange *newTextRange = [YYTextRange rangeWithRange:newRange];
+        newTextRange = [self _correctedTextRange:newTextRange];
+        
+        if (![oldTextRange isEqual:newTextRange]) {
+            [_inputDelegate selectionWillChange:self];
+            _selectedTextRange = newTextRange;
+            [_inputDelegate selectionDidChange:self];
+        }
+        return textChanged;
+    }
+    return NO;
+}
+
+/// Returns whether the text should be detected by the data detector.
+- (BOOL)_shouldDetectText {
+    if (!_dataDetector) return NO;
+    if (!_highlightable) return NO;
+    if (_linkTextAttributes.count == 0 && _highlightTextAttributes.count == 0) return NO;
+    if (self.isFirstResponder || _containerView.isFirstResponder) return NO;
+    return YES;
+}
+
+/// Detect the data in text and add highlight to the data range.
+/// @return Whether detected.
+- (BOOL)_detectText:(NSMutableAttributedString *)text {
+    if (![self _shouldDetectText]) return NO;
+    if (text.length == 0) return NO;
+    __block BOOL detected = NO;
+    [_dataDetector enumerateMatchesInString:text.string options:kNilOptions range:NSMakeRange(0, text.length) usingBlock: ^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
+        switch (result.resultType) {
+            case NSTextCheckingTypeDate:
+            case NSTextCheckingTypeAddress:
+            case NSTextCheckingTypeLink:
+            case NSTextCheckingTypePhoneNumber: {
+                detected = YES;
+                if (_highlightTextAttributes.count) {
+                    YYTextHighlight *highlight = [YYTextHighlight highlightWithAttributes:_highlightTextAttributes];
+                    [text yy_setTextHighlight:highlight range:result.range];
+                }
+                if (_linkTextAttributes.count) {
+                    [_linkTextAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+                        [text yy_setAttribute:key value:obj range:result.range];
+                    }];
+                }
+            } break;
+            default:
+                break;
+        }
+    }];
+    return detected;
+}
+
+/// Returns the `root` view controller (returns nil if not found).
+- (UIViewController *)_getRootViewController {
+    UIViewController *ctrl = nil;
+    UIApplication *app = YYTextSharedApplication();
+    if (!ctrl) ctrl = app.keyWindow.rootViewController;
+    if (!ctrl) ctrl = [app.windows.firstObject rootViewController];
+    if (!ctrl) ctrl = self.yy_viewController;
+    if (!ctrl) return nil;
+    
+    while (!ctrl.view.window && ctrl.presentedViewController) {
+        ctrl = ctrl.presentedViewController;
+    }
+    if (!ctrl.view.window) return nil;
+    return ctrl;
+}
+
+/// Clear the undo and redo stack, and capture current state to undo stack.
+- (void)_resetUndoAndRedoStack {
+    [_undoStack removeAllObjects];
+    [_redoStack removeAllObjects];
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_undoStack addObject:object];
+}
+
+/// Clear the redo stack.
+- (void)_resetRedoStack {
+    [_redoStack removeAllObjects];
+}
+
+/// Capture current state to undo stack.
+- (void)_saveToUndoStack {
+    if (!_allowsUndoAndRedo) return;
+    _YYTextViewUndoObject *lastObject = _undoStack.lastObject;
+    if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
+    
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_undoStack addObject:object];
+    while (_undoStack.count > _maximumUndoLevel) {
+        [_undoStack removeObjectAtIndex:0];
+    }
+}
+
+/// Capture current state to redo stack.
+- (void)_saveToRedoStack {
+    if (!_allowsUndoAndRedo) return;
+    _YYTextViewUndoObject *lastObject = _redoStack.lastObject;
+    if ([lastObject.text isEqualToAttributedString:self.attributedText]) return;
+    
+    _YYTextViewUndoObject *object = [_YYTextViewUndoObject objectWithText:_innerText.copy range:_selectedTextRange.asRange];
+    [_redoStack addObject:object];
+    while (_redoStack.count > _maximumUndoLevel) {
+        [_redoStack removeObjectAtIndex:0];
+    }
+}
+
+- (BOOL)_canUndo {
+    if (_undoStack.count == 0) return NO;
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    if ([object.text isEqualToAttributedString:_innerText]) return NO;
+    return YES;
+}
+
+- (BOOL)_canRedo {
+    if (_redoStack.count == 0) return NO;
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    if ([object.text isEqualToAttributedString:_innerText]) return NO;
+    return YES;
+}
+
+- (void)_undo {
+    if (![self _canUndo]) return;
+    [self _saveToRedoStack];
+    _YYTextViewUndoObject *object = _undoStack.lastObject;
+    [_undoStack removeLastObject];
+    
+    _state.insideUndoBlock = YES;
+    self.attributedText = object.text;
+    self.selectedRange = object.selectedRange;
+    _state.insideUndoBlock = NO;
+}
+
+- (void)_redo {
+    if (![self _canRedo]) return;
+    [self _saveToUndoStack];
+    _YYTextViewUndoObject *object = _redoStack.lastObject;
+    [_redoStack removeLastObject];
+    
+    _state.insideUndoBlock = YES;
+    self.attributedText = object.text;
+    self.selectedRange = object.selectedRange;
+    _state.insideUndoBlock = NO;
+}
+
+- (void)_restoreFirstResponderAfterUndoAlert {
+    if (_state.firstResponderBeforeUndoAlert) {
+        [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+    }
+}
+
+/// Show undo alert if it can undo or redo.
+#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED
+- (void)_showUndoRedoAlert NS_EXTENSION_UNAVAILABLE_IOS(""){
+    _state.firstResponderBeforeUndoAlert = self.isFirstResponder;
+    __weak typeof(self) _self = self;
+    NSArray *strings = [self _localizedUndoStrings];
+    BOOL canUndo = [self _canUndo];
+    BOOL canRedo = [self _canRedo];
+    
+    UIViewController *ctrl = [self _getRootViewController];
+    
+    if (canUndo && canRedo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _undo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[2] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _redo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], strings[2], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    } else if (canUndo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[4] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[3] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _undo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[4] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[3], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    } else if (canRedo) {
+        if (kiOS8Later) {
+            UIAlertController *alert = [UIAlertController alertControllerWithTitle:strings[2] message:@"" preferredStyle:UIAlertControllerStyleAlert];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[1] style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
+                [_self _redo];
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [alert addAction:[UIAlertAction actionWithTitle:strings[0] style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
+                [_self _restoreFirstResponderAfterUndoAlert];
+            }]];
+            [ctrl presentViewController:alert animated:YES completion:nil];
+        } else {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:strings[2] message:@"" delegate:self cancelButtonTitle:strings[0] otherButtonTitles:strings[1], nil];
+            [alert show];
+#pragma clang diagnostic pop
+        }
+    }
+}
+#endif
+
+/// Get the localized undo alert strings based on app's main bundle.
+- (NSArray *)_localizedUndoStrings {
+    static NSArray *strings = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        NSDictionary *dic = @{
+            @"ar" : @[ @"����������", @"����������", @"���������� ��������������", @"����������", @"���������� ���� ��������������" ],
+            @"ca" : @[ @"Cancel��lar", @"Refer", @"Refer l���escriptura", @"Desfer", @"Desfer l���escriptura" ],
+            @"cs" : @[ @"Zru��it", @"Opakovat akci", @"Opakovat akci Ps��t", @"Odvolat akci", @"Odvolat akci Ps��t" ],
+            @"da" : @[ @"Annuller", @"Gentag", @"Gentag Indtastning", @"Fortryd", @"Fortryd Indtastning" ],
+            @"de" : @[ @"Abbrechen", @"Wiederholen", @"Eingabe wiederholen", @"Widerrufen", @"Eingabe widerrufen" ],
+            @"el" : @[ @"��������������", @"������������������", @"������������������ ����������������������������", @"����������������", @"���������������� ����������������������������" ],
+            @"en" : @[ @"Cancel", @"Redo", @"Redo Typing", @"Undo", @"Undo Typing" ],
+            @"es" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
+            @"es_MX" : @[ @"Cancelar", @"Rehacer", @"Rehacer escritura", @"Deshacer", @"Deshacer escritura" ],
+            @"fi" : @[ @"Kumoa", @"Tee sittenkin", @"Kirjoita sittenkin", @"Peru", @"Peru kirjoitus" ],
+            @"fr" : @[ @"Annuler", @"R��tablir", @"R��tablir la saisie", @"Annuler", @"Annuler la saisie" ],
+            @"he" : @[ @"����������", @"�������� ���� ������������ ��������������", @"�������� ���� ����������", @"������", @"������ ����������" ],
+            @"hr" : @[ @"Odustani", @"Ponovi", @"Ponovno upi��i", @"Poni��ti", @"Poni��ti upisivanje" ],
+            @"hu" : @[ @"M��gsem", @"Ism��tl��s", @"G��pel��s ism��tl��se", @"Visszavon��s", @"G��pel��s visszavon��sa" ],
+            @"id" : @[ @"Batalkan", @"Ulang", @"Ulang Pengetikan", @"Kembalikan", @"Batalkan Pengetikan" ],
+            @"it" : @[ @"Annulla", @"Ripristina originale", @"Ripristina Inserimento", @"Annulla", @"Annulla Inserimento" ],
+            @"ja" : @[ @"���������������", @"������������", @"������������ - ������", @"������������", @"������������ - ������" ],
+            @"ko" : @[ @"������", @"������ ������", @"������ ������", @"������ ������", @"������ ������ ������" ],
+            @"ms" : @[ @"Batal", @"Buat semula", @"Ulang Penaipan", @"Buat asal", @"Buat asal Penaipan" ],
+            @"nb" : @[ @"Avbryt", @"Utf��r likevel", @"Utf��r skriving likevel", @"Angre", @"Angre skriving" ],
+            @"nl" : @[ @"Annuleer", @"Opnieuw", @"Opnieuw typen", @"Herstel", @"Herstel typen" ],
+            @"pl" : @[ @"Anuluj", @"Przywr����", @"Przywr���� Wpisz", @"Cofnij", @"Cofnij Wpisz" ],
+            @"pt" : @[ @"Cancelar", @"Refazer", @"Refazer Digita����o", @"Desfazer", @"Desfazer Digita����o" ],
+            @"pt_PT" : @[ @"Cancelar", @"Refazer", @"Refazer digitar", @"Desfazer", @"Desfazer digitar" ],
+            @"ro" : @[ @"Renun����", @"Ref��", @"Ref�� tastare", @"Anuleaz��", @"Anuleaz�� tastare" ],
+            @"ru" : @[ @"����������������", @"������������������", @"������������������ ���������� ���� ��������������������", @"����������������", @"���������������� ���������� ���� ��������������������" ],
+            @"sk" : @[ @"Zru��i��", @"Obnovi��", @"Obnovi�� p��sanie", @"Odvola��", @"Odvola�� p��sanie" ],
+            @"sv" : @[ @"Avbryt", @"G��r om", @"G��r om skriven text", @"��ngra", @"��ngra skriven text" ],
+            @"th" : @[ @"������������������", @"������������������������������������", @"������������������������������������������", @"������������������", @"������������������������" ],
+            @"tr" : @[ @"Vazge��", @"Yinele", @"Yazmay�� Yinele", @"Geri Al", @"Yazmay�� Geri Al" ],
+            @"uk" : @[ @"������������������", @"������������������", @"������������������ ����������������", @"������������������", @"������������������ ����������������" ],
+            @"vi" : @[ @"H���y", @"L��m l���i", @"L��m l���i thao t��c Nh���p", @"Ho��n t��c", @"Ho��n t��c thao t��c Nh���p" ],
+            @"zh" : @[ @"������", @"������", @"������������", @"������", @"������������" ],
+            @"zh_CN" : @[ @"������", @"������", @"������������", @"������", @"������������" ],
+            @"zh_HK" : @[ @"������", @"������", @"������������", @"������", @"������������" ],
+            @"zh_TW" : @[ @"������", @"������", @"������������", @"������", @"������������" ]
+        };
+        NSString *preferred = [[NSBundle mainBundle] preferredLocalizations].firstObject;
+        if (preferred.length == 0) preferred = @"English";
+        NSString *canonical = [NSLocale canonicalLocaleIdentifierFromString:preferred];
+        if (canonical.length == 0) canonical = @"en";
+        strings = dic[canonical];
+        if (!strings  && ([canonical rangeOfString:@"_"].location != NSNotFound)) {
+            NSString *prefix = [canonical componentsSeparatedByString:@"_"].firstObject;
+            if (prefix.length) strings = dic[prefix];
+        }
+        if (!strings) strings = dic[@"en"];
+    });
+    return strings;
+}
+
+/// Returns the default font for text view (same as CoreText).
+- (UIFont *)_defaultFont {
+    return [UIFont systemFontOfSize:12];
+}
+
+/// Returns the default tint color for text view (used for caret and select range background).
+- (UIColor *)_defaultTintColor {
+    return [UIColor colorWithRed:69/255.0 green:111/255.0 blue:238/255.0 alpha:1];
+}
+
+/// Returns the default placeholder color for text view (same as UITextField).
+- (UIColor *)_defaultPlaceholderColor {
+    return [UIColor colorWithRed:0 green:0 blue:25/255.0 alpha:44/255.0];
+}
+
+#pragma mark - Private Setter
+
+- (void)_setText:(NSString *)text {
+    if (_text == text || [_text isEqualToString:text]) return;
+    [self willChangeValueForKey:@"text"];
+    _text = text.copy;
+    if (!_text) _text = @"";
+    [self didChangeValueForKey:@"text"];
+    self.accessibilityLabel = _text;
+}
+
+- (void)_setFont:(UIFont *)font {
+    if (_font == font || [_font isEqual:font]) return;
+    [self willChangeValueForKey:@"font"];
+    _font = font;
+    [self didChangeValueForKey:@"font"];
+}
+
+- (void)_setTextColor:(UIColor *)textColor {
+    if (_textColor == textColor) return;
+    if (_textColor && textColor) {
+        if (CFGetTypeID(_textColor.CGColor) == CFGetTypeID(textColor.CGColor) &&
+            CFGetTypeID(_textColor.CGColor) == CGColorGetTypeID()) {
+            if ([_textColor isEqual:textColor]) {
+                return;
+            }
+        }
+    }
+    [self willChangeValueForKey:@"textColor"];
+    _textColor = textColor;
+    [self didChangeValueForKey:@"textColor"];
+}
+
+- (void)_setTextAlignment:(NSTextAlignment)textAlignment {
+    if (_textAlignment == textAlignment) return;
+    [self willChangeValueForKey:@"textAlignment"];
+    _textAlignment = textAlignment;
+    [self didChangeValueForKey:@"textAlignment"];
+}
+
+- (void)_setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
+    if (_dataDetectorTypes == dataDetectorTypes) return;
+    [self willChangeValueForKey:@"dataDetectorTypes"];
+    _dataDetectorTypes = dataDetectorTypes;
+    [self didChangeValueForKey:@"dataDetectorTypes"];
+}
+
+- (void)_setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
+    if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
+    [self willChangeValueForKey:@"linkTextAttributes"];
+    _linkTextAttributes = linkTextAttributes.copy;
+    [self didChangeValueForKey:@"linkTextAttributes"];
+}
+
+- (void)_setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
+    if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
+    [self willChangeValueForKey:@"highlightTextAttributes"];
+    _highlightTextAttributes = highlightTextAttributes.copy;
+    [self didChangeValueForKey:@"highlightTextAttributes"];
+}
+- (void)_setTextParser:(id<YYTextParser>)textParser {
+    if (_textParser == textParser || [_textParser isEqual:textParser]) return;
+    [self willChangeValueForKey:@"textParser"];
+    _textParser = textParser;
+    [self didChangeValueForKey:@"textParser"];
+}
+
+- (void)_setAttributedText:(NSAttributedString *)attributedText {
+    if (_attributedText == attributedText || [_attributedText isEqual:attributedText]) return;
+    [self willChangeValueForKey:@"attributedText"];
+    _attributedText = attributedText.copy;
+    if (!_attributedText) _attributedText = [NSAttributedString new];
+    [self didChangeValueForKey:@"attributedText"];
+}
+
+- (void)_setTextContainerInset:(UIEdgeInsets)textContainerInset {
+    if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
+    [self willChangeValueForKey:@"textContainerInset"];
+    _textContainerInset = textContainerInset;
+    [self didChangeValueForKey:@"textContainerInset"];
+}
+
+- (void)_setExclusionPaths:(NSArray *)exclusionPaths {
+    if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
+    [self willChangeValueForKey:@"exclusionPaths"];
+    _exclusionPaths = exclusionPaths.copy;
+    [self didChangeValueForKey:@"exclusionPaths"];
+}
+
+- (void)_setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm == verticalForm) return;
+    [self willChangeValueForKey:@"verticalForm"];
+    _verticalForm = verticalForm;
+    [self didChangeValueForKey:@"verticalForm"];
+}
+
+- (void)_setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    if (_linePositionModifier == linePositionModifier) return;
+    [self willChangeValueForKey:@"linePositionModifier"];
+    _linePositionModifier = [(NSObject *)linePositionModifier copy];
+    [self didChangeValueForKey:@"linePositionModifier"];
+}
+
+- (void)_setSelectedRange:(NSRange)selectedRange {
+    if (NSEqualRanges(_selectedRange, selectedRange)) return;
+    [self willChangeValueForKey:@"selectedRange"];
+    _selectedRange = selectedRange;
+    [self didChangeValueForKey:@"selectedRange"];
+    if ([self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
+        [self.delegate textViewDidChangeSelection:self];
+    }
+}
+
+- (void)_setTypingAttributes:(NSDictionary *)typingAttributes {
+    if (_typingAttributes == typingAttributes || [_typingAttributes isEqual:typingAttributes]) return;
+    [self willChangeValueForKey:@"typingAttributes"];
+    _typingAttributes = typingAttributes.copy;
+    [self didChangeValueForKey:@"typingAttributes"];
+}
+
+#pragma mark - Private Init
+
+- (void)_initTextView {
+    self.delaysContentTouches = NO;
+    self.canCancelContentTouches = YES;
+    self.multipleTouchEnabled = NO;
+    self.clipsToBounds = YES;
+    [super setDelegate:self];
+    
+    _text = @"";
+    _attributedText = [NSAttributedString new];
+    
+    // UITextInputTraits
+    _autocapitalizationType = UITextAutocapitalizationTypeSentences;
+    _autocorrectionType = UITextAutocorrectionTypeDefault;
+    _spellCheckingType = UITextSpellCheckingTypeDefault;
+    _keyboardType = UIKeyboardTypeDefault;
+    _keyboardAppearance = UIKeyboardAppearanceDefault;
+    _returnKeyType = UIReturnKeyDefault;
+    _enablesReturnKeyAutomatically = NO;
+    _secureTextEntry = NO;
+    
+    // UITextInput
+    _selectedTextRange = [YYTextRange defaultRange];
+    _markedTextRange = nil;
+    _markedTextStyle = nil;
+    _tokenizer = [[UITextInputStringTokenizer alloc] initWithTextInput:self];
+    
+    _editable = YES;
+    _selectable = YES;
+    _highlightable = YES;
+    _allowsCopyAttributedString = YES;
+    _textAlignment = NSTextAlignmentNatural;
+    
+    _innerText = [NSMutableAttributedString new];
+    _innerContainer = [YYTextContainer new];
+    _innerContainer.insets = kDefaultInset;
+    _textContainerInset = kDefaultInset;
+    _typingAttributesHolder = [[NSMutableAttributedString alloc] initWithString:@" "];
+    _linkTextAttributes = @{NSForegroundColorAttributeName : [self _defaultTintColor],
+                            (id)kCTForegroundColorAttributeName : (id)[self _defaultTintColor].CGColor};
+    
+    YYTextHighlight *highlight = [YYTextHighlight new];
+    YYTextBorder * border = [YYTextBorder new];
+    border.insets = UIEdgeInsetsMake(-2, -2, -2, -2);
+    border.fillColor = [UIColor colorWithWhite:0.1 alpha:0.2];
+    border.cornerRadius = 3;
+    [highlight setBorder:border];
+    _highlightTextAttributes = highlight.attributes.copy;
+    
+    _placeHolderView = [UIImageView new];
+    _placeHolderView.userInteractionEnabled = NO;
+    _placeHolderView.hidden = YES;
+    
+    _containerView = [YYTextContainerView new];
+    _containerView.hostView = self;
+    
+    _selectionView = [YYTextSelectionView new];
+    _selectionView.userInteractionEnabled = NO;
+    _selectionView.hostView = self;
+    _selectionView.color = [self _defaultTintColor];
+    
+    _magnifierCaret = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeCaret];
+    _magnifierCaret.hostView = _containerView;
+    _magnifierRanged = [YYTextMagnifier magnifierWithType:YYTextMagnifierTypeRanged];
+    _magnifierRanged.hostView = _containerView;
+    
+    [self addSubview:_placeHolderView];
+    [self addSubview:_containerView];
+    [self addSubview:_selectionView];
+    
+    _undoStack = [NSMutableArray new];
+    _redoStack = [NSMutableArray new];
+    _allowsUndoAndRedo = YES;
+    _maximumUndoLevel = kDefaultUndoLevelMax;
+    
+    self.debugOption = [YYTextDebugOption sharedDebugOption];
+    [YYTextDebugOption addDebugTarget:self];
+    
+    [self _updateInnerContainerSize];
+    [self _update];
+    
+    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_pasteboardChanged) name:UIPasteboardChangedNotification object:nil];
+    [[YYTextKeyboardManager defaultManager] addObserver:self];
+    
+    self.isAccessibilityElement = YES;
+}
+
+#pragma mark - Public
+
+- (instancetype)initWithFrame:(CGRect)frame {
+    self = [super initWithFrame:frame];
+    if (!self) return nil;
+    [self _initTextView];
+    return self;
+}
+
+- (void)dealloc {
+    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIPasteboardChangedNotification object:nil];
+    [[YYTextKeyboardManager defaultManager] removeObserver:self];
+    
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierCaret];
+    [[YYTextEffectWindow sharedWindow] hideMagnifier:_magnifierRanged];
+    
+    [YYTextDebugOption removeDebugTarget:self];
+    
+    [_longPressTimer invalidate];
+    [_autoScrollTimer invalidate];
+    [_selectionDotFixTimer invalidate];
+}
+
+- (void)scrollRangeToVisible:(NSRange)range {
+    YYTextRange *textRange = [YYTextRange rangeWithRange:range];
+    textRange = [self _correctedTextRange:textRange];
+    [self _scrollRangeToVisible:textRange];
+}
+
+#pragma mark - Property
+
+- (void)setText:(NSString *)text {
+    if (_text == text || [_text isEqualToString:text]) return;
+    [self _setText:text];
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    [self _resetUndoAndRedoStack];
+    [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:text];
+}
+
+- (void)setFont:(UIFont *)font {
+    if (_font == font || [_font isEqual:font]) return;
+    [self _setFont:font];
+    
+    _state.typingAttributesOnce = NO;
+    _typingAttributesHolder.yy_font = font;
+    _innerText.yy_font = font;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTextColor:(UIColor *)textColor {
+    if (_textColor == textColor || [_textColor isEqual:textColor]) return;
+    [self _setTextColor:textColor];
+    
+    _state.typingAttributesOnce = NO;
+    _typingAttributesHolder.yy_color = textColor;
+    _innerText.yy_color = textColor;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTextAlignment:(NSTextAlignment)textAlignment {
+    if (_textAlignment == textAlignment) return;
+    [self _setTextAlignment:textAlignment];
+    
+    _typingAttributesHolder.yy_alignment = textAlignment;
+    _innerText.yy_alignment = textAlignment;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setDataDetectorTypes:(UIDataDetectorTypes)dataDetectorTypes {
+    if (_dataDetectorTypes == dataDetectorTypes) return;
+    [self _setDataDetectorTypes:dataDetectorTypes];
+    NSTextCheckingType type = YYTextNSTextCheckingTypeFromUIDataDetectorType(dataDetectorTypes);
+    _dataDetector = type ? [NSDataDetector dataDetectorWithTypes:type error:NULL] : nil;
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setLinkTextAttributes:(NSDictionary *)linkTextAttributes {
+    if (_linkTextAttributes == linkTextAttributes || [_linkTextAttributes isEqual:linkTextAttributes]) return;
+    [self _setLinkTextAttributes:linkTextAttributes];
+    if (_dataDetector) {
+        [self _commitUpdate];
+    }
+}
+
+- (void)setHighlightTextAttributes:(NSDictionary *)highlightTextAttributes {
+    if (_highlightTextAttributes == highlightTextAttributes || [_highlightTextAttributes isEqual:highlightTextAttributes]) return;
+    [self _setHighlightTextAttributes:highlightTextAttributes];
+    if (_dataDetector) {
+        [self _commitUpdate];
+    }
+}
+
+- (void)setTextParser:(id<YYTextParser>)textParser {
+    if (_textParser == textParser || [_textParser isEqual:textParser]) return;
+    [self _setTextParser:textParser];
+    if (textParser && _text.length) {
+        [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _text.length)] withText:_text];
+    }
+    [self _resetUndoAndRedoStack];
+    [self _commitUpdate];
+}
+
+- (void)setTypingAttributes:(NSDictionary *)typingAttributes {
+    [self _setTypingAttributes:typingAttributes];
+    _state.typingAttributesOnce = YES;
+    [typingAttributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+        [_typingAttributesHolder yy_setAttribute:key value:obj];
+    }];
+    [self _commitUpdate];
+}
+
+- (void)setAttributedText:(NSAttributedString *)attributedText {
+    if (_attributedText == attributedText) return;
+    [self _setAttributedText:attributedText];
+    _state.typingAttributesOnce = NO;
+    
+    NSMutableAttributedString *text = attributedText.mutableCopy;
+    if (text.length == 0) {
+        [self replaceRange:[YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)] withText:@""];
+        return;
+    }
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:NSMakeRange(0, _innerText.length) replacementText:text.string];
+        if (!should) return;
+    }
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    [_inputDelegate selectionWillChange:self];
+    [_inputDelegate textWillChange:self];
+     _innerText = text;
+    [self _parseText];
+    _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    [_inputDelegate textDidChange:self];
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _setAttributedText:text];
+    if (_innerText.length > 0) {
+        _typingAttributesHolder.yy_attributes = [_innerText yy_attributesAtIndex:_innerText.length - 1];
+    }
+    
+    [self _updateOuterProperties];
+    [self _updateLayout];
+    [self _updateSelectionView];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    if (!_state.insideUndoBlock) {
+        [self _resetUndoAndRedoStack];
+    }
+}
+
+- (void)setTextVerticalAlignment:(YYTextVerticalAlignment)textVerticalAlignment {
+    if (_textVerticalAlignment == textVerticalAlignment) return;
+    [self willChangeValueForKey:@"textVerticalAlignment"];
+    _textVerticalAlignment = textVerticalAlignment;
+    [self didChangeValueForKey:@"textVerticalAlignment"];
+    _containerView.textVerticalAlignment = textVerticalAlignment;
+    [self _commitUpdate];
+}
+
+- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset {
+    if (UIEdgeInsetsEqualToEdgeInsets(_textContainerInset, textContainerInset)) return;
+    [self _setTextContainerInset:textContainerInset];
+    _innerContainer.insets = textContainerInset;
+    [self _commitUpdate];
+}
+
+- (void)setExclusionPaths:(NSArray *)exclusionPaths {
+    if (_exclusionPaths == exclusionPaths || [_exclusionPaths isEqual:exclusionPaths]) return;
+    [self _setExclusionPaths:exclusionPaths];
+    _innerContainer.exclusionPaths = exclusionPaths;
+    if (_innerContainer.isVerticalForm) {
+        CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
+        [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
+            [path applyTransform:trans];
+        }];
+    }
+    [self _commitUpdate];
+}
+
+- (void)setVerticalForm:(BOOL)verticalForm {
+    if (_verticalForm == verticalForm) return;
+    [self _setVerticalForm:verticalForm];
+    _innerContainer.verticalForm = verticalForm;
+    _selectionView.verticalForm = verticalForm;
+    
+    [self _updateInnerContainerSize];
+    
+    if (verticalForm) {
+        if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultInset)) {
+            _innerContainer.insets = kDefaultVerticalInset;
+            [self _setTextContainerInset:kDefaultVerticalInset];
+        }
+    } else {
+        if (UIEdgeInsetsEqualToEdgeInsets(_innerContainer.insets, kDefaultVerticalInset)) {
+            _innerContainer.insets = kDefaultInset;
+            [self _setTextContainerInset:kDefaultInset];
+        }
+    }
+    
+    _innerContainer.exclusionPaths = _exclusionPaths;
+    if (verticalForm) {
+        CGAffineTransform trans = CGAffineTransformMakeTranslation(_innerContainer.size.width - self.bounds.size.width, 0);
+        [_innerContainer.exclusionPaths enumerateObjectsUsingBlock:^(UIBezierPath *path, NSUInteger idx, BOOL *stop) {
+            [path applyTransform:trans];
+        }];
+    }
+    
+    [self _keyboardChanged];
+    [self _commitUpdate];
+}
+
+- (void)setLinePositionModifier:(id<YYTextLinePositionModifier>)linePositionModifier {
+    if (_linePositionModifier == linePositionModifier) return;
+    [self _setLinePositionModifier:linePositionModifier];
+    _innerContainer.linePositionModifier = linePositionModifier;
+    [self _commitUpdate];
+}
+
+- (void)setSelectedRange:(NSRange)selectedRange {
+    if (NSEqualRanges(_selectedRange, selectedRange)) return;
+    if (_markedTextRange) return;
+    _state.typingAttributesOnce = NO;
+    
+    YYTextRange *range = [YYTextRange rangeWithRange:selectedRange];
+    range = [self _correctedTextRange:range];
+    [self _endTouchTracking];
+    _selectedTextRange = range;
+    [self _updateSelectionView];
+    
+    [self _setSelectedRange:range.asRange];
+    
+    if (!_state.insideUndoBlock) {
+        [self _resetUndoAndRedoStack];
+    }
+}
+
+- (void)setHighlightable:(BOOL)highlightable {
+    if (_highlightable == highlightable) return;
+    [self willChangeValueForKey:@"highlightable"];
+    _highlightable = highlightable;
+    [self didChangeValueForKey:@"highlightable"];
+    [self _commitUpdate];
+}
+
+- (void)setEditable:(BOOL)editable {
+    if (_editable == editable) return;
+    [self willChangeValueForKey:@"editable"];
+    _editable = editable;
+    [self didChangeValueForKey:@"editable"];
+    if (!editable) {
+        [self resignFirstResponder];
+    }
+}
+
+- (void)setSelectable:(BOOL)selectable {
+    if (_selectable == selectable) return;
+    [self willChangeValueForKey:@"selectable"];
+    _selectable = selectable;
+    [self didChangeValueForKey:@"selectable"];
+    if (!selectable) {
+        if (self.isFirstResponder) {
+            [self resignFirstResponder];
+        } else {
+            _state.selectedWithoutEdit = NO;
+            [self _endTouchTracking];
+            [self _hideMenu];
+            [self _updateSelectionView];
+        }
+    }
+}
+
+- (void)setClearsOnInsertion:(BOOL)clearsOnInsertion {
+    if (_clearsOnInsertion == clearsOnInsertion) return;
+    _clearsOnInsertion = clearsOnInsertion;
+    if (clearsOnInsertion) {
+        if (self.isFirstResponder) {
+            self.selectedRange = NSMakeRange(0, _attributedText.length);
+        } else {
+            _state.clearsOnInsertionOnce = YES;
+        }
+    }
+}
+
+- (void)setDebugOption:(YYTextDebugOption *)debugOption {
+    _containerView.debugOption = debugOption;
+}
+
+- (YYTextDebugOption *)debugOption {
+    return _containerView.debugOption;
+}
+
+- (YYTextLayout *)textLayout {
+    [self _updateIfNeeded];
+    return _innerLayout;
+}
+
+- (void)setPlaceholderText:(NSString *)placeholderText {
+    if (_placeholderAttributedText.length > 0) {
+        if (placeholderText.length > 0) {
+            [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:placeholderText];
+        } else {
+            [((NSMutableAttributedString *)_placeholderAttributedText) replaceCharactersInRange:NSMakeRange(0, _placeholderAttributedText.length) withString:@""];
+        }
+        ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
+        ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
+    } else {
+        if (placeholderText.length > 0) {
+            NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:placeholderText];
+            if (!_placeholderFont) _placeholderFont = _font;
+            if (!_placeholderFont) _placeholderFont = [self _defaultFont];
+            if (!_placeholderTextColor) _placeholderTextColor = [self _defaultPlaceholderColor];
+            atr.yy_font = _placeholderFont;
+            atr.yy_color = _placeholderTextColor;
+            _placeholderAttributedText = atr;
+        }
+    }
+    _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderFont:(UIFont *)placeholderFont {
+    _placeholderFont = placeholderFont;
+    ((NSMutableAttributedString *)_placeholderAttributedText).yy_font = _placeholderFont;
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor {
+    _placeholderTextColor = placeholderTextColor;
+    ((NSMutableAttributedString *)_placeholderAttributedText).yy_color = _placeholderTextColor;
+    [self _commitPlaceholderUpdate];
+}
+
+- (void)setPlaceholderAttributedText:(NSAttributedString *)placeholderAttributedText {
+    _placeholderAttributedText = placeholderAttributedText.mutableCopy;
+    _placeholderText = [_placeholderAttributedText yy_plainTextForRange:NSMakeRange(0, _placeholderAttributedText.length)];
+    _placeholderFont = _placeholderAttributedText.yy_font;
+    _placeholderTextColor = _placeholderAttributedText.yy_color;
+    [self _commitPlaceholderUpdate];
+}
+
+#pragma mark - Override For Protect
+
+- (void)setMultipleTouchEnabled:(BOOL)multipleTouchEnabled {
+    [super setMultipleTouchEnabled:NO]; // must not enabled
+}
+
+- (void)setContentInset:(UIEdgeInsets)contentInset {
+    UIEdgeInsets oldInsets = self.contentInset;
+    if (_insetModifiedByKeyboard) {
+        _originalContentInset = contentInset;
+    } else {
+        [super setContentInset:contentInset];
+        BOOL changed = !UIEdgeInsetsEqualToEdgeInsets(oldInsets, contentInset);
+        if (changed) {
+            [self _updateInnerContainerSize];
+            [self _commitUpdate];
+            [self _commitPlaceholderUpdate];
+        }
+    }
+}
+
+- (void)setScrollIndicatorInsets:(UIEdgeInsets)scrollIndicatorInsets {
+    if (_insetModifiedByKeyboard) {
+        _originalScrollIndicatorInsets = scrollIndicatorInsets;
+    } else {
+        [super setScrollIndicatorInsets:scrollIndicatorInsets];
+    }
+}
+
+- (void)setFrame:(CGRect)frame {
+    CGSize oldSize = self.bounds.size;
+    [super setFrame:frame];
+    CGSize newSize = self.bounds.size;
+    BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
+    if (changed) {
+        [self _updateInnerContainerSize];
+        [self _commitUpdate];
+    }
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        [self _commitPlaceholderUpdate];
+    }
+}
+
+- (void)setBounds:(CGRect)bounds {
+    CGSize oldSize = self.bounds.size;
+    [super setBounds:bounds];
+    CGSize newSize = self.bounds.size;
+    BOOL changed = _innerContainer.isVerticalForm ? (oldSize.height != newSize.height) : (oldSize.width != newSize.width);
+    if (changed) {
+        [self _updateInnerContainerSize];
+        [self _commitUpdate];
+    }
+    if (!CGSizeEqualToSize(oldSize, newSize)) {
+        [self _commitPlaceholderUpdate];
+    }
+}
+
+- (void)tintColorDidChange {
+    if ([self respondsToSelector:@selector(tintColor)]) {
+        UIColor *color = self.tintColor;
+        NSMutableDictionary *attrs = _highlightTextAttributes.mutableCopy;
+        NSMutableDictionary *linkAttrs = _linkTextAttributes.mutableCopy;
+        if (!linkAttrs) linkAttrs = @{}.mutableCopy;
+        if (!color) {
+            [attrs removeObjectForKey:NSForegroundColorAttributeName];
+            [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName];
+            [linkAttrs setObject:[self _defaultTintColor] forKey:NSForegroundColorAttributeName];
+            [linkAttrs setObject:(id)[self _defaultTintColor].CGColor forKey:(id)kCTForegroundColorAttributeName];
+        } else {
+            [attrs setObject:color forKey:NSForegroundColorAttributeName];
+            [attrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
+            [linkAttrs setObject:color forKey:NSForegroundColorAttributeName];
+            [linkAttrs setObject:(id)color.CGColor forKey:(id)kCTForegroundColorAttributeName];
+        }
+        self.highlightTextAttributes = attrs;
+        _selectionView.color = color ? color : [self _defaultTintColor];
+        _linkTextAttributes = linkAttrs;
+        [self _commitUpdate];
+    }
+}
+
+- (CGSize)sizeThatFits:(CGSize)size {
+    if (!_verticalForm && size.width <= 0) size.width = YYTextContainerMaxSize.width;
+    if (_verticalForm && size.height <= 0) size.height = YYTextContainerMaxSize.height;
+    
+    if ((!_verticalForm && size.width == self.bounds.size.width) ||
+        (_verticalForm && size.height == self.bounds.size.height)) {
+        [self _updateIfNeeded];
+        if (!_verticalForm) {
+            if (_containerView.bounds.size.height <= size.height) {
+                return _containerView.bounds.size;
+            }
+        } else {
+            if (_containerView.bounds.size.width <= size.width) {
+                return _containerView.bounds.size;
+            }
+        }
+    }
+    
+    if (!_verticalForm) {
+        size.height = YYTextContainerMaxSize.height;
+    } else {
+        size.width = YYTextContainerMaxSize.width;
+    }
+    
+    YYTextContainer *container = [_innerContainer copy];
+    container.size = size;
+    
+    YYTextLayout *layout = [YYTextLayout layoutWithContainer:container text:_innerText];
+    return layout.textBoundingSize;
+}
+
+#pragma mark - Override UIResponder
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _touchBeganTime = _trackingTime = touch.timestamp;
+    _touchBeganPoint = _trackingPoint = point;
+    _trackingRange = _selectedTextRange;
+    
+    _state.trackingGrabber = NO;
+    _state.trackingCaret = NO;
+    _state.trackingPreSelect = NO;
+    _state.trackingTouch = YES;
+    _state.swallowTouch = YES;
+    _state.touchMoved = NO;
+    
+    if (!self.isFirstResponder && !_state.selectedWithoutEdit && self.highlightable) {
+        _highlight = [self _getHighlightAtPoint:point range:&_highlightRange];
+        _highlightLayout = nil;
+    }
+    
+    if ((!self.selectable && !_highlight) || _state.ignoreTouchBegan) {
+        _state.swallowTouch = NO;
+        _state.trackingTouch = NO;
+    }
+    
+    if (_state.trackingTouch) {
+        [self _startLongPressTimer];
+        if (_highlight) {
+            [self _showHighlightAnimated:NO];
+        } else {
+            if ([_selectionView isGrabberContainsPoint:point]) { // track grabber
+                self.panGestureRecognizer.enabled = NO; // disable scroll view
+                [self _hideMenu];
+                _state.trackingGrabber = [_selectionView isStartGrabberContainsPoint:point] ? kStart : kEnd;
+                _magnifierRangedOffset = [self _getMagnifierRangedOffset];
+            } else {
+                if (_selectedTextRange.asRange.length == 0 && self.isFirstResponder) {
+                    if ([_selectionView isCaretContainsPoint:point]) { // track caret
+                        _state.trackingCaret = YES;
+                        self.panGestureRecognizer.enabled = NO; // disable scroll view
+                    }
+                }
+            }
+        }
+        [self _updateSelectionView];
+    }
+    
+    if (!_state.swallowTouch) [super touchesBegan:touches withEvent:event];
+}
+
+- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _trackingTime = touch.timestamp;
+    _trackingPoint = point;
+    
+    if (!_state.touchMoved) {
+        _state.touchMoved = [self _getMoveDirection];
+        if (_state.touchMoved) [self _endLongPressTimer];
+    }
+    _state.clearsOnInsertionOnce = NO;
+    
+    if (_state.trackingTouch) {
+        BOOL showMagnifierCaret = NO;
+        BOOL showMagnifierRanged = NO;
+        
+        if (_highlight) {
+            
+            YYTextHighlight *highlight = [self _getHighlightAtPoint:_trackingPoint range:NULL];
+            if (highlight == _highlight) {
+                [self _showHighlightAnimated:YES];
+            } else {
+                [self _hideHighlightAnimated:YES];
+            }
+            
+        } else {
+            _trackingRange = _selectedTextRange;
+            if (_state.trackingGrabber) {
+                self.panGestureRecognizer.enabled = NO;
+                [self _hideMenu];
+                [self _updateTextRangeByTrackingGrabber];
+                showMagnifierRanged = YES;
+            } else if (_state.trackingPreSelect) {
+                [self _updateTextRangeByTrackingPreSelect];
+                showMagnifierCaret = YES;
+            } else if (_state.trackingCaret || _markedTextRange || self.isFirstResponder) {
+                if (_state.trackingCaret || _state.touchMoved) {
+                    _state.trackingCaret = YES;
+                    [self _hideMenu];
+                    if (_verticalForm) {
+                        if (_state.touchMoved == kTop || _state.touchMoved == kBottom) {
+                            self.panGestureRecognizer.enabled = NO;
+                        }
+                    } else {
+                        if (_state.touchMoved == kLeft || _state.touchMoved == kRight) {
+                            self.panGestureRecognizer.enabled = NO;
+                        }
+                    }
+                    [self _updateTextRangeByTrackingCaret];
+                    if (_markedTextRange) {
+                        showMagnifierRanged = YES;
+                    } else {
+                        showMagnifierCaret = YES;
+                    }
+                }
+            }
+        }
+        [self _updateSelectionView];
+        if (showMagnifierCaret) [self _showMagnifierCaret];
+        if (showMagnifierRanged) [self _showMagnifierRanged];
+    }
+    
+    CGFloat autoScrollOffset = [self _getAutoscrollOffset];
+    if (_autoScrollOffset != autoScrollOffset) {
+        if (fabs(autoScrollOffset) < fabs(_autoScrollOffset)) {
+            _autoScrollAcceleration *= 0.5;
+        }
+        _autoScrollOffset = autoScrollOffset;
+        if (_autoScrollOffset != 0 && _state.touchMoved) {
+            [self _startAutoScrollTimer];
+        }
+    }
+    
+    if (!_state.swallowTouch) [super touchesMoved:touches withEvent:event];
+}
+
+- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _updateIfNeeded];
+    
+    UITouch *touch = touches.anyObject;
+    CGPoint point = [touch locationInView:_containerView];
+    
+    _trackingTime = touch.timestamp;
+    _trackingPoint = point;
+    
+    if (!_state.touchMoved) {
+        _state.touchMoved = [self _getMoveDirection];
+    }
+    if (_state.trackingTouch) {
+        [self _hideMagnifier];
+        
+        if (_highlight) {
+            if (_state.showingHighlight) {
+                if (_highlight.tapAction) {
+                    CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                    rect = [self _convertRectFromLayout:rect];
+                    _highlight.tapAction(self, _innerText, _highlightRange, rect);
+                } else {
+                    BOOL shouldTap = YES;
+                    if ([self.delegate respondsToSelector:@selector(textView:shouldTapHighlight:inRange:)]) {
+                        shouldTap = [self.delegate textView:self shouldTapHighlight:_highlight inRange:_highlightRange];
+                    }
+                    if (shouldTap && [self.delegate respondsToSelector:@selector(textView:didTapHighlight:inRange:rect:)]) {
+                        CGRect rect = [_innerLayout rectForRange:[YYTextRange rangeWithRange:_highlightRange]];
+                        rect = [self _convertRectFromLayout:rect];
+                        [self.delegate textView:self didTapHighlight:_highlight inRange:_highlightRange rect:rect];
+                    }
+                }
+                [self _removeHighlightAnimated:YES];
+            }
+        } else {
+            if (_state.trackingCaret) {
+                if (_state.touchMoved) {
+                    [self _updateTextRangeByTrackingCaret];
+                    [self _showMenu];
+                } else {
+                    if (_state.showingMenu) [self _hideMenu];
+                    else [self _showMenu];
+                }
+            } else if (_state.trackingGrabber) {
+                [self _updateTextRangeByTrackingGrabber];
+                [self _showMenu];
+            } else if (_state.trackingPreSelect) {
+                [self _updateTextRangeByTrackingPreSelect];
+                if (_trackingRange.asRange.length > 0) {
+                    _state.selectedWithoutEdit = YES;
+                    [self _showMenu];
+                } else {
+                    [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+                }
+            } else if (_state.deleteConfirm || _markedTextRange) {
+                [self _updateTextRangeByTrackingCaret];
+                [self _hideMenu];
+            } else {
+                if (!_state.touchMoved) {
+                    if (_state.selectedWithoutEdit) {
+                        _state.selectedWithoutEdit = NO;
+                        [self _hideMenu];
+                    } else {
+                        if (self.isFirstResponder) {
+                            YYTextRange *_oldRange = _trackingRange;
+                            [self _updateTextRangeByTrackingCaret];
+                            if ([_oldRange isEqual:_trackingRange]) {
+                                if (_state.showingMenu) [self _hideMenu];
+                                else [self _showMenu];
+                            } else {
+                                [self _hideMenu];
+                            }
+                        } else {
+                            [self _hideMenu];
+                            if (_state.clearsOnInsertionOnce) {
+                                _state.clearsOnInsertionOnce = NO;
+                                _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+                                [self _setSelectedRange:_selectedTextRange.asRange];
+                            } else {
+                                [self _updateTextRangeByTrackingCaret];
+                            }
+                            [self performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0];
+                        }
+                    }
+                }
+            }
+        }
+        
+        if (_trackingRange && (![_trackingRange isEqual:_selectedTextRange] || _state.trackingPreSelect)) {
+            if (![_trackingRange isEqual:_selectedTextRange]) {
+                [_inputDelegate selectionWillChange:self];
+                _selectedTextRange = _trackingRange;
+                [_inputDelegate selectionDidChange:self];
+                [self _updateAttributesHolder];
+                [self _updateOuterProperties];
+            }
+            if (!_state.trackingGrabber && !_state.trackingPreSelect) {
+                [self _scrollRangeToVisible:_selectedTextRange];
+            }
+        }
+        
+        [self _endTouchTracking];
+    }
+    
+    if (!_state.swallowTouch) [super touchesEnded:touches withEvent:event];
+}
+
+- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
+    [self _endTouchTracking];
+    [self _hideMenu];
+
+    if (!_state.swallowTouch) [super touchesCancelled:touches withEvent:event];
+}
+
+- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event {
+    if (motion == UIEventSubtypeMotionShake && _allowsUndoAndRedo) {
+        if (!YYTextIsAppExtension()) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+            [self performSelector:@selector(_showUndoRedoAlert)];
+#pragma clang diagnostic pop
+        }
+    } else {
+        [super motionEnded:motion withEvent:event];
+    }
+}
+
+- (BOOL)canBecomeFirstResponder {
+    if (!self.isSelectable) return NO;
+    if (!self.isEditable) return NO;
+    if (_state.ignoreFirstResponder) return NO;
+    if ([self.delegate respondsToSelector:@selector(textViewShouldBeginEditing:)]) {
+        if (![self.delegate textViewShouldBeginEditing:self]) return NO;
+    }
+    return YES;
+}
+
+- (BOOL)becomeFirstResponder {
+    BOOL isFirstResponder = self.isFirstResponder;
+    if (isFirstResponder) return YES;
+    BOOL shouldDetectData = [self _shouldDetectText];
+    BOOL become = [super becomeFirstResponder];
+    if (!isFirstResponder && become) {
+        [self _endTouchTracking];
+        [self _hideMenu];
+        
+        _state.selectedWithoutEdit = NO;
+        if (shouldDetectData != [self _shouldDetectText]) {
+            [self _update];
+        }
+        [self _updateIfNeeded];
+        [self _updateSelectionView];
+        [self performSelector:@selector(_scrollSelectedRangeToVisible) withObject:nil afterDelay:0];
+        if ([self.delegate respondsToSelector:@selector(textViewDidBeginEditing:)]) {
+            [self.delegate textViewDidBeginEditing:self];
+        }
+        [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidBeginEditingNotification object:self];
+    }
+    return become;
+}
+
+- (BOOL)canResignFirstResponder {
+    if (!self.isFirstResponder) return YES;
+    if ([self.delegate respondsToSelector:@selector(textViewShouldEndEditing:)]) {
+        if (![self.delegate textViewShouldEndEditing:self]) return NO;
+    }
+    return YES;
+}
+
+- (BOOL)resignFirstResponder {
+    BOOL isFirstResponder = self.isFirstResponder;
+    if (!isFirstResponder) return YES;
+    BOOL resign = [super resignFirstResponder];
+    if (resign) {
+        if (_markedTextRange) {
+            _markedTextRange = nil;
+            [self _parseText];
+            [self _setText:[_innerText yy_plainTextForRange:NSMakeRange(0, _innerText.length)]];
+        }
+        _state.selectedWithoutEdit = NO;
+        if ([self _shouldDetectText]) {
+            [self _update];
+        }
+        [self _endTouchTracking];
+        [self _hideMenu];
+        [self _updateIfNeeded];
+        [self _updateSelectionView];
+        [self _restoreInsetsAnimated:YES];
+        if ([self.delegate respondsToSelector:@selector(textViewDidEndEditing:)]) {
+            [self.delegate textViewDidEndEditing:self];
+        }
+        [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidEndEditingNotification object:self];
+    }
+    return resign;
+}
+
+- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
+    /*
+     ------------------------------------------------------
+     Default menu actions list:
+     cut:                                   Cut
+     copy:                                  Copy
+     select:                                Select
+     selectAll:                             Select All
+     paste:                                 Paste
+     delete:                                Delete
+     _promptForReplace:                     Replace...
+     _transliterateChinese:                 ���������
+     _showTextStyleOptions:                 ������������
+     _define:                               Define
+     _addShortcut:                          Add...
+     _accessibilitySpeak:                   Speak
+     _accessibilitySpeakLanguageSelection:  Speak...
+     _accessibilityPauseSpeaking:           Pause Speak
+     makeTextWritingDirectionRightToLeft:   ���
+     makeTextWritingDirectionLeftToRight:   ���
+     
+     ------------------------------------------------------
+     Default attribute modifier list:
+     toggleBoldface:
+     toggleItalics:
+     toggleUnderline:
+     increaseSize:
+     decreaseSize:
+     */
+    
+    if (_selectedTextRange.asRange.length == 0) {
+        if (action == @selector(select:) ||
+            action == @selector(selectAll:)) {
+            return _innerText.length > 0;
+        }
+        if (action == @selector(paste:)) {
+            return [self _isPasteboardContainsValidValue];
+        }
+    } else {
+        if (action == @selector(cut:)) {
+            return self.isFirstResponder && self.editable;
+        }
+        if (action == @selector(copy:)) {
+            return YES;
+        }
+        if (action == @selector(selectAll:)) {
+            return _selectedTextRange.asRange.length < _innerText.length;
+        }
+        if (action == @selector(paste:)) {
+            return self.isFirstResponder && self.editable && [self _isPasteboardContainsValidValue];
+        }
+        NSString *selString = NSStringFromSelector(action);
+        if ([selString hasSuffix:@"define:"] && [selString hasPrefix:@"_"]) {
+            return [self _getRootViewController] != nil;
+        }
+    }
+    return NO;
+}
+
+- (void)reloadInputViews {
+    [super reloadInputViews];
+    if (_markedTextRange) {
+        [self unmarkText];
+    }
+}
+
+#pragma mark - Override NSObject(UIResponderStandardEditActions)
+
+- (void)cut:(id)sender {
+    [self _endTouchTracking];
+    if (_selectedTextRange.asRange.length == 0) return;
+    
+    [self _copySelectedTextToPasteboard];
+    [self _saveToUndoStack];
+    [self _resetRedoStack];
+    [self replaceRange:_selectedTextRange withText:@""];
+}
+
+- (void)copy:(id)sender {
+    [self _endTouchTracking];
+    [self _copySelectedTextToPasteboard];
+}
+
+- (void)paste:(id)sender {
+    [self _endTouchTracking];
+    UIPasteboard *p = [UIPasteboard generalPasteboard];
+    NSAttributedString *atr = nil;
+    
+    if (_allowsPasteAttributedString) {
+        atr = p.yy_AttributedString;
+        if (atr.length == 0) atr = nil;
+    }
+    if (!atr && _allowsPasteImage) {
+        UIImage *img = nil;
+        
+        Class cls = NSClassFromString(@"YYImage");
+        if (cls) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wundeclared-selector"
+            if (p.yy_GIFData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_GIFData withObject:nil];
+            }
+            if (!img && p.yy_PNGData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_PNGData withObject:nil];
+            }
+            if (!img && p.yy_WEBPData) {
+                img = [(id)cls performSelector:@selector(imageWithData:scale:) withObject:p.yy_WEBPData withObject:nil];
+            }
+#pragma clang diagnostic pop
+        }
+        
+        if (!img) {
+            img = p.image;
+        }
+        if (!img && p.yy_ImageData) {
+            img = [UIImage imageWithData:p.yy_ImageData scale:YYTextScreenScale()];
+        }
+        if (img && img.size.width > 1 && img.size.height > 1) {
+            id content = img;
+            
+            if (cls) {
+                if ([img conformsToProtocol:NSProtocolFromString(@"YYAnimatedImage")]) {
+                    NSNumber *frameCount = [img valueForKey:@"animatedImageFrameCount"];
+                    if (frameCount.integerValue > 1) {
+                        Class viewCls = NSClassFromString(@"YYAnimatedImageView");
+                        UIImageView *imgView = [(id)viewCls new];
+                        imgView.image = img;
+                        imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
+                        if (imgView) {
+                            content = imgView;
+                        }
+                    }
+                }
+            }
+            
+            if ([content isKindOfClass:[UIImage class]] && img.images.count > 1) {
+                UIImageView *imgView = [UIImageView new];
+                imgView.image = img;
+                imgView.frame = CGRectMake(0, 0, img.size.width, img.size.height);
+                if (imgView) {
+                    content = imgView;
+                }
+            }
+            
+            NSMutableAttributedString *attText = [NSAttributedString yy_attachmentStringWithContent:content contentMode:UIViewContentModeScaleToFill width:img.size.width ascent:img.size.height descent:0];
+            NSDictionary *attrs = _typingAttributesHolder.yy_attributes;
+            if (attrs) [attText addAttributes:attrs range:NSMakeRange(0, attText.length)];
+            atr = attText;
+        }
+    }
+    
+    if (atr) {
+        NSUInteger endPosition = _selectedTextRange.start.offset + atr.length;
+        NSMutableAttributedString *text = _innerText.mutableCopy;
+        [text replaceCharactersInRange:_selectedTextRange.asRange withAttributedString:atr];
+        self.attributedText = text;
+        YYTextPosition *pos = [self _correctedTextPosition:[YYTextPosition positionWithOffset:endPosition]];
+        YYTextRange *range = [_innerLayout textRangeByExtendingPosition:pos];
+        range = [self _correctedTextRange:range];
+        if (range) {
+            self.selectedRange = NSMakeRange(range.end.offset, 0);
+        }
+    } else {
+        NSString *string = p.string;
+        if (string.length > 0) {
+            [self _saveToUndoStack];
+            [self _resetRedoStack];
+            [self replaceRange:_selectedTextRange withText:string];
+        }
+    }
+}
+
+- (void)select:(id)sender {
+    [self _endTouchTracking];
+    
+    if (_selectedTextRange.asRange.length > 0 || _innerText.length == 0) return;
+    YYTextRange *newRange = [self _getClosestTokenRangeAtPosition:_selectedTextRange.start];
+    if (newRange.asRange.length > 0) {
+        [_inputDelegate selectionWillChange:self];
+        _selectedTextRange = newRange;
+        [_inputDelegate selectionDidChange:self];
+    }
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _hideMenu];
+    [self _showMenu];
+}
+
+- (void)selectAll:(id)sender {
+    _trackingRange = nil;
+    [_inputDelegate selectionWillChange:self];
+    _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(0, _innerText.length)];
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _hideMenu];
+    [self _showMenu];
+}
+
+- (void)_define:(id)sender {
+    [self _hideMenu];
+    
+    NSString *string = [_innerText yy_plainTextForRange:_selectedTextRange.asRange];
+    if (string.length == 0) return;
+    BOOL resign = [self resignFirstResponder];
+    if (!resign) return;
+    
+    UIReferenceLibraryViewController* ref = [[UIReferenceLibraryViewController alloc] initWithTerm:string];
+    ref.view.backgroundColor = [UIColor whiteColor];
+    [[self _getRootViewController] presentViewController:ref animated:YES completion:^{}];
+}
+
+
+#pragma mark - Overrice NSObject(NSKeyValueObservingCustomization)
+
++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
+    static NSSet *keys = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        keys = [NSSet setWithArray:@[
+            @"text",
+            @"font",
+            @"textColor",
+            @"textAlignment",
+            @"dataDetectorTypes",
+            @"linkTextAttributes",
+            @"highlightTextAttributes",
+            @"textParser",
+            @"attributedText",
+            @"textVerticalAlignment",
+            @"textContainerInset",
+            @"exclusionPaths",
+            @"verticalForm",
+            @"linePositionModifier",
+            @"selectedRange",
+            @"typingAttributes"
+        ]];
+    });
+    if ([keys containsObject:key]) {
+        return NO;
+    }
+    return [super automaticallyNotifiesObserversForKey:key];
+}
+
+#pragma mark - @protocol NSCoding
+
+- (instancetype)initWithCoder:(NSCoder *)aDecoder {
+    self = [super initWithCoder:aDecoder];
+    [self _initTextView];
+    self.attributedText = [aDecoder decodeObjectForKey:@"attributedText"];
+    self.selectedRange = ((NSValue *)[aDecoder decodeObjectForKey:@"selectedRange"]).rangeValue;
+    self.textVerticalAlignment = [aDecoder decodeIntegerForKey:@"textVerticalAlignment"];
+    self.dataDetectorTypes = [aDecoder decodeIntegerForKey:@"dataDetectorTypes"];
+    self.textContainerInset = ((NSValue *)[aDecoder decodeObjectForKey:@"textContainerInset"]).UIEdgeInsetsValue;
+    self.exclusionPaths = [aDecoder decodeObjectForKey:@"exclusionPaths"];
+    self.verticalForm = [aDecoder decodeBoolForKey:@"verticalForm"];
+    return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+    [super encodeWithCoder:aCoder];
+    [aCoder encodeObject:self.attributedText forKey:@"attributedText"];
+    [aCoder encodeObject:[NSValue valueWithRange:self.selectedRange] forKey:@"selectedRange"];
+    [aCoder encodeInteger:self.textVerticalAlignment forKey:@"textVerticalAlignment"];
+    [aCoder encodeInteger:self.dataDetectorTypes forKey:@"dataDetectorTypes"];
+    [aCoder encodeUIEdgeInsets:self.textContainerInset forKey:@"textContainerInset"];
+    [aCoder encodeObject:self.exclusionPaths forKey:@"exclusionPaths"];
+    [aCoder encodeBool:self.verticalForm forKey:@"verticalForm"];
+}
+
+#pragma mark - @protocol UIScrollViewDelegate
+
+- (id<YYTextViewDelegate>)delegate {
+    return _outerDelegate;
+}
+
+- (void)setDelegate:(id<YYTextViewDelegate>)delegate {
+    _outerDelegate = delegate;
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
+    [[YYTextEffectWindow sharedWindow] hideSelectionDot:_selectionView];
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidScroll:scrollView];
+    }
+}
+
+- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidZoom:scrollView];
+    }
+}
+
+- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginDragging:scrollView];
+    }
+}
+
+- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillEndDragging:scrollView withVelocity:velocity targetContentOffset:targetContentOffset];
+    }
+}
+
+- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
+    if (!decelerate) {
+        [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    }
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
+    }
+}
+
+- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginDecelerating:scrollView];
+    }
+}
+
+- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
+    [[YYTextEffectWindow sharedWindow] showSelectionDot:_selectionView];
+    
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndDecelerating:scrollView];
+    }
+}
+
+- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndScrollingAnimation:scrollView];
+    }
+}
+
+- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        return [_outerDelegate viewForZoomingInScrollView:scrollView];
+    } else {
+        return nil;
+    }
+}
+
+- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view{
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewWillBeginZooming:scrollView withView:view];
+    }
+}
+
+- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidEndZooming:scrollView withView:view atScale:scale];
+    }
+}
+
+- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        return [_outerDelegate scrollViewShouldScrollToTop:scrollView];
+    }
+    return YES;
+}
+
+- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView {
+    if ([_outerDelegate respondsToSelector:_cmd]) {
+        [_outerDelegate scrollViewDidScrollToTop:scrollView];
+    }
+}
+
+#pragma mark - @protocol YYTextKeyboardObserver
+
+- (void)keyboardChangedWithTransition:(YYTextKeyboardTransition)transition {
+    [self _keyboardChanged];
+}
+
+#pragma mark - @protocol UIALertViewDelegate
+
+- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
+    NSString *title = [alertView buttonTitleAtIndex:buttonIndex];
+    if (title.length == 0) return;
+    NSArray *strings = [self _localizedUndoStrings];
+    if ([title isEqualToString:strings[1]] || [title isEqualToString:strings[2]]) {
+        [self _redo];
+    } else if ([title isEqualToString:strings[3]] || [title isEqualToString:strings[4]]) {
+        [self _undo];
+    }
+    [self _restoreFirstResponderAfterUndoAlert];
+}
+
+#pragma mark - @protocol UIKeyInput
+
+- (BOOL)hasText {
+    return _innerText.length > 0;
+}
+
+- (void)insertText:(NSString *)text {
+    if (text.length == 0) return;
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    [self replaceRange:_selectedTextRange withText:text];
+}
+
+- (void)deleteBackward {
+    [self _updateIfNeeded];
+    NSRange range = _selectedTextRange.asRange;
+    if (range.location == 0 && range.length == 0) return;
+    _state.typingAttributesOnce = NO;
+    
+    // test if there's 'TextBinding' before the caret
+    if (!_state.deleteConfirm && range.length == 0 && range.location > 0) {
+        NSRange effectiveRange;
+        YYTextBinding *binding = [_innerText attribute:YYTextBindingAttributeName atIndex:range.location - 1 longestEffectiveRange:&effectiveRange inRange:NSMakeRange(0, _innerText.length)];
+        if (binding && binding.deleteConfirm) {
+            _state.deleteConfirm = YES;
+            [_inputDelegate selectionWillChange:self];
+            _selectedTextRange = [YYTextRange rangeWithRange:effectiveRange];
+            _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+            [_inputDelegate selectionDidChange:self];
+            
+            [self _updateOuterProperties];
+            [self _updateSelectionView];
+            return;
+        }
+    }
+    
+    _state.deleteConfirm = NO;
+    if (range.length == 0) {
+        YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:_selectedTextRange.end inDirection:UITextLayoutDirectionLeft offset:1];
+        if ([self _isTextRangeValid:extendRange]) {
+            range = extendRange.asRange;
+        }
+    }
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    [self replaceRange:[YYTextRange rangeWithRange:range] withText:@""];
+}
+
+#pragma mark - @protocol UITextInput
+
+- (void)setInputDelegate:(id<UITextInputDelegate>)inputDelegate {
+    _inputDelegate = inputDelegate;
+}
+
+- (void)setSelectedTextRange:(YYTextRange *)selectedTextRange {
+    if (!selectedTextRange) return;
+    selectedTextRange = [self _correctedTextRange:selectedTextRange];
+    if ([selectedTextRange isEqual:_selectedTextRange]) return;
+    [self _updateIfNeeded];
+    [self _endTouchTracking];
+    [self _hideMenu];
+    _state.deleteConfirm = NO;
+    _state.typingAttributesOnce = NO;
+    
+    [_inputDelegate selectionWillChange:self];
+    _selectedTextRange = selectedTextRange;
+    _lastTypeRange = _selectedTextRange.asRange;
+    [_inputDelegate selectionDidChange:self];
+    
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+}
+
+- (void)setMarkedTextStyle:(NSDictionary *)markedTextStyle {
+    _markedTextStyle = markedTextStyle.copy;
+}
+
+/*
+ Replace current markedText with the new markedText
+ @param markedText     New marked text.
+ @param selectedRange  The range from the '_markedTextRange'
+ */
+- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange {
+    [self _updateIfNeeded];
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        NSRange range = _markedTextRange ? _markedTextRange.asRange : NSMakeRange(_selectedTextRange.end.offset, 0);
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:range replacementText:markedText];
+        if (!should) return;
+    }
+    
+    
+    if (!NSEqualRanges(_lastTypeRange, _selectedTextRange.asRange)) {
+        [self _saveToUndoStack];
+        [self _resetRedoStack];
+    }
+    
+    BOOL needApplyHolderAttribute = NO;
+    if (_innerText.length > 0 && _markedTextRange) {
+        [self _updateAttributesHolder];
+    } else {
+        needApplyHolderAttribute = YES;
+    }
+    
+    if (_selectedTextRange.asRange.length > 0) {
+        [self replaceRange:_selectedTextRange withText:@""];
+    }
+    
+    [_inputDelegate textWillChange:self];
+    [_inputDelegate selectionWillChange:self];
+    
+    if (!markedText) markedText = @"";
+    if (_markedTextRange == nil) {
+        _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.end.offset, markedText.length)];
+        [_innerText replaceCharactersInRange:NSMakeRange(_selectedTextRange.end.offset, 0) withString:markedText];
+        _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_selectedTextRange.start.offset + selectedRange.location, selectedRange.length)];
+    } else {
+        _markedTextRange = [self _correctedTextRange:_markedTextRange];
+        [_innerText replaceCharactersInRange:_markedTextRange.asRange withString:markedText];
+        _markedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset, markedText.length)];
+        _selectedTextRange = [YYTextRange rangeWithRange:NSMakeRange(_markedTextRange.start.offset + selectedRange.location, selectedRange.length)];
+    }
+    
+    _selectedTextRange = [self _correctedTextRange:_selectedTextRange];
+    _markedTextRange = [self _correctedTextRange:_markedTextRange];
+    if (_markedTextRange.asRange.length == 0) {
+        _markedTextRange = nil;
+    } else {
+        if (needApplyHolderAttribute) {
+            [_innerText setAttributes:_typingAttributesHolder.yy_attributes range:_markedTextRange.asRange];
+        }
+        [_innerText yy_removeDiscontinuousAttributesInRange:_markedTextRange.asRange];
+    }
+    
+    [_inputDelegate selectionDidChange:self];
+    [_inputDelegate textDidChange:self];
+    
+    [self _updateOuterProperties];
+    [self _updateLayout];
+    [self _updateSelectionView];
+    [self _scrollRangeToVisible:_selectedTextRange];
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    _lastTypeRange = _selectedTextRange.asRange;
+}
+
+- (void)unmarkText {
+    _markedTextRange = nil;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    if ([self _parseText]) _state.needUpdate = YES;
+    
+    [self _updateIfNeeded];
+    [self _updateOuterProperties];
+    [self _updateSelectionView];
+    [self _scrollRangeToVisible:_selectedTextRange];
+}
+
+- (void)replaceRange:(YYTextRange *)range withText:(NSString *)text {
+    if (!range) return;
+    if (!text) text = @"";
+    if (range.asRange.length == 0 && text.length == 0) return;
+    range = [self _correctedTextRange:range];
+    
+    if ([self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
+        BOOL should = [self.delegate textView:self shouldChangeTextInRange:range.asRange replacementText:text];
+        if (!should) return;
+    }
+    
+    BOOL useInnerAttributes = NO;
+    if (_innerText.length > 0) {
+        if (range.start.offset == 0 && range.end.offset == _innerText.length) {
+            if (text.length == 0) {
+                NSMutableDictionary *attrs = [_innerText yy_attributesAtIndex:0].mutableCopy;
+                [attrs removeObjectsForKeys:[NSMutableAttributedString yy_allDiscontinuousAttributeKeys]];
+                _typingAttributesHolder.yy_attributes = attrs;
+            }
+        }
+    } else { // no text
+        useInnerAttributes = YES;
+    }
+    BOOL applyTypingAttributes = NO;
+    if (_state.typingAttributesOnce) {
+        _state.typingAttributesOnce = NO;
+        if (!useInnerAttributes) {
+            if (range.asRange.length == 0 && text.length > 0) {
+                applyTypingAttributes = YES;
+            }
+        }
+    }
+    
+    _state.selectedWithoutEdit = NO;
+    _state.deleteConfirm = NO;
+    [self _endTouchTracking];
+    [self _hideMenu];
+    
+    [self _replaceRange:range withText:text notifyToDelegate:YES];
+    if (useInnerAttributes) {
+        [_innerText yy_setAttributes:_typingAttributesHolder.yy_attributes];
+    } else if (applyTypingAttributes) {
+        NSRange newRange = NSMakeRange(range.asRange.location, text.length);
+        [_typingAttributesHolder.yy_attributes enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
+            [_innerText yy_setAttribute:key value:obj range:newRange];
+        }];
+    }
+    [self _parseText];
+    [self _updateOuterProperties];
+    [self _update];
+    
+    if (self.isFirstResponder) {
+        [self _scrollRangeToVisible:_selectedTextRange];
+    }
+    
+    if ([self.delegate respondsToSelector:@selector(textViewDidChange:)]) {
+        [self.delegate textViewDidChange:self];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:YYTextViewTextDidChangeNotification object:self];
+    
+    _lastTypeRange = _selectedTextRange.asRange;
+}
+
+- (void)setBaseWritingDirection:(UITextWritingDirection)writingDirection forRange:(YYTextRange *)range {
+    if (!range) return;
+    range = [self _correctedTextRange:range];
+    [_innerText yy_setBaseWritingDirection:(NSWritingDirection)writingDirection range:range.asRange];
+    [self _commitUpdate];
+}
+
+- (NSString *)textInRange:(YYTextRange *)range {
+    range = [self _correctedTextRange:range];
+    if (!range) return @"";
+    return [_innerText.string substringWithRange:range.asRange];
+}
+
+- (UITextWritingDirection)baseWritingDirectionForPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
+    [self _updateIfNeeded];
+    position = [self _correctedTextPosition:position];
+    if (!position) return UITextWritingDirectionNatural;
+    if (_innerText.length == 0) return UITextWritingDirectionNatural;
+    NSUInteger idx = position.offset;
+    if (idx == _innerText.length) idx--;
+    
+    NSDictionary *attrs = [_innerText yy_attributesAtIndex:idx];
+    CTParagraphStyleRef paraStyle = (__bridge CFTypeRef)(attrs[NSParagraphStyleAttributeName]);
+    if (paraStyle) {
+        CTWritingDirection baseWritingDirection;
+        if (CTParagraphStyleGetValueForSpecifier(paraStyle, kCTParagraphStyleSpecifierBaseWritingDirection, sizeof(CTWritingDirection), &baseWritingDirection)) {
+            return (UITextWritingDirection)baseWritingDirection;
+        }
+    }
+    
+    return UITextWritingDirectionNatural;
+}
+
+- (YYTextPosition *)beginningOfDocument {
+    return [YYTextPosition positionWithOffset:0];
+}
+
+- (YYTextPosition *)endOfDocument {
+    return [YYTextPosition positionWithOffset:_innerText.length];
+}
+
+- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position offset:(NSInteger)offset {
+    if (offset == 0) return position;
+    
+    NSUInteger location = position.offset;
+    NSInteger newLocation = (NSInteger)location + offset;
+    if (newLocation < 0 || newLocation > _innerText.length) return nil;
+    
+    if (newLocation != 0 && newLocation != _innerText.length) {
+        // fix emoji
+        [self _updateIfNeeded];
+        YYTextRange *extendRange = [_innerLayout textRangeByExtendingPosition:[YYTextPosition positionWithOffset:newLocation]];
+        if (extendRange.asRange.length > 0) {
+            if (offset < 0) {
+                newLocation = extendRange.start.offset;
+            } else {
+                newLocation = extendRange.end.offset;
+            }
+        }
+    }
+    
+    YYTextPosition *p = [YYTextPosition positionWithOffset:newLocation];
+    return [self _correctedTextPosition:p];
+}
+
+- (YYTextPosition *)positionFromPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset {
+    [self _updateIfNeeded];
+    YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:offset];
+    
+    BOOL forward;
+    if (_innerContainer.isVerticalForm) {
+        forward = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown;
+    } else {
+        forward = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight;
+    }
+    if (!forward && offset < 0) {
+        forward = -forward;
+    }
+    
+    YYTextPosition *newPosition = forward ? range.end : range.start;
+    if (newPosition.offset > _innerText.length) {
+        newPosition = [YYTextPosition positionWithOffset:_innerText.length affinity:YYTextAffinityBackward];
+    }
+    
+    return [self _correctedTextPosition:newPosition];
+}
+
+- (YYTextRange *)textRangeFromPosition:(YYTextPosition *)fromPosition toPosition:(YYTextPosition *)toPosition {
+    return [YYTextRange rangeWithStart:fromPosition end:toPosition];
+}
+
+- (NSComparisonResult)comparePosition:(YYTextPosition *)position toPosition:(YYTextPosition *)other {
+    return [position compare:other];
+}
+
+- (NSInteger)offsetFromPosition:(YYTextPosition *)from toPosition:(YYTextPosition *)toPosition {
+    return toPosition.offset - from.offset;
+}
+
+- (YYTextPosition *)positionWithinRange:(YYTextRange *)range farthestInDirection:(UITextLayoutDirection)direction {
+    NSRange nsRange = range.asRange;
+    if (direction == UITextLayoutDirectionLeft | direction == UITextLayoutDirectionUp) {
+        return [YYTextPosition positionWithOffset:nsRange.location];
+    } else {
+        return [YYTextPosition positionWithOffset:nsRange.location + nsRange.length affinity:YYTextAffinityBackward];
+    }
+}
+
+- (YYTextRange *)characterRangeByExtendingPosition:(YYTextPosition *)position inDirection:(UITextLayoutDirection)direction {
+    [self _updateIfNeeded];
+    YYTextRange *range = [_innerLayout textRangeByExtendingPosition:position inDirection:direction offset:1];
+    return [self _correctedTextRange:range];
+}
+
+- (YYTextPosition *)closestPositionToPoint:(CGPoint)point {
+    [self _updateIfNeeded];
+    point = [self _convertPointToLayout:point];
+    YYTextPosition *position = [_innerLayout closestPositionToPoint:point];
+    return [self _correctedTextPosition:position];
+}
+
+- (YYTextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(YYTextRange *)range {
+    YYTextPosition *pos = (id)[self closestPositionToPoint:point];
+    if (!pos) return nil;
+    
+    range = [self _correctedTextRange:range];
+    if ([pos compare:range.start] == NSOrderedAscending) {
+        pos = range.start;
+    } else if ([pos compare:range.end] == NSOrderedDescending) {
+        pos = range.end;
+    }
+    return pos;
+}
+
+- (YYTextRange *)characterRangeAtPoint:(CGPoint)point {
+    [self _updateIfNeeded];
+    point = [self _convertPointToLayout:point];
+    YYTextRange *r = [_innerLayout closestTextRangeAtPoint:point];
+    return [self _correctedTextRange:r];
+}
+
+- (CGRect)firstRectForRange:(YYTextRange *)range {
+    [self _updateIfNeeded];
+    CGRect rect = [_innerLayout firstRectForRange:range];
+    if (CGRectIsNull(rect)) rect = CGRectZero;
+    return [self _convertRectFromLayout:rect];
+}
+
+- (CGRect)caretRectForPosition:(YYTextPosition *)position {
+    [self _updateIfNeeded];
+    CGRect caretRect = [_innerLayout caretRectForPosition:position];
+    if (!CGRectIsNull(caretRect)) {
+        caretRect = [self _convertRectFromLayout:caretRect];
+        caretRect = CGRectStandardize(caretRect);
+        if (_verticalForm) {
+            if (caretRect.size.height == 0) {
+                caretRect.size.height = 2;
+                caretRect.origin.y -= 2 * 0.5;
+            }
+            if (caretRect.origin.y < 0) {
+                caretRect.origin.y = 0;
+            } else if (caretRect.origin.y + caretRect.size.height > self.bounds.size.height) {
+                caretRect.origin.y = self.bounds.size.height - caretRect.size.height;
+            }
+        } else {
+            if (caretRect.size.width == 0) {
+                caretRect.size.width = 2;
+                caretRect.origin.x -= 2 * 0.5;
+            }
+            if (caretRect.origin.x < 0) {
+                caretRect.origin.x = 0;
+            } else if (caretRect.origin.x + caretRect.size.width > self.bounds.size.width) {
+                caretRect.origin.x = self.bounds.size.width - caretRect.size.width;
+            }
+        }
+        return YYTextCGRectPixelRound(caretRect);
+    }
+    return CGRectZero;
+}
+
+- (NSArray *)selectionRectsForRange:(YYTextRange *)range {
+    [self _updateIfNeeded];
+    NSArray *rects = [_innerLayout selectionRectsForRange:range];
+    [rects enumerateObjectsUsingBlock:^(YYTextSelectionRect *rect, NSUInteger idx, BOOL *stop) {
+        rect.rect = [self _convertRectFromLayout:rect.rect];
+    }];
+    return rects;
+}
+
+#pragma mark - @protocol UITextInput optional
+
+- (UITextStorageDirection)selectionAffinity {
+    if (_selectedTextRange.end.affinity == YYTextAffinityForward) {
+        return UITextStorageDirectionForward;
+    } else {
+        return UITextStorageDirectionBackward;
+    }
+}
+
+- (void)setSelectionAffinity:(UITextStorageDirection)selectionAffinity {
+    _selectedTextRange = [YYTextRange rangeWithRange:_selectedTextRange.asRange affinity:selectionAffinity == UITextStorageDirectionForward ? YYTextAffinityForward : YYTextAffinityBackward];
+    [self _updateSelectionView];
+}
+
+- (NSDictionary *)textStylingAtPosition:(YYTextPosition *)position inDirection:(UITextStorageDirection)direction {
+    if (!position) return nil;
+    if (_innerText.length == 0) return _typingAttributesHolder.yy_attributes;
+    NSDictionary *attrs = nil;
+    if (0 <= position.offset  && position.offset <= _innerText.length) {
+        NSUInteger ofs = position.offset;
+        if (position.offset == _innerText.length ||
+            direction == UITextStorageDirectionBackward) {
+             ofs--;
+        }
+        attrs = [_innerText attributesAtIndex:ofs effectiveRange:NULL];
+    }
+    return attrs;
+}
+
+- (YYTextPosition *)positionWithinRange:(YYTextRange *)range atCharacterOffset:(NSInteger)offset {
+    if (!range) return nil;
+    if (offset < range.start.offset || offset > range.end.offset) return nil;
+    if (offset == range.start.offset) return range.start;
+    else if (offset == range.end.offset) return range.end;
+    else return [YYTextPosition positionWithOffset:offset];
+}
+
+- (NSInteger)characterOffsetOfPosition:(YYTextPosition *)position withinRange:(YYTextRange *)range {
+    return position ? position.offset : NSNotFound;
+}
+
+@end
+
+
+
+@interface YYTextView(IBInspectableProperties)
+@end
+
+@implementation YYTextView(IBInspectableProperties)
+
+- (BOOL)fontIsBold_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return NO;
+    return (font.fontDescriptor.symbolicTraits & UIFontDescriptorTraitBold) > 0;
+}
+
+- (UIFont *)boldFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitBold] size:font.pointSize];
+}
+
+- (UIFont *)normalFont_:(UIFont *)font {
+    if (![font respondsToSelector:@selector(fontDescriptor)]) return font;
+    return [UIFont fontWithDescriptor:[font.fontDescriptor fontDescriptorWithSymbolicTraits:0] size:font.pointSize];
+}
+
+- (void)setFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+    }
+    if (font) self.font = font;
+}
+
+- (void)setFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if (!font) font = [self _defaultFont];
+    font = [font fontWithSize:fontSize];
+    if (font) self.font = font;
+}
+
+- (void)setFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.font;
+    if (!font) font = [self _defaultFont];
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.font = font;
+}
+
+- (void)setPlaceholderFontName_:(NSString *)fontName {
+    if (!fontName) return;
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    if ((fontName.length == 0 || [fontName.lowercaseString isEqualToString:@"system"]) && ![self fontIsBold_:font]) {
+        font = [UIFont systemFontOfSize:font.pointSize];
+    } else if ([fontName.lowercaseString isEqualToString:@"system bold"]) {
+        font = [UIFont boldSystemFontOfSize:font.pointSize];
+    } else {
+        if ([self fontIsBold_:font] && ([fontName.lowercaseString rangeOfString:@"bold"].location == NSNotFound)) {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+            font = [self boldFont_:font];
+        } else {
+            font = [UIFont fontWithName:fontName size:font.pointSize];
+        }
+    }
+    if (font) self.placeholderFont = font;
+}
+
+- (void)setPlaceholderFontSize_:(CGFloat)fontSize {
+    if (fontSize <= 0) return;
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    font = [font fontWithSize:fontSize];
+    if (font) self.placeholderFont = font;
+}
+
+- (void)setPlaceholderFontIsBold_:(BOOL)fontBold {
+    UIFont *font = self.placeholderFont;
+    if (!font) font = [self _defaultFont];
+    if ([self fontIsBold_:font] == fontBold) return;
+    if (fontBold) {
+        font = [self boldFont_:font];
+    } else {
+        font = [self normalFont_:font];
+    }
+    if (font) self.placeholderFont = font;
+}
+
+- (void)setInsetTop_:(CGFloat)textInsetTop {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.top = textInsetTop;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetBottom_:(CGFloat)textInsetBottom {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.bottom = textInsetBottom;
+    self.textContainerInset = insets;
+}
+
+- (void)setInsetLeft_:(CGFloat)textInsetLeft {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.left = textInsetLeft;
+    self.textContainerInset = insets;
+    
+}
+
+- (void)setInsetRight_:(CGFloat)textInsetRight {
+    UIEdgeInsets insets = self.textContainerInset;
+    insets.right = textInsetRight;
+    self.textContainerInset = insets;
+}
+
+- (void)setDebugEnabled_:(BOOL)enabled {
+    if (!enabled) {
+        self.debugOption = nil;
+    } else {
+        YYTextDebugOption *debugOption = [YYTextDebugOption new];
+        debugOption.baselineColor = [UIColor redColor];
+        debugOption.CTFrameBorderColor = [UIColor redColor];
+        debugOption.CTLineFillColor = [UIColor colorWithRed:0.000 green:0.463 blue:1.000 alpha:0.180];
+        debugOption.CGGlyphBorderColor = [UIColor colorWithRed:1.000 green:0.524 blue:0.000 alpha:0.200];
+        self.debugOption = debugOption;
+    }
+}
+
+@end

--
Gitblit v1.8.0