react-native-enriched-markdown-scaffold 1.0.2-scaffold.4 → 1.0.2-scaffold.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +12 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +2 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +12 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +2 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  7. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +2 -0
  9. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +107 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +19 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +14 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +48 -0
  13. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +14 -0
  14. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +42 -0
  15. package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +14 -0
  16. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +230 -0
  17. package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +11 -1
  18. package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +8 -1
  19. package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +8 -1
  20. package/docs/API_REFERENCE.md +58 -0
  21. package/ios/EnrichedMarkdown.mm +174 -2
  22. package/ios/EnrichedMarkdownText.mm +103 -1
  23. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  24. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  25. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  26. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +2 -0
  27. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  28. package/ios/utils/ENRMLinkChip.m +14 -14
  29. package/ios/utils/ENRMLinkLayout.m +12 -12
  30. package/ios/utils/ENRMSelectionChange.h +96 -0
  31. package/ios/utils/ENRMSelectionChange.m +204 -0
  32. package/ios/utils/EditMenuUtils+macOS.m +1 -1
  33. package/ios/utils/MarkdownAccessibilityElementBuilder.m +1 -2
  34. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  35. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  36. package/ios/views/ENRMMathContainerView.h +3 -0
  37. package/ios/views/ENRMMathContainerView.m +3 -0
  38. package/ios/views/TableContainerView.h +3 -0
  39. package/ios/views/TableContainerView.m +4 -1
  40. package/lib/module/EnrichedMarkdownNativeComponent.ts +60 -1
  41. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +60 -1
  42. package/lib/module/index.js.map +1 -1
  43. package/lib/module/jest/index.js +13 -0
  44. package/lib/module/jest/index.js.map +1 -1
  45. package/lib/module/native/EnrichedMarkdownText.js +44 -4
  46. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  47. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +53 -2
  48. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  49. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +53 -2
  50. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  51. package/lib/typescript/src/index.d.ts +2 -2
  52. package/lib/typescript/src/index.d.ts.map +1 -1
  53. package/lib/typescript/src/jest/index.d.ts +1 -1
  54. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  55. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  56. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  57. package/lib/typescript/src/types/MarkdownTextProps.d.ts +36 -3
  58. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  59. package/lib/typescript/src/types/events.d.ts +36 -0
  60. package/lib/typescript/src/types/events.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/EnrichedMarkdownNativeComponent.ts +60 -1
  63. package/src/EnrichedMarkdownTextNativeComponent.ts +60 -1
  64. package/src/index.tsx +3 -0
  65. package/src/jest/index.tsx +21 -1
  66. package/src/native/EnrichedMarkdownText.tsx +85 -9
  67. package/src/types/MarkdownTextProps.ts +43 -2
  68. package/src/types/events.ts +38 -0
@@ -19,6 +19,8 @@
19
19
  #import "ENRMMathContainerView.h"
20
20
  #endif
21
21
  #import "ENRMCodeBlockContainerView.h"
22
+ #import "ENRMLinkLayout.h"
23
+ #import "ENRMSelectionChange.h"
22
24
  #import "ENRMSpoilerCapable.h"
23
25
  #import "ENRMSpoilerOverlayView.h"
24
26
  #import "ENRMSpoilerTapUtils.h"
@@ -38,7 +40,6 @@
38
40
  #import "SegmentReconciler.h"
39
41
  #import "SegmentRenderer.h"
40
42
  #import "SegmentViewRegistry.h"
41
- #import "ENRMLinkLayout.h"
42
43
  #import "SelectionColorUtils.h"
43
44
  #import "StreamingMarkdownFilter.h"
44
45
  #import "StyleConfig.h"
@@ -128,6 +129,15 @@ static char kENRMSegmentFadeAnimatorKey;
128
129
  BOOL _emitLinkLayout;
129
130
  NSArray<NSDictionary *> *_lastLinkLayout;
130
131
 
