diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1-iOS10@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1-iOS10@2x.png
index 367040097cd..c463ab9e5c7 100644
Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1-iOS10@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1-iOS10@2x.png differ
diff --git a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1@2x.png b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1@2x.png
index cc42c3edc7a..2d3ff3048f9 100644
Binary files a/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1@2x.png and b/Examples/UIExplorer/UIExplorerIntegrationTests/ReferenceImages/Examples-UIExplorer-js-UIExplorerApp.ios/testTextExample_1@2x.png differ
diff --git a/Examples/UIExplorer/js/TextExample.ios.js b/Examples/UIExplorer/js/TextExample.ios.js
index 461641b64c1..752258fc9ff 100644
--- a/Examples/UIExplorer/js/TextExample.ios.js
+++ b/Examples/UIExplorer/js/TextExample.ios.js
@@ -276,6 +276,17 @@ exports.examples = [
);
},
+}, {
+ title: 'Selectable',
+ render: function() {
+ return (
+
+
+ This text is selectable if you click-and-hold.
+
+
+ );
+ },
}, {
title: 'Text Decoration',
render: function() {
diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h
index 2c8944221e0..94e82db2d4a 100644
--- a/Libraries/Text/RCTShadowText.h
+++ b/Libraries/Text/RCTShadowText.h
@@ -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;
diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m
index 99ed6703edd..312ae35c30f 100644
--- a/Libraries/Text/RCTShadowText.m
+++ b/Libraries/Text/RCTShadowText.m
@@ -115,10 +115,12 @@ static CSSSize RCTMeasure(CSSNodeRef node, float width, CSSMeasureMode widthMode
NSNumber *parentTag = [[self reactSuperview] reactTag];
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSSMeasureModeExactly];
CGRect textFrame = [self calculateTextFrame:textStorage];
+ BOOL selectable = _selectable;
[applierBlocks addObject:^(NSDictionary *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
diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h
index aef97630364..cff9c5c52db 100644
--- a/Libraries/Text/RCTText.h
+++ b/Libraries/Text/RCTText.h
@@ -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
diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m
index 427c9ac689a..c14b4f1176b 100644
--- a/Libraries/Text/RCTText.m
+++ b/Libraries/Text/RCTText.m
@@ -9,6 +9,8 @@
#import "RCTText.h"
+#import
+
#import "RCTShadowText.h"
#import "RCTUtils.h"
#import "UIView+React.h"
@@ -28,6 +30,7 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
{
NSTextStorage *_textStorage;
CAShapeLayer *_highlightLayer;
+ UILongPressGestureRecognizer *_longPressGestureRecognizer;
}
- (instancetype)initWithFrame:(CGRect)frame
@@ -51,6 +54,22 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
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.
@@ -177,4 +196,72 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
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
diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m
index 0f47dfd2555..6e070049e67 100644
--- a/Libraries/Text/RCTTextManager.m
+++ b/Libraries/Text/RCTTextManager.m
@@ -84,6 +84,7 @@ RCT_EXPORT_SHADOW_PROPERTY(textShadowRadius, CGFloat)
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 *)shadowViewRegistry
{
diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js
index 656af10cca7..1ecde22773c 100644
--- a/Libraries/Text/Text.js
+++ b/Libraries/Text/Text.js
@@ -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,
/**