@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.
- package/LICENSE +20 -0
- package/README.md +225 -0
- package/android/generated/jni/CMakeLists.txt +36 -0
- package/android/generated/jni/RNUITextViewSpec-generated.cpp +22 -0
- package/android/generated/jni/RNUITextViewSpec.h +24 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ComponentDescriptors.cpp +23 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ComponentDescriptors.h +25 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/EventEmitters.cpp +61 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/EventEmitters.h +49 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/Props.cpp +47 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/Props.h +182 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/RNUITextViewSpecJSI-generated.cpp +17 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/RNUITextViewSpecJSI.h +19 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ShadowNodes.cpp +18 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/ShadowNodes.h +43 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/RNUITextViewSpec/States.h +41 -0
- package/ios/RNUITextView.h +23 -0
- package/ios/RNUITextView.mm +474 -0
- package/ios/RNUITextViewChild.h +24 -0
- package/ios/RNUITextViewChild.mm +76 -0
- package/ios/RNUITextViewChildComponentDescriptor.h +13 -0
- package/ios/RNUITextViewChildShadowNode.cpp +6 -0
- package/ios/RNUITextViewChildShadowNode.h +16 -0
- package/ios/RNUITextViewComponentDescriptor.h +13 -0
- package/ios/RNUITextViewEventEmitter.h +26 -0
- package/ios/RNUITextViewManager.mm +36 -0
- package/ios/RNUITextViewShadowNode.cpp +127 -0
- package/ios/RNUITextViewShadowNode.h +48 -0
- package/ios/RNUITextViewSpecJSI-generated.cpp +17 -0
- package/ios/RNUITextViewSpecJSI.h +19 -0
- package/ios/generated/RNUITextViewSpec/ComponentDescriptors.cpp +23 -0
- package/ios/generated/RNUITextViewSpec/ComponentDescriptors.h +25 -0
- package/ios/generated/RNUITextViewSpec/EventEmitters.cpp +61 -0
- package/ios/generated/RNUITextViewSpec/EventEmitters.h +49 -0
- package/ios/generated/RNUITextViewSpec/Props.cpp +47 -0
- package/ios/generated/RNUITextViewSpec/Props.h +182 -0
- package/ios/generated/RNUITextViewSpec/RCTComponentViewHelpers.h +24 -0
- package/ios/generated/RNUITextViewSpec/RNUITextViewSpec-generated.mm +16 -0
- package/ios/generated/RNUITextViewSpec/RNUITextViewSpec.h +38 -0
- package/ios/generated/RNUITextViewSpec/ShadowNodes.cpp +18 -0
- package/ios/generated/RNUITextViewSpec/ShadowNodes.h +43 -0
- package/ios/generated/RNUITextViewSpec/States.cpp +16 -0
- package/ios/generated/RNUITextViewSpec/States.h +41 -0
- package/ios/generated/RNUITextViewSpecJSI-generated.cpp +17 -0
- package/ios/generated/RNUITextViewSpecJSI.h +19 -0
- package/lib/commonjs/RNUITextViewChildNativeComponent.ts +48 -0
- package/lib/commonjs/RNUITextViewNativeComponent.ts +47 -0
- package/lib/commonjs/Text.js +140 -0
- package/lib/commonjs/Text.js.map +1 -0
- package/lib/commonjs/index.js +17 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/util.js +64 -0
- package/lib/commonjs/util.js.map +1 -0
- package/lib/module/RNUITextViewChildNativeComponent.ts +48 -0
- package/lib/module/RNUITextViewNativeComponent.ts +47 -0
- package/lib/module/Text.js +136 -0
- package/lib/module/Text.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/util.js +60 -0
- package/lib/module/util.js.map +1 -0
- package/lib/typescript/commonjs/example/src/App.d.ts +2 -0
- package/lib/typescript/commonjs/example/src/App.d.ts.map +1 -0
- package/lib/typescript/commonjs/example/src/CustomMenuExample.d.ts +3 -0
- package/lib/typescript/commonjs/example/src/CustomMenuExample.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/src/RNUITextViewChildNativeComponent.d.ts +28 -0
- package/lib/typescript/commonjs/src/RNUITextViewChildNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/RNUITextViewNativeComponent.d.ts +29 -0
- package/lib/typescript/commonjs/src/RNUITextViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/Text.d.ts +17 -0
- package/lib/typescript/commonjs/src/Text.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +2 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/util.d.ts +160 -0
- package/lib/typescript/commonjs/src/util.d.ts.map +1 -0
- package/lib/typescript/module/example/src/App.d.ts +2 -0
- package/lib/typescript/module/example/src/App.d.ts.map +1 -0
- package/lib/typescript/module/example/src/CustomMenuExample.d.ts +3 -0
- package/lib/typescript/module/example/src/CustomMenuExample.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/src/RNUITextViewChildNativeComponent.d.ts +28 -0
- package/lib/typescript/module/src/RNUITextViewChildNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/RNUITextViewNativeComponent.d.ts +29 -0
- package/lib/typescript/module/src/RNUITextViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/Text.d.ts +17 -0
- package/lib/typescript/module/src/Text.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +2 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -0
- package/lib/typescript/module/src/util.d.ts +160 -0
- package/lib/typescript/module/src/util.d.ts.map +1 -0
- package/package.json +187 -0
- package/react-native-uitextview.podspec +21 -0
- package/react-native.config.js +28 -0
- package/src/RNUITextViewChildNativeComponent.ts +48 -0
- package/src/RNUITextViewNativeComponent.ts +47 -0
- package/src/Text.tsx +148 -0
- package/src/index.tsx +1 -0
- 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,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
|