132
+ ENRMSelectionChangeReporter *_selectionReporter;
133
+ // At most one segment holds a selection; a new one clears the previous.
134
+ __weak ENRMPlatformTextView *_selectionOwner;
135
+ // A block long-pressed while contextMenuHidden is on, reported as the selection.
136
+ __weak RCTUIView *_activeBlockView;
137
+ NSString *_activeBlockSource;
138
+ NSString *_activeBlockMarkdown;
139
+ NSString *_activeBlockText;
140
+
131
141
  NSLineBreakStrategy _lineBreakStrategy;
132
142
 
133
143
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -199,10 +209,117 @@ static char kENRMSegmentFadeAnimatorKey;
199
209
  [strongSelf renderMarkdownContent:strongSelf->_cachedMarkdown];
200
210
  }
201
211
  };
212
+
213
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
214
+ initWithSnapshot:^NSDictionary * {
215
+ EnrichedMarkdown *strongSelf = weakSelf;
216
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
217
+ }
218
+ emit:^(NSDictionary *payload) {
219
+ EnrichedMarkdown *strongSelf = weakSelf;
220
+ if (!strongSelf)
221
+ return;
222
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(strongSelf->_eventEmitter);
223
+ if (emitter)
224
+ emitter->onSelectionChange(
225
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
226
+ }];
202
227
  }
203
228
  return self;
204
229
  }
205
230
 
231
+ #pragma mark - Selection
232
+
233
+ /// The current selection (or active block) as an `onSelectionChange` payload.
234
+ /// Markdown and `whole` follow the same rules as the intercepted Copy.
235
+ - (NSDictionary *)selectionPayload
236
+ {
237
+ RCTUIView *blockView = _activeBlockView;
238
+ if (blockView != nil) {
239
+ if (blockView.superview != self)
240
+ return ENRMInactiveSelectionPayload();
241
+ return ENRMBlockSelectionPayload(_activeBlockSource, _activeBlockMarkdown, _activeBlockText, blockView.frame);
242
+ }
243
+
244
+ #if !TARGET_OS_OSX
245
+ UITextView *owner = _selectionOwner;
246
+ if (owner == nil || ![owner isDescendantOfView:self])
247
+ return ENRMInactiveSelectionPayload();
248
+ NSDictionary *selection = ENRMTextSelectionInTextView(owner, self);
249
+ if (!selection)
250
+ return ENRMInactiveSelectionPayload();
251
+
252
+ NSString *text = selection[ENRMSelectionTextKey];
253
+ // Segments only see their own text, so a selection is the whole component
254
+ // only when it spans the sole segment; the markdown is then the original.
255
+ BOOL whole = [selection[ENRMSelectionFullKey] boolValue] && _segmentViews.count == 1 && _cachedMarkdown.length > 0;
256
+ NSString *markdown =
257
+ whole ? _cachedMarkdown : extractMarkdownFromAttributedString(owner.attributedText, owner.selectedRange);
258
+ NSMutableDictionary *payload = [selection mutableCopy];
259
+ [payload removeObjectForKey:ENRMSelectionFullKey];
260
+ payload[ENRMSelectionActiveKey] = @YES;
261
+ payload[ENRMSelectionSourceKey] = @"selection";
262
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
263
+ payload[ENRMSelectionWholeKey] = @(whole);
264
+ return payload;
265
+ #else
266
+ return ENRMInactiveSelectionPayload();
267
+ #endif
268
+ }
269
+
270
+ - (void)clearActiveBlock
271
+ {
272
+ _activeBlockView = nil;
273
+ _activeBlockSource = nil;
274
+ _activeBlockMarkdown = nil;
275
+ _activeBlockText = nil;
276
+ }
277
+
278
+ - (void)clearSegmentSelection:(ENRMPlatformTextView *)textView
279
+ {
280
+ #if !TARGET_OS_OSX
281
+ textView.selectedRange = NSMakeRange(0, 0);
282
+ [textView resignFirstResponder];
283
+ #endif
284
+ }
285
+
286
+ - (void)clearSelection
287
+ {
288
+ ENRMPlatformTextView *owner = _selectionOwner;
289
+ _selectionOwner = nil;
290
+ [self clearActiveBlock];
291
+ if (owner)
292
+ [self clearSegmentSelection:owner];
293
+ [_selectionReporter flushNow];
294
+ }
295
+
296
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
297
+ {
298
+ RCTEnrichedMarkdownHandleCommand(self, commandName, args);
299
+ }
300
+
301
+ - (ENRMBlockCopyRequestBlock)blockMenuSuppressedForSource:(NSString *)source view:(RCTUIView *)view
302
+ {
303
+ __weak EnrichedMarkdown *weakSelf = self;
304
+ __weak RCTUIView *weakView = view;
305
+ return ^(NSString *markdown, NSString *text) {
306
+ EnrichedMarkdown *strongSelf = weakSelf;
307
+ RCTUIView *strongView = weakView;
308
+ if (!strongSelf || !strongView || !strongSelf->_selectionReporter.enabled)
309
+ return;
310
+ ENRMPlatformTextView *owner = strongSelf->_selectionOwner;
311
+ strongSelf->_selectionOwner = nil;
312
+ if (owner)
313
+ [strongSelf clearSegmentSelection:owner];
314
+ strongSelf->_activeBlockView = strongView;
315
+ strongSelf->_activeBlockSource = source;
316
+ strongSelf->_activeBlockMarkdown = markdown;
317
+ strongSelf->_activeBlockText = text;
318
+ [strongSelf->_selectionReporter forgetLastReported];
319
+ [strongSelf->_selectionReporter flushNow];
320
+ };
321
+ }
322
+
206
323
  - (void)configureSegmentViewRegistry
