diff --git a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js index 8cc1369..6d5e45e 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js +++ b/node_modules/react-native/Libraries/Components/TextInput/AndroidTextInputNativeComponent.js @@ -449,6 +449,21 @@ export type AndroidTextInputNativeProps = $ReadOnly<{ }>, >, + /** + * Invoked when the user performs the paste action. + */ + onPaste?: ?DirectEventHandler< + $ReadOnly<{| + target: Int32, + items: $ReadOnlyArray< + $ReadOnly<{| + type: string, + data: string, + |}>, + >, + |}>, + >, + /** * The string that will be rendered before text input has been entered. */ @@ -640,6 +655,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { topScroll: { registrationName: 'onScroll', }, + topPaste: { + registrationName: 'onPaste', + }, }, validAttributes: { acceptDragAndDropTypes: true, @@ -695,6 +713,7 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig = { textBreakStrategy: true, onScroll: true, onContentSizeChange: true, + onPaste: true, disableFullscreenUI: true, includeFontPadding: true, fontWeight: true, diff --git a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js index 05bddcb..69c11a2 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js +++ b/node_modules/react-native/Libraries/Components/TextInput/RCTTextInputViewConfig.js @@ -82,6 +82,9 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { topContentSizeChange: { registrationName: 'onContentSizeChange', }, + topPaste: { + registrationName: 'onPaste', + }, topChangeSync: { registrationName: 'onChangeSync', }, @@ -160,6 +163,7 @@ const RCTTextInputViewConfig: PartialViewConfigWithoutName = { onSelectionChange: true, onContentSizeChange: true, onScroll: true, + onPaste: true, onChangeSync: true, onKeyPressSync: true, }), diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts index b3ca156..36504b5 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.d.ts @@ -565,6 +565,22 @@ export interface TextInputSubmitEditingEventData { export type TextInputSubmitEditingEvent = NativeSyntheticEvent; +/** + * @deprecated Use `TextInputPasteEvent` instead + */ +export interface TextInputPasteEventData extends TargetedEvent { + items: Array<{ + type: string; + data: string; + }>; +} + +/** + * @see TextInputProps.onPaste + */ +export type TextInputPasteEvent = + NativeSyntheticEvent; + /** * @see https://reactnative.dev/docs/textinput#props */ @@ -896,6 +912,11 @@ export interface TextInputProps */ onKeyPress?: ((e: TextInputKeyPressEvent) => void) | undefined; + /** + * Invoked when the user performs the paste action. + */ + onPaste?: ((e: TextInputPasteEvent) => void) | undefined; + /** * The string that will be rendered before text input has been entered */ diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js index 5fa8811..7eb26a7 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.flow.js @@ -137,6 +137,22 @@ export type TextInputSubmitEditingEvent = export type TextInputEditingEvent = NativeSyntheticEvent; +type TextInputPasteEventData = $ReadOnly<{| + target: number, + items: $ReadOnlyArray< + $ReadOnly<{| + type: string, + data: string, + |}>, + >, +|}> + +/** + * @see TextInputProps.onPaste + */ +export type TextInputPasteEvent = + NativeSyntheticEvent; + type DataDetectorTypesType = | 'phoneNumber' | 'link' @@ -884,6 +900,11 @@ type TextInputBaseProps = $ReadOnly<{ */ onScroll?: ?(e: ScrollEvent) => mixed, + /** + * Invoked when the user performs the paste action. + */ + onPaste?: ?(e: TextInputPasteEvent) => mixed, + /** * The string that will be rendered before text input has been entered. */ diff --git a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js index 1e95eb6..bb489db 100644 --- a/node_modules/react-native/Libraries/Components/TextInput/TextInput.js +++ b/node_modules/react-native/Libraries/Components/TextInput/TextInput.js @@ -45,6 +45,7 @@ import type { TextInputInstance, TextInputIOSProps, TextInputKeyPressEvent, + TextInputPasteEvent, TextInputProps, TextInputSelectionChangeEvent, TextInputSubmitEditingEvent, @@ -115,6 +116,7 @@ export type { TextInputFocusEvent, TextInputIOSProps, TextInputKeyPressEvent, + TextInputPasteEvent, TextInputProps, TextInputSelectionChangeEvent, TextInputSubmitEditingEvent, diff --git a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm index 6e9c384..17d157d 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.mm @@ -13,6 +13,10 @@ #import #import +#import +#import +#import + @implementation RCTUITextView { UILabel *_placeholderView; UITextView *_detachedTextView; @@ -209,7 +213,32 @@ - (void)scrollRangeToVisible:(NSRange)range - (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; + if (clipboard.hasImages) { + for (NSItemProvider *itemProvider in clipboard.itemProviders) { + if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { + for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { + if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { + NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); + NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); + NSString *fileName = [NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString], fileExtension]; + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + NSData *fileData = [clipboard dataForPasteboardType:identifier]; + [fileData writeToFile:filePath atomically:YES]; + [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; + break; + } + } + break; + } + } + } else { + if (clipboard.hasStrings) { + [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; + } + [super paste:sender]; + } } // Turn off scroll animation to fix flaky scrolling. @@ -301,6 +330,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } + if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { + return YES; + } + return [super canPerformAction:action withSender:sender]; } diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h index 7187177..748c4cc 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegate.h @@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)textInputDidChange; - (void)textInputDidChangeSelection; +- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data; @optional diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h index f1c32e6..0ce9dfe 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; - (void)selectedTextRangeWasSet; +- (void)didPaste:(NSString *)type withData:(NSString *)data; @end @@ -30,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithTextView:(UITextView *)backedTextInputView; - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)textRange; +- (void)didPaste:(NSString *)type withData:(NSString *)data; @end diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm index 82d9a79..8cc48ec 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm @@ -148,6 +148,11 @@ - (void)selectedTextRangeWasSet [self textFieldProbablyDidChangeSelection]; } +- (void)didPaste:(NSString *)type withData:(NSString *)data +{ + [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; +} + #pragma mark - Generalization - (void)textFieldProbablyDidChangeSelection @@ -330,6 +335,11 @@ - (void)skipNextTextInputDidChangeSelectionEventWithTextRange:(UITextRange *)tex _previousSelectedTextRange = textRange; } +- (void)didPaste:(NSString *)type withData:(NSString *)data +{ + [_backedTextInputView.textInputDelegate textInputDidPaste:type withData:data]; +} + #pragma mark - Generalization - (void)textViewProbablyDidChangeSelection diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h index 4804624..90b7081 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.h @@ -37,6 +37,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, nullable) RCTDirectEventBlock onChange; @property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync; @property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll; +@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; @property (nonatomic, assign) NSInteger mostRecentEventCount; @property (nonatomic, assign, readonly) NSInteger nativeEventCount; diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm index 6a2d4f8..b6e6060 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm @@ -599,6 +599,26 @@ - (void)textInputDidChangeSelection }); } +- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data +{ + if (!_onPaste) { + return; + } + + NSMutableArray *items = [NSMutableArray new]; + [items addObject:@{ + @"type" : type, + @"data" : data, + }]; + + NSDictionary *payload = @{ + @"target" : self.reactTag, + @"items" : items, + }; + + _onPaste(payload); +} + - (void)updateLocalData { [self enforceTextAttributesIfNeeded]; diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm index 47adc53..1865e0a 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputViewManager.mm @@ -70,6 +70,7 @@ @implementation RCTBaseTextInputViewManager { RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock) RCT_EXPORT_SHADOW_PROPERTY(text, NSString) RCT_EXPORT_SHADOW_PROPERTY(placeholder, NSString) diff --git a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm index 377f41e..be99a7c 100644 --- a/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm +++ b/node_modules/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm @@ -12,6 +12,10 @@ #import #import +#import +#import +#import + @implementation RCTUITextField { RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; NSDictionary *_defaultTextAttributes; @@ -180,6 +184,10 @@ - (BOOL)canPerformAction:(SEL)action withSender:(id)sender return NO; } + if (action == @selector(paste:) && [UIPasteboard generalPasteboard].hasImages) { + return YES; + } + return [super canPerformAction:action withSender:sender]; } @@ -263,7 +271,32 @@ - (void)scrollRangeToVisible:(NSRange)range - (void)paste:(id)sender { _textWasPasted = YES; - [super paste:sender]; + UIPasteboard *clipboard = [UIPasteboard generalPasteboard]; + if (clipboard.hasImages) { + for (NSItemProvider *itemProvider in clipboard.itemProviders) { + if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { + for (NSString *identifier in itemProvider.registeredTypeIdentifiers) { + if (UTTypeConformsTo((__bridge CFStringRef)identifier, kUTTypeImage)) { + NSString *MIMEType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassMIMEType); + NSString *fileExtension = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)identifier, kUTTagClassFilenameExtension); + NSString *fileName = [NSString stringWithFormat:@"%@.%@", [[NSUUID UUID] UUIDString], fileExtension]; + NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + NSData *fileData = [clipboard dataForPasteboardType:identifier]; + [fileData writeToFile:filePath atomically:YES]; + [_textInputDelegateAdapter didPaste:MIMEType withData:[fileURL absoluteString]]; + break; + } + } + break; + } + } + } else { + if (clipboard.hasStrings) { + [_textInputDelegateAdapter didPaste:@"text/plain" withData:clipboard.string]; + } + [super paste:sender]; + } } #pragma mark - Layout diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h index 914a249..0deac55 100644 --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h +++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h @@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN */ @interface RCTPullToRefreshViewComponentView : RCTViewComponentView +- (void)beginRefreshingProgrammatically; + @end NS_ASSUME_NONNULL_END diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm index 1494fd2..df643f5 100644 --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm +++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm @@ -1038,6 +1038,11 @@ - (void)_adjustForMaintainVisibleContentPosition } } ++ (BOOL)shouldBeRecycled +{ + return NO; +} + @end Class RCTScrollViewCls(void) diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm index 577bebe..bbde7fd 100644 --- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm +++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm @@ -516,6 +516,13 @@ - (void)textInputDidChangeSelection } } +- (void)textInputDidPaste:(NSString *)type withData:(NSString *)data +{ + if (_eventEmitter) { + static_cast(*_eventEmitter).onPaste(std::string([type UTF8String]), std::string([data UTF8String])); + } +} + #pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate) - (void)scrollViewDidScroll:(UIScrollView *)scrollView diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h index e9b330f..ec5f58c 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h @@ -15,5 +15,8 @@ @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) RCTDirectEventBlock onRefresh; @property (nonatomic, weak) UIScrollView *scrollView; +@property (nonatomic, copy) UIColor *customTintColor; + +- (void)forwarderBeginRefreshing; @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m index 53bfd04..ff1b1ed 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m @@ -23,6 +23,7 @@ @implementation RCTRefreshControl { UIColor *_titleColor; CGFloat _progressViewOffset; BOOL _hasMovedToWindow; + UIColor *_customTintColor; } - (instancetype)init @@ -58,6 +59,12 @@ - (void)layoutSubviews _isInitialRender = false; } +- (void)didMoveToSuperview +{ + [super didMoveToSuperview]; + [self setTintColor:_customTintColor]; +} + - (void)didMoveToWindow { [super didMoveToWindow]; @@ -221,4 +228,50 @@ - (void)refreshControlValueChanged } } +// Fix for https://github.com/facebook/react-native/issues/43388 +// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor +// is set before the refresh control gets added to the scrollview. We'll call this +// function whenever the superview changes. We'll also call it if the value of customTintColor +// changes. +- (void)setTintColor:(UIColor *)tintColor +{ + if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) { + [super setTintColor:tintColor]; + } +} + +// This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native +// libraries to perform a refresh of a scrollview and access the refresh control's onRefresh +// function. +- (void)forwarderBeginRefreshing +{ + _refreshingProgrammatically = NO; + + [self sizeToFit]; + + if (!self.scrollView) { + return; + } + + UIScrollView *scrollView = (UIScrollView *)self.scrollView; + + [UIView animateWithDuration:0.3 + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^(void) { + // Whenever we call this method, the scrollview will always be at a position of + // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl + [scrollView setContentOffset:CGPointMake(0, -65)]; + } + completion:^(__unused BOOL finished) { + [super beginRefreshing]; + [self setCurrentRefreshingState:super.refreshing]; + + if (self->_onRefresh) { + self->_onRefresh(nil); + } + } + ]; +} + @end diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m index 40aaf9c..1c60164 100644 --- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m +++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m @@ -22,11 +22,12 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL) -RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat) +RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor) + RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing) { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt new file mode 100644 index 0000000..95e45ce --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/PasteWatcher.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.textinput + +/** + * Implement this interface to be informed of paste events in the + * ReactTextEdit. This is used by the ReactTextInputManager to forward events + * from the EditText to JS. + */ +interface PasteWatcher { + fun onPaste(type: String, data: String) +} diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt index 42f6e03..86f624f 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt @@ -7,6 +7,10 @@ package com.facebook.react.views.textinput +import android.content.ClipboardManager +import android.content.ClipData +import android.content.ClipDescription +import android.content.ContentResolver import android.annotation.SuppressLint import android.content.Context import android.content.res.Configuration @@ -15,6 +19,7 @@ import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Build import android.os.Bundle import android.text.Editable @@ -128,6 +133,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private var selectionWatcher: SelectionWatcher? = null private var contentSizeWatcher: ContentSizeWatcher? = null private var scrollWatcher: ScrollWatcher? + private var pasteWatcher: PasteWatcher? private var keyListener: InternalKeyListener? = null private var detectScrollMovement = false private var onKeyPress = false @@ -162,9 +168,9 @@ public open class ReactEditText public constructor(context: Context) : AppCompat private val isSecureText: Boolean get() = - ((inputType and - (InputType.TYPE_NUMBER_VARIATION_PASSWORD or InputType.TYPE_TEXT_VARIATION_PASSWORD)) != - 0) + ((inputType and + (InputType.TYPE_NUMBER_VARIATION_PASSWORD or + InputType.TYPE_TEXT_VARIATION_PASSWORD)) != 0) public var disableFullscreenUI: Boolean get() = disableFullscreen @@ -181,13 +187,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat internal var gravityHorizontal: Int get() = - (gravity and (Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) + (gravity and + (Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) set(value) { val newValue = if (value == 0) defaultGravityHorizontal else value gravity = - ((gravity and - Gravity.HORIZONTAL_GRAVITY_MASK.inv() and - Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK.inv()) or newValue) + ((gravity and + Gravity.HORIZONTAL_GRAVITY_MASK.inv() and + Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK.inv()) or newValue) } internal var gravityVertical: Int @@ -199,9 +206,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat init { inputMethodManager = - context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager defaultGravityHorizontal = - gravity and (Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) + gravity and + (Gravity.HORIZONTAL_GRAVITY_MASK or Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) defaultGravityVertical = gravity and Gravity.VERTICAL_GRAVITY_MASK nativeEventCount = 0 isSettingTextFromJS = false @@ -219,49 +227,61 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Turn off hardware acceleration for Oreo (T40484798) // see https://issuetracker.google.com/issues/67102093 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && - Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1 + ) { setLayerType(LAYER_TYPE_SOFTWARE, null) } val editTextAccessibilityDelegate: ReactAccessibilityDelegate = - object : - ReactAccessibilityDelegate(this, this.isFocusable, this.importantForAccessibility) { - override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { - if (action == AccessibilityNodeInfo.ACTION_CLICK) { - val length = checkNotNull(text).length - if (length > 0) { - // For some reason, when you swipe to focus on a text input that already has text in - // it, it clears the selection and resets the cursor to the beginning of the input. - // Since this is not typically (ever?) what you want, let's just explicitly set the - // selection on accessibility click to undo that. - setSelection(length) + object : + ReactAccessibilityDelegate( + this, + this.isFocusable, + this.importantForAccessibility + ) { + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityNodeInfo.ACTION_CLICK) { + val length = checkNotNull(text).length + if (length > 0) { + // For some reason, when you swipe to focus on a text input that already has + // text in + // it, it clears the selection and resets the cursor to the beginning of the + // input. + // Since this is not typically (ever?) what you want, let's just explicitly set + // the + // selection on accessibility click to undo that. + setSelection(length) + } + return requestFocusProgrammatically() + } + return super.performAccessibilityAction(host, action, args) } - return requestFocusProgrammatically() } - return super.performAccessibilityAction(host, action, args) - } - } ViewCompat.setAccessibilityDelegate(this, editTextAccessibilityDelegate) val customActionModeCallback: ActionMode.Callback = - object : ActionMode.Callback { - /* - * Editor onCreateActionMode adds the cut, copy, paste, share, autofill, - * and paste as plain text items to the context menu. - */ - override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { - if (contextMenuHidden) { - return false - } - menu.removeItem(android.R.id.pasteAsPlainText) - return true - } + object : ActionMode.Callback { + /* + * Editor onCreateActionMode adds the cut, copy, paste, share, autofill, + * and paste as plain text items to the context menu. + */ + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + if (contextMenuHidden) { + return false + } + menu.removeItem(android.R.id.pasteAsPlainText) + return true + } - override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = true + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean = true - override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = false + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean = false - override fun onDestroyActionMode(mode: ActionMode) = Unit - } + override fun onDestroyActionMode(mode: ActionMode) = Unit + } customSelectionActionModeCallback = customActionModeCallback customInsertionActionModeCallback = customActionModeCallback } @@ -300,18 +320,18 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // capturing these touches or not. this.parent.requestDisallowInterceptTouchEvent(true) } - MotionEvent.ACTION_MOVE -> - if (detectScrollMovement) { - if (!canScrollVertically(-1) && - !canScrollVertically(1) && - !canScrollHorizontally(-1) && - !canScrollHorizontally(1)) { - // We cannot scroll, let parent views take care of these touches. - this.parent.requestDisallowInterceptTouchEvent(false) - } - detectScrollMovement = false - } + if (detectScrollMovement) { + if (!canScrollVertically(-1) && + !canScrollVertically(1) && + !canScrollHorizontally(-1) && + !canScrollHorizontally(1) + ) { + // We cannot scroll, let parent views take care of these touches. + this.parent.requestDisallowInterceptTouchEvent(false) + } + detectScrollMovement = false + } } return super.onTouchEvent(ev) } @@ -341,8 +361,12 @@ public open class ReactEditText public constructor(context: Context) : AppCompat var inputConnection = super.onCreateInputConnection(outAttrs) if (inputConnection != null && onKeyPress) { inputConnection = - ReactEditTextInputConnectionWrapper( - inputConnection, reactContext, this, checkNotNull(eventDispatcher)) + ReactEditTextInputConnectionWrapper( + inputConnection, + reactContext, + this, + checkNotNull(eventDispatcher) + ) } if (isMultiline && (shouldBlurOnReturn() || shouldSubmitOnReturn())) { @@ -356,9 +380,40 @@ public open class ReactEditText public constructor(context: Context) : AppCompat * Called when a context menu option for the text view is selected. * React Native replaces copy (as rich text) with copy as plain text. */ - override fun onTextContextMenuItem(id: Int): Boolean = - super.onTextContextMenuItem( - if (id == android.R.id.paste) android.R.id.pasteAsPlainText else id) + override fun onTextContextMenuItem(id: Int): Boolean { + var newId = id + if (id == android.R.id.paste || id == android.R.id.pasteAsPlainText) { + newId = android.R.id.pasteAsPlainText + pasteWatcher?.let { watcher -> + val clipboardManager = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = clipboardManager.primaryClip + + var type: String? = null + var data: String? = null + + if (clipData != null && clipData.itemCount > 0) { + val desc = clipData.description + if (desc.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) { + type = ClipDescription.MIMETYPE_TEXT_PLAIN + data = clipData.getItemAt(0).text?.toString() + } else { + val itemUri = clipData.getItemAt(0).uri + if (itemUri != null) { + val cr = getReactContext(this).contentResolver + type = cr.getType(itemUri) + data = itemUri.toString() + } + } + } + + if (type != null && data != null) { + watcher.onPaste(type, data) + } + } + } + return super.onTextContextMenuItem(newId) + } internal fun clearFocusAndMaybeRefocus() { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P || !isInTouchMode) { @@ -421,6 +476,10 @@ public open class ReactEditText public constructor(context: Context) : AppCompat this.scrollWatcher = scrollWatcher } + public fun setPasteWatcher(pasteWatcher: PasteWatcher?) { + this.pasteWatcher = pasteWatcher + } + /** * Attempt to set a selection or fail silently. Intentionally meant to handle bad inputs. * EventCounter is the same one used as with text. @@ -492,11 +551,11 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Default shouldBlur val shouldBlur = - if (submitBehavior == null) { - !isMultiline - } else { - submitBehavior == "blurAndSubmit" - } + if (submitBehavior == null) { + !isMultiline + } else { + submitBehavior == "blurAndSubmit" + } return shouldBlur } @@ -506,11 +565,11 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Default shouldSubmit val shouldSubmit = - if (submitBehavior == null) { - !isMultiline - } else { - submitBehavior == "submit" || submitBehavior == "blurAndSubmit" - } + if (submitBehavior == null) { + !isMultiline + } else { + submitBehavior == "submit" || submitBehavior == "blurAndSubmit" + } return shouldSubmit } @@ -600,14 +659,15 @@ public open class ReactEditText public constructor(context: Context) : AppCompat // Match behavior of CustomStyleSpan and enable SUBPIXEL_TEXT_FLAG when setting anything // nonstandard paintFlags = - if (fontStyle != ReactConstants.UNSET || - fontWeight != ReactConstants.UNSET || - fontFamily != null || - fontFeatureSettings != null) { - paintFlags or Paint.SUBPIXEL_TEXT_FLAG - } else { - paintFlags and (Paint.SUBPIXEL_TEXT_FLAG.inv()) - } + if (fontStyle != ReactConstants.UNSET || + fontWeight != ReactConstants.UNSET || + fontFamily != null || + fontFeatureSettings != null + ) { + paintFlags or Paint.SUBPIXEL_TEXT_FLAG + } else { + paintFlags and (Paint.SUBPIXEL_TEXT_FLAG.inv()) + } } public fun requestFocusFromJS() { @@ -642,8 +702,14 @@ public open class ReactEditText public constructor(context: Context) : AppCompat if (DEBUG_MODE) { FLog.e( - TAG, - ("maybeSetText[" + id + "]: current text: " + text + " update: " + reactTextUpdate.text)) + TAG, + ("maybeSetText[" + + id + + "]: current text: " + + text + + " update: " + + reactTextUpdate.text) + ) } // The current text gets replaced with the text received from JS. However, the spans on the @@ -655,8 +721,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat manageSpans(spannableStringBuilder) stripStyleEquivalentSpans(spannableStringBuilder) - @Suppress("DEPRECATION") - containsImages = reactTextUpdate.containsImages() + @Suppress("DEPRECATION") containsImages = reactTextUpdate.containsImages() // When we update text, we trigger onChangeText code that will // try to update state if the wrapper is available. Temporarily disable @@ -695,7 +760,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat val span = spans[spanIdx] val spanFlags = text.getSpanFlags(span) val isExclusiveExclusive = - (spanFlags and Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + (spanFlags and Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == Spanned.SPAN_EXCLUSIVE_EXCLUSIVE // Remove all styling spans we might have previously set if (span is ReactSpan) { @@ -751,16 +816,16 @@ public open class ReactEditText public constructor(context: Context) : AppCompat stripSpansOfKind(sb, CustomStyleSpan::class.java) { span: CustomStyleSpan -> span.style == fontStyle && - span.fontFamily == fontFamily && - span.weight == fontWeight && - span.fontFeatureSettings == fontFeatureSettings + span.fontFamily == fontFamily && + span.weight == fontWeight && + span.fontFeatureSettings == fontFeatureSettings } } private fun stripSpansOfKind( - sb: SpannableStringBuilder, - clazz: Class, - shouldStrip: Predicate + sb: SpannableStringBuilder, + clazz: Class, + shouldStrip: Predicate ) { val spans = sb.getSpans(0, sb.length, clazz) @@ -783,15 +848,27 @@ public open class ReactEditText public constructor(context: Context) : AppCompat spanFlags = spanFlags or Spannable.SPAN_PRIORITY workingText.setSpan( - ReactAbsoluteSizeSpan(textAttributes.effectiveFontSize), 0, workingText.length, spanFlags) + ReactAbsoluteSizeSpan(textAttributes.effectiveFontSize), + 0, + workingText.length, + spanFlags + ) workingText.setSpan( - ReactForegroundColorSpan(currentTextColor), 0, workingText.length, spanFlags) + ReactForegroundColorSpan(currentTextColor), + 0, + workingText.length, + spanFlags + ) val backgroundColor = getBackgroundColor(this) if (backgroundColor != null && backgroundColor != Color.TRANSPARENT) { workingText.setSpan( - ReactBackgroundColorSpan(backgroundColor), 0, workingText.length, spanFlags) + ReactBackgroundColorSpan(backgroundColor), + 0, + workingText.length, + spanFlags + ) } if ((paintFlags and Paint.STRIKE_THRU_TEXT_FLAG) != 0) { @@ -805,18 +882,30 @@ public open class ReactEditText public constructor(context: Context) : AppCompat val effectiveLetterSpacing = textAttributes.effectiveLetterSpacing if (!effectiveLetterSpacing.isNaN()) { workingText.setSpan( - CustomLetterSpacingSpan(effectiveLetterSpacing), 0, workingText.length, spanFlags) + CustomLetterSpacingSpan(effectiveLetterSpacing), + 0, + workingText.length, + spanFlags + ) } if (fontStyle != ReactConstants.UNSET || - fontWeight != ReactConstants.UNSET || - fontFamily != null || - fontFeatureSettings != null) { + fontWeight != ReactConstants.UNSET || + fontFamily != null || + fontFeatureSettings != null + ) { workingText.setSpan( - CustomStyleSpan(fontStyle, fontWeight, fontFeatureSettings, fontFamily, context.assets), - 0, - workingText.length, - spanFlags) + CustomStyleSpan( + fontStyle, + fontWeight, + fontFeatureSettings, + fontFamily, + context.assets + ), + 0, + workingText.length, + spanFlags + ) } val lineHeight = textAttributes.effectiveLineHeight @@ -870,11 +959,11 @@ public open class ReactEditText public constructor(context: Context) : AppCompat } imeOptions = - if (disableFullscreen) { - returnKeyFlag or EditorInfo.IME_FLAG_NO_FULLSCREEN - } else { - returnKeyFlag - } + if (disableFullscreen) { + returnKeyFlag or EditorInfo.IME_FLAG_NO_FULLSCREEN + } else { + returnKeyFlag + } } override fun verifyDrawable(drawable: Drawable): Boolean { @@ -929,7 +1018,8 @@ public open class ReactEditText public constructor(context: Context) : AppCompat super.onConfigurationChanged(newConfig) if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture() && - ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout()) { + ReactNativeFeatureFlags.enableFontScaleChangesUpdatingLayout() + ) { applyTextAttributes() } } @@ -987,7 +1077,7 @@ public open class ReactEditText public constructor(context: Context) : AppCompat } public fun getBorderColor(position: Int): Int = - getBorderColor(this, LogicalEdge.entries[position]) ?: Color.TRANSPARENT + getBorderColor(this, LogicalEdge.entries[position]) ?: Color.TRANSPARENT public fun setBorderRadius(borderRadius: Float) { setBorderRadius(borderRadius, BorderRadiusProp.BORDER_RADIUS.ordinal) @@ -995,11 +1085,11 @@ public open class ReactEditText public constructor(context: Context) : AppCompat public fun setBorderRadius(borderRadius: Float, position: Int) { val radius = - if (borderRadius.isNaN()) { - null - } else { - LengthPercentage(toDIPFromPixel(borderRadius), LengthPercentageType.POINT) - } + if (borderRadius.isNaN()) { + null + } else { + LengthPercentage(toDIPFromPixel(borderRadius), LengthPercentageType.POINT) + } setBorderRadius(this, BorderRadiusProp.entries[position], radius) } @@ -1134,10 +1224,11 @@ public open class ReactEditText public constructor(context: Context) : AppCompat addSpansFromStyleAttributes(sb) sb.setSpan( - ReactTextPaintHolderSpan(TextPaint(paint)), - 0, - sb.length, - Spannable.SPAN_INCLUSIVE_INCLUSIVE) + ReactTextPaintHolderSpan(TextPaint(paint)), + 0, + sb.length, + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ) TextLayoutManager.setCachedSpannableForTag(id, sb) } @@ -1227,6 +1318,48 @@ public open class ReactEditText public constructor(context: Context) : AppCompat _inputType = inputType } + /* + * All overrides of key handling defer to the underlying KeyListener which is shared by all + * ReactEditText instances. It will basically allow any/all keyboard input whether from + * physical keyboard or from soft input. + */ + override fun onKeyDown(view: View, text: Editable, keyCode: Int, event: KeyEvent): Boolean = + keyListener.onKeyDown(view, text, keyCode, event) + + override fun onKeyUp(view: View, text: Editable, keyCode: Int, event: KeyEvent): Boolean = + keyListener.onKeyUp(view, text, keyCode, event) + + override fun onKeyOther(view: View, text: Editable, event: KeyEvent): Boolean = + keyListener.onKeyOther(view, text, event) + + override fun clearMetaKeyState(view: View, content: Editable, states: Int) { + keyListener.clearMetaKeyState(view, content, states) + } + } + + public companion object { + public val DEBUG_MODE: Boolean = ReactBuildConfig.DEBUG && false + + private val keyListener: KeyListener = QwertyKeyListener.getInstanceForFullKeyboard() + + private fun sameTextForSpan( + oldText: Editable, + newText: SpannableStringBuilder, + start: Int, + end: Int + ): Boolean { + if (start > newText.length || end > newText.length) { + return false + } + for (charIdx in start ..< end) { + if (oldText[charIdx] != newText[charIdx]) { + return false + } + } + return true + } + } +} /* * All overrides of key handling defer to the underlying KeyListener which is shared by all * ReactEditText instances. It will basically allow any/all keyboard input whether from diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt index 42c13a1..597770b 100644 --- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt @@ -132,7 +132,11 @@ public open class ReactTextInputManager public constructor() : val baseEventTypeConstants = super.getExportedCustomDirectEventTypeConstants() val eventTypeConstants = baseEventTypeConstants ?: mutableMapOf() eventTypeConstants.putAll( - mapOf(getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"))) + mapOf( + getJSEventName(ScrollEventType.SCROLL) to mapOf("registrationName" to "onScroll"), + "topPaste" to mapOf("registrationName" to "onPaste"), + ) + ) return eventTypeConstants } @@ -327,6 +331,15 @@ public open class ReactTextInputManager public constructor() : } } + @ReactProp(name = "onPaste", defaultBoolean = false) + fun setOnPaste(view: ReactEditText, onPaste: Boolean) { + if (onPaste) { + view.setPasteWatcher(ReactTextPasteWatcher(view)) + } else { + view.setPasteWatcher(null) + } + } + @ReactProp(name = "onKeyPress", defaultBoolean = false) public fun setOnKeyPress(view: ReactEditText, onKeyPress: Boolean) { view.setOnKeyPress(onKeyPress) diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt new file mode 100644 index 0000000..91396dc --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputPasteEvent.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.textinput + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.common.ViewUtil +import com.facebook.react.uimanager.events.Event + +/** + * Event emitted by ReactEditText when clipboard content is pasted. + */ +internal class ReactTextInputPasteEvent( + surfaceId: Int, + viewId: Int, + private val type: String, + private val data: String +) : Event(surfaceId, viewId) { + + @Deprecated("Use the constructor with surfaceId.") + constructor(viewId: Int, type: String, data: String) : + this(ViewUtil.NO_SURFACE_ID, viewId, type, data) + + override fun getEventName(): String = EVENT_NAME + + override fun canCoalesce(): Boolean = false + + override fun getEventData(): WritableMap? { + val eventData: WritableMap = Arguments.createMap() + + val items: WritableArray = Arguments.createArray() + val item: WritableMap = Arguments.createMap() + item.putString("type", type) + item.putString("data", data) + items.pushMap(item) + + eventData.putArray("items", items) + + return eventData + } + + private companion object { + private const val EVENT_NAME = "topPaste" + } +} diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextPasteWatcher.kt b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextPasteWatcher.kt new file mode 100644 index 0000000..5eb9647 --- /dev/null +++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextPasteWatcher.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.textinput + +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.events.EventDispatcher + +/** + * Dispatches a paste event from a ReactEditText to JS. + */ +internal class ReactTextPasteWatcher(private val editText: ReactEditText) : PasteWatcher { + private val eventDispatcher: EventDispatcher? + private val surfaceId: Int + + init { + val reactContext = UIManagerHelper.getReactContext(editText) + eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, editText.id) + surfaceId = UIManagerHelper.getSurfaceId(reactContext) + } + + override fun onPaste(type: String, data: String) { + eventDispatcher?.dispatchEvent( + ReactTextInputPasteEvent(surfaceId, editText.id, type, data) + ) + } +} diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp index a9bc219..7ebec12 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp +++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.cpp @@ -177,6 +177,19 @@ void TextInputEventEmitter::onScroll(const Metrics& textInputMetrics) const { }); } +void TextInputEventEmitter::onPaste(const std::string& type, const std::string& data) const { + dispatchEvent("onPaste", [type, data](jsi::Runtime& runtime) { + auto payload = jsi::Object(runtime); + auto items = jsi::Array(runtime, 1); + auto item = jsi::Object(runtime); + item.setProperty(runtime, "type", type); + item.setProperty(runtime, "data", data); + items.setValueAtIndex(runtime, 0, item); + payload.setProperty(runtime, "items", items); + return payload; + }); +} + void TextInputEventEmitter::dispatchTextInputEvent( const std::string& name, const Metrics& textInputMetrics, diff --git a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h index dbce575..5b03581 100644 --- a/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h +++ b/node_modules/react-native/ReactCommon/react/renderer/components/textinput/TextInputEventEmitter.h @@ -44,6 +44,7 @@ class TextInputEventEmitter : public ViewEventEmitter { void onSubmitEditing(const Metrics& textInputMetrics) const; void onKeyPress(const KeyPressMetrics& keyPressMetrics) const; void onScroll(const Metrics& textInputMetrics) const; + void onPaste(const std::string& type, const std::string& data) const; private: void dispatchTextInputEvent(