3831 lines
152 KiB
Objective-C
3831 lines
152 KiB
Objective-C
//
|
||
// 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
|