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, /**