Skip to content

Commit

Permalink
Added support of <Text>'s selectable attribute on iOS
Browse files Browse the repository at this point in the history
Reviewed By: mmmulani

Differential Revision: D4187562

fbshipit-source-id: 131ece141fe8b895914043a7a01c6e042e858331
  • Loading branch information
shergin authored and Facebook Github Bot committed Nov 18, 2016
1 parent 2a2ba52 commit 5d03ff8
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 3 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 11 additions & 0 deletions Examples/UIExplorer/js/TextExample.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,17 @@ exports.examples = [
</View>
);
},
}, {
title: 'Selectable',
render: function() {
return (
<View>
<Text selectable={true}>
This text is <Text style={{fontWeight: 'bold'}}>selectable</Text> if you click-and-hold.
</Text>
</View>
);
},
}, {
title: 'Text Decoration',
render: function() {
Expand Down
1 change: 1 addition & 0 deletions Libraries/Text/RCTShadowText.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, strong) UIColor *textShadowColor;
@property (nonatomic, assign) BOOL adjustsFontSizeToFit;
@property (nonatomic, assign) CGFloat minimumFontScale;
@property (nonatomic, assign) BOOL selectable;

- (void)recomputeText;

Expand Down
2 changes: 2 additions & 0 deletions Libraries/Text/RCTShadowText.m
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,12 @@ - (void)contentSizeMultiplierDidChange:(NSNotification *)note
NSNumber *parentTag = [[self reactSuperview] reactTag];
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSSMeasureModeExactly];
CGRect textFrame = [self calculateTextFrame:textStorage];
BOOL selectable = _selectable;
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTText *view = (RCTText *)viewRegistry[self.reactTag];
view.textFrame = textFrame;
view.textStorage = textStorage;
view.selectable = selectable;

/**
* NOTE: this logic is included to support rich text editing inside multiline
Expand Down
2 changes: 1 addition & 1 deletion Libraries/Text/RCTText.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, strong) NSTextStorage *textStorage;
@property (nonatomic, assign) CGRect textFrame;

@property (nonatomic, assign) BOOL selectable;

@end
87 changes: 87 additions & 0 deletions Libraries/Text/RCTText.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

#import "RCTText.h"

#import <MobileCoreServices/UTCoreTypes.h>

#import "RCTShadowText.h"
#import "RCTUtils.h"
#import "UIView+React.h"
Expand All @@ -28,6 +30,7 @@ @implementation RCTText
{
NSTextStorage *_textStorage;
CAShapeLayer *_highlightLayer;
UILongPressGestureRecognizer *_longPressGestureRecognizer;
}

- (instancetype)initWithFrame:(CGRect)frame
Expand All @@ -51,6 +54,22 @@ - (NSString *)description
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
}

- (void)setSelectable:(BOOL)selectable
{
if (_selectable == selectable) {
return;
}

_selectable = selectable;

if (_selectable) {
[self enableContextMenu];
}
else {
[self disableContextMenu];
}
}

- (void)reactSetFrame:(CGRect)frame
{
// Text looks super weird if its frame is animated.
Expand Down Expand Up @@ -177,4 +196,72 @@ - (NSString *)accessibilityLabel
return _textStorage.string;
}

#pragma mark - Context Menu

- (void)enableContextMenu
{
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
[self addGestureRecognizer:_longPressGestureRecognizer];
}

- (void)disableContextMenu
{
[self removeGestureRecognizer:_longPressGestureRecognizer];
_longPressGestureRecognizer = nil;
}

- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
#if !TARGET_OS_TV
UIMenuController *menuController = [UIMenuController sharedMenuController];

if (menuController.isMenuVisible) {
return;
}

if (!self.isFirstResponder) {
[self becomeFirstResponder];
}

[menuController setTargetRect:self.bounds inView:self];
[menuController setMenuVisible:YES animated:YES];
#endif
}

- (BOOL)canBecomeFirstResponder
{
return _selectable;
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(copy:)) {
return YES;
}

return [super canPerformAction:action withSender:sender];
}

- (void)copy:(id)sender
{
#if !TARGET_OS_TV
NSAttributedString *attributedString = _textStorage;

NSMutableDictionary *item = [NSMutableDictionary new];

NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
documentAttributes:@{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
error:nil];

if (rtf) {
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
}

[item setObject:attributedString.string forKey:(id)kUTTypeUTF8PlainText];

UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.items = @[item];
#endif
}

@end
1 change: 1 addition & 0 deletions Libraries/Text/RCTTextManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ - (RCTShadowView *)shadowView
RCT_EXPORT_SHADOW_PROPERTY(textShadowColor, UIColor)
RCT_EXPORT_SHADOW_PROPERTY(adjustsFontSizeToFit, BOOL)
RCT_EXPORT_SHADOW_PROPERTY(minimumFontScale, CGFloat)
RCT_EXPORT_SHADOW_PROPERTY(selectable, BOOL)

- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry
{
Expand Down
2 changes: 0 additions & 2 deletions Libraries/Text/Text.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,6 @@ const Text = React.createClass({
onLongPress: React.PropTypes.func,
/**
* Lets the user select text, to use the native copy and paste functionality.
*
* @platform android
*/
selectable: React.PropTypes.bool,
/**
Expand Down

3 comments on commit 5d03ff8

@messense
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job!.

@mikelambert
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay! I believe this fixes https://productpains.com/post/react-native/support-selecting-text-within-text-elements-for-copypaste-etc (though someone with admin privileges will have to mark it as fixed, as I don't have that power)

@nihgwu
Copy link
Contributor

@nihgwu nihgwu commented on 5d03ff8 Dec 2, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikelambert Actually that pain hasn't been fixed, this PR only support copy the text in but you can't select a section of it

Please sign in to comment.