207
324
  {
208
325
  __weak EnrichedMarkdown *weakSelf = self;
@@ -839,6 +956,7 @@ static char kENRMSegmentFadeAnimatorKey;
839
956
  tableView.contextMenuHidden = _contextMenuHidden;
840
957
  tableView.menuOptions = [self blockMenuOptions];
841
958
  tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
959
+ tableView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"table" view:tableView];
842
960
 
843
961
  __weak EnrichedMarkdown *weakSelf = self;
844
962
 
@@ -881,6 +999,7 @@ static char kENRMSegmentFadeAnimatorKey;
881
999
  mathView.contextMenuHidden = _contextMenuHidden;
882
1000
  mathView.menuOptions = [self blockMenuOptions];
883
1001
  mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
1002
+ mathView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"math" view:mathView];
884
1003
  [mathView applyLatex:mathSegment.latex];
885
1004
  return mathView;
886
1005
  }
@@ -894,6 +1013,7 @@ static char kENRMSegmentFadeAnimatorKey;
894
1013
  codeBlockView.contextMenuHidden = _contextMenuHidden;
895
1014
  codeBlockView.menuOptions = [self blockMenuOptions];
896
1015
  codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
1016
+ codeBlockView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"code" view:codeBlockView];
897
1017
 
898
1018
  __weak EnrichedMarkdown *weakSelf = self;
899
1019
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -947,6 +1067,8 @@ static char kENRMSegmentFadeAnimatorKey;
947
1067
  [super layoutSubviews];
948
1068
  [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
949
1069
  [self emitLinkLayoutIfNeeded];
1070
+ // Segments may have moved or been replaced; re-read the selection (deduplicated).
1071
+ [_selectionReporter schedule];
950
1072
  }
951
1073
 
952
1074
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -988,6 +1110,8 @@ static char kENRMSegmentFadeAnimatorKey;
988
1110
  [self setNeedsLayout];
989
1111
  }
990
1112
 
1113
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
1114
+
991
1115
  for (RCTUIView *segment in _segmentViews) {
992
1116
  if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
993
1117
  EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
@@ -1185,6 +1309,9 @@ static char kENRMSegmentFadeAnimatorKey;
1185
1309
  _config = nil;
1186
1310
  _emitLinkLayout = NO;
1187
1311
  _lastLinkLayout = nil;
1312
+ _selectionReporter.enabled = NO;
1313
+ _selectionOwner = nil;
1314
+ [self clearActiveBlock];
1188
1315
  _md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
1189
1316
  _maxFontSizeMultiplier = 0;
1190
1317
  _allowTrailingMargin = NO;
@@ -1406,14 +1533,33 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1406
1533
  ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; });
1407
1534
  }
1408
1535
 
