1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147 |
- //
- // SlackTextViewController
- // https://github.com/slackhq/SlackTextViewController
- //
- // Copyright 2014-2016 Slack Technologies, Inc.
- // Licence: MIT-Licence
- //
- #import "SLKTextView.h"
- #import "SLKTextView+SLKAdditions.h"
- #import "SLKUIConstants.h"
- NSString * const SLKTextViewTextWillChangeNotification = @"SLKTextViewTextWillChangeNotification";
- NSString * const SLKTextViewContentSizeDidChangeNotification = @"SLKTextViewContentSizeDidChangeNotification";
- NSString * const SLKTextViewSelectedRangeDidChangeNotification = @"SLKTextViewSelectedRangeDidChangeNotification";
- NSString * const SLKTextViewDidPasteItemNotification = @"SLKTextViewDidPasteItemNotification";
- NSString * const SLKTextViewDidShakeNotification = @"SLKTextViewDidShakeNotification";
- NSString * const SLKTextViewPastedItemContentType = @"SLKTextViewPastedItemContentType";
- NSString * const SLKTextViewPastedItemMediaType = @"SLKTextViewPastedItemMediaType";
- NSString * const SLKTextViewPastedItemData = @"SLKTextViewPastedItemData";
- static NSString *const SLKTextViewGenericFormattingSelectorPrefix = @"slk_format_";
- @interface SLKTextView ()
- // The label used as placeholder
- @property (nonatomic, strong) UILabel *placeholderLabel;
- // The initial font point size, used for dynamic type calculations
- @property (nonatomic) CGFloat initialFontSize;
- // Used for moving the caret up/down
- @property (nonatomic) UITextLayoutDirection verticalMoveDirection;
- @property (nonatomic) CGRect verticalMoveStartCaretRect;
- @property (nonatomic) CGRect verticalMoveLastCaretRect;
- // Used for detecting if the scroll indicator was previously flashed
- @property (nonatomic) BOOL didFlashScrollIndicators;
- @property (nonatomic, strong) NSMutableArray *registeredFormattingTitles;
- @property (nonatomic, strong) NSMutableArray *registeredFormattingSymbols;
- @property (nonatomic, getter=isFormatting) BOOL formatting;
- // The keyboard commands available for external keyboards
- @property (nonatomic, strong) NSMutableDictionary *registeredKeyCommands;
- @property (nonatomic, strong) NSMutableDictionary *registeredKeyCallbacks;
- @end
- @implementation SLKTextView
- @dynamic delegate;
- #pragma mark - Initialization
- - (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer
- {
- if (self = [super initWithFrame:frame textContainer:textContainer]) {
- [self slk_commonInit];
- }
- return self;
- }
- - (instancetype)initWithCoder:(NSCoder *)coder
- {
- if (self = [super initWithCoder:coder]) {
- [self slk_commonInit];
- }
- return self;
- }
- - (void)slk_commonInit
- {
- _pastableMediaTypes = SLKPastableMediaTypeNone;
- _dynamicTypeEnabled = YES;
- self.undoManagerEnabled = YES;
-
- self.editable = YES;
- self.selectable = YES;
- self.scrollEnabled = YES;
- self.scrollsToTop = NO;
- self.directionalLockEnabled = YES;
- self.dataDetectorTypes = UIDataDetectorTypeNone;
-
- [self slk_registerNotifications];
-
- [self addObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) options:NSKeyValueObservingOptionNew context:NULL];
- }
- #pragma mark - UIView Overrides
- - (CGSize)intrinsicContentSize
- {
- CGFloat height = self.font.lineHeight;
- height += self.textContainerInset.top + self.textContainerInset.bottom;
-
- return CGSizeMake(UIViewNoIntrinsicMetric, height);
- }
- + (BOOL)requiresConstraintBasedLayout
- {
- return YES;
- }
- - (void)layoutIfNeeded
- {
- if (!self.window) {
- return;
- }
-
- [super layoutIfNeeded];
- }
- - (void)layoutSubviews
- {
- [super layoutSubviews];
-
- self.placeholderLabel.hidden = [self slk_shouldHidePlaceholder];
-
- if (!self.placeholderLabel.hidden) {
-
- [UIView performWithoutAnimation:^{
- self.placeholderLabel.frame = [self slk_placeholderRectThatFits:self.bounds];
- [self sendSubviewToBack:self.placeholderLabel];
- }];
- }
- }
- #pragma mark - Getters
- - (UILabel *)placeholderLabel
- {
- if (!_placeholderLabel) {
- _placeholderLabel = [UILabel new];
- _placeholderLabel.clipsToBounds = NO;
- _placeholderLabel.numberOfLines = 1;
- _placeholderLabel.autoresizesSubviews = NO;
- _placeholderLabel.font = self.font;
- _placeholderLabel.backgroundColor = [UIColor clearColor];
- _placeholderLabel.textColor = [UIColor lightGrayColor];
- _placeholderLabel.hidden = YES;
- _placeholderLabel.isAccessibilityElement = NO;
-
- [self addSubview:_placeholderLabel];
- }
- return _placeholderLabel;
- }
- - (NSString *)placeholder
- {
- return self.placeholderLabel.text;
- }
- - (UIColor *)placeholderColor
- {
- return self.placeholderLabel.textColor;
- }
- - (UIFont *)placeholderFont
- {
- return self.placeholderLabel.font;
- }
- - (NSUInteger)numberOfLines
- {
- CGSize contentSize = self.contentSize;
-
- CGFloat contentHeight = contentSize.height;
- contentHeight -= self.textContainerInset.top + self.textContainerInset.bottom;
-
- NSUInteger lines = fabs(contentHeight/self.font.lineHeight);
-
- // This helps preventing the content's height to be larger that the bounds' height
- // Avoiding this way to have unnecessary scrolling in the text view when there is only 1 line of content
- if (lines == 1 && contentSize.height > self.bounds.size.height) {
- contentSize.height = self.bounds.size.height;
- self.contentSize = contentSize;
- }
-
- // Let's fallback to the minimum line count
- if (lines == 0) {
- lines = 1;
- }
-
- return lines;
- }
- - (NSUInteger)maxNumberOfLines
- {
- NSUInteger numberOfLines = _maxNumberOfLines;
-
- if (SLK_IS_LANDSCAPE && SLK_IS_IPHONE) {
- numberOfLines /= 2.0; // Half size on larger iPhone
- }
-
- if (self.isDynamicTypeEnabled) {
- NSString *contentSizeCategory = [UIScreen mainScreen].traitCollection.preferredContentSizeCategory;
- CGFloat pointSizeDifference = SLKPointSizeDifferenceForCategory(contentSizeCategory);
-
- CGFloat factor = pointSizeDifference/self.initialFontSize;
-
- if (fabs(factor) > 0.75) {
- factor = 0.75;
- }
-
- numberOfLines -= floorf(numberOfLines * factor); // Calculates a dynamic number of lines depending of the user preferred font size
- }
-
- return numberOfLines;
- }
- - (BOOL)isTypingSuggestionEnabled
- {
- return (self.autocorrectionType == UITextAutocorrectionTypeNo) ? NO : YES;
- }
- - (BOOL)isFormattingEnabled
- {
- return (self.registeredFormattingSymbols.count > 0) ? YES : NO;
- }
- // Returns only a supported pasted item
- - (id)slk_pastedItem
- {
- NSString *contentType = [self slk_pasteboardContentType];
- NSData *data = [[UIPasteboard generalPasteboard] dataForPasteboardType:contentType];
-
- if (data && [data isKindOfClass:[NSData class]])
- {
- SLKPastableMediaType mediaType = SLKPastableMediaTypeFromNSString(contentType);
-
- NSDictionary *userInfo = @{SLKTextViewPastedItemContentType: contentType,
- SLKTextViewPastedItemMediaType: @(mediaType),
- SLKTextViewPastedItemData: data};
- return userInfo;
- }
- if ([[UIPasteboard generalPasteboard] URL]) {
- return [[[UIPasteboard generalPasteboard] URL] absoluteString];
- }
- if ([[UIPasteboard generalPasteboard] string]) {
- return [[UIPasteboard generalPasteboard] string];
- }
-
- return nil;
- }
- // Checks if any supported media found in the general pasteboard
- - (BOOL)slk_isPasteboardItemSupported
- {
- if ([self slk_pasteboardContentType].length > 0) {
- return YES;
- }
- return NO;
- }
- - (NSString *)slk_pasteboardContentType
- {
- NSArray *pasteboardTypes = [[UIPasteboard generalPasteboard] pasteboardTypes];
- NSMutableArray *subpredicates = [NSMutableArray new];
-
- for (NSString *type in [self slk_supportedMediaTypes]) {
- [subpredicates addObject:[NSPredicate predicateWithFormat:@"SELF == %@", type]];
- }
-
- return [[pasteboardTypes filteredArrayUsingPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:subpredicates]] firstObject];
- }
- - (NSArray *)slk_supportedMediaTypes
- {
- if (self.pastableMediaTypes == SLKPastableMediaTypeNone) {
- return nil;
- }
-
- NSMutableArray *types = [NSMutableArray new];
-
- if (self.pastableMediaTypes & SLKPastableMediaTypePNG) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)];
- }
- if (self.pastableMediaTypes & SLKPastableMediaTypeJPEG) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)];
- }
- if (self.pastableMediaTypes & SLKPastableMediaTypeTIFF) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)];
- }
- if (self.pastableMediaTypes & SLKPastableMediaTypeGIF) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)];
- }
- if (self.pastableMediaTypes & SLKPastableMediaTypeMOV) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)];
- }
- if (self.pastableMediaTypes & SLKPastableMediaTypePassbook) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)];
- }
-
- if (self.pastableMediaTypes & SLKPastableMediaTypeImages) {
- [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)];
- }
-
- return types;
- }
- NSString *NSStringFromSLKPastableMediaType(SLKPastableMediaType type)
- {
- if (type == SLKPastableMediaTypePNG) {
- return @"public.png";
- }
- if (type == SLKPastableMediaTypeJPEG) {
- return @"public.jpeg";
- }
- if (type == SLKPastableMediaTypeTIFF) {
- return @"public.tiff";
- }
- if (type == SLKPastableMediaTypeGIF) {
- return @"com.compuserve.gif";
- }
- if (type == SLKPastableMediaTypeMOV) {
- return @"com.apple.quicktime";
- }
- if (type == SLKPastableMediaTypePassbook) {
- return @"com.apple.pkpass";
- }
- if (type == SLKPastableMediaTypeImages) {
- return @"com.apple.uikit.image";
- }
-
- return nil;
- }
- SLKPastableMediaType SLKPastableMediaTypeFromNSString(NSString *string)
- {
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)]) {
- return SLKPastableMediaTypePNG;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)]) {
- return SLKPastableMediaTypeJPEG;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)]) {
- return SLKPastableMediaTypeTIFF;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)]) {
- return SLKPastableMediaTypeGIF;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)]) {
- return SLKPastableMediaTypeMOV;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)]) {
- return SLKPastableMediaTypePassbook;
- }
- if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)]) {
- return SLKPastableMediaTypeImages;
- }
- return SLKPastableMediaTypeNone;
- }
- - (BOOL)isExpanding
- {
- if (self.numberOfLines >= self.maxNumberOfLines) {
- return YES;
- }
- return NO;
- }
- - (BOOL)slk_shouldHidePlaceholder
- {
- if (self.placeholder.length == 0 || self.text.length > 0) {
- return YES;
- }
- return NO;
- }
- - (CGRect)slk_placeholderRectThatFits:(CGRect)bounds
- {
- CGFloat padding = self.textContainer.lineFragmentPadding;
-
- CGRect rect = CGRectZero;
- rect.size.height = [self.placeholderLabel sizeThatFits:bounds.size].height;
- rect.size.width = bounds.size.width - padding * 2.0 - self.textContainerInset.left - self.textContainerInset.right;
- rect.origin = UIEdgeInsetsInsetRect(bounds, self.textContainerInset).origin;
- rect.origin.x += padding;
-
- return rect;
- }
- #pragma mark - Setters
- - (void)setPlaceholder:(NSString *)placeholder
- {
- self.placeholderLabel.text = placeholder;
- self.accessibilityLabel = placeholder;
-
- [self setNeedsLayout];
- }
- - (void)setPlaceholderColor:(UIColor *)color
- {
- self.placeholderLabel.textColor = color;
- }
- - (void)setPlaceholderNumberOfLines:(NSInteger)numberOfLines
- {
- self.placeholderLabel.numberOfLines = numberOfLines;
-
- [self setNeedsLayout];
- }
- - (void)setPlaceholderFont:(UIFont *)placeholderFont
- {
- if (!placeholderFont) {
- self.placeholderLabel.font = self.font;
- }
- else {
- self.placeholderLabel.font = placeholderFont;
- }
- }
- - (void)setUndoManagerEnabled:(BOOL)enabled
- {
- if (self.undoManagerEnabled == enabled) {
- return;
- }
-
- self.undoManager.levelsOfUndo = 10;
- [self.undoManager removeAllActions];
- [self.undoManager setActionIsDiscardable:YES];
-
- _undoManagerEnabled = enabled;
- }
- - (void)setTypingSuggestionEnabled:(BOOL)enabled
- {
- if (self.isTypingSuggestionEnabled == enabled) {
- return;
- }
-
- self.autocorrectionType = enabled ? UITextAutocorrectionTypeDefault : UITextAutocorrectionTypeNo;
- self.spellCheckingType = enabled ? UITextSpellCheckingTypeDefault : UITextSpellCheckingTypeNo;
-
- if (@available(iOS 16.0, *)) {
- // On iOS 16 using "refreshFirstResponder" leads to a unwanted keyboard animation
- // "refreshInputViews" is enough here.
- [self refreshInputViews];
- } else {
- [self refreshFirstResponder];
- }
- }
- - (void)setContentOffset:(CGPoint)contentOffset
- {
- // At times during a layout pass, the content offset's x value may change.
- // Since we only care about vertical offset, let's override its horizontal value to avoid other layout issues.
- [super setContentOffset:CGPointMake(0.0, contentOffset.y)];
- }
- #pragma mark - UITextView Overrides
- - (void)setSelectedRange:(NSRange)selectedRange
- {
- [super setSelectedRange:selectedRange];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
- }
- - (void)setSelectedTextRange:(UITextRange *)selectedTextRange
- {
- [super setSelectedTextRange:selectedTextRange];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
- }
- - (void)setText:(NSString *)text
- {
- // Registers for undo management
- [self slk_prepareForUndo:@"Text Set"];
- if (text) {
- [self setAttributedText:[self slk_defaultAttributedStringForText:text]];
- }
- else {
- [self setAttributedText:nil];
- }
-
- [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self];
- }
- - (NSString *)text
- {
- return self.attributedText.string;
- }
- - (void)setAttributedText:(NSAttributedString *)attributedText
- {
- // Registers for undo management
- [self slk_prepareForUndo:@"Attributed Text Set"];
-
- [super setAttributedText:attributedText];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self];
- }
- - (void)setFont:(UIFont *)font
- {
- NSString *contentSizeCategory = [UIScreen mainScreen].traitCollection.preferredContentSizeCategory;
- [self setFont:font pointSize:font.pointSize withContentSizeCategory:contentSizeCategory];
-
- self.initialFontSize = font.pointSize;
- }
- - (void)setFont:(UIFont *)font pointSize:(CGFloat)pointSize withContentSizeCategory:(NSString *)contentSizeCategory
- {
- if (self.isDynamicTypeEnabled) {
- pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory);
- }
-
- UIFont *dynamicFont = [UIFont fontWithDescriptor:font.fontDescriptor size:pointSize];
-
- [super setFont:dynamicFont];
-
- // Updates the placeholder font too
- self.placeholderLabel.font = dynamicFont;
- }
- - (void)setDynamicTypeEnabled:(BOOL)dynamicTypeEnabled
- {
- if (self.isDynamicTypeEnabled == dynamicTypeEnabled) {
- return;
- }
-
- _dynamicTypeEnabled = dynamicTypeEnabled;
-
- NSString *contentSizeCategory = [UIScreen mainScreen].traitCollection.preferredContentSizeCategory;
- [self setFont:self.font pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory];
- }
- - (void)setTextAlignment:(NSTextAlignment)textAlignment
- {
- [super setTextAlignment:textAlignment];
-
- // Updates the placeholder text alignment too
- self.placeholderLabel.textAlignment = textAlignment;
- }
- #pragma mark - UITextInput Overrides
- #ifdef __IPHONE_9_0
- - (void)beginFloatingCursorAtPoint:(CGPoint)point
- {
- [super beginFloatingCursorAtPoint:point];
-
- _trackpadEnabled = YES;
- }
- - (void)updateFloatingCursorAtPoint:(CGPoint)point
- {
- [super updateFloatingCursorAtPoint:point];
- }
- - (void)endFloatingCursor
- {
- [super endFloatingCursor];
- _trackpadEnabled = NO;
-
- // We still need to notify a selection change in the textview after the trackpad is disabled
- if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
- [self.delegate textViewDidChangeSelection:self];
- }
-
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
- }
- #endif
- #pragma mark - UIResponder Overrides
- - (BOOL)canBecomeFirstResponder
- {
- [self slk_addCustomMenuControllerItems];
-
- return [super canBecomeFirstResponder];
- }
- - (BOOL)becomeFirstResponder
- {
- return [super becomeFirstResponder];
- }
- - (BOOL)canResignFirstResponder
- {
- // Removes undo/redo items
- if (self.undoManagerEnabled) {
- [self.undoManager removeAllActions];
- }
-
- return [super canResignFirstResponder];
- }
- - (BOOL)resignFirstResponder
- {
- return [super resignFirstResponder];
- }
- - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
- {
- if (self.isFormatting) {
- NSString *title = [self slk_formattingTitleFromSelector:action];
- NSString *symbol = [self slk_formattingSymbolWithTitle:title];
-
- if (symbol.length > 0) {
- if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldOfferFormattingForSymbol:)]) {
- return [self.delegate textView:self shouldOfferFormattingForSymbol:symbol];
- }
- else {
- return YES;
- }
- }
-
- return NO;
- }
- if (action == @selector(delete:)) {
- return NO;
- }
-
- if (action == @selector(slk_presentFormattingMenu:)) {
- return self.selectedRange.length > 0 ? YES : NO;
- }
-
- if (action == @selector(paste:) && [self slk_isPasteboardItemSupported]) {
- return YES;
- }
-
- if (self.undoManagerEnabled) {
- if (action == @selector(slk_undo:)) {
- if (self.undoManager.undoActionIsDiscardable) {
- return NO;
- }
- return [self.undoManager canUndo];
- }
- if (action == @selector(slk_redo:)) {
- if (self.undoManager.redoActionIsDiscardable) {
- return NO;
- }
- return [self.undoManager canRedo];
- }
- }
-
- return [super canPerformAction:action withSender:sender];
- }
- - (void)paste:(id)sender
- {
- id pastedItem = [self slk_pastedItem];
-
- if ([pastedItem isKindOfClass:[NSDictionary class]]) {
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidPasteItemNotification object:nil userInfo:pastedItem];
- }
- else if ([pastedItem isKindOfClass:[NSString class]]) {
- // Respect the delegate yo!
- if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
- if (![self.delegate textView:self shouldChangeTextInRange:self.selectedRange replacementText:pastedItem]) {
- return;
- }
- }
-
- // Inserting the text fixes a UITextView bug whitch automatically scrolls to the bottom
- // and beyond scroll content size sometimes when the text is too long
- [self slk_insertTextAtCaretRange:pastedItem];
- }
- }
- #pragma mark - NSObject Overrides
- - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
- {
- if ([super methodSignatureForSelector:sel]) {
- return [super methodSignatureForSelector:sel];
- }
- return [super methodSignatureForSelector:@selector(slk_format:)];
- }
- - (void)forwardInvocation:(NSInvocation *)invocation
- {
- NSString *title = [self slk_formattingTitleFromSelector:[invocation selector]];
-
- if (title.length > 0) {
- [self slk_format:title];
- }
- else {
- [super forwardInvocation:invocation];
- }
- }
- #pragma mark - Custom Actions
- - (void)slk_flashScrollIndicatorsIfNeeded
- {
- if (self.numberOfLines == self.maxNumberOfLines+1) {
- if (!_didFlashScrollIndicators) {
- _didFlashScrollIndicators = YES;
- [super flashScrollIndicators];
- }
- }
- else if (_didFlashScrollIndicators) {
- _didFlashScrollIndicators = NO;
- }
- }
- - (void)refreshFirstResponder
- {
- if (!self.isFirstResponder) {
- return;
- }
-
- _didNotResignFirstResponder = YES;
- [self resignFirstResponder];
-
- _didNotResignFirstResponder = NO;
- [self becomeFirstResponder];
- }
- - (void)refreshInputViews
- {
- _didNotResignFirstResponder = YES;
-
- [super reloadInputViews];
-
- _didNotResignFirstResponder = NO;
- }
- - (void)slk_addCustomMenuControllerItems
- {
- UIMenuItem *undo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Undo", nil) action:@selector(slk_undo:)];
- UIMenuItem *redo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Redo", nil) action:@selector(slk_redo:)];
-
- NSMutableArray *items = [NSMutableArray arrayWithObjects:undo, redo, nil];
-
- if (self.registeredFormattingTitles.count > 0) {
- UIMenuItem *format = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Format", nil) action:@selector(slk_presentFormattingMenu:)];
- [items addObject:format];
- }
-
- [[UIMenuController sharedMenuController] setMenuItems:items];
- }
- - (void)slk_undo:(id)sender
- {
- [self.undoManager undo];
- }
- - (void)slk_redo:(id)sender
- {
- [self.undoManager redo];
- }
- - (void)slk_presentFormattingMenu:(id)sender
- {
- NSMutableArray *items = [NSMutableArray arrayWithCapacity:self.registeredFormattingTitles.count];
-
- for (NSString *name in self.registeredFormattingTitles) {
-
- NSString *sel = [NSString stringWithFormat:@"%@%@", SLKTextViewGenericFormattingSelectorPrefix, name];
-
- UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:name action:NSSelectorFromString(sel)];
- [items addObject:item];
- }
-
- self.formatting = YES;
-
- UIMenuController *menu = [UIMenuController sharedMenuController];
- [menu setMenuItems:items];
-
- NSLayoutManager *manager = self.layoutManager;
- CGRect targetRect = [manager boundingRectForGlyphRange:self.selectedRange inTextContainer:self.textContainer];
-
- [menu setTargetRect:targetRect inView:self];
-
- [menu setMenuVisible:YES animated:YES];
- }
- - (NSString *)slk_formattingTitleFromSelector:(SEL)selector
- {
- NSString *selectorString = NSStringFromSelector(selector);
- NSRange match = [selectorString rangeOfString:SLKTextViewGenericFormattingSelectorPrefix];
-
- if (match.location != NSNotFound) {
- return [selectorString substringFromIndex:SLKTextViewGenericFormattingSelectorPrefix.length];
- }
-
- return nil;
- }
- - (NSString *)slk_formattingSymbolWithTitle:(NSString *)title
- {
- NSUInteger idx = [self.registeredFormattingTitles indexOfObject:title];
-
- if (idx <= self.registeredFormattingSymbols.count -1) {
- return self.registeredFormattingSymbols[idx];
- }
-
- return nil;
- }
- - (void)slk_format:(NSString *)titles
- {
- NSString *symbol = [self slk_formattingSymbolWithTitle:titles];
-
- if (symbol.length > 0) {
- NSRange selection = self.selectedRange;
-
- NSRange range = [self slk_insertText:symbol inRange:NSMakeRange(selection.location, 0)];
- range.location += selection.length;
- range.length = 0;
-
- // The default behavior is to add a closure
- BOOL addClosure = YES;
-
- if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldInsertSuffixForFormattingWithSymbol:prefixRange:)]) {
- addClosure = [self.delegate textView:self shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:selection];
- }
-
- if (addClosure) {
- self.selectedRange = [self slk_insertText:symbol inRange:range];
- }
- }
- }
- #pragma mark - Markdown Formatting
- - (void)registerMarkdownFormattingSymbol:(NSString *)symbol withTitle:(NSString *)title
- {
- if (!symbol || !title) {
- return;
- }
-
- if (!_registeredFormattingTitles) {
- _registeredFormattingTitles = [NSMutableArray new];
- _registeredFormattingSymbols = [NSMutableArray new];
- }
-
- // Adds the symbol if not contained already
- if (![self.registeredSymbols containsObject:symbol]) {
- [self.registeredFormattingTitles addObject:title];
- [self.registeredFormattingSymbols addObject:symbol];
- }
- }
- - (NSArray *)registeredSymbols
- {
- return self.registeredFormattingSymbols;
- }
- #pragma mark - Notification Events
- - (void)slk_didBeginEditing:(NSNotification *)notification
- {
- if (![notification.object isEqual:self]) {
- return;
- }
-
- // Do something
- }
- - (void)slk_didChangeText:(NSNotification *)notification
- {
- if (![notification.object isEqual:self]) {
- return;
- }
-
- if (self.placeholderLabel.hidden != [self slk_shouldHidePlaceholder]) {
- [self setNeedsLayout];
- }
-
- [self slk_flashScrollIndicatorsIfNeeded];
- }
- - (void)slk_didEndEditing:(NSNotification *)notification
- {
- if (![notification.object isEqual:self]) {
- return;
- }
-
- // Do something
- }
- - (void)slk_didChangeTextInputMode:(NSNotification *)notification
- {
- // Do something
- }
- - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification
- {
- if (!self.isDynamicTypeEnabled) {
- return;
- }
-
- NSString *contentSizeCategory = notification.userInfo[UIContentSizeCategoryNewValueKey];
-
- [self setFont:self.font pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory];
-
- NSString *text = [self.text copy];
-
- // Reloads the content size of the text view
- [self setText:@" "];
- [self setText:text];
- }
- - (void)slk_willShowMenuController:(NSNotification *)notification
- {
- // Do something
- }
- - (void)slk_didHideMenuController:(NSNotification *)notification
- {
- self.formatting = NO;
-
- [self slk_addCustomMenuControllerItems];
- }
- #pragma mark - KVO Listener
- - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
- {
- if ([object isEqual:self] && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewContentSizeDidChangeNotification object:self userInfo:nil];
- }
- else {
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
- }
- }
- #pragma mark - Motion Events
- - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
- {
- if (event.type == UIEventTypeMotion && event.subtype == UIEventSubtypeMotionShake) {
- [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidShakeNotification object:self];
- }
- }
- #pragma mark - External Keyboard Support
- typedef void (^SLKKeyCommandHandler)(UIKeyCommand *keyCommand);
- - (void)observeKeyInput:(NSString *)input modifiers:(UIKeyModifierFlags)modifiers title:(NSString *_Nullable)title completion:(void (^)(UIKeyCommand *keyCommand))completion
- {
- NSAssert([input isKindOfClass:[NSString class]], @"You must provide a string with one or more characters corresponding to the keys to observe.");
- NSAssert(completion != nil, @"You must provide a non-nil completion block.");
-
- if (!input || !completion) {
- return;
- }
-
- UIKeyCommand *keyCommand = [UIKeyCommand keyCommandWithInput:input modifierFlags:modifiers action:@selector(didDetectKeyCommand:)];
-
- #ifdef __IPHONE_9_0
- if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) {
- keyCommand.discoverabilityTitle = title;
- }
- #endif
-
- if (!_registeredKeyCommands) {
- _registeredKeyCommands = [NSMutableDictionary new];
- _registeredKeyCallbacks = [NSMutableDictionary new];
- }
-
- NSString *key = [self keyForKeyCommand:keyCommand];
-
- self.registeredKeyCommands[key] = keyCommand;
- self.registeredKeyCallbacks[key] = completion;
- }
- - (void)didDetectKeyCommand:(UIKeyCommand *)keyCommand
- {
- NSString *key = [self keyForKeyCommand:keyCommand];
-
- SLKKeyCommandHandler completion = self.registeredKeyCallbacks[key];
-
- if (completion) {
- completion(keyCommand);
- }
- }
- - (NSString *)keyForKeyCommand:(UIKeyCommand *)keyCommand
- {
- return [NSString stringWithFormat:@"%@_%ld", keyCommand.input, (long)keyCommand.modifierFlags];
- }
- - (NSArray *)keyCommands
- {
- if (self.registeredKeyCommands) {
- return [self.registeredKeyCommands allValues];
- }
-
- return nil;
- }
- #pragma mark Up/Down Cursor Movement
- - (void)didPressArrowKey:(UIKeyCommand *)keyCommand
- {
- if (![keyCommand isKindOfClass:[UIKeyCommand class]] || self.text.length == 0 || self.numberOfLines < 2) {
- return;
- }
-
- if ([keyCommand.input isEqualToString:UIKeyInputUpArrow]) {
- [self slk_moveCursorTodirection:UITextLayoutDirectionUp];
- }
- else if ([keyCommand.input isEqualToString:UIKeyInputDownArrow]) {
- [self slk_moveCursorTodirection:UITextLayoutDirectionDown];
- }
- }
- - (void)slk_moveCursorTodirection:(UITextLayoutDirection)direction
- {
- UITextPosition *start = (direction == UITextLayoutDirectionUp) ? self.selectedTextRange.start : self.selectedTextRange.end;
-
- if ([self slk_isNewVerticalMovementForPosition:start inDirection:direction]) {
- self.verticalMoveDirection = direction;
- self.verticalMoveStartCaretRect = [self caretRectForPosition:start];
- }
-
- if (start) {
- UITextPosition *end = [self slk_closestPositionToPosition:start inDirection:direction];
-
- if (end) {
- self.verticalMoveLastCaretRect = [self caretRectForPosition:end];
- self.selectedTextRange = [self textRangeFromPosition:end toPosition:end];
-
- [self slk_scrollToCaretPositonAnimated:NO];
- }
- }
- }
- // Based on code from Ruben Cabaco
- // https://gist.github.com/rcabaco/6765778
- - (UITextPosition *)slk_closestPositionToPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
- {
- // Only up/down are implemented. No real need for left/right since that is native to UITextInput.
- NSParameterAssert(direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown);
-
- // Translate the vertical direction to a horizontal direction.
- UITextLayoutDirection lookupDirection = (direction == UITextLayoutDirectionUp) ? UITextLayoutDirectionLeft : UITextLayoutDirectionRight;
-
- // Walk one character at a time in `lookupDirection` until the next line is reached.
- UITextPosition *checkPosition = position;
- UITextPosition *closestPosition = position;
- CGRect startingCaretRect = [self caretRectForPosition:position];
- CGRect nextLineCaretRect = CGRectZero;
- BOOL isInNextLine = NO;
-
- while (YES) {
- UITextPosition *nextPosition = [self positionFromPosition:checkPosition inDirection:lookupDirection offset:1];
-
- // End of line.
- if (!nextPosition || [self comparePosition:checkPosition toPosition:nextPosition] == NSOrderedSame) {
- break;
- }
-
- checkPosition = nextPosition;
- CGRect checkRect = [self caretRectForPosition:checkPosition];
- if (CGRectGetMidY(startingCaretRect) != CGRectGetMidY(checkRect)) {
- // While on the next line stop just above/below the starting position.
- if (lookupDirection == UITextLayoutDirectionLeft && CGRectGetMidX(checkRect) <= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
- closestPosition = checkPosition;
- break;
- }
- if (lookupDirection == UITextLayoutDirectionRight && CGRectGetMidX(checkRect) >= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
- closestPosition = checkPosition;
- break;
- }
- // But don't skip lines.
- if (isInNextLine && CGRectGetMidY(checkRect) != CGRectGetMidY(nextLineCaretRect)) {
- break;
- }
-
- isInNextLine = YES;
- nextLineCaretRect = checkRect;
- closestPosition = checkPosition;
- }
- }
- return closestPosition;
- }
- - (BOOL)slk_isNewVerticalMovementForPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
- {
- CGRect caretRect = [self caretRectForPosition:position];
- BOOL noPreviousStartPosition = CGRectEqualToRect(self.verticalMoveStartCaretRect, CGRectZero);
- BOOL caretMovedSinceLastPosition = !CGRectEqualToRect(caretRect, self.verticalMoveLastCaretRect);
- BOOL directionChanged = self.verticalMoveDirection != direction;
-
- BOOL newMovement = noPreviousStartPosition || caretMovedSinceLastPosition || directionChanged;
- return newMovement;
- }
- #pragma mark - NSNotificationCenter registration
- - (void)slk_registerNotifications
- {
- [self slk_unregisterNotifications];
-
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeText:) name:UITextViewTextDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextInputMode:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willShowMenuController:) name:UIMenuControllerWillShowMenuNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didHideMenuController:) name:UIMenuControllerDidHideMenuNotification object:nil];
- }
- - (void)slk_unregisterNotifications
- {
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextInputCurrentInputModeDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
- }
- #pragma mark - Lifeterm
- - (void)dealloc
- {
- [self slk_unregisterNotifications];
-
- [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize))];
- }
- @end
|