@dongsuo/react-native-uitextview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +225 -0
  3. package/android/generated/jni/CMakeLists.txt +36 -0
  4. package/android/generated/jni/RNUITextViewSpec-generated.cpp +22 -0
  5. package/android/generated/jni/RNUITextViewSpec.h +24 -0
  6. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ComponentDescriptors.cpp +23 -0
  7. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ComponentDescriptors.h +25 -0
  8. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/EventEmitters.cpp +61 -0
  9. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/EventEmitters.h +49 -0
  10. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/Props.cpp +47 -0
  11. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/Props.h +182 -0
  12. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/RNUITextViewSpecJSI-generated.cpp +17 -0
  13. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/RNUITextViewSpecJSI.h +19 -0
  14. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ShadowNodes.cpp +18 -0
  15. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ShadowNodes.h +43 -0
  16. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/States.cpp +16 -0
  17. package/android/generated/jni/react/renderer/components/RNUITextViewSpec/States.h +41 -0
  18. package/ios/RNUITextView.h +23 -0
  19. package/ios/RNUITextView.mm +474 -0
  20. package/ios/RNUITextViewChild.h +24 -0
  21. package/ios/RNUITextViewChild.mm +76 -0
  22. package/ios/RNUITextViewChildComponentDescriptor.h +13 -0
  23. package/ios/RNUITextViewChildShadowNode.cpp +6 -0
  24. package/ios/RNUITextViewChildShadowNode.h +16 -0
  25. package/ios/RNUITextViewComponentDescriptor.h +13 -0
  26. package/ios/RNUITextViewEventEmitter.h +26 -0
  27. package/ios/RNUITextViewManager.mm +36 -0
  28. package/ios/RNUITextViewShadowNode.cpp +127 -0
  29. package/ios/RNUITextViewShadowNode.h +48 -0
  30. package/ios/RNUITextViewSpecJSI-generated.cpp +17 -0
  31. package/ios/RNUITextViewSpecJSI.h +19 -0
  32. package/ios/generated/RNUITextViewSpec/ComponentDescriptors.cpp +23 -0
  33. package/ios/generated/RNUITextViewSpec/ComponentDescriptors.h +25 -0
  34. package/ios/generated/RNUITextViewSpec/EventEmitters.cpp +61 -0
  35. package/ios/generated/RNUITextViewSpec/EventEmitters.h +49 -0
  36. package/ios/generated/RNUITextViewSpec/Props.cpp +47 -0
  37. package/ios/generated/RNUITextViewSpec/Props.h +182 -0
  38. package/ios/generated/RNUITextViewSpec/RCTComponentViewHelpers.h +24 -0
  39. package/ios/generated/RNUITextViewSpec/RNUITextViewSpec-generated.mm +16 -0
  40. package/ios/generated/RNUITextViewSpec/RNUITextViewSpec.h +38 -0
  41. package/ios/generated/RNUITextViewSpec/ShadowNodes.cpp +18 -0
  42. package/ios/generated/RNUITextViewSpec/ShadowNodes.h +43 -0
  43. package/ios/generated/RNUITextViewSpec/States.cpp +16 -0
  44. package/ios/generated/RNUITextViewSpec/States.h +41 -0
  45. package/ios/generated/RNUITextViewSpecJSI-generated.cpp +17 -0
  46. package/ios/generated/RNUITextViewSpecJSI.h +19 -0
  47. package/lib/commonjs/RNUITextViewChildNativeComponent.ts +48 -0
  48. package/lib/commonjs/RNUITextViewNativeComponent.ts +47 -0
  49. package/lib/commonjs/Text.js +140 -0
  50. package/lib/commonjs/Text.js.map +1 -0
  51. package/lib/commonjs/index.js +17 -0
  52. package/lib/commonjs/index.js.map +1 -0
  53. package/lib/commonjs/package.json +1 -0
  54. package/lib/commonjs/util.js +64 -0
  55. package/lib/commonjs/util.js.map +1 -0
  56. package/lib/module/RNUITextViewChildNativeComponent.ts +48 -0
  57. package/lib/module/RNUITextViewNativeComponent.ts +47 -0
  58. package/lib/module/Text.js +136 -0
  59. package/lib/module/Text.js.map +1 -0
  60. package/lib/module/index.js +4 -0
  61. package/lib/module/index.js.map +1 -0
  62. package/lib/module/package.json +1 -0
  63. package/lib/module/util.js +60 -0
  64. package/lib/module/util.js.map +1 -0
  65. package/lib/typescript/commonjs/example/src/App.d.ts +2 -0
  66. package/lib/typescript/commonjs/example/src/App.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/example/src/CustomMenuExample.d.ts +3 -0
  68. package/lib/typescript/commonjs/example/src/CustomMenuExample.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/package.json +1 -0
  70. package/lib/typescript/commonjs/src/RNUITextViewChildNativeComponent.d.ts +28 -0
  71. package/lib/typescript/commonjs/src/RNUITextViewChildNativeComponent.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/src/RNUITextViewNativeComponent.d.ts +29 -0
  73. package/lib/typescript/commonjs/src/RNUITextViewNativeComponent.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/src/Text.d.ts +17 -0
  75. package/lib/typescript/commonjs/src/Text.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/src/index.d.ts +2 -0
  77. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/src/util.d.ts +160 -0
  79. package/lib/typescript/commonjs/src/util.d.ts.map +1 -0
  80. package/lib/typescript/module/example/src/App.d.ts +2 -0
  81. package/lib/typescript/module/example/src/App.d.ts.map +1 -0
  82. package/lib/typescript/module/example/src/CustomMenuExample.d.ts +3 -0
  83. package/lib/typescript/module/example/src/CustomMenuExample.d.ts.map +1 -0
  84. package/lib/typescript/module/package.json +1 -0
  85. package/lib/typescript/module/src/RNUITextViewChildNativeComponent.d.ts +28 -0
  86. package/lib/typescript/module/src/RNUITextViewChildNativeComponent.d.ts.map +1 -0
  87. package/lib/typescript/module/src/RNUITextViewNativeComponent.d.ts +29 -0
  88. package/lib/typescript/module/src/RNUITextViewNativeComponent.d.ts.map +1 -0
  89. package/lib/typescript/module/src/Text.d.ts +17 -0
  90. package/lib/typescript/module/src/Text.d.ts.map +1 -0
  91. package/lib/typescript/module/src/index.d.ts +2 -0
  92. package/lib/typescript/module/src/index.d.ts.map +1 -0
  93. package/lib/typescript/module/src/util.d.ts +160 -0
  94. package/lib/typescript/module/src/util.d.ts.map +1 -0
  95. package/package.json +187 -0
  96. package/react-native-uitextview.podspec +21 -0
  97. package/react-native.config.js +28 -0
  98. package/src/RNUITextViewChildNativeComponent.ts +48 -0
  99. package/src/RNUITextViewNativeComponent.ts +47 -0
  100. package/src/Text.tsx +148 -0
  101. package/src/index.tsx +1 -0
  102. package/src/util.ts +65 -0