1536
+ #if !TARGET_OS_OSX
1537
+ - (void)textViewDidChangeSelection:(UITextView *)textView
1538
+ {
1539
+ if (textView.selectedRange.length > 0) {
1540
+ // Take ownership first so the previous owner's own "cleared" callback is ignored.
1541
+ ENRMPlatformTextView *previous = _selectionOwner;
1542
+ _selectionOwner = textView;
1543
+ [self clearActiveBlock];
1544
+ if (previous && previous != textView)
1545
+ previous.selectedRange = NSMakeRange(0, 0);
1546
+ [_selectionReporter schedule];
1547
+ } else if (textView == _selectionOwner) {
1548
+ [_selectionReporter selectionChanged:NO];
1549
+ }
1550
+ }
1551
+ #endif
1552
+
1409
1553
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
1410
1554
  #if !TARGET_OS_OSX
1411
1555
  - (UIMenu *)textView:(UITextView *)textView
1412
1556
  editMenuForTextInRange:(NSRange)range
1413
1557
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1414
1558
  {
1559
+ // An empty menu presents nothing; nil would make UIKit show its default menu
1560
+ // (the selection and handles stay either way).
1415
1561
  if (_contextMenuHidden) {
1416
- return nil;
1562
+ return [UIMenu menuWithChildren:@[]];
1417
1563
  }
1418
1564
 
1419
1565
  __weak EnrichedMarkdown *weakSelf = self;
@@ -1448,6 +1594,32 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1448
1594
  customActions, _selectionMenuConfig, onCopyRequest);
1449
1595
  }
1450
1596
 
1597
+ // Attachments other than images (inline math) are text, not items: without
1598
+ // this a long press on one in an unfocused text view starts UIKit's text item
1599
+ // interaction, which presents nothing and leaves the view unable to select.
1600
+ - (BOOL)textView:(UITextView *)textView
1601
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
1602
+ inRange:(NSRange)characterRange
1603
+ interaction:(UITextItemInteraction)interaction
1604
+ {
1605
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
1606
+ return YES;
1607
+ }
1608
+ // A long press selects the attachment like a word, as it does once the view is focused.
1609
+ if (interaction == UITextItemInteractionPresentActions) {
1610
+ __weak UITextView *weakTextView = textView;
1611
+ dispatch_async(dispatch_get_main_queue(), ^{
1612
+ UITextView *strongTextView = weakTextView;
1613
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1614
+ return;
1615
+ }
1616
+ [strongTextView becomeFirstResponder];
1617
+ strongTextView.selectedRange = characterRange;
1618
+ });
1619
+ }
1620
+ return NO;
1621
+ }
1622
+
1451
1623
  - (BOOL)textView:(UITextView *)textView
1452
1624
  shouldInteractWithURL:(NSURL *)URL
1453
1625
  inRange:(NSRange)characterRange
@@ -8,6 +8,7 @@
8
8
  #import "ENRMImageAttachment.h"
9
9
  #import "ENRMLinkLayout.h"
10
10
  #import "ENRMMarkdownParser.h"
11
+ #import "ENRMSelectionChange.h"
11
12
  #import "ENRMSpoilerOverlayManager.h"
12
13
  #import "ENRMSpoilerTapUtils.h"
13
14
  #import "ENRMTailFadeInAnimator.h"
@@ -26,6 +27,7 @@
26
27
  #import "MarkdownExtractor.h"
27
28
  #import "MeasurementCache.h"
28
29
  #import "ParagraphStyleUtils.h"
30
+ #import "PasteboardUtils.h"
29
31
  #import "RuntimeKeys.h"
30
32
  #import "SelectionColorUtils.h"
31
33
  #import "StylePropsUtils.h"
@@ -115,6 +117,8 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
115
117
  BOOL _emitLinkLayout;
116
118
  NSArray<NSDictionary *> *_lastLinkLayout;
117
119
 
120
+ ENRMSelectionChangeReporter *_selectionReporter;
121
+
118
122
  NSLineBreakStrategy _lineBreakStrategy;
119
123
 
120
124
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -272,11 +276,68 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
272
276
  };
273
277
 
274
278
  [self setupTextView];
279
+ [self setupSelectionReporter];
275
280
  }
276
281
 
277
282
  return self;
278
283
  }
279
284
 
