From 83b9d5c682b21d88133f24da0f94dd56bd79e687 Mon Sep 17 00:00:00 2001
From: 单军华
Date: Thu, 19 Jul 2018 13:38:55 +0800
Subject: [PATCH] change
---
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