Skip to content

Commit

Permalink
Add TextInput paste support
Browse files Browse the repository at this point in the history
This adds an `onPaste` event for `MultilineTextInput` that is fired when a pasting into the input, and includes the same `dataTransfer` info as the `onDrop` callback which is discussed here #842
This also fixes a typo in the API name

```onPaste``` exists atm only for macOS, but we have the corresponding change to also add it rnw if that is desired
Should we also add a property ```pasteTypes``` similar to drag&drag drag(ged)Types?
  • Loading branch information
appden authored and christophpurrer committed Sep 8, 2022
1 parent aa96341 commit bceb321
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 19 deletions.
29 changes: 28 additions & 1 deletion Libraries/Components/TextInput/TextInput.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,27 @@ export type SettingChangeEvent = SyntheticEvent<
$ReadOnly<{|
enabled: boolean,
|}>,
>; // ]TODO(macOS GH#774)
>;

export type PasteEvent = SyntheticEvent<
$ReadOnly<{|
dataTransfer: {|
files: $ReadOnlyArray<{|
height: number,
size: number,
type: string,
uri: string,
width: number,
|}>,
items: $ReadOnlyArray<{|
kind: string,
type: string,
|}>,
types: $ReadOnlyArray<string>,
|},
|}>,
>;
// ]TODO(macOS GH#774)

