forked from Astrocoders/react-native-selectable-text
-
Notifications
You must be signed in to change notification settings - Fork 15
/
RNSelectableTextView.m
275 lines (213 loc) · 8.96 KB
/
RNSelectableTextView.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#if __has_include(<RCTText/RCTTextSelection.h>)
#import <RCTText/RCTTextSelection.h>
#else
#import "RCTTextSelection.h"
#endif
#if __has_include(<RCTText/RCTUITextView.h>)
#import <RCTText/RCTUITextView.h>
#else
#import "RCTUITextView.h"
#endif
#import "RNSelectableTextView.h"
#if __has_include(<RCTText/RCTTextAttributes.h>)
#import <RCTText/RCTTextAttributes.h>
#else
#import "RCTTextAttributes.h"
#endif
#import <React/RCTUtils.h>
@implementation RNSelectableTextView
{
RCTUITextView *_backedTextInputView;
}
NSString *const CUSTOM_SELECTOR = @"_CUSTOM_SELECTOR_";
UITextPosition *selectionStart;
UITextPosition* beginning;
- (instancetype)initWithBridge:(RCTBridge *)bridge
{
if (self = [super initWithBridge:bridge]) {
// `blurOnSubmit` defaults to `false` for <TextInput multiline={true}> by design.
self.blurOnSubmit = NO;
_backedTextInputView = [[RCTUITextView alloc] initWithFrame:self.bounds];
_backedTextInputView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_backedTextInputView.backgroundColor = [UIColor clearColor];
_backedTextInputView.textColor = [UIColor blackColor];
// This line actually removes 5pt (default value) left and right padding in UITextView.
_backedTextInputView.textContainer.lineFragmentPadding = 0;
#if !TARGET_OS_TV
_backedTextInputView.scrollsToTop = NO;
#endif
_backedTextInputView.scrollEnabled = NO;
_backedTextInputView.textInputDelegate = self;
_backedTextInputView.editable = NO;
_backedTextInputView.selectable = YES;
_backedTextInputView.contextMenuHidden = YES;
beginning = _backedTextInputView.beginningOfDocument;
for (UIGestureRecognizer *gesture in [_backedTextInputView gestureRecognizers]) {
if (
[gesture isKindOfClass:[UIPanGestureRecognizer class]]
) {
[_backedTextInputView setExclusiveTouch:NO];
gesture.enabled = YES;
} else {
gesture.enabled = NO;
}
}
[self addSubview:_backedTextInputView];
UILongPressGestureRecognizer *longPressGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
UITapGestureRecognizer *tapGesture = [ [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
tapGesture.numberOfTapsRequired = 2;
UITapGestureRecognizer *singleTapGesture = [ [UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)];
singleTapGesture.numberOfTapsRequired = 1;
[_backedTextInputView addGestureRecognizer:longPressGesture];
[_backedTextInputView addGestureRecognizer:tapGesture];
[_backedTextInputView addGestureRecognizer:singleTapGesture];
[self setUserInteractionEnabled:YES];
}
return self;
}
-(void) _handleGesture
{
if (!_backedTextInputView.isFirstResponder) {
[_backedTextInputView becomeFirstResponder];
}
UIMenuController *menuController = [UIMenuController sharedMenuController];
if (menuController.isMenuVisible) return;
NSMutableArray *menuControllerItems = [NSMutableArray arrayWithCapacity:self.menuItems.count];
for(NSString *menuItemName in self.menuItems) {
NSString *sel = [NSString stringWithFormat:@"%@%@", CUSTOM_SELECTOR, menuItemName];
UIMenuItem *item = [[UIMenuItem alloc] initWithTitle: menuItemName
action: NSSelectorFromString(sel)];
[menuControllerItems addObject: item];
}
menuController.menuItems = menuControllerItems;
[menuController setTargetRect:self.bounds inView:self];
[menuController setMenuVisible:YES animated:YES];
}
-(void) handleSingleTap: (UITapGestureRecognizer *) gesture
{
CGPoint pos = [gesture locationInView:_backedTextInputView];
pos.y += _backedTextInputView.contentOffset.y;
UITextPosition *tapPos = [_backedTextInputView closestPositionToPoint:pos];
UITextRange *word = [_backedTextInputView.tokenizer rangeEnclosingPosition:tapPos withGranularity:(UITextGranularityWord) inDirection:UITextLayoutDirectionRight];
UITextPosition* beginning = _backedTextInputView.beginningOfDocument;
UITextPosition *selectionStart = word.start;
UITextPosition *selectionEnd = word.end;
const NSInteger location = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionStart];
const NSInteger endLocation = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionEnd];
self.onHighlightPress(@{
@"clickedRangeStart": @(location),
@"clickedRangeEnd": @(endLocation),
});
}
-(void) handleLongPress: (UILongPressGestureRecognizer *) gesture
{
CGPoint pos = [gesture locationInView:_backedTextInputView];
pos.y += _backedTextInputView.contentOffset.y;
UITextPosition *tapPos = [_backedTextInputView closestPositionToPoint:pos];
UITextRange *word = [_backedTextInputView.tokenizer rangeEnclosingPosition:tapPos withGranularity:(UITextGranularityWord) inDirection:UITextLayoutDirectionRight];
switch ([gesture state]) {
case UIGestureRecognizerStateBegan:
selectionStart = word.start;
break;
case UIGestureRecognizerStateChanged:
break;
case UIGestureRecognizerStateEnded:
selectionStart = nil;
[self _handleGesture];
return;
default:
break;
}
UITextPosition *selectionEnd = word.end;
const NSInteger location = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionStart];
const NSInteger endLocation = [_backedTextInputView offsetFromPosition:beginning toPosition:selectionEnd];
if (location == 0 && endLocation == 0) return;
[_backedTextInputView select:self];
[_backedTextInputView setSelectedRange:NSMakeRange(location, endLocation - location)];
}
-(void) handleTap: (UITapGestureRecognizer *) gesture
{
[_backedTextInputView select:self];
[_backedTextInputView selectAll:self];
[self _handleGesture];
}
- (void)setAttributedText:(NSAttributedString *)attributedText
{
if (self.value) {
NSAttributedString *str = [[NSAttributedString alloc] initWithString:self.value attributes:self.textAttributes.effectiveTextAttributes];
[super setAttributedText:str];
} else {
[super setAttributedText:attributedText];
}
}
- (id<RCTBackedTextInputViewProtocol>)backedTextInputView
{
return _backedTextInputView;
}
- (void)tappedMenuItem:(NSString *)eventType
{
RCTTextSelection *selection = self.selection;
NSUInteger start = selection.start;
NSUInteger end = selection.end - selection.start;
self.onSelection(@{
@"content": [[self.attributedText string] substringWithRange:NSMakeRange(start, end)],
@"eventType": eventType,
@"selectionStart": @(start),
@"selectionEnd": @(selection.end)
});
[_backedTextInputView setSelectedTextRange:nil notifyDelegate:false];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
if ([super methodSignatureForSelector:sel]) {
return [super methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:@selector(tappedMenuItem:)];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
NSString *sel = NSStringFromSelector([invocation selector]);
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
if (match.location == 0) {
[self tappedMenuItem:[sel substringFromIndex:17]];
} else {
[super forwardInvocation:invocation];
}
}
- (BOOL)canBecomeFirstResponder
{
return YES;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if(selectionStart != nil) {return NO;}
NSString *sel = NSStringFromSelector(action);
NSRange match = [sel rangeOfString:CUSTOM_SELECTOR];
if (match.location == 0) {
return YES;
}
return NO;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!_backedTextInputView.isFirstResponder) {
[_backedTextInputView setSelectedTextRange:nil notifyDelegate:true];
} else {
UIView *sub = nil;
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point toView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (!result.isFirstResponder) {
NSString *name = NSStringFromClass([result class]);
if ([name isEqual:@"UITextRangeView"]) {
sub = result;
}
}
}
if (sub == nil) {
[_backedTextInputView setSelectedTextRange:nil notifyDelegate:true];
}
}
return [super hitTest:point withEvent:event];
}
@end