/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTParagraphComponentView.h" #import "RCTParagraphComponentAccessibilityProvider.h" #import #import #import #import #import #import #import #import #import #import #import "RCTConversions.h" #import "RCTFabricComponentsPlugins.h" using namespace facebook::react; @implementation RCTParagraphComponentView { ParagraphShadowNode::ConcreteState::Shared _state; ParagraphAttributes _paragraphAttributes; RCTParagraphComponentAccessibilityProvider *_accessibilityProvider; UILongPressGestureRecognizer *_longPressGestureRecognizer; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } return self; } - (NSString *)description { NSString *superDescription = [super description]; // Cutting the last `>` character. if (superDescription.length > 0 && [superDescription characterAtIndex:superDescription.length - 1] == '>') { superDescription = [superDescription substringToIndex:superDescription.length - 1]; } return [NSString stringWithFormat:@"%@; attributedText = %@>", superDescription, self.attributedText]; } - (NSAttributedString *_Nullable)attributedText { if (!_state) { return nil; } return RCTNSAttributedStringFromAttributedString(_state->getData().attributedString); } #pragma mark - RCTComponentViewProtocol + (ComponentDescriptorProvider)componentDescriptorProvider { return concreteComponentDescriptorProvider(); } + (std::vector)supplementalComponentDescriptorProviders { return { concreteComponentDescriptorProvider(), concreteComponentDescriptorProvider()}; } - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps { auto const &oldParagraphProps = *std::static_pointer_cast(_props); auto const &newParagraphProps = *std::static_pointer_cast(props); _paragraphAttributes = newParagraphProps.paragraphAttributes; if (newParagraphProps.isSelectable != oldParagraphProps.isSelectable) { if (newParagraphProps.isSelectable) { [self enableContextMenu]; } else { [self disableContextMenu]; } } [super updateProps:props oldProps:oldProps]; } - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState { _state = std::static_pointer_cast(state); [self setNeedsDisplay]; } - (void)prepareForRecycle { [super prepareForRecycle]; _state.reset(); _accessibilityProvider = nil; } - (void)drawRect:(CGRect)rect { if (!_state) { return; } auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); auto nsTextStorage = _state->getData().paragraphLayoutManager.getHostTextStorage(); RCTTextLayoutManager *nativeTextLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); [nativeTextLayoutManager drawAttributedString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame textStorage:unwrapManagedObject(nsTextStorage)]; } #pragma mark - Accessibility - (NSString *)accessibilityLabel { return self.attributedText.string; } - (BOOL)isAccessibilityElement { // All accessibility functionality of the component is implemented in `accessibilityElements` method below. // Hence to avoid calling all other methods from `UIAccessibilityContainer` protocol (most of them have default // implementations), we return here `NO`. return NO; } - (NSArray *)accessibilityElements { auto const ¶graphProps = *std::static_pointer_cast(_props); // If the component is not `accessible`, we return an empty array. // We do this because logically all nested components represent the content of the component; // in other words, all nested components individually have no sense without the . if (!_state || !paragraphProps.accessible) { return [NSArray new]; } auto &data = _state->getData(); if (![_accessibilityProvider isUpToDate:data.attributedString]) { auto textLayoutManager = data.paragraphLayoutManager.getTextLayoutManager(); RCTTextLayoutManager *nativeTextLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); _accessibilityProvider = [[RCTParagraphComponentAccessibilityProvider alloc] initWithString:data.attributedString layoutManager:nativeTextLayoutManager paragraphAttributes:data.paragraphAttributes frame:frame view:self]; } return _accessibilityProvider.accessibilityElements; } - (UIAccessibilityTraits)accessibilityTraits { return [super accessibilityTraits] | UIAccessibilityTraitStaticText; } #pragma mark - RCTTouchableComponentViewProtocol - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { if (!_state) { return _eventEmitter; } auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager(); RCTTextLayoutManager *nativeTextLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager()); CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); auto eventEmitter = [nativeTextLayoutManager getEventEmitterWithAttributeString:_state->getData().attributedString paragraphAttributes:_paragraphAttributes frame:frame atPoint:point]; if (!eventEmitter) { return _eventEmitter; } assert(std::dynamic_pointer_cast(eventEmitter)); return std::static_pointer_cast(eventEmitter); } #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 { // TODO: Adopt showMenuFromRect (necessary for UIKitForMac) #if !TARGET_OS_UIKITFORMAC 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 { auto const ¶graphProps = *std::static_pointer_cast(_props); return paragraphProps.isSelectable; } - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { auto const ¶graphProps = *std::static_pointer_cast(_props); if (paragraphProps.isSelectable && action == @selector(copy:)) { return YES; } return [self.nextResponder canPerformAction:action withSender:sender]; } - (void)copy:(id)sender { NSAttributedString *attributedText = self.attributedText; NSMutableDictionary *item = [NSMutableDictionary new]; NSData *rtf = [attributedText dataFromRange:NSMakeRange(0, attributedText.length) documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType} error:nil]; if (rtf) { [item setObject:rtf forKey:(id)kUTTypeFlatRTFD]; } [item setObject:attributedText.string forKey:(id)kUTTypeUTF8PlainText]; UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; pasteboard.items = @[ item ]; } @end Class RCTParagraphCls(void) { return RCTParagraphComponentView.class; }