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.
Files changed (41) hide show
  1. package/README.md +251 -10
  2. package/ReactNativeTypeRich.podspec +41 -0
  3. package/android/src/main/java/com/typerich/TypeRichTextInputView.kt +37 -10
  4. package/android/src/main/java/com/typerich/TypeRichTextInputViewManager.kt +5 -0
  5. package/ios/TypeRichTextInputView.h +27 -7
  6. package/ios/TypeRichTextInputView.mm +809 -26
  7. package/ios/cpp/TypeRichTextInputViewComponentDescriptor.h +19 -0
  8. package/ios/cpp/TypeRichTextInputViewShadowNode.h +44 -0
  9. package/ios/cpp/TypeRichTextInputViewShadowNode.mm +110 -0
  10. package/ios/cpp/TypeRichTextInputViewState.cpp +10 -0
  11. package/ios/cpp/TypeRichTextInputViewState.h +22 -0
  12. package/ios/inputTextView/TypeRichUITextView.h +14 -0
  13. package/ios/inputTextView/TypeRichUITextView.mm +100 -0
  14. package/ios/modules/commands/TypeRichTextInputCommands.h +24 -0
  15. package/ios/modules/commands/TypeRichTextInputCommands.mm +392 -0
  16. package/ios/utils/StringUtils.h +19 -0
  17. package/ios/utils/StringUtils.mm +15 -0
  18. package/ios/utils/TextInputUtils.h +26 -0
  19. package/ios/utils/TextInputUtils.mm +58 -0
  20. package/lib/module/TypeRichTextInput.js +13 -36
  21. package/lib/module/TypeRichTextInput.js.map +1 -1
  22. package/lib/module/TypeRichTextInputNativeComponent.ts +266 -52
  23. package/lib/module/index.js +1 -0
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/types/TypeRichTextInput.js +4 -0
  26. package/lib/module/types/TypeRichTextInput.js.map +1 -0
  27. package/lib/typescript/src/TypeRichTextInput.d.ts +2 -22
  28. package/lib/typescript/src/TypeRichTextInput.d.ts.map +1 -1
  29. package/lib/typescript/src/TypeRichTextInputNativeComponent.d.ts +200 -14
  30. package/lib/typescript/src/TypeRichTextInputNativeComponent.d.ts.map +1 -1
  31. package/lib/typescript/src/index.d.ts +1 -1
  32. package/lib/typescript/src/index.d.ts.map +1 -1
  33. package/lib/typescript/src/types/TypeRichTextInput.d.ts +95 -0
  34. package/lib/typescript/src/types/TypeRichTextInput.d.ts.map +1 -0
  35. package/package.json +1 -1
  36. package/src/TypeRichTextInput.tsx +20 -70
  37. package/src/TypeRichTextInputNativeComponent.ts +266 -52
  38. package/src/index.tsx +1 -5
  39. package/src/types/TypeRichTextInput.tsx +116 -0
  40. package/TypeRichTextInput.podspec +0 -20
  41. package/ios/TypeRichTextInputViewManager.mm +0 -27
@@ -1,54 +1,837 @@
1
1
  #import "TypeRichTextInputView.h"
2
2
 
3
- #import <react/renderer/components/TypeRichTextInputViewSpec/EventEmitters.h>
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
- @interface TypeRichTextInputView () <RCTTypeRichTextInputViewViewProtocol>
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
- UIView *_view;
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
- - (instancetype)initWithFrame:(CGRect)frame
170
+ #pragma mark - Props (JS → Native)
171
+
172
+ - (void)updateProps:(Props::Shared const &)props
173
+ oldProps:(Props::Shared const &)oldProps
19
174
  {
20
- if (self = [super initWithFrame:frame]) {
21
- static const auto defaultProps = std::make_shared<const TypeRichTextInputViewProps>();
22
- _props = defaultProps;
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
- _view = [[UIView alloc] init];
25
- _view.backgroundColor = [UIColor lightGrayColor]; // Visual indicator it's a dummy view
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
- self.contentView = _view;
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
- return self;
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
- - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
34
- {
35
- const auto &oldViewProps = *std::static_pointer_cast<TypeRichTextInputViewProps const>(_props);
36
- const auto &newViewProps = *std::static_pointer_cast<TypeRichTextInputViewProps const>(props);
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
- // No-op: just accept props without doing anything
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
- [super updateProps:props oldProps:oldProps];
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
- // Dummy command implementations (no-op)
44
- - (void)handleCommand:(NSString *)commandName args:(NSArray *)args
45
- {
46
- // Commands like focus, blur, setValue, setSelection do nothing
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
- @end
563
+ - (void)invalidateTextLayoutDuringTyping {
564
+ if (!_textView || _isTouchInProgress) {
565
+ return;
566
+ }
50
567
 
51
- Class<RCTComponentViewProtocol> TypeRichTextInputViewCls(void)
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
- return TypeRichTextInputView.class;
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