285
+ - (void)setupSelectionReporter
286
+ {
287
+ __weak EnrichedMarkdownText *weakSelf = self;
288
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
289
+ initWithSnapshot:^NSDictionary * {
290
+ EnrichedMarkdownText *strongSelf = weakSelf;
291
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
292
+ }
293
+ emit:^(NSDictionary *payload) {
294
+ EnrichedMarkdownText *strongSelf = weakSelf;
295
+ if (!strongSelf)
296
+ return;
297
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(strongSelf->_eventEmitter);
298
+ if (emitter)
299
+ emitter->onSelectionChange(
300
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownTextEventEmitter::OnSelectionChange>(payload));
301
+ }];
302
+ }
303
+
304
+ /// The current selection as an `onSelectionChange` payload. Markdown and
305
+ /// `whole` follow the same rules as the intercepted Copy (`onCopyRequest`).
306
+ - (NSDictionary *)selectionPayload
307
+ {
308
+ #if !TARGET_OS_OSX
309
+ NSDictionary *selection = ENRMTextSelectionInTextView(_textView, self);
310
+ if (!selection)
311
+ return ENRMInactiveSelectionPayload();
312
+
313
+ NSString *text = selection[ENRMSelectionTextKey];
314
+ NSString *markdown = markdownForRange(_textView.attributedText, _textView.selectedRange, _cachedMarkdown);
315
+ NSMutableDictionary *payload = [selection mutableCopy];
316
+ [payload removeObjectForKey:ENRMSelectionFullKey];
317
+ payload[ENRMSelectionActiveKey] = @YES;
318
+ payload[ENRMSelectionSourceKey] = @"selection";
319
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
320
+ payload[ENRMSelectionWholeKey] = @([selection[ENRMSelectionFullKey] boolValue] && _cachedMarkdown.length > 0);
321
+ return payload;
322
+ #else
323
+ return ENRMInactiveSelectionPayload();
324
+ #endif
325
+ }
326
+
327
+ - (void)clearSelection
328
+ {
329
+ #if !TARGET_OS_OSX
330
+ _textView.selectedRange = NSMakeRange(0, 0);
331
+ [_textView resignFirstResponder];
332
+ #endif
333
+ [_selectionReporter flushNow];
334
+ }
335
+
336
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
337
+ {
338
+ RCTEnrichedMarkdownTextHandleCommand(self, commandName, args);
339
+ }
340
+
280
341
  - (void)setupTextView
281
342
  {
282
343
  #if !TARGET_OS_OSX
@@ -477,6 +538,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
477
538
  [self emitLinkLayoutIfNeeded];
478
539
  }
479
540
 
541
+ // New text drops the selection; setting attributedText doesn't call the delegate.
542
+ [_selectionReporter schedule];
543
+
480
544
  if (_textView.hidden) {
481
545
  dispatch_async(dispatch_get_main_queue(), ^{ self->_textView.hidden = NO; });
482
546
  }
@@ -539,6 +603,8 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
539
603
  [self emitLinkLayoutIfNeeded];
540
604
  }
541
605
 
606
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
607
+
542
608
  if (_textView.selectable != newViewProps.selectable) {
543
609
  _textView.selectable = newViewProps.selectable;
544
610
  }
@@ -717,6 +783,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
717
783
  _spoilerManager = nil;
718
784
  _emitLinkLayout = NO;
719
785
  _lastLinkLayout = nil;
786
+ _selectionReporter.enabled = NO;
720
787
  if (_textView != nil) {
721
788
  ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
722
789
  _textView.hidden = YES;
@@ -839,6 +906,32 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
839
906
  #pragma mark - UITextViewDelegate (Link Interaction)
840
907
 
841
908
  #if !TARGET_OS_OSX
909
+ // Attachments other than images (inline math) are text, not items: without
910
+ // this a long press on one in an unfocused text view starts UIKit's text item
911
+ // interaction, which presents nothing and leaves the view unable to select.
912
+ - (BOOL)textView:(UITextView *)textView
913
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
914
+ inRange:(NSRange)characterRange
915
+ interaction:(UITextItemInteraction)interaction
916
+ {
917
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
918
+ return YES;
919
+ }
920
+ // A long press selects the attachment like a word, as it does once the view is focused.
921
+ if (interaction == UITextItemInteractionPresentActions) {
922
+ __weak UITextView *weakTextView = textView;
923
+ dispatch_async(dispatch_get_main_queue(), ^{
924
+ UITextView *strongTextView = weakTextView;
925
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
926
+ return;
927
+ }
928
+ [strongTextView becomeFirstResponder];
929
+ strongTextView.selectedRange = characterRange;
930
+ });
931
+ }
932
+ return NO;
933
+ }
934
+
842
935
  - (BOOL)textView:(ENRMPlatformTextView *)textView