@@ -0,0 +1,474 @@
1
+ #import "RNUITextView.h"
2
+ #import "RNUITextViewChild.h"
3
+ #import "RNUITextViewComponentDescriptor.h"
4
+
5
+ #import <React/RCTConversions.h>
6
+
7
+ #import <react-native-uitextview/EventEmitters.h>
8
+ #import <react-native-uitextview/Props.h>
9
+ #import <react-native-uitextview/RCTComponentViewHelpers.h>
10
+ #import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
11
+ #import <objc/runtime.h>
12
+ #import "RCTFabricComponentsPlugins.h"
13
+
14
+ using namespace facebook::react;
15
+
16
+ @interface RNUITextView () <RCTRNUITextViewViewProtocol, UIGestureRecognizerDelegate, UITextViewDelegate>
17
+
18
+ // 用于跟踪自定义菜单项
19
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *menuItemsMap;
20
+
21
+ @end
22
+
23
+ @implementation RNUITextView{
24
+ UIView * _view;
25
+ UITextView * _textView;
26
+ RNUITextViewShadowNode::ConcreteState::Shared _state;
27
+ }
28
+
29
+ + (ComponentDescriptorProvider)componentDescriptorProvider
30
+ {
31
+ return concreteComponentDescriptorProvider<RNUITextViewComponentDescriptor>();
32
+ }
33
+
34
+ - (instancetype)initWithFrame:(CGRect)frame
35
+ {
36
+ if (self = [super initWithFrame:frame]) {
37
+ static const auto defaultProps = std::make_shared<const RNUITextViewProps>();
38
+ _props = defaultProps;
39
+
40
+ _view = [[UIView alloc] init];
41
+ self.contentView = _view;
42
+ self.clipsToBounds = true;
43
+
44
+ // 初始化菜单项映射
45
+ self.menuItemsMap = [NSMutableDictionary new];
46
+
47
+ _textView = [[UITextView alloc] init];
48
+ _textView.scrollEnabled = false;
49
+ _textView.editable = false;
50
+ _textView.selectable = YES; // 确保可选择
51
+ _textView.delegate = self; // 设置 delegate 以便自定义菜单
52
+ _textView.textContainerInset = UIEdgeInsetsZero;
53
+ _textView.textContainer.lineFragmentPadding = 0;
54
+ _textView.userInteractionEnabled = YES; // 确保可交互
55
+ [self addSubview:_textView];
56
+
57
+ const auto longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
58
+ action:@selector(handleLongPressIfNecessary:)];
59
+ longPressGestureRecognizer.delegate = self;
60
+
61
+ const auto pressGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
62
+ action:@selector(handlePressIfNecessary:)];
63
+ pressGestureRecognizer.delegate = self;
64
+ [pressGestureRecognizer requireGestureRecognizerToFail:longPressGestureRecognizer];
65
+
66
+ [_textView addGestureRecognizer:pressGestureRecognizer];
67
+ [_textView addGestureRecognizer:longPressGestureRecognizer];
68
+
69
+ // 预注册一些常用的 selector
70
+ SEL translate = NSSelectorFromString(@"customMenuItemAction_translate:");
71
+ SEL share = NSSelectorFromString(@"customMenuItemAction_share:");
72
+ SEL search = NSSelectorFromString(@"customMenuItemAction_search:");
73
+
74
+ if (![self respondsToSelector:translate]) {
75
+ class_addMethod([self class], translate, (IMP)customMenuItemIMP, "v@:@");
76
+ }
77
+ if (![self respondsToSelector:share]) {
78
+ class_addMethod([self class], share, (IMP)customMenuItemIMP, "v@:@");
79
+ }
80
+ if (![self respondsToSelector:search]) {
81
+ class_addMethod([self class], search, (IMP)customMenuItemIMP, "v@:@");
82
+ }
83
+ }
84
+
85
+ return self;
86
+ }
87
+
88
+ // See RCTParagraphComponentView
89
+ - (void)prepareForRecycle
90
+ {
91
+ [super prepareForRecycle];
92
+ _state.reset();
93
+
94
+ // Reset the frame to zero so that when it properly lays out on the next use
95
+ _textView.frame = CGRectZero;
96
+ _textView.attributedText = nil;
97
+
98
+ // 清空自定义菜单项
99
+ [self.menuItemsMap removeAllObjects];
100
+ self.customMenuItems = nil;
101
+ }
102
+
103
+ - (void)drawRect:(CGRect)rect
104
+ {
105
+ if (!_state) {
106
+ return;
107
+ }
108
+
109
+ const auto &props = *std::static_pointer_cast<RNUITextViewProps const>(_props);
110
+
111
+ const auto attrString = _state->getData().attributedString;
112
+ const auto convertedAttrString = RCTNSAttributedStringFromAttributedString(attrString);
113
+ _textView.attributedText = convertedAttrString;
114
+ _textView.frame = _view.frame;
115
+
116
+ const auto lines = new std::vector<std::string>();
117
+ [_textView.layoutManager enumerateLineFragmentsForGlyphRange:NSMakeRange(0, convertedAttrString.string.length) usingBlock:^(CGRect rect,
118
+ CGRect usedRect,
119
+ NSTextContainer * _Nonnull textContainer,
120
+ NSRange glyphRange,
121
+ BOOL * _Nonnull stop) {
122
+ const auto charRange = [self->_textView.layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
123
+ const auto line = [self->_textView.text substringWithRange:charRange];
124
+
125
+ if (props.numberOfLines && props.numberOfLines > 0 && lines->size() < props.numberOfLines) {
126
+ lines->push_back(line.UTF8String);
127
+ }
128
+ }];
129
+
130
+ if (_eventEmitter != nullptr) {
131
+ std::dynamic_pointer_cast<const facebook::react::RNUITextViewEventEmitter>(_eventEmitter)
132
+ ->onTextLayout(facebook::react::RNUITextViewEventEmitter::OnTextLayout{static_cast<int>(self.tag), *lines});
133
+ };
134
+ }
135
+
136
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
137
+ {
138
+ const auto &oldViewProps = *std::static_pointer_cast<RNUITextViewProps const>(_props);
139
+ const auto &newViewProps = *std::static_pointer_cast<RNUITextViewProps const>(props);
140
+
141
+ if (oldViewProps.numberOfLines != newViewProps.numberOfLines) {
142
+ _textView.textContainer.maximumNumberOfLines = newViewProps.numberOfLines;
143
+ }
144
+
145
+ if (oldViewProps.selectable != newViewProps.selectable) {
146
+ _textView.selectable = newViewProps.selectable;
147
+ }
148
+
149
+ if (oldViewProps.ellipsizeMode != newViewProps.ellipsizeMode) {
150
+ if (newViewProps.ellipsizeMode == RNUITextViewEllipsizeMode::Head) {
151
+ _textView.textContainer.lineBreakMode = NSLineBreakMode::NSLineBreakByTruncatingHead;
152
+ } else if (newViewProps.ellipsizeMode == RNUITextViewEllipsizeMode::Middle) {
153
+ _textView.textContainer.lineBreakMode = NSLineBreakMode::NSLineBreakByTruncatingMiddle;
154
+ } else if (newViewProps.ellipsizeMode == RNUITextViewEllipsizeMode::Tail) {
155
+ _textView.textContainer.lineBreakMode = NSLineBreakMode::NSLineBreakByTruncatingTail;
156
+ } else if (newViewProps.ellipsizeMode == RNUITextViewEllipsizeMode::Clip) {
157
+ _textView.textContainer.lineBreakMode = NSLineBreakMode::NSLineBreakByClipping;
158
+ }
159
+ }
160
+
161
+ // I'm not sure if this is really the right way to handle this style. This means that the entire _view_ the text
162
+ // is in will have this background color applied. To apply it just to a particular part of a string, you'd need
163
+ // to do <Text><Text style={{backgroundColor: 'blue'}}>Hello</Text></Text>.
164
+ // This is how the base <Text> component works though, so we'll go with it for now. Can change later if we want.
165
+ if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
166
+ _textView.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
167
+ }
168
+ if (newViewProps.customMenuItems.size() > 0) {
169
+ NSMutableArray *items = [NSMutableArray array];
170
+ for (const auto &item : newViewProps.customMenuItems) {
171
+ [items addObject:@{
172
+ @"title": [NSString stringWithUTF8String:item.title.c_str()],
173
+ @"actionId": [NSString stringWithUTF8String:item.actionId.c_str()]
174
+ }];
175
+ }
176
+ self.customMenuItems = items;
177
+ }
178
+ // 自定义菜单项属性会在 JS 层设置,这里不需要从 props 中提取
179
+
180
+ [super updateProps:props oldProps:oldProps];
181
+ }
182
+
183
+ // See RCTParagraphComponentView
184
+ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(const facebook::react::State::Shared &)oldState
185
+ {
186
+ _state = std::static_pointer_cast<const RNUITextViewShadowNode::ConcreteState>(state);
187
+ [self setNeedsDisplay];
188
+ }
189
+
190
+ // MARK: - UIGestureRecognizerDelegate
191
+
192
+ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
193
+ {
194
+ return YES;
195
+ }
196
+
197
+ // MARK: - Touch handling
198
+
199
+ - (CGPoint)getLocationOfPress:(UIGestureRecognizer*)sender
200
+ {
201
+ return [sender locationInView:_textView];
202
+ }
203
+
204
+ - (RNUITextViewChild*)getTouchChild:(CGPoint)location
205
+ {
206
+ const auto charIndex = [_textView.layoutManager characterIndexForPoint:location
207
+ inTextContainer:_textView.textContainer
208
+ fractionOfDistanceBetweenInsertionPoints:nil
209
+ ];
210
+
211
+ int currIndex = -1;
212
+ for (UIView* child in self.subviews) {
213
+ if (![child isKindOfClass:[RNUITextViewChild class]]) {
214
+ continue;
215
+ }
216
+
217
+ RNUITextViewChild* textChild = (RNUITextViewChild*)child;
218
+
219
+ // This is UTF16 code units!!
220
+ currIndex += textChild.text.length;
221
+
222
+ if (charIndex <= currIndex) {
223
+ return textChild;
224
+ }
225
+ }
226
+
227
+ return nil;
228
+ }
229
+
230
+ - (void)handlePressIfNecessary:(UITapGestureRecognizer*)sender
231
+ {
232
+ const auto location = [self getLocationOfPress:sender];
233
+ const auto child = [self getTouchChild:location];
234
+
235
+ if (child) {
236
+ [child onPress];
237
+ }
238
+ }
239
+
240
+ - (void)handleLongPressIfNecessary:(UILongPressGestureRecognizer*)sender
241
+ {
242
+ const auto location = [self getLocationOfPress:sender];
243
+ const auto child = [self getTouchChild:location];
244
+
245
+ if (child) {
246
+ [child onLongPress];
247
+ }
248
+
249
+ // 尝试显示自定义菜单 - 只在手势开始时
250
+ if (sender.state == UIGestureRecognizerStateBegan && self.customMenuItems.count > 0) {
251
+ // 让 textView 成为第一响应者
252
+ [_textView becomeFirstResponder];
253
+
254
+ // 尝试选择文本(整段文字)
255
+ if (_textView.text.length > 0) {
256
+ UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:0];
257
+ UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:_textView.text.length];
258
+ if (start && end) {
259
+ _textView.selectedTextRange = [_textView textRangeFromPosition:start toPosition:end];
260
+ // Menu is now shown by textView:editMenuForTextInRange:suggestedActions: delegate
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ Class<RCTComponentViewProtocol> RNUITextViewCls(void)
267
+ {
268
+ return RNUITextView.class;
269
+ }
270
+
271
+ #pragma mark - Custom Menu Items Methods
272
+
273
+ // 设置自定义菜单项
274
+ - (void)setCustomMenuItems:(NSArray<NSDictionary *> *)customMenuItems {
275
+ NSLog(@"[RNUITextView] setCustomMenuItems: %@", customMenuItems);
276
+ _customMenuItems = customMenuItems;
277
+
278
+ // 清空现有映射
279
+ [self.menuItemsMap removeAllObjects];
280
+
281
+ // 为每个自定义菜单项创建唯一的 selector,并动态注册 IMP
282
+ for (NSDictionary *item in customMenuItems) {
283
+ NSString *title = item[@"title"];
284
+ NSString *actionId = item[@"actionId"];
285
+ if (title && actionId) {
286
+ self.menuItemsMap[title] = actionId;
287
+ // 注册 selector
288
+ SEL sel = NSSelectorFromString([NSString stringWithFormat:@"customMenuItemAction_%@:", actionId]);
289
+ if (![self respondsToSelector:sel]) {
290
+ class_addMethod([self class], sel, (IMP)customMenuItemIMP, "v@:@");
291
+ }
292
+ }
293
+ }
294
+ // 更新菜单项(如果已选中)
295
+ // Custom menu is now handled by textView:editMenuForTextInRange:suggestedActions:
296
+ }
297
+
298
+ // C 函数:菜单项 IMP
299
+ void customMenuItemIMP(id self, SEL _cmd, id sender) {
300
+ NSString *selName = NSStringFromSelector(_cmd);
301
+ NSString *actionId = [[selName componentsSeparatedByString:@"customMenuItemAction_"].lastObject stringByReplacingOccurrencesOfString:@":" withString:@""];
302
+ if ([self respondsToSelector:@selector(fireCustomMenuActionWithId:)]) {
303
+ [self fireCustomMenuActionWithId:actionId];
304
+ }
305
+ }
306
+
307
+ // 发送事件到 JS
308
+ - (void)fireCustomMenuActionWithId:(NSString *)actionId {
309
+ if (actionId && _eventEmitter != nullptr) {
310
+ NSString *selectedText = @"";
311
+ if (_textView.selectedRange.length > 0 && _textView.selectedRange.location != NSNotFound) {
312
+ selectedText = [_textView.text substringWithRange:_textView.selectedRange];
313
+ }
314
+ std::dynamic_pointer_cast<const facebook::react::RNUITextViewEventEmitter>(_eventEmitter)
315
+ ->onCustomMenuAction(facebook::react::RNUITextViewEventEmitter::OnCustomMenuAction{
316
+ static_cast<int>(self.tag),
317
+ std::string([actionId UTF8String]),
318
+ std::string([selectedText UTF8String])
319
+ });
320
+ }
321
+ }
322
+
323
+ // selection 变化时设置自定义菜单项
324
+ - (void)textViewDidChangeSelection:(UITextView *)textView {
325
+ NSLog(@"selection changed: range=%@ length=%lu", NSStringFromRange(textView.selectedRange), (unsigned long)textView.selectedRange.length);
326
+
327
+ // 如果有选中文本,则显示菜单
328
+ if (textView.selectedRange.length > 0) {
329
+ // Menu is now shown by textView:editMenuForTextInRange:suggestedActions: delegate
330
+ }
331
+ }
332
+
333
+ // 开始编辑时
334
+ - (void)textViewDidBeginEditing:(UITextView *)textView {
335
+ NSLog(@"textViewDidBeginEditing");
336
+ // 如果有选中文本,则显示菜单
337
+ if (textView.selectedRange.length > 0) {
338
+ // Do nothing here, menu will be shown by textView:editMenuForTextInRange:suggestedActions:
339
+ }
340
+ }
341
+
342
+ #pragma mark - UITextViewDelegate Methods
343
+
344
+ // Implement the modern API for customizing the edit menu (iOS 13+)
345
+ - (UIMenu *)textView:(UITextView *)textView editMenuForTextInRange:(NSRange)range suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions {
346
+ NSLog(@"textView:editMenuForTextInRange: called. Range length: %lu, Custom items: %lu", (unsigned long)range.length, (unsigned long)self.customMenuItems.count);
347
+
348
+ // Only show custom menu if text is selected and custom items exist
349
+ if (self.customMenuItems.count == 0 || range.length == 0) {
350
+ // Return an empty menu to hide all system items if no custom items or no selection.
351
+ return [UIMenu menuWithChildren:@[]];
352
+ }
353
+
354
+ NSMutableArray<UIMenuElement *> *customActions = [NSMutableArray array];
355
+ for (NSDictionary *item in self.customMenuItems) {
356
+ NSString *title = item[@"title"];
357
+ NSString *actionId = item[@"actionId"];
358
+ SEL selector = NSSelectorFromString([NSString stringWithFormat:@"customMenuItemAction_%@:", actionId]);
359
+
360
+ if ([self respondsToSelector:selector]) {
361
+ UIAction *uiAction = [UIAction actionWithTitle:title
362
+ image:nil // No image for custom actions for now
363
+ identifier:nil // Not strictly needed here
364
+ handler:^(__kindof UIAction * _Nonnull triggeredAction) {
365
+ // Ensure the text view is first responder before performing action
366
+ if (![self->_textView isFirstResponder]) {
367
+ [self->_textView becomeFirstResponder];
368
+ }
369
+ // Perform the custom action
370
+ #pragma clang diagnostic push
371
+ #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
372
+ [self performSelector:selector withObject:self->_textView]; // Pass textView as a sender context if needed
373
+ #pragma clang diagnostic pop
374
+ }];
375
+ [customActions addObject:uiAction];
376
+ NSLog(@"[RNUITextView] Adding UIAction: '%@' for actionId: '%@'", title, actionId);
377
+ } else {
378
+ NSLog(@"[RNUITextView] Warning: Cannot respond to selector %@ for custom menu item '%@' (actionId: '%@')", NSStringFromSelector(selector), title, actionId);
379
+ }
380
+ }
381
+
382
+ // Return a menu with only our custom actions.
383
+ // This replaces the entire system menu.
384
+ return [UIMenu menuWithTitle:@"" children:customActions];
385
+ }
386
+
387
+ // 决定哪些菜单项可以显示 - Strictest version
388
+ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
389
+ NSString *actionName = NSStringFromSelector(action);
390
+ NSLog(@"[RNUITextView] canPerformAction: %@, selectedRange.length: %lu", actionName, (unsigned long)_textView.selectedRange.length);
391
+
392
+ if (_textView.selectedRange.length > 0) {
393
+ // Text is selected. Only allow our custom actions.
394
+ // Deny common system actions to prevent them from appearing in the menu alongside custom items.
395
+ for (NSDictionary *item in self.customMenuItems) {
396
+ NSString *actionId = item[@"actionId"];
397
+ SEL customSel = NSSelectorFromString([NSString stringWithFormat:@"customMenuItemAction_%@:", actionId]);
398
+ if (action == customSel) {
399
+ NSLog(@"[RNUITextView] Allowing custom action: %@", actionName);
400
+ return YES;
401
+ }
402
+ }
403
+
404
+ // Explicitly deny common system actions when text is selected to try to hide them from the menu.
405
+ // This is because the menu system might still show actions if canPerformAction: returns YES for them,
406
+ // even if they are not in the UIMenu returned by the delegate.
407
+ if (action == @selector(copy:) ||
408
+ action == @selector(cut:) ||
409
+ action == @selector(paste:) ||
410
+ action == @selector(select:) || // Handles 'Select' if it appears
411
+ action == @selector(selectAll:) ||
412
+ action == @selector(delete:) ||
413
+ action == @selector(_lookup:) || // Common system actions
414
+ action == @selector(_define:) ||
415
+ action == @selector(_translate:) || // System translate, if different from custom
416
+ action == @selector(share:) || // System share, if different from custom (e.g., UIActivityViewController)
417
+ action == NSSelectorFromString(@"_share:") || // Another variant of system share
418
+ action == NSSelectorFromString(@"promptForReplace:") ||
419
+ action == NSSelectorFromString(@"_promptForReplace:") ||
420
+ action == NSSelectorFromString(@"transliterateChinese:") ||
421
+ action == NSSelectorFromString(@"_transliterateChinese:") ||
422
+ action == NSSelectorFromString(@"_insertDrawing:") ||
423
+ action == NSSelectorFromString(@"captureTextFromCamera:") ||
424
+ action == NSSelectorFromString(@"_startWritingTools:") ||
425
+ action == @selector(toggleBoldface:) ||
426
+ action == @selector(toggleItalics:) ||
427
+ action == @selector(toggleUnderline:) ||
428
+ action == @selector(makeTextWritingDirectionLeftToRight:) ||
429
+ action == @selector(makeTextWritingDirectionRightToLeft:) ||
430
+ action == NSSelectorFromString(@"_showTextFormattingOptions:") ||
431
+ action == NSSelectorFromString(@"findSelected:") ||
432
+ action == NSSelectorFromString(@"_findSelected:") ||
433
+ action == NSSelectorFromString(@"addShortcut:") ||
434
+ action == NSSelectorFromString(@"_accessibilitySpeak:") ||
435
+ action == NSSelectorFromString(@"_accessibilitySpeakLanguageSelection:") ||
436
+ action == NSSelectorFromString(@"_accessibilityPauseSpeaking:"))
437
+ {
438
+ NSLog(@"[RNUITextView] Denying system action '%@' explicitly when text is selected to hide from menu.", actionName);
439
+ return NO;
440
+ }
441
+
442
+ // For any other unhandled actions when text is selected, deny them to keep the menu clean.
443
+ NSLog(@"[RNUITextView] Denying other unrecognized action '%@' by default when text is selected.", actionName);
444
+ return NO;
445
+
446
+ } else {
447
+ // No text is selected.
448
+ // Allow selectAll: so the user can select all text in the text view.
449
+ if (action == @selector(selectAll:)) {
450
+ NSLog(@"[RNUITextView] Allowing selectAll: as no text is selected.");
451
+ return YES;
452
+ }
453
+ // If you wanted to allow pasting into an empty text view, you would allow @selector(paste:) here.
454
+ // For now, deny all other actions if no text is selected.
455
+ NSLog(@"[RNUITextView] Denying action: %@ because no text is selected (and not selectAll).", actionName);
456
+ return NO;
457
+ }
458
+ }
459
+
460
+ // 实现 selectAll: 方法,转发到 _textView
461
+ - (void)selectAll:(id)sender {
462
+ [_textView selectAll:sender];
463
+ }
464
+
465
+ // 实现 copy: 方法,转发到 _textView
466
+ - (void)copy:(id)sender {
467
+ [_textView copy:sender];
468
+ }
469
+
470
+
471
+ // 移除 textViewShouldBeginEditing 和 customMenuItemAction(不再需要,已用 selection 变化和动态 selector 实现)
472
+
473
+
474
+ @end
@@ -0,0 +1,24 @@
1
+ // This guard prevent this file to be compiled in the old architecture.
2
+ #ifdef RCT_NEW_ARCH_ENABLED
3
+ #import <React/RCTViewComponentView.h>
4
+ #import <React/RCTComponent.h>
5
+ #import <UIKit/UIKit.h>
6
+
7
+ #ifndef RNUITextViewChildNativeComponent_h
8
+ #define RNUITextViewChildNativeComponent_h
9
+
10
+ NS_ASSUME_NONNULL_BEGIN
11
+
12
+ @interface RNUITextViewChild : RCTViewComponentView
13
+
14
+ @property (nonatomic, copy, nullable) NSString *text;
15
+
16
+ - (void)onPress;
17
+ - (void)onLongPress;
18
+
19
+ @end
20
+
21
+ NS_ASSUME_NONNULL_END
22
+
23
+ #endif /* UitextviewViewNativeComponent_h */
24
+ #endif /* RCT_NEW_ARCH_ENABLED */
@@ -0,0 +1,76 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+ #import "RNUITextViewChild.h"
3
+ #import "RNUITextView.h"
4
+ #import "RNUITextViewChildComponentDescriptor.h"
5
+
6
+ #import <react-native-uitextview/EventEmitters.h>
7
+ #import <react-native-uitextview/Props.h>
8
+ #import <react-native-uitextview/RCTComponentViewHelpers.h>
9
+
10
+ #import "RCTFabricComponentsPlugins.h"
11
+ #import "Utils.h"
12
+
13
+ using namespace facebook::react;
14
+
15
+ @interface RNUITextViewChild () <RCTRNUITextViewChildViewProtocol>
16
+
17
+ @end
18
+
19
+ @implementation RNUITextViewChild {
20
+ NSString * _text;
21
+ RCTBubblingEventBlock _onPress;
22
+ RCTBubblingEventBlock _onLongPress;
23
+ }
24
+
25
+ + (ComponentDescriptorProvider)componentDescriptorProvider
26
+ {
27
+ return concreteComponentDescriptorProvider<RNUITextViewChildComponentDescriptor>();
28
+ }
29
+
30
+ - (instancetype)initWithFrame:(CGRect)frame
31
+ {
32
+ if (self = [super initWithFrame:frame]) {
33
+ static const auto defaultProps = std::make_shared<const RNUITextViewChildProps>();
34
+ _props = defaultProps;
35
+ }
36
+ return self;
37
+ }
38
+
39
+ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
40
+ {
41
+ const auto &oldViewProps = *std::static_pointer_cast<RNUITextViewChildProps const>(_props);
42
+ const auto &newViewProps = *std::static_pointer_cast<RNUITextViewChildProps const>(props);
43
+
44
+ if (newViewProps.text != oldViewProps.text) {
45
+ NSString *text = [NSString stringWithUTF8String:newViewProps.text.c_str()];
46
+ _text = text;
47
+ }
48
+
49
+ [super updateProps:props oldProps:oldProps];
50
+ }
51
+
52
+ - (void)onPress {
53
+ if (_eventEmitter != nullptr) {
54
+ std::dynamic_pointer_cast<const facebook::react::RNUITextViewChildEventEmitter>(_eventEmitter)
55
+ ->onPress(facebook::react::RNUITextViewChildEventEmitter::OnPress{});
56
+ }
57
+ }
58
+
59
+ - (void)onLongPress {
60
+ if (_eventEmitter != nullptr) {
61
+ std::dynamic_pointer_cast<const facebook::react::RNUITextViewChildEventEmitter>(_eventEmitter)
62
+ ->onLongPress(facebook::react::RNUITextViewChildEventEmitter::OnLongPress{});
63
+ }
64
+ }
65
+
66
+ + (BOOL)shouldBeRecycled {
67
+ return NO;
68
+ }
69
+
70
+ Class<RCTComponentViewProtocol> RNUITextViewChildCls(void)
71
+ {
72
+ return RNUITextViewChild.class;
73
+ }
74
+
75
+ @end
76
+ #endif
@@ -0,0 +1,13 @@
1
+ #pragma once
2
+
3
+ #include "RNUITextViewChildShadowNode.h"
4
+
5
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
6
+ #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
7
+
8
+ namespace facebook::react {
9
+ using RNUITextViewChildComponentDescriptor = ConcreteComponentDescriptor<RNUITextViewChildShadowNode>;
10
+
11
+ void RNUITextViewChildSpec_registerComponentDescriptorsFromCodegen(
12
+ std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);
13
+ }
@@ -0,0 +1,6 @@
1
+ #include "RNUITextViewChildShadowNode.h"
2
+
3
+ namespace facebook::react {
4
+
5
+ extern const char RNUITextViewChildComponentName[] = "RNUITextViewChild";
6
+ } // namespace facebook::react
@@ -0,0 +1,16 @@
1
+ #pragma once
2
+
3
+ #include <react-native-uitextview/EventEmitters.h>
4
+ #include <react-native-uitextview/Props.h>
5
+ #include <react-native-uitextview/States.h>
6
+ #include <react/renderer/components/view/ConcreteViewShadowNode.h>
7
+
8
+ namespace facebook::react {
9
+ extern const char RNUITextViewChildComponentName[];
10
+
11
+ using RNUITextViewChildShadowNode = ConcreteViewShadowNode<
12
+ RNUITextViewChildComponentName,
13
+ RNUITextViewChildProps,
14
+ RNUITextViewChildEventEmitter,
15
+ RNUITextViewChildState>;
16
+ }
@@ -0,0 +1,13 @@
1
+ #pragma once
2
+
3
+ #include "RNUITextViewShadowNode.h"
4
+
5
+ #include <react/renderer/core/ConcreteComponentDescriptor.h>
6
+ #include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
7
+
8
+ namespace facebook::react {
9
+ using RNUITextViewComponentDescriptor = ConcreteComponentDescriptor<RNUITextViewShadowNode>;
10
+
11
+ void RNUITextViewSpec_registerComponentDescriptorsFromCodegen(
12
+ std::shared_ptr<const ComponentDescriptorProviderRegistry> registry);
13
+ }
@@ -0,0 +1,26 @@
1
+ #pragma once
2
+
3
+ #include <react/renderer/components/view/ViewEventEmitter.h>
4
+
5
+ namespace facebook::react {
6
+
7
+ class RNUITextViewEventEmitter : public ViewEventEmitter {
8
+ public:
9
+ using ViewEventEmitter::ViewEventEmitter;
10
+
11
+ struct OnTextLayout {
12
+ int target;
13
+ std::vector<std::string> lines;
14
+ };
15
+
16
+ struct OnCustomMenuAction {
17
+ int target;
18
+ std::string actionId;
19
+ std::string selectedText;
20
+ };
21
+
22
+ void onTextLayout(OnTextLayout value) const;
23
+ void onCustomMenuAction(OnCustomMenuAction value) const;
24
+ };
25
+
26
+ } // namespace facebook::react
@@ -0,0 +1,36 @@
1
+ #import <React/RCTViewManager.h>
2
+ #import <React/RCTUIManager.h>
3
+ #import "RCTBridge.h"
4
+ #import "Utils.h"
5
+
6
+ @interface RNUITextViewManager : RCTViewManager
7
+ @end
8
+
9
+ @implementation RNUITextViewManager
10
+
11
+ RCT_EXPORT_MODULE(RNUITextView)
12
+
13
+ - (UIView *)view
14
+ {
15
+ return [[UIView alloc] init];
16
+ }
17
+
18
+ RCT_CUSTOM_VIEW_PROPERTY(color, NSString, UIView)
19
+ {
20
+ }
21
+
22
+ @end
23
+
24
+ @interface RNUITextViewChildManager : RCTViewManager
25
+ @end
26
+
27
+ @implementation RNUITextViewChildManager
28
+
29
+ RCT_EXPORT_MODULE(RNUITextViewChild)
30
+
31
+ - (UIView *)view
32
+ {
33
+ return nil;
34
+ }
35
+
36
+ @end