react-native-typerich 1.0.0 → 2.2.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/README.md +251 -10
- package/ReactNativeTypeRich.podspec +41 -0
- package/android/src/main/java/com/typerich/TypeRichTextInputView.kt +37 -10
- package/android/src/main/java/com/typerich/TypeRichTextInputViewManager.kt +5 -0
- package/ios/TypeRichTextInputView.h +27 -7
- package/ios/TypeRichTextInputView.mm +809 -26
- package/ios/cpp/TypeRichTextInputViewComponentDescriptor.h +19 -0
- package/ios/cpp/TypeRichTextInputViewShadowNode.h +44 -0
- package/ios/cpp/TypeRichTextInputViewShadowNode.mm +110 -0
- package/ios/cpp/TypeRichTextInputViewState.cpp +10 -0
- package/ios/cpp/TypeRichTextInputViewState.h +22 -0
- package/ios/inputTextView/TypeRichUITextView.h +14 -0
- package/ios/inputTextView/TypeRichUITextView.mm +100 -0
- package/ios/modules/commands/TypeRichTextInputCommands.h +24 -0
- package/ios/modules/commands/TypeRichTextInputCommands.mm +392 -0
- package/ios/utils/StringUtils.h +19 -0
- package/ios/utils/StringUtils.mm +15 -0
- package/ios/utils/TextInputUtils.h +26 -0
- package/ios/utils/TextInputUtils.mm +58 -0
- package/lib/module/TypeRichTextInput.js +13 -36
- package/lib/module/TypeRichTextInput.js.map +1 -1
- package/lib/module/TypeRichTextInputNativeComponent.ts +266 -52
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/TypeRichTextInput.js +4 -0
- package/lib/module/types/TypeRichTextInput.js.map +1 -0
- package/lib/typescript/src/TypeRichTextInput.d.ts +2 -22
- package/lib/typescript/src/TypeRichTextInput.d.ts.map +1 -1
- package/lib/typescript/src/TypeRichTextInputNativeComponent.d.ts +200 -14
- package/lib/typescript/src/TypeRichTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/TypeRichTextInput.d.ts +95 -0
- package/lib/typescript/src/types/TypeRichTextInput.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/TypeRichTextInput.tsx +20 -70
- package/src/TypeRichTextInputNativeComponent.ts +266 -52
- package/src/index.tsx +1 -5
- package/src/types/TypeRichTextInput.tsx +116 -0
- package/TypeRichTextInput.podspec +0 -20
- package/ios/TypeRichTextInputViewManager.mm +0 -27
|
@@ -1,54 +1,837 @@
|
|
|
1
1
|
#import "TypeRichTextInputView.h"
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// Fabric / Codegen
|
|
4
4
|
#import <react/renderer/components/TypeRichTextInputViewSpec/Props.h>
|
|
5
5
|
#import <react/renderer/components/TypeRichTextInputViewSpec/RCTComponentViewHelpers.h>
|
|
6
|
-
|
|
6
|
+
#import "cpp/TypeRichTextInputViewComponentDescriptor.h"
|
|
7
7
|
#import "RCTFabricComponentsPlugins.h"
|
|
8
8
|
|
|
9
|
+
// React utils
|
|
10
|
+
#import <React/RCTConversions.h>
|
|
11
|
+
#import <react/utils/ManagedObjectWrapper.h>
|
|
12
|
+
|
|
13
|
+
// local utils
|
|
14
|
+
#import "utils/StringUtils.h"
|
|
15
|
+
#import "utils/TextInputUtils.h"
|
|
16
|
+
#import "inputTextView/TypeRichUITextView.h"
|
|
17
|
+
|
|
18
|
+
// local modules for code splitting
|
|
19
|
+
#import "modules/commands/TypeRichTextInputCommands.h"
|
|
20
|
+
|
|
21
|
+
|
|
9
22
|
using namespace facebook::react;
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
#pragma mark - Private interface
|
|
25
|
+
|
|
26
|
+
@interface TypeRichTextInputView () <
|
|
27
|
+
RCTTypeRichTextInputViewViewProtocol,
|
|
28
|
+
UITextViewDelegate,
|
|
29
|
+
UIScrollViewDelegate
|
|
30
|
+
>
|
|
12
31
|
@end
|
|
13
32
|
|
|
33
|
+
#pragma mark - Implementation
|
|
34
|
+
|
|
14
35
|
@implementation TypeRichTextInputView {
|
|
15
|
-
|
|
36
|
+
/// Native text input
|
|
37
|
+
UITextView *_textView;
|
|
38
|
+
|
|
39
|
+
/// Placeholder label (RN-style, not UITextView.placeholder)
|
|
40
|
+
UILabel *_placeholderLabel;
|
|
41
|
+
UIColor *_placeholderColor;
|
|
42
|
+
|
|
43
|
+
/// Fabric state reference (owned by ShadowNode)
|
|
44
|
+
TypeRichTextInputViewShadowNode::ConcreteState::Shared _state;
|
|
45
|
+
|
|
46
|
+
/// Incremented whenever text height changes to force re-measure
|
|
47
|
+
int _heightRevision;
|
|
48
|
+
|
|
49
|
+
/// Flag to prevent layout updates during touch handling
|
|
50
|
+
BOOL _isTouchInProgress;
|
|
51
|
+
|
|
52
|
+
/// Commands to call from js side
|
|
53
|
+
TypeRichTextInputCommands *_commandHandler;
|
|
54
|
+
// BOOL _isHandlingUserInput;
|
|
55
|
+
|
|
56
|
+
/// Disabling Image Pasing
|
|
57
|
+
BOOL _disableImagePasting;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#pragma mark - Fabric registration
|
|
61
|
+
|
|
62
|
+
/// Registers this view with Fabric
|
|
63
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
64
|
+
return concreteComponentDescriptorProvider<
|
|
65
|
+
TypeRichTextInputViewComponentDescriptor>();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Required entry point for Fabric
|
|
69
|
+
Class<RCTComponentViewProtocol> TypeRichTextInputViewCls(void) {
|
|
70
|
+
return TypeRichTextInputView.class;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#pragma mark - Init
|
|
74
|
+
|
|
75
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
76
|
+
if (self = [super initWithFrame:frame]) {
|
|
77
|
+
_heightRevision = 0;
|
|
78
|
+
_isTouchInProgress = NO;
|
|
79
|
+
|
|
80
|
+
// ---------------------------
|
|
81
|
+
// UITextView FIRST
|
|
82
|
+
// ---------------------------
|
|
83
|
+
TypeRichUITextView *tv =
|
|
84
|
+
[[TypeRichUITextView alloc] initWithFrame:CGRectZero];
|
|
85
|
+
tv.owner = self;
|
|
86
|
+
_textView = tv;
|
|
87
|
+
|
|
88
|
+
_textView.delegate = self;
|
|
89
|
+
_textView.scrollEnabled = YES;
|
|
90
|
+
_textView.backgroundColor = UIColor.clearColor;
|
|
91
|
+
_textView.textContainerInset = UIEdgeInsetsZero;
|
|
92
|
+
_textView.textContainer.lineFragmentPadding = 0;
|
|
93
|
+
|
|
94
|
+
// KEY FIX: Allow text container to grow beyond visible bounds
|
|
95
|
+
_textView.textContainer.heightTracksTextView = NO;
|
|
96
|
+
|
|
97
|
+
// Disable delaysContentTouches to prevent scroll conflicts
|
|
98
|
+
_textView.delaysContentTouches = NO;
|
|
99
|
+
|
|
100
|
+
// initialise commandHandler
|
|
101
|
+
_commandHandler =
|
|
102
|
+
[[TypeRichTextInputCommands alloc] initWithTextView:_textView
|
|
103
|
+
owner:self];
|
|
104
|
+
|
|
105
|
+
// ---------------------------
|
|
106
|
+
// Placeholder label (ONCE)
|
|
107
|
+
// ---------------------------
|
|
108
|
+
_placeholderLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
|
109
|
+
_placeholderLabel.hidden = YES;
|
|
110
|
+
_placeholderLabel.numberOfLines = 0;
|
|
111
|
+
_placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
|
112
|
+
|
|
113
|
+
_placeholderColor = [UIColor colorWithWhite:0 alpha:0.3];
|
|
114
|
+
_placeholderLabel.textColor = _placeholderColor;
|
|
115
|
+
|
|
116
|
+
[_textView addSubview:_placeholderLabel];
|
|
117
|
+
|
|
118
|
+
// Constraints (match text layout)
|
|
119
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
120
|
+
[_placeholderLabel.leadingAnchor constraintEqualToAnchor:_textView.leadingAnchor],
|
|
121
|
+
[_placeholderLabel.trailingAnchor constraintEqualToAnchor:_textView.trailingAnchor],
|
|
122
|
+
[_placeholderLabel.topAnchor constraintEqualToAnchor:_textView.topAnchor]
|
|
123
|
+
]];
|
|
124
|
+
|
|
125
|
+
// Set initial font
|
|
126
|
+
UIFont *defaultFont = [UIFont systemFontOfSize:14];
|
|
127
|
+
_textView.font = defaultFont;
|
|
128
|
+
_placeholderLabel.font = defaultFont;
|
|
129
|
+
|
|
130
|
+
_disableImagePasting = NO;
|
|
131
|
+
|
|
132
|
+
// Add textView as subview (not contentView)
|
|
133
|
+
[self addSubview:_textView];
|
|
134
|
+
|
|
135
|
+
[self updatePlaceholderVisibility];
|
|
136
|
+
}
|
|
137
|
+
return self;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#pragma mark - UIGestureRecognizerDelegate
|
|
141
|
+
|
|
142
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
|
143
|
+
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
144
|
+
// Allow simultaneous recognition with RN's gesture recognizers
|
|
145
|
+
return YES;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
|
|
149
|
+
// UITextView's pan gesture should not block other gestures
|
|
150
|
+
return NO;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#pragma mark - Touch Handling
|
|
154
|
+
|
|
155
|
+
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
156
|
+
_isTouchInProgress = YES;
|
|
157
|
+
[super touchesBegan:touches withEvent:event];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
161
|
+
_isTouchInProgress = NO;
|
|
162
|
+
[super touchesEnded:touches withEvent:event];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
|
|
166
|
+
_isTouchInProgress = NO;
|
|
167
|
+
[super touchesCancelled:touches withEvent:event];
|
|
16
168
|
}
|
|
17
169
|
|
|
18
|
-
- (
|
|
170
|
+
#pragma mark - Props (JS → Native)
|
|
171
|
+
|
|
172
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
173
|
+
oldProps:(Props::Shared const &)oldProps
|
|
19
174
|
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
175
|
+
const auto &newProps =
|
|
176
|
+
*std::static_pointer_cast<TypeRichTextInputViewProps const>(props);
|
|
177
|
+
|
|
178
|
+
const auto *oldPropsPtr =
|
|
179
|
+
oldProps
|
|
180
|
+
? std::static_pointer_cast<TypeRichTextInputViewProps const>(oldProps).get()
|
|
181
|
+
: nullptr;
|
|
182
|
+
|
|
183
|
+
#pragma mark - Base Props
|
|
184
|
+
|
|
185
|
+
// Text value (controlled)
|
|
186
|
+
if (!oldPropsPtr || newProps.value != oldPropsPtr->value) {
|
|
187
|
+
NSString *newText = NSStringFromCppString(newProps.value);
|
|
188
|
+
NSString *currentText = _textView.text ?: @"";
|
|
189
|
+
|
|
190
|
+
if (![currentText isEqualToString:newText]) {
|
|
191
|
+
NSRange prevSelection = _textView.selectedRange;
|
|
192
|
+
|
|
193
|
+
self.blockEmitting = YES;
|
|
194
|
+
_textView.text = newText;
|
|
195
|
+
|
|
196
|
+
NSInteger len = newText.length;
|
|
197
|
+
NSInteger start = MIN(prevSelection.location, len);
|
|
198
|
+
NSInteger end = MIN(prevSelection.location + prevSelection.length, len);
|
|
199
|
+
|
|
200
|
+
_textView.selectedRange = NSMakeRange(start, end - start);
|
|
201
|
+
self.blockEmitting = NO;
|
|
202
|
+
}
|
|
203
|
+
[self invalidateTextLayout];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// defaultValue (mount only)
|
|
207
|
+
if (oldProps == nullptr && !newProps.defaultValue.empty()) {
|
|
208
|
+
_textView.text = NSStringFromCppString(newProps.defaultValue);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Placeholder
|
|
212
|
+
if (!oldPropsPtr || newProps.placeholder != oldPropsPtr->placeholder) {
|
|
213
|
+
_placeholderLabel.text =
|
|
214
|
+
NSStringFromCppString(newProps.placeholder);
|
|
215
|
+
[self updatePlaceholderVisibility];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// placeholderTextColor
|
|
219
|
+
if (!oldPropsPtr || newProps.placeholderTextColor != oldPropsPtr->placeholderTextColor) {
|
|
220
|
+
if (isColorMeaningful(newProps.placeholderTextColor)) {
|
|
221
|
+
_placeholderColor =
|
|
222
|
+
RCTUIColorFromSharedColor(newProps.placeholderTextColor);
|
|
223
|
+
_placeholderLabel.textColor = _placeholderColor;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Editable
|
|
228
|
+
if (!oldPropsPtr || newProps.editable != oldPropsPtr->editable) {
|
|
229
|
+
_textView.editable = newProps.editable;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Auto focus
|
|
233
|
+
if (oldProps == nullptr && newProps.autoFocus) {
|
|
234
|
+
[_textView becomeFirstResponder];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// cursor color
|
|
238
|
+
if (!oldPropsPtr || newProps.cursorColor != oldPropsPtr->cursorColor) {
|
|
239
|
+
if (isColorMeaningful(newProps.cursorColor)) {
|
|
240
|
+
_textView.tintColor =
|
|
241
|
+
RCTUIColorFromSharedColor(newProps.cursorColor);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// selectionColor
|
|
246
|
+
if (!oldPropsPtr || newProps.selectionColor != oldPropsPtr->selectionColor) {
|
|
247
|
+
if (isColorMeaningful(newProps.selectionColor)) {
|
|
248
|
+
_textView.tintColor =
|
|
249
|
+
RCTUIColorFromSharedColor(newProps.selectionColor);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// autoCapitalise
|
|
254
|
+
if (!oldPropsPtr || newProps.autoCapitalize != oldPropsPtr->autoCapitalize) {
|
|
255
|
+
if (!newProps.autoCapitalize.empty()) {
|
|
256
|
+
_textView.autocapitalizationType =
|
|
257
|
+
AutocapitalizeFromString(
|
|
258
|
+
NSStringFromCppString(newProps.autoCapitalize)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// scrollEnabled
|
|
264
|
+
if (!oldPropsPtr || newProps.scrollEnabled != oldPropsPtr->scrollEnabled) {
|
|
265
|
+
_textView.scrollEnabled = newProps.scrollEnabled;
|
|
266
|
+
_textView.showsVerticalScrollIndicator = newProps.scrollEnabled;
|
|
267
|
+
|
|
268
|
+
// KEY FIX: Control text container height tracking
|
|
269
|
+
_textView.textContainer.heightTracksTextView = !newProps.scrollEnabled;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// multiline
|
|
273
|
+
if (!oldPropsPtr || newProps.multiline != oldPropsPtr->multiline) {
|
|
274
|
+
// Do NOT set maximumNumberOfLines here - handle it in numberOfLines
|
|
275
|
+
// This prevents premature line limiting
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!oldPropsPtr ||
|
|
279
|
+
newProps.numberOfLines != oldPropsPtr->numberOfLines ||
|
|
280
|
+
newProps.multiline != oldPropsPtr->multiline ||
|
|
281
|
+
newProps.scrollEnabled != oldPropsPtr->scrollEnabled) {
|
|
282
|
+
|
|
283
|
+
if (newProps.multiline && newProps.numberOfLines > 0) {
|
|
284
|
+
// KEY FIX: Only limit lines when scrolling is DISABLED
|
|
285
|
+
// When scrolling is enabled, all content should be laid out
|
|
286
|
+
if (!newProps.scrollEnabled) {
|
|
287
|
+
_textView.textContainer.maximumNumberOfLines = newProps.numberOfLines;
|
|
288
|
+
} else {
|
|
289
|
+
// Allow unlimited lines when scrolling
|
|
290
|
+
_textView.textContainer.maximumNumberOfLines = 0;
|
|
291
|
+
}
|
|
292
|
+
} else if (newProps.multiline) {
|
|
293
|
+
_textView.textContainer.maximumNumberOfLines = 0;
|
|
294
|
+
} else {
|
|
295
|
+
_textView.textContainer.maximumNumberOfLines = 1;
|
|
296
|
+
}
|
|
297
|
+
[self invalidateTextLayout];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// keyboardAppearance
|
|
301
|
+
if (!oldPropsPtr ||
|
|
302
|
+
newProps.keyboardAppearance != oldPropsPtr->keyboardAppearance) {
|
|
303
|
+
|
|
304
|
+
_textView.keyboardAppearance =
|
|
305
|
+
KeyboardAppearanceFromEnum(
|
|
306
|
+
newProps.keyboardAppearance
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// disableImagePasting
|
|
311
|
+
if (!oldPropsPtr || newProps.disableImagePasting != oldPropsPtr->disableImagePasting) {
|
|
312
|
+
_disableImagePasting = newProps.disableImagePasting;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#pragma mark - Style Props
|
|
316
|
+
|
|
317
|
+
// Text color
|
|
318
|
+
if (!oldPropsPtr || newProps.color != oldPropsPtr->color) {
|
|
319
|
+
if (isColorMeaningful(newProps.color)) {
|
|
320
|
+
_textView.textColor = RCTUIColorFromSharedColor(newProps.color);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Font Block ------------------------------------------------------------------
|
|
325
|
+
BOOL fontChanged = !oldPropsPtr ||
|
|
326
|
+
newProps.fontSize != oldPropsPtr->fontSize ||
|
|
327
|
+
newProps.fontFamily != oldPropsPtr->fontFamily ||
|
|
328
|
+
newProps.fontWeight != oldPropsPtr->fontWeight ||
|
|
329
|
+
newProps.fontStyle != oldPropsPtr->fontStyle;
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
if (fontChanged) {
|
|
333
|
+
|
|
334
|
+
// Font size
|
|
335
|
+
CGFloat size =
|
|
336
|
+
newProps.fontSize > 0 ? (CGFloat)newProps.fontSize : 14.0;
|
|
337
|
+
|
|
338
|
+
// font family
|
|
339
|
+
NSString *family = nil;
|
|
340
|
+
if (!newProps.fontFamily.empty()) {
|
|
341
|
+
family = NSStringFromCppString(newProps.fontFamily);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Resolve font weight
|
|
345
|
+
// Values: "100"–"900", "normal", "bold
|
|
346
|
+
NSString *weightStr = nil;
|
|
347
|
+
if (!newProps.fontWeight.empty()) {
|
|
348
|
+
weightStr = NSStringFromCppString(newProps.fontWeight);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// font style
|
|
352
|
+
// Values: "italic" | "normal"
|
|
353
|
+
NSString *styleStr = @"normal";
|
|
354
|
+
if (!newProps.fontStyle.empty()) {
|
|
355
|
+
styleStr = NSStringFromCppString(newProps.fontStyle);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
// Create Base UIFont
|
|
360
|
+
UIFont *font = nil;
|
|
361
|
+
|
|
362
|
+
if (family) {
|
|
363
|
+
// Custom font family
|
|
364
|
+
font = [UIFont fontWithName:family size:size];
|
|
365
|
+
|
|
366
|
+
// Fallback if custom font not found
|
|
367
|
+
if (!font) {
|
|
368
|
+
NSLog(@"Font '%@' not found, using system font", family);
|
|
369
|
+
font = [UIFont systemFontOfSize:size];
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// System font path with weight support
|
|
373
|
+
UIFontWeight weight =
|
|
374
|
+
weightStr ? FontWeightFromString(weightStr)
|
|
375
|
+
: UIFontWeightRegular;
|
|
376
|
+
|
|
377
|
+
font = [UIFont systemFontOfSize:size weight:weight];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Apply fontStyle (italic / normal) with font descriptor
|
|
381
|
+
UIFontDescriptorSymbolicTraits traits =
|
|
382
|
+
font.fontDescriptor.symbolicTraits;
|
|
383
|
+
|
|
384
|
+
if ([styleStr isEqualToString:@"italic"]) {
|
|
385
|
+
traits |= UIFontDescriptorTraitItalic;
|
|
386
|
+
} else {
|
|
387
|
+
// Explicitly remove italic when switching back to "normal"
|
|
388
|
+
traits &= ~UIFontDescriptorTraitItalic;
|
|
389
|
+
}
|
|
390
|
+
UIFontDescriptor *descriptor =
|
|
391
|
+
[font.fontDescriptor fontDescriptorWithSymbolicTraits:traits];
|
|
392
|
+
|
|
393
|
+
if (descriptor) {
|
|
394
|
+
font = [UIFont fontWithDescriptor:descriptor size:size];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
#pragma mark - Setting font
|
|
398
|
+
// Apply font to UITextView and placeholder
|
|
399
|
+
_textView.font = font;
|
|
400
|
+
_placeholderLabel.font = font;
|
|
401
|
+
|
|
402
|
+
NSLog(
|
|
403
|
+
@"Font updated: size=%.1f, family=%@, weight=%@, style=%@",
|
|
404
|
+
size,
|
|
405
|
+
family ?: @"system",
|
|
406
|
+
weightStr ?: @"regular",
|
|
407
|
+
styleStr ?: @"normal"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
// End Font Block ------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
// lineheight
|
|
413
|
+
BOOL lineHeightChanged =
|
|
414
|
+
!oldPropsPtr || newProps.lineHeight != oldPropsPtr->lineHeight;
|
|
415
|
+
|
|
416
|
+
if (lineHeightChanged && newProps.lineHeight > 0) {
|
|
417
|
+
CGFloat lineHeight = newProps.lineHeight;
|
|
418
|
+
UIFont *font = _textView.font;
|
|
419
|
+
|
|
420
|
+
// do not go below fontsize's lineheight
|
|
421
|
+
if (lineHeight < font.lineHeight) {
|
|
422
|
+
lineHeight = font.lineHeight;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
NSMutableParagraphStyle *paragraphStyle =
|
|
426
|
+
[[NSMutableParagraphStyle alloc] init];
|
|
427
|
+
|
|
428
|
+
paragraphStyle.minimumLineHeight = lineHeight;
|
|
429
|
+
paragraphStyle.maximumLineHeight = lineHeight;
|
|
430
|
+
|
|
431
|
+
// Baseline fix (prevents overlap)
|
|
432
|
+
CGFloat baselineOffset =
|
|
433
|
+
(lineHeight - font.lineHeight) / 2.0;
|
|
434
|
+
|
|
435
|
+
NSDictionary *attributes = @{
|
|
436
|
+
NSFontAttributeName: font,
|
|
437
|
+
NSParagraphStyleAttributeName: paragraphStyle,
|
|
438
|
+
NSBaselineOffsetAttributeName: @(baselineOffset)
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
// Always update typingAttributes (safe)
|
|
442
|
+
_textView.typingAttributes = attributes;
|
|
23
443
|
|
|
24
|
-
|
|
25
|
-
|
|
444
|
+
// Do not touch attributedText during composition
|
|
445
|
+
if (_textView.markedTextRange != nil) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Apply to existing text
|
|
450
|
+
NSMutableAttributedString *attributedText =
|
|
451
|
+
[[NSMutableAttributedString alloc]
|
|
452
|
+
initWithString:_textView.text ?: @""
|
|
453
|
+
attributes:attributes];
|
|
454
|
+
|
|
455
|
+
_textView.typingAttributes = attributes;
|
|
456
|
+
|
|
457
|
+
if (!_textView.isFirstResponder &&
|
|
458
|
+
_textView.markedTextRange == nil) {
|
|
26
459
|
|
|
27
|
-
|
|
460
|
+
NSRange sel = _textView.selectedRange;
|
|
461
|
+
|
|
462
|
+
NSMutableAttributedString *attr =
|
|
463
|
+
[[NSMutableAttributedString alloc]
|
|
464
|
+
initWithString:_textView.text ?: @""
|
|
465
|
+
attributes:attributes];
|
|
466
|
+
|
|
467
|
+
_textView.attributedText = attr;
|
|
468
|
+
_textView.selectedRange = sel;
|
|
28
469
|
}
|
|
29
470
|
|
|
30
|
-
|
|
471
|
+
|
|
472
|
+
// Apply to future typing
|
|
473
|
+
_textView.typingAttributes = attributes;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
#pragma mark - updating props
|
|
477
|
+
// Update placeholder visibility
|
|
478
|
+
[self updatePlaceholderVisibility];
|
|
479
|
+
[self invalidateTextLayout];
|
|
480
|
+
[super updateProps:props oldProps:oldProps];
|
|
31
481
|
}
|
|
32
482
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
483
|
+
#pragma mark - Measurement (ShadowNode → View)
|
|
484
|
+
|
|
485
|
+
/// Fabric calls this to measure height for given width
|
|
486
|
+
- (CGSize)measureSize:(CGFloat)maxWidth {
|
|
487
|
+
UIEdgeInsets inset = _textView.textContainerInset;
|
|
488
|
+
CGFloat availableWidth = MAX(0, maxWidth - inset.left - inset.right);
|
|
37
489
|
|
|
38
|
-
|
|
490
|
+
// Get the props to check numberOfLines and scrollEnabled
|
|
491
|
+
if (_eventEmitter) {
|
|
492
|
+
auto props = std::static_pointer_cast<const TypeRichTextInputViewProps >(_props);
|
|
39
493
|
|
|
40
|
-
|
|
494
|
+
// If scrollEnabled with numberOfLines, return fixed height
|
|
495
|
+
if (props->scrollEnabled && props->multiline && props->numberOfLines > 0) {
|
|
496
|
+
// Calculate height for specified number of lines
|
|
497
|
+
UIFont *font = _textView.font ?: [UIFont systemFontOfSize:14];
|
|
498
|
+
CGFloat lineHeight = font.lineHeight;
|
|
499
|
+
|
|
500
|
+
// Apply custom lineHeight if set
|
|
501
|
+
if (props->lineHeight > 0) {
|
|
502
|
+
lineHeight = MAX(props->lineHeight, font.lineHeight);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
CGFloat height = lineHeight * props->numberOfLines;
|
|
506
|
+
return CGSizeMake(maxWidth, ceil(height));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// For non-scrollable or unlimited lines, measure actual content
|
|
511
|
+
CGSize fitting = [_textView sizeThatFits:CGSizeMake(availableWidth, CGFLOAT_MAX)];
|
|
512
|
+
return CGSizeMake(maxWidth, ceil(fitting.height));
|
|
41
513
|
}
|
|
42
514
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
515
|
+
#pragma mark - Layout invalidation
|
|
516
|
+
|
|
517
|
+
/// Forces UITextView to re-layout text and notifies Fabric to re-measure
|
|
518
|
+
- (void)invalidateTextLayout {
|
|
519
|
+
if (!_textView ||
|
|
520
|
+
_isTouchInProgress ||
|
|
521
|
+
_textView.isFirstResponder) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// ---- UIKit layout invalidation ----
|
|
526
|
+
NSLayoutManager *layoutManager = _textView.layoutManager;
|
|
527
|
+
NSTextStorage *textStorage = _textView.textStorage;
|
|
528
|
+
|
|
529
|
+
NSRange fullRange = NSMakeRange(0, textStorage.length);
|
|
530
|
+
|
|
531
|
+
// Invalidate glyphs & layout
|
|
532
|
+
[layoutManager invalidateLayoutForCharacterRange:fullRange
|
|
533
|
+
actualCharacterRange:NULL];
|
|
534
|
+
[layoutManager invalidateDisplayForCharacterRange:fullRange];
|
|
535
|
+
|
|
536
|
+
// Ensure layout is recalculated immediately
|
|
537
|
+
[layoutManager ensureLayoutForCharacterRange:fullRange];
|
|
538
|
+
|
|
539
|
+
// ---- Force UITextView to recompute contentSize ----
|
|
540
|
+
[_textView setNeedsLayout];
|
|
541
|
+
[_textView layoutIfNeeded];
|
|
542
|
+
|
|
543
|
+
// ---- Notify Fabric to re-measure ----
|
|
544
|
+
if (_state == nullptr) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
_heightRevision++;
|
|
549
|
+
|
|
550
|
+
auto selfRef = wrapManagedObjectWeakly(self);
|
|
551
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
552
|
+
if (self->_state == nullptr || self->_isTouchInProgress) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
self->_heightRevision++;
|
|
557
|
+
self->_state->updateState(
|
|
558
|
+
TypeRichTextInputViewState(self->_heightRevision, selfRef)
|
|
559
|
+
);
|
|
560
|
+
});
|
|
47
561
|
}
|
|
48
562
|
|
|
49
|
-
|
|
563
|
+
- (void)invalidateTextLayoutDuringTyping {
|
|
564
|
+
if (!_textView || _isTouchInProgress) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
50
567
|
|
|
51
|
-
|
|
568
|
+
if (_state == nullptr) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
_heightRevision++;
|
|
573
|
+
|
|
574
|
+
auto selfRef = wrapManagedObjectWeakly(self);
|
|
575
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
576
|
+
if (self->_state == nullptr) {
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
self->_state->updateState(
|
|
581
|
+
TypeRichTextInputViewState(self->_heightRevision, selfRef)
|
|
582
|
+
);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
#pragma mark - State (ShadowNode → View)
|
|
588
|
+
|
|
589
|
+
/// Store state reference so we can update it later
|
|
590
|
+
- (void)updateState:(State::Shared const &)state
|
|
591
|
+
oldState:(State::Shared const &)oldState
|
|
52
592
|
{
|
|
53
|
-
|
|
54
|
-
|
|
593
|
+
_state =
|
|
594
|
+
std::static_pointer_cast<
|
|
595
|
+
const TypeRichTextInputViewShadowNode::ConcreteState
|
|
596
|
+
>(state);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
#pragma mark - Events (UITextViewDelegate)
|
|
600
|
+
|
|
601
|
+
#pragma mark -- Text Changed event
|
|
602
|
+
- (void)textViewDidChange:(UITextView *)textView {
|
|
603
|
+
|
|
604
|
+
if (self.blockEmitting) return;
|
|
605
|
+
// _isHandlingUserInput = YES;
|
|
606
|
+
|
|
607
|
+
self.isUserTyping = YES;
|
|
608
|
+
self.lastTypingTime = CACurrentMediaTime();
|
|
609
|
+
|
|
610
|
+
[self updatePlaceholderVisibility];
|
|
611
|
+
|
|
612
|
+
// Emit JS onChangeText
|
|
613
|
+
auto emitter = [self getEventEmitter];
|
|
614
|
+
if (emitter) {
|
|
615
|
+
emitter->onChangeText({
|
|
616
|
+
.value = std::string(textView.text.UTF8String ?: "")
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Ensure cursor stays visible when scrolling
|
|
621
|
+
if (textView.scrollEnabled) {
|
|
622
|
+
[textView scrollRangeToVisible:textView.selectedRange];
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
[self updatePlaceholderVisibilityFromCommand];
|
|
626
|
+
[self invalidateTextLayoutDuringTyping];
|
|
627
|
+
|
|
628
|
+
// dispatch_async(dispatch_get_main_queue(), ^{
|
|
629
|
+
// self->_isHandlingUserInput = NO;
|
|
630
|
+
// });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
#pragma mark -- focus / blur event
|
|
634
|
+
- (void)textViewDidBeginEditing:(UITextView *)textView {
|
|
635
|
+
auto emitter = [self getEventEmitter];
|
|
636
|
+
if (emitter) {
|
|
637
|
+
emitter->onInputFocus({});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
- (void)textViewDidEndEditing:(UITextView *)textView {
|
|
642
|
+
auto emitter = [self getEventEmitter];
|
|
643
|
+
if (emitter) {
|
|
644
|
+
emitter->onInputBlur({});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
#pragma mark -- Selection event
|
|
649
|
+
|
|
650
|
+
- (void)textViewDidChangeSelection:(UITextView *)textView {
|
|
651
|
+
if (self.blockEmitting) return;
|
|
652
|
+
|
|
653
|
+
self.isUserTyping = NO;
|
|
654
|
+
|
|
655
|
+
auto emitter = [self getEventEmitter];
|
|
656
|
+
if (!emitter) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
NSRange range = textView.selectedRange;
|
|
661
|
+
|
|
662
|
+
emitter->onChangeSelection({
|
|
663
|
+
.start = (int)range.location,
|
|
664
|
+
.end = (int)(range.location + range.length),
|
|
665
|
+
.text = std::string(textView.text.UTF8String ?: "")
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
#pragma mark - Paste Image
|
|
670
|
+
|
|
671
|
+
- (void)emitPasteImageEventWith:(NSString *)uri
|
|
672
|
+
type:(NSString *)type
|
|
673
|
+
fileName:(NSString *)fileName
|
|
674
|
+
fileSize:(NSUInteger)fileSize {
|
|
675
|
+
auto emitter = [self getEventEmitter];
|
|
676
|
+
if (!emitter) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
emitter->onPasteImage({
|
|
681
|
+
.uri = std::string(uri.UTF8String),
|
|
682
|
+
.type = std::string(type.UTF8String),
|
|
683
|
+
.fileName = std::string(fileName.UTF8String),
|
|
684
|
+
.fileSize = (double)fileSize,
|
|
685
|
+
.source =
|
|
686
|
+
TypeRichTextInputViewEventEmitter::OnPasteImageSource::Clipboard
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
#pragma mark - Commands
|
|
691
|
+
|
|
692
|
+
- (void)handleCommand:(const NSString *)commandName
|
|
693
|
+
args:(const NSArray *)args{
|
|
694
|
+
if (!_commandHandler) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if ([commandName isEqualToString:@"focus"]) {
|
|
699
|
+
[_commandHandler focus];
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if ([commandName isEqualToString:@"blur"]) {
|
|
704
|
+
[_commandHandler blur];
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if ([commandName isEqualToString:@"setText"]) {
|
|
709
|
+
NSString *text = args.count > 0 ? args[0] : @"";
|
|
710
|
+
[_commandHandler setText:text];
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if ([commandName isEqualToString:@"setSelection"]) {
|
|
715
|
+
if (args.count >= 2) {
|
|
716
|
+
[_commandHandler setSelectionStart:[args[0] integerValue]
|
|
717
|
+
end:[args[1] integerValue]];
|
|
718
|
+
}
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if ([commandName isEqualToString:@"insertTextAt"]) {
|
|
723
|
+
if (args.count >= 3) {
|
|
724
|
+
[_commandHandler insertTextAtStart:[args[0] integerValue]
|
|
725
|
+
end:[args[1] integerValue]
|
|
726
|
+
text:args[2]];
|
|
727
|
+
}
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
#pragma mark - UIScrollViewDelegate
|
|
734
|
+
|
|
735
|
+
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
|
|
736
|
+
_isTouchInProgress = YES;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
|
|
740
|
+
if (!decelerate) {
|
|
741
|
+
_isTouchInProgress = NO;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
|
|
746
|
+
_isTouchInProgress = NO;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#pragma mark - Placeholder helpers
|
|
750
|
+
|
|
751
|
+
/// Show placeholder only when text is empty
|
|
752
|
+
- (void)updatePlaceholderVisibility {
|
|
753
|
+
_placeholderLabel.hidden = _textView.text.length > 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
- (void)layoutSubviews {
|
|
757
|
+
[super layoutSubviews];
|
|
758
|
+
|
|
759
|
+
// Ensure text view fills the parent bounds
|
|
760
|
+
_textView.frame = self.bounds;
|
|
761
|
+
|
|
762
|
+
// Debug: Log frame and content size
|
|
763
|
+
NSLog(@"TextView frame: %@, contentSize: %@, maxLines: %ld",
|
|
764
|
+
NSStringFromCGRect(_textView.frame),
|
|
765
|
+
NSStringFromCGSize(_textView.contentSize),
|
|
766
|
+
(long)_textView.textContainer.maximumNumberOfLines);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
#pragma mark - Event emitter
|
|
770
|
+
|
|
771
|
+
- (std::shared_ptr<TypeRichTextInputViewEventEmitter>)getEventEmitter {
|
|
772
|
+
if (_eventEmitter == nullptr) {
|
|
773
|
+
return nullptr;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
auto const &emitter =
|
|
777
|
+
static_cast<TypeRichTextInputViewEventEmitter const &>(*_eventEmitter);
|
|
778
|
+
|
|
779
|
+
return std::make_shared<TypeRichTextInputViewEventEmitter>(emitter);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#pragma mark - Helpers
|
|
783
|
+
|
|
784
|
+
- (BOOL)isTouchInProgress {
|
|
785
|
+
return _isTouchInProgress;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
- (void)invalidateTextLayoutFromCommand {
|
|
789
|
+
if (_isTouchInProgress) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// layout invalidation after setting text via commands
|
|
794
|
+
[self invalidateTextLayoutDuringTyping];
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
- (void)updatePlaceholderVisibilityFromCommand {
|
|
798
|
+
if (_isTouchInProgress) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// placeholder updation after setting text via commands
|
|
803
|
+
[self updatePlaceholderVisibility];
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
- (void)dispatchSelectionChangeIfNeeded {
|
|
807
|
+
if (self.blockEmitting) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
auto emitter = [self getEventEmitter];
|
|
812
|
+
if (!emitter) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
UITextView *tv = _textView;
|
|
817
|
+
if (!tv) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
NSRange range = tv.selectedRange;
|
|
822
|
+
|
|
823
|
+
emitter->onChangeSelection({
|
|
824
|
+
.start = (int)range.location,
|
|
825
|
+
.end = (int)(range.location + range.length),
|
|
826
|
+
.text = std::string(tv.text.UTF8String ?: "")
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
//- (BOOL)isHandlingUserInput {
|
|
831
|
+
// return _isHandlingUserInput;
|
|
832
|
+
//}
|
|
833
|
+
|
|
834
|
+
- (BOOL)isDisableImagePasting{
|
|
835
|
+
return _disableImagePasting;
|
|
836
|
+
}
|
|
837
|
+
@end
|