843
936
  shouldInteractWithURL:(NSURL *)URL
844
937
  inRange:(NSRange)characterRange
@@ -858,6 +951,13 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
858
951
  return NO;
859
952
  }
860
953
 
954
+ #pragma mark - UITextViewDelegate (Selection)
955
+
956
+ - (void)textViewDidChangeSelection:(UITextView *)textView
957
+ {
958
+ [_selectionReporter selectionChanged:textView.selectedRange.length > 0];
959
+ }
960
+
861
961
  #pragma mark - UITextViewDelegate (Edit Menu)
862
962
 
863
963
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
@@ -865,8 +965,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
865
965
  editMenuForTextInRange:(NSRange)range
866
966
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
867
967
  {
968
+ // An empty menu presents nothing; nil would make UIKit show its default menu
969
+ // (the selection and handles stay either way).
868
970
  if (_contextMenuHidden) {
869
- return nil;
971
+ return [UIMenu menuWithChildren:@[]];
870
972
  }
871
973
 
872
974
  __weak EnrichedMarkdownText *weakSelf = self;
@@ -55,6 +55,43 @@ fragmentsObject.setProperty(runtime, "height", fragmentsValue.height);
55
55
  }
56
56
 
57
57
 
58
+ void EnrichedMarkdownEventEmitter::onSelectionChange(OnSelectionChange event) const {
59
+ dispatchEvent("selectionChange", [event=std::move(event)](jsi::Runtime &runtime) {
60
+ auto payload = jsi::Object(runtime);
61
+ payload.setProperty(runtime, "active", event.active);
62
+ payload.setProperty(runtime, "source", event.source);
63
+ payload.setProperty(runtime, "text", event.text);
64
+ payload.setProperty(runtime, "markdown", event.markdown);
65
+ payload.setProperty(runtime, "whole", event.whole);
66
+ {
67
+ auto rect = jsi::Object(runtime);
68
+ rect.setProperty(runtime, "x", event.rect.x);
69
+ rect.setProperty(runtime, "y", event.rect.y);
70
+ rect.setProperty(runtime, "width", event.rect.width);
71
+ rect.setProperty(runtime, "height", event.rect.height);
72
+ payload.setProperty(runtime, "rect", rect);
73
+ }
74
+ {
75
+ auto startRect = jsi::Object(runtime);
76
+ startRect.setProperty(runtime, "x", event.startRect.x);
77
+ startRect.setProperty(runtime, "y", event.startRect.y);
78
+ startRect.setProperty(runtime, "width", event.startRect.width);
79
+ startRect.setProperty(runtime, "height", event.startRect.height);
80
+ payload.setProperty(runtime, "startRect", startRect);
81
+ }
82
+ {
83
+ auto endRect = jsi::Object(runtime);
84
+ endRect.setProperty(runtime, "x", event.endRect.x);
85
+ endRect.setProperty(runtime, "y", event.endRect.y);
86
+ endRect.setProperty(runtime, "width", event.endRect.width);
87
+ endRect.setProperty(runtime, "height", event.endRect.height);
88
+ payload.setProperty(runtime, "endRect", endRect);
89
+ }
90
+ return payload;
91
+ });
92
+ }
93
+
94
+
58
95
  void EnrichedMarkdownEventEmitter::onTaskListItemPress(OnTaskListItemPress event) const {
59
96
  dispatchEvent("taskListItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
60
97
  auto payload = jsi::Object(runtime);
@@ -418,6 +455,43 @@ fragmentsObject.setProperty(runtime, "height", fragmentsValue.height);
418
455
  }
419
456
 
420
457
 
458
+ void EnrichedMarkdownTextEventEmitter::onSelectionChange(OnSelectionChange event) const {
459
+ dispatchEvent("selectionChange", [event=std::move(event)](jsi::Runtime &runtime) {
460
+ auto payload = jsi::Object(runtime);
461
+ payload.setProperty(runtime, "active", event.active);
462
+ payload.setProperty(runtime, "source", event.source);
463
+ payload.setProperty(runtime, "text", event.text);
464
+ payload.setProperty(runtime, "markdown", event.markdown);
465
+ payload.setProperty(runtime, "whole", event.whole);
466
+ {
467
+ auto rect = jsi::Object(runtime);
468
+ rect.setProperty(runtime, "x", event.rect.x);
469
+ rect.setProperty(runtime, "y", event.rect.y);
470
+ rect.setProperty(runtime, "width", event.rect.width);
471
+ rect.setProperty(runtime, "height", event.rect.height);
472
+ payload.setProperty(runtime, "rect", rect);
473
+ }
474
+ {
475
+ auto startRect = jsi::Object(runtime);
476
+ startRect.setProperty(runtime, "x", event.startRect.x);
477
+ startRect.setProperty(runtime, "y", event.startRect.y);
478
+ startRect.setProperty(runtime, "width", event.startRect.width);
479
+ startRect.setProperty(runtime, "height", event.startRect.height);
480
+ payload.setProperty(runtime, "startRect", startRect);
481
+ }
482
+ {
483
+ auto endRect = jsi::Object(runtime);
484
+ endRect.setProperty(runtime, "x", event.endRect.x);
485
+ endRect.setProperty(runtime, "y", event.endRect.y);
486
+ endRect.setProperty(runtime, "width", event.endRect.width);
487
+ endRect.setProperty(runtime, "height", event.endRect.height);
488
+ payload.setProperty(runtime, "endRect", endRect);
489
+ }
490
+ return payload;
491
+ });
492
+ }
493
+
494
+
421
495
  void EnrichedMarkdownTextEventEmitter::onTaskListItemPress(OnTaskListItemPress event) const {
422
496
  dispatchEvent("taskListItemPress", [event=std::move(event)](jsi::Runtime &runtime) {
423
497
  auto payload = jsi::Object(runtime);
@@ -39,6 +39,38 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
39
39
  std::vector<OnLinkLayoutFragments> fragments;
40
40
  };
41
41
 
42
+ struct OnSelectionChangeRect {
43
+ double x;
44
+ double y;
45
+ double width;
46
+ double height;
47
+ };
48
+
49
+ struct OnSelectionChangeStartRect {
50
+ double x;
51
+ double y;
52
+ double width;
53
+ double height;
54
+ };
55
+
56
+ struct OnSelectionChangeEndRect {
57
+ double x;
58
+ double y;
59
+ double width;
60
+ double height;
61
+ };
62
+
63
+ struct OnSelectionChange {
64
+ bool active;
65
+ std::string source;
66
+ std::string text;
67
+ std::string markdown;
68
+ bool whole;
69
+ OnSelectionChangeRect rect;
70
+ OnSelectionChangeStartRect startRect;
71
+ OnSelectionChangeEndRect endRect;
72
+ };
73
+
42
74
  struct OnTaskListItemPress {
43
75
  int index;
44
76
  bool checked;
@@ -69,6 +101,8 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
69
101
 
70
102
  void onLinkLayout(OnLinkLayout value) const;
71
103
 
104
+ void onSelectionChange(OnSelectionChange value) const;
105
+
72
106
  void onTaskListItemPress(OnTaskListItemPress value) const;
73
107
 
74
108
  void onCopyPress(OnCopyPress value) const;
@@ -331,6 +365,38 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
331
365
  std::vector<OnLinkLayoutFragments> fragments;
332
366
  };
333
367
 
368
+ struct OnSelectionChangeRect {
369
+ double x;
370
+ double y;
371
+ double width;
372
+ double height;
373
+ };
374
+
375
+ struct OnSelectionChangeStartRect {
376
+ double x;
377
+ double y;
378
+ double width;
379
+ double height;
380
+ };
381
+
382
+ struct OnSelectionChangeEndRect {
383
+ double x;
384
+ double y;
385
+ double width;
386
+ double height;
387
+ };
388
+
389
+ struct OnSelectionChange {
390
+ bool active;
391
+ std::string source;
392
+ std::string text;
393
+ std::string markdown;
394
+ bool whole;
395
+ OnSelectionChangeRect rect;
396
+ OnSelectionChangeStartRect startRect;
397
+ OnSelectionChangeEndRect endRect;
398
+ };
399
+
334
400
  struct OnTaskListItemPress {
335
401
  int index;
336
402
  bool checked;
@@ -361,6 +427,8 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
361
427
 
362
428
  void onLinkLayout(OnLinkLayout value) const;
363
429
 
430
+ void onSelectionChange(OnSelectionChange value) const;
431
+
364
432
  void onTaskListItemPress(OnTaskListItemPress value) const;
365
433
 
366
434
  void onCopyPress(OnCopyPress value) const;
@@ -22,6 +22,7 @@ EnrichedMarkdownProps::EnrichedMarkdownProps(
22
22
  markdown(convertRawProp(context, rawProps, "markdown", sourceProps.markdown, {})),
23
23
  markdownStyle(convertRawProp(context, rawProps, "markdownStyle", sourceProps.markdownStyle, {})),
24
24
  emitLinkLayout(convertRawProp(context, rawProps, "emitLinkLayout", sourceProps.emitLinkLayout, {false})),
25
+ emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
25
26
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
26
27
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
27
28
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
@@ -71,6 +72,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
71
72
  result["emitLinkLayout"] = emitLinkLayout;
72
73
  }
73
74
 
75
+ if (emitSelectionChange != oldProps->emitSelectionChange) {
76
+ result["emitSelectionChange"] = emitSelectionChange;
77
+ }
78
+
74
79
  if (enableTaskListItemToggle != oldProps->enableTaskListItemToggle) {
75
80
  result["enableTaskListItemToggle"] = enableTaskListItemToggle;
76
81
  }
@@ -310,6 +315,7 @@ EnrichedMarkdownTextProps::EnrichedMarkdownTextProps(
310
315
  markdown(convertRawProp(context, rawProps, "markdown", sourceProps.markdown, {})),
311
316
  markdownStyle(convertRawProp(context, rawProps, "markdownStyle", sourceProps.markdownStyle, {})),
312
317
  emitLinkLayout(convertRawProp(context, rawProps, "emitLinkLayout", sourceProps.emitLinkLayout, {false})),
318
+ emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
313
319
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
314
320
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
315
321
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
@@ -359,6 +365,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
359
365
  result["emitLinkLayout"] = emitLinkLayout;
360
366
  }
361
367
 
368
+ if (emitSelectionChange != oldProps->emitSelectionChange) {
369
+ result["emitSelectionChange"] = emitSelectionChange;
370
+ }
371
+
362
372
  if (enableTaskListItemToggle != oldProps->enableTaskListItemToggle) {
363
373
  result["enableTaskListItemToggle"] = enableTaskListItemToggle;
364
374
  }
@@ -2887,6 +2887,7 @@ class EnrichedMarkdownProps final : public ViewProps {
2887
2887
  std::string markdown{};
2888
2888
  EnrichedMarkdownMarkdownStyleStruct markdownStyle{};
2889
2889
  bool emitLinkLayout{false};
2890
+ bool emitSelectionChange{false};
2890
2891
  bool enableTaskListItemToggle{true};
2891
2892
  bool enableLinkPreview{true};
2892
2893
  bool selectable{false};
@@ -6931,6 +6932,7 @@ class EnrichedMarkdownTextProps final : public ViewProps {
6931
6932
  std::string markdown{};
6932
6933
  EnrichedMarkdownTextMarkdownStyleStruct markdownStyle{};
6933
6934
  bool emitLinkLayout{false};
6935
+ bool emitSelectionChange{false};
6934
6936
  bool enableTaskListItemToggle{true};
6935
6937
  bool enableLinkPreview{true};
6936
6938
  bool selectable{false};