type DataDetectorTypesType =
// iOS+macOS
Expand Down Expand Up @@ -740,6 +760,13 @@ export type Props = $ReadOnly<{|
*/
onPressOut?: ?(event: PressEvent) => mixed,

/**
* Fired when a supported element is pasted
*
* @platform macos
*/
onPaste?: (event: PasteEvent) => void, // TODO(macOS GH#774)

/**
* Callback that is called when the text input selection is changed.
* This will be called with
Expand Down
17 changes: 15 additions & 2 deletions Libraries/Text/TextInput/Multiline/RCTUITextView.m
Original file line number Diff line number Diff line change
Expand Up @@ -342,12 +342,25 @@ - (BOOL)performDragOperation:(id<NSDraggingInfo>)draggingInfo
}
return YES;
}
- (NSArray *)readablePasteboardTypes
{
NSArray *types = [super readablePasteboardTypes];
// TODO: Optionally support files/images with a prop
return [types arrayByAddingObjectsFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]];
}

#endif // ]TODO(macOS GH#774)

- (void)paste:(id)sender
{
[super paste:sender];
_textWasPasted = YES;
#if TARGET_OS_OSX // TODO(macOS GH#774)
if ([self.textInputDelegate textInputShouldHandlePaste:self]) {
#endif
[super paste:sender];
_textWasPasted = YES;
#if TARGET_OS_OSX // TODO(macOS GH#774)
}
#endif
}

// Turn off scroll animation to fix flaky scrolling.
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBackedTextInputDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)textInputDraggingExited:(id<NSDraggingInfo>)draggingInfo;
- (BOOL)textInputShouldHandleDragOperation:(id<NSDraggingInfo>)draggingInfo;
- (void)textInputDidCancel; // Handle `Escape` key press.
- (BOOL)textInputShouldHandlePaste:(id<RCTBackedTextInputViewProtocol>)sender; // Return `YES` to have the paste event handled normally. Return `NO` to disallow it and handle it yourself.
#endif // ]TODO(macOS GH#774)

@optional
Expand Down
7 changes: 6 additions & 1 deletion Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,12 @@ - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doComman
}
//paste
} else if (commandSelector == @selector(paste:)) {
_backedTextInputView.textWasPasted = YES;
id<RCTBackedTextInputDelegate> textInputDelegate = [_backedTextInputView textInputDelegate];
if (textInputDelegate != nil && ![textInputDelegate textInputShouldHandlePaste:_backedTextInputView]) {
commandHandled = YES;
} else {
_backedTextInputView.textWasPasted = YES;
}
//escape
} else if (commandSelector == @selector(cancelOperation:)) {
[textInputDelegate textInputDidCancel];
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, copy, nullable) RCTDirectEventBlock onContentSizeChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onSelectionChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onChange;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onPaste; // TODO(OSS Candidate GH#774)
@property (nonatomic, copy, nullable) RCTDirectEventBlock onChangeSync;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onTextInput;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onScroll;
Expand Down
14 changes: 14 additions & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputView.m
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,20 @@ - (void)textInputDidCancel {
- (BOOL)textInputShouldHandleKeyEvent:(NSEvent *)event {
return ![self handleKeyboardEvent:event];
}

- (BOOL)textInputShouldHandlePaste:(__unused id)sender
{
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSPasteboardType fileType = [pasteboard availableTypeFromArray:@[NSFilenamesPboardType, NSPasteboardTypePNG, NSPasteboardTypeTIFF]];

// If there's a fileType that is of interest, notify JS. Also blocks notifying JS if it's a text paste
if (_onPaste && fileType != nil) {
_onPaste([self dataTransferInfoFromPasteboard:pasteboard]);
}

// Only allow pasting text.
return fileType == nil;
}
#endif // ]TODO(macOS GH#774)

- (void)updateLocalData
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/TextInput/RCTBaseTextInputViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ @implementation RCTBaseTextInputViewManager
RCT_EXPORT_VIEW_PROPERTY(onKeyPressSync, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onChangeSync, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelectionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPaste, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onTextInput, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onScroll, RCTDirectEventBlock)

Expand Down
2 changes: 2 additions & 0 deletions React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,8 @@ + (NSArray *)CGColorArray:(id)json

if ([type isEqualToString:@"fileUrl"]) {
return @[NSFilenamesPboardType];
} else if ([type isEqualToString:@"image"]) {
return @[NSPasteboardTypePNG, NSPasteboardTypeTIFF];
}

return @[];
Expand Down
1 change: 1 addition & 0 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
- (BOOL)resignFirstResponder;

#if TARGET_OS_OSX
- (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard;
- (BOOL)handleKeyboardEvent:(NSEvent *)event;
#endif
// ]TODO(OSS Candidate ISS#2710739)
Expand Down
56 changes: 41 additions & 15 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -1527,13 +1527,9 @@ - (void)sendMouseEventWithBlock:(RCTDirectEventBlock)block
block(body);
}

- (NSDictionary*)dataTransferInfoFromPastboard:(NSPasteboard*)pastboard
- (NSDictionary*)dataTransferInfoFromPasteboard:(NSPasteboard*)pasteboard
{
if (![pastboard.types containsObject:NSFilenamesPboardType]) {
return @{};
}

NSArray *fileNames = [pastboard propertyListForType:NSFilenamesPboardType];
NSArray *fileNames = [pasteboard propertyListForType:NSFilenamesPboardType] ?: @[];
NSMutableArray *files = [[NSMutableArray alloc] initWithCapacity:fileNames.count];
NSMutableArray *items = [[NSMutableArray alloc] initWithCapacity:fileNames.count];
NSMutableArray *types = [[NSMutableArray alloc] initWithCapacity:fileNames.count];
Expand All @@ -1559,11 +1555,21 @@ - (NSDictionary*)dataTransferInfoFromPastboard:(NSPasteboard*)pastboard
BOOL success = [fileURL getResourceValue:&fileSizeValue
forKey:NSURLFileSizeKey
error:&fileSizeError];


NSNumber *width = nil;
NSNumber *height = nil;
if ([MIMETypeString hasPrefix:@"image/"]) {
NSImage *image = [[NSImage alloc] initWithContentsOfURL:fileURL];
width = @(image.size.width);
height = @(image.size.height);
}

[files addObject:@{@"name": RCTNullIfNil(fileURL.lastPathComponent),
@"type": RCTNullIfNil(MIMETypeString),
@"uri": RCTNullIfNil(fileURL.absoluteString),
@"size": success ? fileSizeValue : (id)kCFNull
@"uri": RCTNullIfNil(fileURL.path),
@"size": success ? fileSizeValue : (id)kCFNull,
@"width": RCTNullIfNil(width),
@"height": RCTNullIfNil(height)
}];

[items addObject:@{@"kind": @"file",
Expand All @@ -1573,7 +1579,27 @@ - (NSDictionary*)dataTransferInfoFromPastboard:(NSPasteboard*)pastboard
[types addObject:RCTNullIfNil(MIMETypeString)];
}
}


NSPasteboardType imageType = [pasteboard availableTypeFromArray:@[NSPasteboardTypePNG, NSPasteboardTypeTIFF]];
if (imageType && fileNames.count == 0) {
NSString *MIMETypeString = imageType == NSPasteboardTypePNG ? @"image/png" : @"image/tiff";
NSData *imageData = [pasteboard dataForType:imageType];
NSImage *image = [[NSImage alloc] initWithData:imageData];

[files addObject:@{@"type": RCTNullIfNil(MIMETypeString),
@"uri": RCTDataURL(MIMETypeString, imageData).absoluteString,
@"size": @(imageData.length),
@"width": @(image.size.width),
@"height": @(image.size.height),
}];

[items addObject:@{@"kind": @"image",
@"type": RCTNullIfNil(MIMETypeString),
}];

[types addObject:RCTNullIfNil(MIMETypeString)];
}

return @{@"dataTransfer": @{@"files": files,
@"items": items,
@"types": types}};
Expand All @@ -1598,9 +1624,9 @@ - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
[self sendMouseEventWithBlock:self.onDragEnter
locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation]
modifierFlags:0
additionalData:[self dataTransferInfoFromPastboard:pboard]];
if ([pboard.types containsObject:NSFilenamesPboardType]) {
additionalData:[self dataTransferInfoFromPasteboard:pboard]];

if ([pboard availableTypeFromArray:self.registeredDraggedTypes]) {
if (sourceDragMask & NSDragOperationLink) {
return NSDragOperationLink;
} else if (sourceDragMask & NSDragOperationCopy) {
Expand All @@ -1615,15 +1641,15 @@ - (void)draggingExited:(id<NSDraggingInfo>)sender
[self sendMouseEventWithBlock:self.onDragLeave
locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation]
modifierFlags:0
additionalData:[self dataTransferInfoFromPastboard:sender.draggingPasteboard]];
additionalData:[self dataTransferInfoFromPasteboard:sender.draggingPasteboard]];
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
[self sendMouseEventWithBlock:self.onDrop
locationInfo:[self locationInfoFromDraggingLocation:sender.draggingLocation]
modifierFlags:0
additionalData:[self dataTransferInfoFromPastboard:[sender draggingPasteboard]]];
additionalData:[self dataTransferInfoFromPasteboard:sender.draggingPasteboard]];
return YES;
}
#endif // ]TODO(macOS GH#774)
Expand Down
43 changes: 43 additions & 0 deletions packages/rn-tester/js/examples/TextInput/TextInputExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
Text,
TextInput,
View,
Image, // TODO(macOS GH#774)
Platform, // TODO(macOS GH#774)
StyleSheet,
Slider,
Expand All @@ -27,6 +28,7 @@ const {
import type {
KeyboardType,
SettingChangeEvent,
PasteEvent,
} from 'react-native/Libraries/Components/TextInput/TextInput'; // [TODO(macOS GH#774)

const TextInputSharedExamples = require('./TextInputSharedExamples.js');
Expand Down Expand Up @@ -401,6 +403,41 @@ function OnDragEnterOnDragLeaveOnDrop(): React.Node {
</>
);
}

function OnPaste(): React.Node {
const [log, setLog] = React.useState([]);
const appendLog = (line: string) => {
const limit = 3;
let newLog = log.slice(0, limit - 1);
newLog.unshift(line);
setLog(newLog);
};
const [imageUri, setImageUri] = React.useState('');
return (
<>
<TextInput
multiline={true}
style={styles.multiline}
onPaste={(e: PasteEvent) => {
appendLog(JSON.stringify(e.nativeEvent.dataTransfer.types));
setImageUri(e.nativeEvent.dataTransfer.files[0].uri);
}}
placeholder="MULTI LINE with onPaste() for PNG and TIFF images"
/>
<Text style={{height: 30}}>{log.join('\n')}</Text>
<Image
source={{uri: imageUri}}
style={{
width: 128,
height: 128,
margin: 4,
borderWidth: 1,
borderColor: 'white',
}}
/>
</>
);
}
// ]TODO(macOS GH#774)

exports.displayName = (undefined: ?string);
Expand Down Expand Up @@ -915,6 +952,12 @@ if (Platform.OS === 'macos') {
return <OnDragEnterOnDragLeaveOnDrop />;
},
},
{
title: 'onPaste - MultiLineTextInput',
render: function (): React.Node {
return <OnPaste />;
},
},
);
}
// ]TODO(macOS GH#774)

0 comments on commit bceb321

Please sign in to comment.