@@ -50,12 +50,21 @@ @interface RCTParagraphTextView : UIView
5050
5151@end
5252
53+ #if !TARGET_OS_TV
54+ @interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>
55+
56+ @property (nonatomic , nullable ) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE (ios(16.0 ));
57+
58+ @end
59+ #else
5360@interface RCTParagraphComponentView ()
5461@end
62+ #endif
5563
5664@implementation RCTParagraphComponentView {
5765 ParagraphAttributes _paragraphAttributes;
5866 RCTParagraphComponentAccessibilityProvider *_accessibilityProvider;
67+ UILongPressGestureRecognizer *_longPressGestureRecognizer;
5968 RCTParagraphTextView *_textView;
6069 CGRect _textLayoutFrame;
6170#if !TARGET_OS_TV
@@ -186,7 +195,12 @@ - (void)prepareForRecycle
186195 _textView.state = nullptr ;
187196 _accessibilityProvider = nil ;
188197#if !TARGET_OS_TV
189- [self disableContextMenu ];
198+ // Recycling removes only the selection text view. The long press stays,
199+ // because the view keeps its `_props`: when the next paragraph is selectable
200+ // too, `updateProps:` sees no change and does not add the long press again.
201+ if (ReactNativeFeatureFlags::enableIOSPartialTextSelection ()) {
202+ [self disableTextSelection ];
203+ }
190204#endif
191205}
192206
@@ -217,7 +231,7 @@ - (void)_updateTextViewFrame
217231
218232#if !TARGET_OS_TV
219233 const auto ¶graphProps = static_cast <const ParagraphProps &>(*_props);
220- if (paragraphProps.isSelectable ) {
234+ if (paragraphProps.isSelectable && ReactNativeFeatureFlags::enableIOSPartialTextSelection () ) {
221235 // `drawingFrame` is the frame `RCTParagraphTextView` draws the glyphs into,
222236 // compression adjustment included. The selection must use the same frame,
223237 // or the selection rects sit away from the glyphs they select.
@@ -370,18 +384,120 @@ - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
370384#pragma mark - Context Menu
371385
372386#if !TARGET_OS_TV
387+ - (void )enableContextMenu
388+ {
389+ if (ReactNativeFeatureFlags::enableIOSPartialTextSelection ()) {
390+ [self enableTextSelection ];
391+ return ;
392+ }
393+
394+ _longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc ] initWithTarget: self
395+ action: @selector (handleLongPress: )];
396+
397+ if (@available (iOS 16.0 , *)) {
398+ _editMenuInteraction = [[UIEditMenuInteraction alloc ] initWithDelegate: self ];
399+ [self addInteraction: _editMenuInteraction];
400+ }
401+ [self addGestureRecognizer: _longPressGestureRecognizer];
402+ }
403+
404+ - (void )disableContextMenu
405+ {
406+ if (ReactNativeFeatureFlags::enableIOSPartialTextSelection ()) {
407+ [self disableTextSelection ];
408+ return ;
409+ }
410+
411+ [self removeGestureRecognizer: _longPressGestureRecognizer];
412+ if (@available (iOS 16.0 , *)) {
413+ [self removeInteraction: _editMenuInteraction];
414+ _editMenuInteraction = nil ;
415+ }
416+ _longPressGestureRecognizer = nil ;
417+ }
418+
419+ - (void )handleLongPress : (UILongPressGestureRecognizer *)gesture
420+ {
421+ if (@available (iOS 16.0 , macCatalyst 16.0 , *)) {
422+ CGPoint location = [gesture locationInView: self ];
423+ UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier: nil sourcePoint: location];
424+ if (_editMenuInteraction) {
425+ [_editMenuInteraction presentEditMenuWithConfiguration: config];
426+ }
427+ } else {
428+ UIMenuController *menuController = [UIMenuController sharedMenuController ];
429+
430+ if (menuController.isMenuVisible ) {
431+ return ;
432+ }
433+
434+ [menuController showMenuFromView: self rect: self .bounds];
435+ }
436+ }
437+
438+ - (BOOL )canBecomeFirstResponder
439+ {
440+ // With partial selection, `_selectableTextView` is the responder that owns the selection.
441+ if (ReactNativeFeatureFlags::enableIOSPartialTextSelection ()) {
442+ return NO ;
443+ }
444+
445+ const auto ¶graphProps = static_cast <const ParagraphProps &>(*_props);
446+ return paragraphProps.isSelectable ;
447+ }
448+
449+ - (BOOL )canPerformAction : (SEL )action withSender : (id )sender
450+ {
451+ // With partial selection, `_selectableTextView` copies the selected range.
452+ // This class implements `copy:` for the long-press menu, so the inherited
453+ // answer for `copy:` is YES and would make the paragraph copy all its text.
454+ if (ReactNativeFeatureFlags::enableIOSPartialTextSelection ()) {
455+ return action != @selector (copy: ) && [super canPerformAction: action withSender: sender];
456+ }
457+
458+ const auto ¶graphProps = static_cast <const ParagraphProps &>(*_props);
459+
460+ if (paragraphProps.isSelectable && action == @selector (copy: )) {
461+ return YES ;
462+ }
463+
464+ return [self .nextResponder canPerformAction: action withSender: sender];
465+ }
466+
467+ - (void )copy : (id )sender
468+ {
469+ NSAttributedString *attributedText = self.attributedText ;
470+
471+ NSMutableDictionary *item = [NSMutableDictionary new ];
472+
473+ NSData *rtf = [attributedText dataFromRange: NSMakeRange (0 , attributedText.length)
474+ documentAttributes: @{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType }
475+ error: nil ];
476+
477+ if (rtf) {
478+ [item setObject: rtf forKey: (id )kUTTypeFlatRTFD ];
479+ }
480+
481+ [item setObject: attributedText.string forKey: (id )kUTTypeUTF8PlainText ];
482+
483+ UIPasteboard *pasteboard = [UIPasteboard generalPasteboard ];
484+ pasteboard.items = @[ item ];
485+ }
486+
487+ #pragma mark - Text Selection
488+
373489/*
374490 * Selection is provided by a `UITextView` laid out with the paragraph's own
375491 * TextKit stack, which gives the platform behaviour users expect: long press to
376492 * select a word, drag handles to extend the range and an edit menu that copies
377493 * only what is selected.
378494 */
379- - (void )enableContextMenu
495+ - (void )enableTextSelection
380496{
381497 _selectableTextViewNeedsUpdate = YES ;
382498}
383499
384- - (void )disableContextMenu
500+ - (void )disableTextSelection
385501{
386502 _selectableTextViewNeedsUpdate = NO ;
387503 [self removeSelectableTextView ];
@@ -445,13 +561,6 @@ - (void)updateSelectableTextViewWithDrawingFrame:(CGRect)drawingFrame
445561 _selectableTextView.frame = drawingFrame;
446562 _selectableTextView.sourceAttributedText = attributedText;
447563}
448-
449- - (BOOL )canBecomeFirstResponder
450- {
451- // While selectable, `_selectableTextView` is the responder that owns the selection.
452- return NO ;
453- }
454-
455564#else
456565- (void )enableContextMenu
457566{
0 commit comments