SLKTextView.m 35 KB

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