You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1155 lines
36 KiB

5 years ago
  1. //
  2. // SlackTextViewController
  3. // https://github.com/slackhq/SlackTextViewController
  4. //
  5. // Copyright 2014-2016 Slack Technologies, Inc.
  6. // Licence: MIT-Licence
  7. //
  8. #import "SLKTextView.h"
  9. #import "SLKTextView+SLKAdditions.h"
  10. #import "SLKUIConstants.h"
  11. NSString * const SLKTextViewTextWillChangeNotification = @"SLKTextViewTextWillChangeNotification";
  12. NSString * const SLKTextViewContentSizeDidChangeNotification = @"SLKTextViewContentSizeDidChangeNotification";
  13. NSString * const SLKTextViewSelectedRangeDidChangeNotification = @"SLKTextViewSelectedRangeDidChangeNotification";
  14. NSString * const SLKTextViewDidPasteItemNotification = @"SLKTextViewDidPasteItemNotification";
  15. NSString * const SLKTextViewDidShakeNotification = @"SLKTextViewDidShakeNotification";
  16. NSString * const SLKTextViewPastedItemContentType = @"SLKTextViewPastedItemContentType";
  17. NSString * const SLKTextViewPastedItemMediaType = @"SLKTextViewPastedItemMediaType";
  18. NSString * const SLKTextViewPastedItemData = @"SLKTextViewPastedItemData";
  19. static NSString *const SLKTextViewGenericFormattingSelectorPrefix = @"slk_format_";
  20. @interface SLKTextView ()
  21. // The label used as placeholder
  22. @property (nonatomic, strong) UILabel *placeholderLabel;
  23. // The initial font point size, used for dynamic type calculations
  24. @property (nonatomic) CGFloat initialFontSize;
  25. // Used for moving the caret up/down
  26. @property (nonatomic) UITextLayoutDirection verticalMoveDirection;
  27. @property (nonatomic) CGRect verticalMoveStartCaretRect;
  28. @property (nonatomic) CGRect verticalMoveLastCaretRect;
  29. // Used for detecting if the scroll indicator was previously flashed
  30. @property (nonatomic) BOOL didFlashScrollIndicators;
  31. @property (nonatomic, strong) NSMutableArray *registeredFormattingTitles;
  32. @property (nonatomic, strong) NSMutableArray *registeredFormattingSymbols;
  33. @property (nonatomic, getter=isFormatting) BOOL formatting;
  34. // The keyboard commands available for external keyboards
  35. @property (nonatomic, strong) NSMutableDictionary *registeredKeyCommands;
  36. @property (nonatomic, strong) NSMutableDictionary *registeredKeyCallbacks;
  37. @end
  38. @implementation SLKTextView
  39. @dynamic delegate;
  40. #pragma mark - Initialization
  41. - (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer
  42. {
  43. if (self = [super initWithFrame:frame textContainer:textContainer]) {
  44. [self slk_commonInit];
  45. }
  46. return self;
  47. }
  48. - (instancetype)initWithCoder:(NSCoder *)coder
  49. {
  50. if (self = [super initWithCoder:coder]) {
  51. [self slk_commonInit];
  52. }
  53. return self;
  54. }
  55. - (void)slk_commonInit
  56. {
  57. _pastableMediaTypes = SLKPastableMediaTypeNone;
  58. _dynamicTypeEnabled = YES;
  59. self.undoManagerEnabled = YES;
  60. self.editable = YES;
  61. self.selectable = YES;
  62. self.scrollEnabled = YES;
  63. self.scrollsToTop = NO;
  64. self.directionalLockEnabled = YES;
  65. self.dataDetectorTypes = UIDataDetectorTypeNone;
  66. [self slk_registerNotifications];
  67. [self addObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize)) options:NSKeyValueObservingOptionNew context:NULL];
  68. }
  69. #pragma mark - UIView Overrides
  70. - (CGSize)intrinsicContentSize
  71. {
  72. CGFloat height = self.font.lineHeight;
  73. height += self.textContainerInset.top + self.textContainerInset.bottom;
  74. return CGSizeMake(UIViewNoIntrinsicMetric, height);
  75. }
  76. + (BOOL)requiresConstraintBasedLayout
  77. {
  78. return YES;
  79. }
  80. - (void)layoutIfNeeded
  81. {
  82. if (!self.window) {
  83. return;
  84. }
  85. [super layoutIfNeeded];
  86. }
  87. - (void)layoutSubviews
  88. {
  89. [super layoutSubviews];
  90. self.placeholderLabel.hidden = [self slk_shouldHidePlaceholder];
  91. if (!self.placeholderLabel.hidden) {
  92. [UIView performWithoutAnimation:^{
  93. self.placeholderLabel.frame = [self slk_placeholderRectThatFits:self.bounds];
  94. [self sendSubviewToBack:self.placeholderLabel];
  95. }];
  96. }
  97. }
  98. #pragma mark - Getters
  99. - (UILabel *)placeholderLabel
  100. {
  101. if (!_placeholderLabel) {
  102. _placeholderLabel = [UILabel new];
  103. _placeholderLabel.clipsToBounds = NO;
  104. _placeholderLabel.numberOfLines = 1;
  105. _placeholderLabel.autoresizesSubviews = NO;
  106. _placeholderLabel.font = self.font;
  107. _placeholderLabel.backgroundColor = [UIColor clearColor];
  108. _placeholderLabel.textColor = [UIColor lightGrayColor];
  109. _placeholderLabel.hidden = YES;
  110. _placeholderLabel.isAccessibilityElement = NO;
  111. [self addSubview:_placeholderLabel];
  112. }
  113. return _placeholderLabel;
  114. }
  115. - (NSString *)placeholder
  116. {
  117. return self.placeholderLabel.text;
  118. }
  119. - (UIColor *)placeholderColor
  120. {
  121. return self.placeholderLabel.textColor;
  122. }
  123. - (UIFont *)placeholderFont
  124. {
  125. return self.placeholderLabel.font;
  126. }
  127. - (NSUInteger)numberOfLines
  128. {
  129. CGSize contentSize = self.contentSize;
  130. CGFloat contentHeight = contentSize.height;
  131. contentHeight -= self.textContainerInset.top + self.textContainerInset.bottom;
  132. NSUInteger lines = fabs(contentHeight/self.font.lineHeight);
  133. // This helps preventing the content's height to be larger that the bounds' height
  134. // Avoiding this way to have unnecessary scrolling in the text view when there is only 1 line of content
  135. if (lines == 1 && contentSize.height > self.bounds.size.height) {
  136. contentSize.height = self.bounds.size.height;
  137. self.contentSize = contentSize;
  138. }
  139. // Let's fallback to the minimum line count
  140. if (lines == 0) {
  141. lines = 1;
  142. }
  143. return lines;
  144. }
  145. - (NSUInteger)maxNumberOfLines
  146. {
  147. NSUInteger numberOfLines = _maxNumberOfLines;
  148. if (SLK_IS_LANDSCAPE) {
  149. if ((SLK_IS_IPHONE4 || SLK_IS_IPHONE5)) {
  150. numberOfLines = 2.0; // 2 lines max on smaller iPhones
  151. }
  152. else if (SLK_IS_IPHONE) {
  153. numberOfLines /= 2.0; // Half size on larger iPhone
  154. }
  155. }
  156. if (self.isDynamicTypeEnabled) {
  157. NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory];
  158. CGFloat pointSizeDifference = SLKPointSizeDifferenceForCategory(contentSizeCategory);
  159. CGFloat factor = pointSizeDifference/self.initialFontSize;
  160. if (fabs(factor) > 0.75) {
  161. factor = 0.75;
  162. }
  163. numberOfLines -= floorf(numberOfLines * factor); // Calculates a dynamic number of lines depending of the user preferred font size
  164. }
  165. return numberOfLines;
  166. }
  167. - (BOOL)isTypingSuggestionEnabled
  168. {
  169. return (self.autocorrectionType == UITextAutocorrectionTypeNo) ? NO : YES;
  170. }
  171. - (BOOL)isFormattingEnabled
  172. {
  173. return (self.registeredFormattingSymbols.count > 0) ? YES : NO;
  174. }
  175. // Returns only a supported pasted item
  176. - (id)slk_pastedItem
  177. {
  178. NSString *contentType = [self slk_pasteboardContentType];
  179. NSData *data = [[UIPasteboard generalPasteboard] dataForPasteboardType:contentType];
  180. if (data && [data isKindOfClass:[NSData class]])
  181. {
  182. SLKPastableMediaType mediaType = SLKPastableMediaTypeFromNSString(contentType);
  183. NSDictionary *userInfo = @{SLKTextViewPastedItemContentType: contentType,
  184. SLKTextViewPastedItemMediaType: @(mediaType),
  185. SLKTextViewPastedItemData: data};
  186. return userInfo;
  187. }
  188. if ([[UIPasteboard generalPasteboard] URL]) {
  189. return [[[UIPasteboard generalPasteboard] URL] absoluteString];
  190. }
  191. if ([[UIPasteboard generalPasteboard] string]) {
  192. return [[UIPasteboard generalPasteboard] string];
  193. }
  194. return nil;
  195. }
  196. // Checks if any supported media found in the general pasteboard
  197. - (BOOL)slk_isPasteboardItemSupported
  198. {
  199. if ([self slk_pasteboardContentType].length > 0) {
  200. return YES;
  201. }
  202. return NO;
  203. }
  204. - (NSString *)slk_pasteboardContentType
  205. {
  206. NSArray *pasteboardTypes = [[UIPasteboard generalPasteboard] pasteboardTypes];
  207. NSMutableArray *subpredicates = [NSMutableArray new];
  208. for (NSString *type in [self slk_supportedMediaTypes]) {
  209. [subpredicates addObject:[NSPredicate predicateWithFormat:@"SELF == %@", type]];
  210. }
  211. return [[pasteboardTypes filteredArrayUsingPredicate:[NSCompoundPredicate orPredicateWithSubpredicates:subpredicates]] firstObject];
  212. }
  213. - (NSArray *)slk_supportedMediaTypes
  214. {
  215. if (self.pastableMediaTypes == SLKPastableMediaTypeNone) {
  216. return nil;
  217. }
  218. NSMutableArray *types = [NSMutableArray new];
  219. if (self.pastableMediaTypes & SLKPastableMediaTypePNG) {
  220. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)];
  221. }
  222. if (self.pastableMediaTypes & SLKPastableMediaTypeJPEG) {
  223. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)];
  224. }
  225. if (self.pastableMediaTypes & SLKPastableMediaTypeTIFF) {
  226. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)];
  227. }
  228. if (self.pastableMediaTypes & SLKPastableMediaTypeGIF) {
  229. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)];
  230. }
  231. if (self.pastableMediaTypes & SLKPastableMediaTypeMOV) {
  232. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)];
  233. }
  234. if (self.pastableMediaTypes & SLKPastableMediaTypePassbook) {
  235. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)];
  236. }
  237. if (self.pastableMediaTypes & SLKPastableMediaTypeImages) {
  238. [types addObject:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)];
  239. }
  240. return types;
  241. }
  242. NSString *NSStringFromSLKPastableMediaType(SLKPastableMediaType type)
  243. {
  244. if (type == SLKPastableMediaTypePNG) {
  245. return @"public.png";
  246. }
  247. if (type == SLKPastableMediaTypeJPEG) {
  248. return @"public.jpeg";
  249. }
  250. if (type == SLKPastableMediaTypeTIFF) {
  251. return @"public.tiff";
  252. }
  253. if (type == SLKPastableMediaTypeGIF) {
  254. return @"com.compuserve.gif";
  255. }
  256. if (type == SLKPastableMediaTypeMOV) {
  257. return @"com.apple.quicktime";
  258. }
  259. if (type == SLKPastableMediaTypePassbook) {
  260. return @"com.apple.pkpass";
  261. }
  262. if (type == SLKPastableMediaTypeImages) {
  263. return @"com.apple.uikit.image";
  264. }
  265. return nil;
  266. }
  267. SLKPastableMediaType SLKPastableMediaTypeFromNSString(NSString *string)
  268. {
  269. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePNG)]) {
  270. return SLKPastableMediaTypePNG;
  271. }
  272. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeJPEG)]) {
  273. return SLKPastableMediaTypeJPEG;
  274. }
  275. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeTIFF)]) {
  276. return SLKPastableMediaTypeTIFF;
  277. }
  278. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeGIF)]) {
  279. return SLKPastableMediaTypeGIF;
  280. }
  281. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeMOV)]) {
  282. return SLKPastableMediaTypeMOV;
  283. }
  284. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypePassbook)]) {
  285. return SLKPastableMediaTypePassbook;
  286. }
  287. if ([string isEqualToString:NSStringFromSLKPastableMediaType(SLKPastableMediaTypeImages)]) {
  288. return SLKPastableMediaTypeImages;
  289. }
  290. return SLKPastableMediaTypeNone;
  291. }
  292. - (BOOL)isExpanding
  293. {
  294. if (self.numberOfLines >= self.maxNumberOfLines) {
  295. return YES;
  296. }
  297. return NO;
  298. }
  299. - (BOOL)slk_shouldHidePlaceholder
  300. {
  301. if (self.placeholder.length == 0 || self.text.length > 0) {
  302. return YES;
  303. }
  304. return NO;
  305. }
  306. - (CGRect)slk_placeholderRectThatFits:(CGRect)bounds
  307. {
  308. CGFloat padding = self.textContainer.lineFragmentPadding;
  309. CGRect rect = CGRectZero;
  310. rect.size.height = [self.placeholderLabel sizeThatFits:bounds.size].height;
  311. rect.size.width = self.textContainer.size.width - padding*2.0;
  312. rect.origin = UIEdgeInsetsInsetRect(bounds, self.textContainerInset).origin;
  313. rect.origin.x += padding;
  314. return rect;
  315. }
  316. #pragma mark - Setters
  317. - (void)setPlaceholder:(NSString *)placeholder
  318. {
  319. self.placeholderLabel.text = placeholder;
  320. self.accessibilityLabel = placeholder;
  321. [self setNeedsLayout];
  322. }
  323. - (void)setAttributedPlaceholder:(NSAttributedString *)attributedPlaceholder
  324. {
  325. self.placeholderLabel.attributedText = attributedPlaceholder;
  326. self.accessibilityLabel = attributedPlaceholder.string;
  327. [self setNeedsLayout];
  328. }
  329. - (void)setPlaceholderColor:(UIColor *)color
  330. {
  331. self.placeholderLabel.textColor = color;
  332. }
  333. - (void)setPlaceholderNumberOfLines:(NSInteger)numberOfLines
  334. {
  335. self.placeholderLabel.numberOfLines = numberOfLines;
  336. [self setNeedsLayout];
  337. }
  338. - (void)setPlaceholderFont:(UIFont *)placeholderFont
  339. {
  340. if (!placeholderFont) {
  341. self.placeholderLabel.font = self.font;
  342. }
  343. else {
  344. self.placeholderLabel.font = placeholderFont;
  345. }
  346. }
  347. - (void)setUndoManagerEnabled:(BOOL)enabled
  348. {
  349. if (self.undoManagerEnabled == enabled) {
  350. return;
  351. }
  352. self.undoManager.levelsOfUndo = 10;
  353. [self.undoManager removeAllActions];
  354. [self.undoManager setActionIsDiscardable:YES];
  355. _undoManagerEnabled = enabled;
  356. }
  357. - (void)setTypingSuggestionEnabled:(BOOL)enabled
  358. {
  359. if (self.isTypingSuggestionEnabled == enabled) {
  360. return;
  361. }
  362. self.autocorrectionType = enabled ? UITextAutocorrectionTypeDefault : UITextAutocorrectionTypeNo;
  363. self.spellCheckingType = enabled ? UITextSpellCheckingTypeDefault : UITextSpellCheckingTypeNo;
  364. //NOTE: this cause to resign textview responder which we don't want to
  365. //[self refreshFirstResponder];
  366. }
  367. - (void)setContentOffset:(CGPoint)contentOffset
  368. {
  369. // At times during a layout pass, the content offset's x value may change.
  370. // Since we only care about vertical offset, let's override its horizontal value to avoid other layout issues.
  371. [super setContentOffset:CGPointMake(0.0, contentOffset.y)];
  372. }
  373. #pragma mark - UITextView Overrides
  374. - (void)setSelectedRange:(NSRange)selectedRange
  375. {
  376. [super setSelectedRange:selectedRange];
  377. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
  378. }
  379. - (void)setSelectedTextRange:(UITextRange *)selectedTextRange
  380. {
  381. [super setSelectedTextRange:selectedTextRange];
  382. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
  383. }
  384. - (void)setText:(NSString *)text
  385. {
  386. // Registers for undo management
  387. [self slk_prepareForUndo:@"Text Set"];
  388. if (text) {
  389. [self setAttributedText:[self slk_defaultAttributedStringForText:text]];
  390. }
  391. else {
  392. [self setAttributedText:nil];
  393. }
  394. [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self];
  395. }
  396. - (NSString *)text
  397. {
  398. return self.attributedText.string;
  399. }
  400. - (void)setAttributedText:(NSAttributedString *)attributedText
  401. {
  402. // Registers for undo management
  403. [self slk_prepareForUndo:@"Attributed Text Set"];
  404. [super setAttributedText:attributedText];
  405. [[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:self];
  406. }
  407. - (void)setFont:(UIFont *)font
  408. {
  409. NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory];
  410. [self setFontName:font.fontName pointSize:font.pointSize withContentSizeCategory:contentSizeCategory];
  411. self.initialFontSize = font.pointSize;
  412. }
  413. - (void)setFontName:(NSString *)fontName pointSize:(CGFloat)pointSize withContentSizeCategory:(NSString *)contentSizeCategory
  414. {
  415. if (self.isDynamicTypeEnabled) {
  416. pointSize += SLKPointSizeDifferenceForCategory(contentSizeCategory);
  417. }
  418. UIFont *dynamicFont = [UIFont fontWithName:fontName size:pointSize];
  419. [super setFont:dynamicFont];
  420. // Updates the placeholder font too
  421. self.placeholderLabel.font = dynamicFont;
  422. }
  423. - (void)setDynamicTypeEnabled:(BOOL)dynamicTypeEnabled
  424. {
  425. if (self.isDynamicTypeEnabled == dynamicTypeEnabled) {
  426. return;
  427. }
  428. _dynamicTypeEnabled = dynamicTypeEnabled;
  429. NSString *contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory];
  430. [self setFontName:self.font.fontName pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory];
  431. }
  432. - (void)setTextAlignment:(NSTextAlignment)textAlignment
  433. {
  434. [super setTextAlignment:textAlignment];
  435. // Updates the placeholder text alignment too
  436. self.placeholderLabel.textAlignment = textAlignment;
  437. }
  438. #pragma mark - UITextInput Overrides
  439. #ifdef __IPHONE_9_0
  440. - (void)beginFloatingCursorAtPoint:(CGPoint)point
  441. {
  442. [super beginFloatingCursorAtPoint:point];
  443. _trackpadEnabled = YES;
  444. }
  445. - (void)updateFloatingCursorAtPoint:(CGPoint)point
  446. {
  447. [super updateFloatingCursorAtPoint:point];
  448. }
  449. - (void)endFloatingCursor
  450. {
  451. [super endFloatingCursor];
  452. _trackpadEnabled = NO;
  453. // We still need to notify a selection change in the textview after the trackpad is disabled
  454. if (self.delegate && [self.delegate respondsToSelector:@selector(textViewDidChangeSelection:)]) {
  455. [self.delegate textViewDidChangeSelection:self];
  456. }
  457. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewSelectedRangeDidChangeNotification object:self userInfo:nil];
  458. }
  459. #endif
  460. #pragma mark - UIResponder Overrides
  461. - (BOOL)canBecomeFirstResponder
  462. {
  463. [self slk_addCustomMenuControllerItems];
  464. return [super canBecomeFirstResponder];
  465. }
  466. - (BOOL)becomeFirstResponder
  467. {
  468. return [super becomeFirstResponder];
  469. }
  470. - (BOOL)canResignFirstResponder
  471. {
  472. // Removes undo/redo items
  473. if (self.undoManagerEnabled) {
  474. [self.undoManager removeAllActions];
  475. }
  476. return [super canResignFirstResponder];
  477. }
  478. - (BOOL)resignFirstResponder
  479. {
  480. return [super resignFirstResponder];
  481. }
  482. - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
  483. {
  484. if (self.isFormatting) {
  485. NSString *title = [self slk_formattingTitleFromSelector:action];
  486. NSString *symbol = [self slk_formattingSymbolWithTitle:title];
  487. if (symbol.length > 0) {
  488. if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldOfferFormattingForSymbol:)]) {
  489. return [self.delegate textView:self shouldOfferFormattingForSymbol:symbol];
  490. }
  491. else {
  492. return YES;
  493. }
  494. }
  495. return NO;
  496. }
  497. if (action == @selector(delete:)) {
  498. return NO;
  499. }
  500. if (action == @selector(slk_presentFormattingMenu:)) {
  501. return self.selectedRange.length > 0 ? YES : NO;
  502. }
  503. if (action == @selector(paste:) && [self slk_isPasteboardItemSupported]) {
  504. return YES;
  505. }
  506. if (self.undoManagerEnabled) {
  507. if (action == @selector(slk_undo:)) {
  508. if (self.undoManager.undoActionIsDiscardable) {
  509. return NO;
  510. }
  511. return [self.undoManager canUndo];
  512. }
  513. if (action == @selector(slk_redo:)) {
  514. if (self.undoManager.redoActionIsDiscardable) {
  515. return NO;
  516. }
  517. return [self.undoManager canRedo];
  518. }
  519. }
  520. return [super canPerformAction:action withSender:sender];
  521. }
  522. - (void)paste:(id)sender
  523. {
  524. id pastedItem = [self slk_pastedItem];
  525. if ([pastedItem isKindOfClass:[NSDictionary class]]) {
  526. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidPasteItemNotification object:nil userInfo:pastedItem];
  527. }
  528. else if ([pastedItem isKindOfClass:[NSString class]]) {
  529. // Respect the delegate yo!
  530. if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldChangeTextInRange:replacementText:)]) {
  531. if (![self.delegate textView:self shouldChangeTextInRange:self.selectedRange replacementText:pastedItem]) {
  532. return;
  533. }
  534. }
  535. // Inserting the text fixes a UITextView bug whitch automatically scrolls to the bottom
  536. // and beyond scroll content size sometimes when the text is too long
  537. [self slk_insertTextAtCaretRange:pastedItem];
  538. }
  539. }
  540. #pragma mark - NSObject Overrides
  541. - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
  542. {
  543. if ([super methodSignatureForSelector:sel]) {
  544. return [super methodSignatureForSelector:sel];
  545. }
  546. return [super methodSignatureForSelector:@selector(slk_format:)];
  547. }
  548. - (void)forwardInvocation:(NSInvocation *)invocation
  549. {
  550. NSString *title = [self slk_formattingTitleFromSelector:[invocation selector]];
  551. if (title.length > 0) {
  552. [self slk_format:title];
  553. }
  554. else {
  555. [super forwardInvocation:invocation];
  556. }
  557. }
  558. #pragma mark - Custom Actions
  559. - (void)slk_flashScrollIndicatorsIfNeeded
  560. {
  561. if (self.numberOfLines == self.maxNumberOfLines+1) {
  562. if (!_didFlashScrollIndicators) {
  563. _didFlashScrollIndicators = YES;
  564. [super flashScrollIndicators];
  565. }
  566. }
  567. else if (_didFlashScrollIndicators) {
  568. _didFlashScrollIndicators = NO;
  569. }
  570. }
  571. - (void)refreshFirstResponder
  572. {
  573. if (!self.isFirstResponder) {
  574. return;
  575. }
  576. _didNotResignFirstResponder = YES;
  577. [self resignFirstResponder];
  578. _didNotResignFirstResponder = NO;
  579. [self becomeFirstResponder];
  580. }
  581. - (void)refreshInputViews
  582. {
  583. _didNotResignFirstResponder = YES;
  584. [super reloadInputViews];
  585. _didNotResignFirstResponder = NO;
  586. }
  587. - (void)slk_addCustomMenuControllerItems
  588. {
  589. UIMenuItem *undo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Undo", nil) action:@selector(slk_undo:)];
  590. UIMenuItem *redo = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Redo", nil) action:@selector(slk_redo:)];
  591. NSMutableArray *items = [NSMutableArray arrayWithObjects:undo, redo, nil];
  592. if (self.registeredFormattingTitles.count > 0) {
  593. UIMenuItem *format = [[UIMenuItem alloc] initWithTitle:NSLocalizedString(@"Format", nil) action:@selector(slk_presentFormattingMenu:)];
  594. [items addObject:format];
  595. }
  596. [[UIMenuController sharedMenuController] setMenuItems:items];
  597. }
  598. - (void)slk_undo:(id)sender
  599. {
  600. [self.undoManager undo];
  601. }
  602. - (void)slk_redo:(id)sender
  603. {
  604. [self.undoManager redo];
  605. }
  606. - (void)slk_presentFormattingMenu:(id)sender
  607. {
  608. NSMutableArray *items = [NSMutableArray arrayWithCapacity:self.registeredFormattingTitles.count];
  609. for (NSString *name in self.registeredFormattingTitles) {
  610. NSString *sel = [NSString stringWithFormat:@"%@%@", SLKTextViewGenericFormattingSelectorPrefix, name];
  611. UIMenuItem *item = [[UIMenuItem alloc] initWithTitle:name action:NSSelectorFromString(sel)];
  612. [items addObject:item];
  613. }
  614. self.formatting = YES;
  615. UIMenuController *menu = [UIMenuController sharedMenuController];
  616. [menu setMenuItems:items];
  617. NSLayoutManager *manager = self.layoutManager;
  618. CGRect targetRect = [manager boundingRectForGlyphRange:self.selectedRange inTextContainer:self.textContainer];
  619. [menu setTargetRect:targetRect inView:self];
  620. [menu setMenuVisible:YES animated:YES];
  621. }
  622. - (NSString *)slk_formattingTitleFromSelector:(SEL)selector
  623. {
  624. NSString *selectorString = NSStringFromSelector(selector);
  625. NSRange match = [selectorString rangeOfString:SLKTextViewGenericFormattingSelectorPrefix];
  626. if (match.location != NSNotFound) {
  627. return [selectorString substringFromIndex:SLKTextViewGenericFormattingSelectorPrefix.length];
  628. }
  629. return nil;
  630. }
  631. - (NSString *)slk_formattingSymbolWithTitle:(NSString *)title
  632. {
  633. NSUInteger idx = [self.registeredFormattingTitles indexOfObject:title];
  634. if (idx <= self.registeredFormattingSymbols.count -1) {
  635. return self.registeredFormattingSymbols[idx];
  636. }
  637. return nil;
  638. }
  639. - (void)slk_format:(NSString *)titles
  640. {
  641. NSString *symbol = [self slk_formattingSymbolWithTitle:titles];
  642. if (symbol.length > 0) {
  643. NSRange selection = self.selectedRange;
  644. NSRange range = [self slk_insertText:symbol inRange:NSMakeRange(selection.location, 0)];
  645. range.location += selection.length;
  646. range.length = 0;
  647. // The default behavior is to add a closure
  648. BOOL addClosure = YES;
  649. if (self.delegate && [self.delegate respondsToSelector:@selector(textView:shouldInsertSuffixForFormattingWithSymbol:prefixRange:)]) {
  650. addClosure = [self.delegate textView:self shouldInsertSuffixForFormattingWithSymbol:symbol prefixRange:selection];
  651. }
  652. if (addClosure) {
  653. self.selectedRange = [self slk_insertText:symbol inRange:range];
  654. }
  655. }
  656. }
  657. #pragma mark - Markdown Formatting
  658. - (void)registerMarkdownFormattingSymbol:(NSString *)symbol withTitle:(NSString *)title
  659. {
  660. if (!symbol || !title) {
  661. return;
  662. }
  663. if (!_registeredFormattingTitles) {
  664. _registeredFormattingTitles = [NSMutableArray new];
  665. _registeredFormattingSymbols = [NSMutableArray new];
  666. }
  667. // Adds the symbol if not contained already
  668. if (![self.registeredSymbols containsObject:symbol]) {
  669. [self.registeredFormattingTitles addObject:title];
  670. [self.registeredFormattingSymbols addObject:symbol];
  671. }
  672. }
  673. - (NSArray *)registeredSymbols
  674. {
  675. return self.registeredFormattingSymbols;
  676. }
  677. #pragma mark - Notification Events
  678. - (void)slk_didBeginEditing:(NSNotification *)notification
  679. {
  680. if (![notification.object isEqual:self]) {
  681. return;
  682. }
  683. // Do something
  684. }
  685. - (void)slk_didChangeText:(NSNotification *)notification
  686. {
  687. if (![notification.object isEqual:self]) {
  688. return;
  689. }
  690. if (self.placeholderLabel.hidden != [self slk_shouldHidePlaceholder]) {
  691. [self setNeedsLayout];
  692. }
  693. [self slk_flashScrollIndicatorsIfNeeded];
  694. }
  695. - (void)slk_didEndEditing:(NSNotification *)notification
  696. {
  697. if (![notification.object isEqual:self]) {
  698. return;
  699. }
  700. // Do something
  701. }
  702. - (void)slk_didChangeTextInputMode:(NSNotification *)notification
  703. {
  704. // Do something
  705. }
  706. - (void)slk_didChangeContentSizeCategory:(NSNotification *)notification
  707. {
  708. if (!self.isDynamicTypeEnabled) {
  709. return;
  710. }
  711. NSString *contentSizeCategory = notification.userInfo[UIContentSizeCategoryNewValueKey];
  712. [self setFontName:self.font.fontName pointSize:self.initialFontSize withContentSizeCategory:contentSizeCategory];
  713. NSString *text = [self.text copy];
  714. // Reloads the content size of the text view
  715. [self setText:@" "];
  716. [self setText:text];
  717. }
  718. - (void)slk_willShowMenuController:(NSNotification *)notification
  719. {
  720. // Do something
  721. }
  722. - (void)slk_didHideMenuController:(NSNotification *)notification
  723. {
  724. self.formatting = NO;
  725. [self slk_addCustomMenuControllerItems];
  726. }
  727. #pragma mark - KVO Listener
  728. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  729. {
  730. if ([object isEqual:self] && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
  731. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewContentSizeDidChangeNotification object:self userInfo:nil];
  732. }
  733. else {
  734. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
  735. }
  736. }
  737. #pragma mark - Motion Events
  738. - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event
  739. {
  740. if (event.type == UIEventTypeMotion && event.subtype == UIEventSubtypeMotionShake) {
  741. [[NSNotificationCenter defaultCenter] postNotificationName:SLKTextViewDidShakeNotification object:self];
  742. }
  743. }
  744. #pragma mark - External Keyboard Support
  745. typedef void (^SLKKeyCommandHandler)(UIKeyCommand *keyCommand);
  746. - (void)observeKeyInput:(NSString *)input modifiers:(UIKeyModifierFlags)modifiers title:(NSString *_Nullable)title completion:(void (^)(UIKeyCommand *keyCommand))completion
  747. {
  748. NSAssert([input isKindOfClass:[NSString class]], @"You must provide a string with one or more characters corresponding to the keys to observe.");
  749. NSAssert(completion != nil, @"You must provide a non-nil completion block.");
  750. if (!input || !completion) {
  751. return;
  752. }
  753. UIKeyCommand *keyCommand = [UIKeyCommand keyCommandWithInput:input modifierFlags:modifiers action:@selector(didDetectKeyCommand:)];
  754. #ifdef __IPHONE_9_0
  755. if ([UIKeyCommand respondsToSelector:@selector(keyCommandWithInput:modifierFlags:action:discoverabilityTitle:)] ) {
  756. keyCommand.discoverabilityTitle = title;
  757. }
  758. #endif
  759. if (!_registeredKeyCommands) {
  760. _registeredKeyCommands = [NSMutableDictionary new];
  761. _registeredKeyCallbacks = [NSMutableDictionary new];
  762. }
  763. NSString *key = [self keyForKeyCommand:keyCommand];
  764. self.registeredKeyCommands[key] = keyCommand;
  765. self.registeredKeyCallbacks[key] = completion;
  766. }
  767. - (void)didDetectKeyCommand:(UIKeyCommand *)keyCommand
  768. {
  769. NSString *key = [self keyForKeyCommand:keyCommand];
  770. SLKKeyCommandHandler completion = self.registeredKeyCallbacks[key];
  771. if (completion) {
  772. completion(keyCommand);
  773. }
  774. }
  775. - (NSString *)keyForKeyCommand:(UIKeyCommand *)keyCommand
  776. {
  777. return [NSString stringWithFormat:@"%@_%ld", keyCommand.input, (long)keyCommand.modifierFlags];
  778. }
  779. - (NSArray *)keyCommands
  780. {
  781. if (self.registeredKeyCommands) {
  782. return [self.registeredKeyCommands allValues];
  783. }
  784. return nil;
  785. }
  786. #pragma mark Up/Down Cursor Movement
  787. - (void)didPressArrowKey:(UIKeyCommand *)keyCommand
  788. {
  789. if (![keyCommand isKindOfClass:[UIKeyCommand class]] || self.text.length == 0 || self.numberOfLines < 2) {
  790. return;
  791. }
  792. if ([keyCommand.input isEqualToString:UIKeyInputUpArrow]) {
  793. [self slk_moveCursorTodirection:UITextLayoutDirectionUp];
  794. }
  795. else if ([keyCommand.input isEqualToString:UIKeyInputDownArrow]) {
  796. [self slk_moveCursorTodirection:UITextLayoutDirectionDown];
  797. }
  798. }
  799. - (void)slk_moveCursorTodirection:(UITextLayoutDirection)direction
  800. {
  801. UITextPosition *start = (direction == UITextLayoutDirectionUp) ? self.selectedTextRange.start : self.selectedTextRange.end;
  802. if ([self slk_isNewVerticalMovementForPosition:start inDirection:direction]) {
  803. self.verticalMoveDirection = direction;
  804. self.verticalMoveStartCaretRect = [self caretRectForPosition:start];
  805. }
  806. if (start) {
  807. UITextPosition *end = [self slk_closestPositionToPosition:start inDirection:direction];
  808. if (end) {
  809. self.verticalMoveLastCaretRect = [self caretRectForPosition:end];
  810. self.selectedTextRange = [self textRangeFromPosition:end toPosition:end];
  811. [self slk_scrollToCaretPositonAnimated:NO];
  812. }
  813. }
  814. }
  815. // Based on code from Ruben Cabaco
  816. // https://gist.github.com/rcabaco/6765778
  817. - (UITextPosition *)slk_closestPositionToPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
  818. {
  819. // Only up/down are implemented. No real need for left/right since that is native to UITextInput.
  820. NSParameterAssert(direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown);
  821. // Translate the vertical direction to a horizontal direction.
  822. UITextLayoutDirection lookupDirection = (direction == UITextLayoutDirectionUp) ? UITextLayoutDirectionLeft : UITextLayoutDirectionRight;
  823. // Walk one character at a time in `lookupDirection` until the next line is reached.
  824. UITextPosition *checkPosition = position;
  825. UITextPosition *closestPosition = position;
  826. CGRect startingCaretRect = [self caretRectForPosition:position];
  827. CGRect nextLineCaretRect = CGRectZero;
  828. BOOL isInNextLine = NO;
  829. while (YES) {
  830. UITextPosition *nextPosition = [self positionFromPosition:checkPosition inDirection:lookupDirection offset:1];
  831. // End of line.
  832. if (!nextPosition || [self comparePosition:checkPosition toPosition:nextPosition] == NSOrderedSame) {
  833. break;
  834. }
  835. checkPosition = nextPosition;
  836. CGRect checkRect = [self caretRectForPosition:checkPosition];
  837. if (CGRectGetMidY(startingCaretRect) != CGRectGetMidY(checkRect)) {
  838. // While on the next line stop just above/below the starting position.
  839. if (lookupDirection == UITextLayoutDirectionLeft && CGRectGetMidX(checkRect) <= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
  840. closestPosition = checkPosition;
  841. break;
  842. }
  843. if (lookupDirection == UITextLayoutDirectionRight && CGRectGetMidX(checkRect) >= CGRectGetMidX(self.verticalMoveStartCaretRect)) {
  844. closestPosition = checkPosition;
  845. break;
  846. }
  847. // But don't skip lines.
  848. if (isInNextLine && CGRectGetMidY(checkRect) != CGRectGetMidY(nextLineCaretRect)) {
  849. break;
  850. }
  851. isInNextLine = YES;
  852. nextLineCaretRect = checkRect;
  853. closestPosition = checkPosition;
  854. }
  855. }
  856. return closestPosition;
  857. }
  858. - (BOOL)slk_isNewVerticalMovementForPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction
  859. {
  860. CGRect caretRect = [self caretRectForPosition:position];
  861. BOOL noPreviousStartPosition = CGRectEqualToRect(self.verticalMoveStartCaretRect, CGRectZero);
  862. BOOL caretMovedSinceLastPosition = !CGRectEqualToRect(caretRect, self.verticalMoveLastCaretRect);
  863. BOOL directionChanged = self.verticalMoveDirection != direction;
  864. BOOL newMovement = noPreviousStartPosition || caretMovedSinceLastPosition || directionChanged;
  865. return newMovement;
  866. }
  867. #pragma mark - NSNotificationCenter registration
  868. - (void)slk_registerNotifications
  869. {
  870. [self slk_unregisterNotifications];
  871. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
  872. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeText:) name:UITextViewTextDidChangeNotification object:nil];
  873. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
  874. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeTextInputMode:) name:UITextInputCurrentInputModeDidChangeNotification object:nil];
  875. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didChangeContentSizeCategory:) name:UIContentSizeCategoryDidChangeNotification object:nil];
  876. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_willShowMenuController:) name:UIMenuControllerWillShowMenuNotification object:nil];
  877. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(slk_didHideMenuController:) name:UIMenuControllerDidHideMenuNotification object:nil];
  878. }
  879. - (void)slk_unregisterNotifications
  880. {
  881. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
  882. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:nil];
  883. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
  884. [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextInputCurrentInputModeDidChangeNotification object:nil];
  885. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
  886. }
  887. #pragma mark - Lifeterm
  888. - (void)dealloc
  889. {
  890. [self slk_unregisterNotifications];
  891. [self removeObserver:self forKeyPath:NSStringFromSelector(@selector(contentSize))];
  892. }
  893. @end