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

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 (71) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +15 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +3 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +15 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +3 -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 +20 -0
  8. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +4 -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 +22 -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 +22 -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 +68 -0
  21. package/docs/WEB.md +1 -0
  22. package/ios/EnrichedMarkdown.mm +188 -2
  23. package/ios/EnrichedMarkdownText.mm +117 -1
  24. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +74 -0
  25. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +68 -0
  26. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +20 -0
  27. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +4 -0
  28. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
  29. package/ios/utils/ENRMLinkChip.m +38 -20
  30. package/ios/utils/ENRMLinkLayout.m +12 -12
  31. package/ios/utils/ENRMSelectionChange.h +96 -0
  32. package/ios/utils/ENRMSelectionChange.m +204 -0
  33. package/ios/utils/EditMenuUtils+macOS.m +1 -1
  34. package/ios/utils/EditMenuUtils.h +3 -0
  35. package/ios/utils/EditMenuUtils.m +116 -0
  36. package/ios/utils/MarkdownAccessibilityElementBuilder.m +1 -2
  37. package/ios/views/ENRMCodeBlockContainerView.h +3 -0
  38. package/ios/views/ENRMCodeBlockContainerView.m +3 -0
  39. package/ios/views/ENRMMathContainerView.h +3 -0
  40. package/ios/views/ENRMMathContainerView.m +3 -0
  41. package/ios/views/TableContainerView.h +3 -0
  42. package/ios/views/TableContainerView.m +4 -1
  43. package/lib/module/EnrichedMarkdownNativeComponent.ts +70 -1
  44. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +70 -1
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/jest/index.js +14 -0
  47. package/lib/module/jest/index.js.map +1 -1
  48. package/lib/module/native/EnrichedMarkdownText.js +46 -4
  49. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  50. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +63 -2
  51. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  52. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +63 -2
  53. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  54. package/lib/typescript/src/index.d.ts +2 -2
  55. package/lib/typescript/src/index.d.ts.map +1 -1
  56. package/lib/typescript/src/jest/index.d.ts +1 -1
  57. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  58. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
  59. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  60. package/lib/typescript/src/types/MarkdownTextProps.d.ts +46 -3
  61. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  62. package/lib/typescript/src/types/events.d.ts +36 -0
  63. package/lib/typescript/src/types/events.d.ts.map +1 -1
  64. package/package.json +1 -1
  65. package/src/EnrichedMarkdownNativeComponent.ts +70 -1
  66. package/src/EnrichedMarkdownTextNativeComponent.ts +70 -1
  67. package/src/index.tsx +3 -0
  68. package/src/jest/index.tsx +22 -1
  69. package/src/native/EnrichedMarkdownText.tsx +87 -9
  70. package/src/types/MarkdownTextProps.ts +53 -2
  71. 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"
@@ -106,6 +107,7 @@ static char kENRMSegmentFadeAnimatorKey;
106
107
  BOOL _allowTrailingMargin;
107
108
  BOOL _selectable;
108
109
  BOOL _enableLinkPreview;
110
+ BOOL _linkLongPressSelects;
109
111
  BOOL _enableTaskListItemToggle;
110
112
  BOOL _contextMenuHidden;
111
113
  BOOL _streamingAnimation;
@@ -128,6 +130,15 @@ static char kENRMSegmentFadeAnimatorKey;
128
130
  BOOL _emitLinkLayout;
129
131
  NSArray<NSDictionary *> *_lastLinkLayout;
130
132
 
133
+ ENRMSelectionChangeReporter *_selectionReporter;
134
+ // At most one segment holds a selection; a new one clears the previous.
135
+ __weak ENRMPlatformTextView *_selectionOwner;
136
+ // A block long-pressed while contextMenuHidden is on, reported as the selection.
137
+ __weak RCTUIView *_activeBlockView;
138
+ NSString *_activeBlockSource;
139
+ NSString *_activeBlockMarkdown;
140
+ NSString *_activeBlockText;
141
+
131
142
  NSLineBreakStrategy _lineBreakStrategy;
132
143
 
133
144
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -199,10 +210,117 @@ static char kENRMSegmentFadeAnimatorKey;
199
210
  [strongSelf renderMarkdownContent:strongSelf->_cachedMarkdown];
200
211
  }
201
212
  };
213
+
214
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
215
+ initWithSnapshot:^NSDictionary * {
216
+ EnrichedMarkdown *strongSelf = weakSelf;
217
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
218
+ }
219
+ emit:^(NSDictionary *payload) {
220
+ EnrichedMarkdown *strongSelf = weakSelf;
221
+ if (!strongSelf)
222
+ return;
223
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownEventEmitter const>(strongSelf->_eventEmitter);
224
+ if (emitter)
225
+ emitter->onSelectionChange(
226
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
227
+ }];
202
228
  }
203
229
  return self;
204
230
  }
205
231
 
232
+ #pragma mark - Selection
233
+
234
+ /// The current selection (or active block) as an `onSelectionChange` payload.
235
+ /// Markdown and `whole` follow the same rules as the intercepted Copy.
236
+ - (NSDictionary *)selectionPayload
237
+ {
238
+ RCTUIView *blockView = _activeBlockView;
239
+ if (blockView != nil) {
240
+ if (blockView.superview != self)
241
+ return ENRMInactiveSelectionPayload();
242
+ return ENRMBlockSelectionPayload(_activeBlockSource, _activeBlockMarkdown, _activeBlockText, blockView.frame);
243
+ }
244
+
245
+ #if !TARGET_OS_OSX
246
+ UITextView *owner = _selectionOwner;
247
+ if (owner == nil || ![owner isDescendantOfView:self])
248
+ return ENRMInactiveSelectionPayload();
249
+ NSDictionary *selection = ENRMTextSelectionInTextView(owner, self);
250
+ if (!selection)
251
+ return ENRMInactiveSelectionPayload();
252
+
253
+ NSString *text = selection[ENRMSelectionTextKey];
254
+ // Segments only see their own text, so a selection is the whole component
255
+ // only when it spans the sole segment; the markdown is then the original.
256
+ BOOL whole = [selection[ENRMSelectionFullKey] boolValue] && _segmentViews.count == 1 && _cachedMarkdown.length > 0;
257
+ NSString *markdown =
258
+ whole ? _cachedMarkdown : extractMarkdownFromAttributedString(owner.attributedText, owner.selectedRange);
259
+ NSMutableDictionary *payload = [selection mutableCopy];
260
+ [payload removeObjectForKey:ENRMSelectionFullKey];
261
+ payload[ENRMSelectionActiveKey] = @YES;
262
+ payload[ENRMSelectionSourceKey] = @"selection";
263
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
264
+ payload[ENRMSelectionWholeKey] = @(whole);
265
+ return payload;
266
+ #else
267
+ return ENRMInactiveSelectionPayload();
268
+ #endif
269
+ }
270
+
271
+ - (void)clearActiveBlock
272
+ {
273
+ _activeBlockView = nil;
274
+ _activeBlockSource = nil;
275
+ _activeBlockMarkdown = nil;
276
+ _activeBlockText = nil;
277
+ }
278
+
279
+ - (void)clearSegmentSelection:(ENRMPlatformTextView *)textView
280
+ {
281
+ #if !TARGET_OS_OSX
282
+ textView.selectedRange = NSMakeRange(0, 0);
283
+ [textView resignFirstResponder];
284
+ #endif
285
+ }
286
+
287
+ - (void)clearSelection
288
+ {
289
+ ENRMPlatformTextView *owner = _selectionOwner;
290
+ _selectionOwner = nil;
291
+ [self clearActiveBlock];
292
+ if (owner)
293
+ [self clearSegmentSelection:owner];
294
+ [_selectionReporter flushNow];
295
+ }
296
+
297
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
298
+ {
299
+ RCTEnrichedMarkdownHandleCommand(self, commandName, args);
300
+ }
301
+
302
+ - (ENRMBlockCopyRequestBlock)blockMenuSuppressedForSource:(NSString *)source view:(RCTUIView *)view
303
+ {
304
+ __weak EnrichedMarkdown *weakSelf = self;
305
+ __weak RCTUIView *weakView = view;
306
+ return ^(NSString *markdown, NSString *text) {
307
+ EnrichedMarkdown *strongSelf = weakSelf;
308
+ RCTUIView *strongView = weakView;
309
+ if (!strongSelf || !strongView || !strongSelf->_selectionReporter.enabled)
310
+ return;
311
+ ENRMPlatformTextView *owner = strongSelf->_selectionOwner;
312
+ strongSelf->_selectionOwner = nil;
313
+ if (owner)
314
+ [strongSelf clearSegmentSelection:owner];
315
+ strongSelf->_activeBlockView = strongView;
316
+ strongSelf->_activeBlockSource = source;
317
+ strongSelf->_activeBlockMarkdown = markdown;
318
+ strongSelf->_activeBlockText = text;
319
+ [strongSelf->_selectionReporter forgetLastReported];
320
+ [strongSelf->_selectionReporter flushNow];
321
+ };
322
+ }
323
+
206
324
  - (void)configureSegmentViewRegistry
207
325
  {
208
326
  __weak EnrichedMarkdown *weakSelf = self;
@@ -839,6 +957,7 @@ static char kENRMSegmentFadeAnimatorKey;
839
957
  tableView.contextMenuHidden = _contextMenuHidden;
840
958
  tableView.menuOptions = [self blockMenuOptions];
841
959
  tableView.onCopyRequest = [self blockCopyRequestForSource:@"table"];
960
+ tableView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"table" view:tableView];
842
961
 
843
962
  __weak EnrichedMarkdown *weakSelf = self;
844
963
 
@@ -881,6 +1000,7 @@ static char kENRMSegmentFadeAnimatorKey;
881
1000
  mathView.contextMenuHidden = _contextMenuHidden;
882
1001
  mathView.menuOptions = [self blockMenuOptions];
883
1002
  mathView.onCopyRequest = [self blockCopyRequestForSource:@"math"];
1003
+ mathView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"math" view:mathView];
884
1004
  [mathView applyLatex:mathSegment.latex];
885
1005
  return mathView;
886
1006
  }
@@ -894,6 +1014,7 @@ static char kENRMSegmentFadeAnimatorKey;
894
1014
  codeBlockView.contextMenuHidden = _contextMenuHidden;
895
1015
  codeBlockView.menuOptions = [self blockMenuOptions];
896
1016
  codeBlockView.onCopyRequest = [self blockCopyRequestForSource:@"code"];
1017
+ codeBlockView.onMenuSuppressed = [self blockMenuSuppressedForSource:@"code" view:codeBlockView];
897
1018
 
898
1019
  __weak EnrichedMarkdown *weakSelf = self;
899
1020
  codeBlockView.onCopyPress = ^(NSString *code, NSString *language) {
@@ -947,6 +1068,8 @@ static char kENRMSegmentFadeAnimatorKey;
947
1068
  [super layoutSubviews];
948
1069
  [self computeSegmentLayoutForWidth:self.bounds.size.width applyFrames:YES];
949
1070
  [self emitLinkLayoutIfNeeded];
1071
+ // Segments may have moved or been replaced; re-read the selection (deduplicated).
1072
+ [_selectionReporter schedule];
950
1073
  }
951
1074
 
952
1075
  - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
@@ -988,6 +1111,8 @@ static char kENRMSegmentFadeAnimatorKey;
988
1111
  [self setNeedsLayout];
989
1112
  }
990
1113
 
1114
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
1115
+
991
1116
  for (RCTUIView *segment in _segmentViews) {
992
1117
  if ([segment isKindOfClass:[EnrichedMarkdownInternalText class]]) {
993
1118
  EnrichedMarkdownInternalText *textSegment = (EnrichedMarkdownInternalText *)segment;
@@ -1029,6 +1154,7 @@ static char kENRMSegmentFadeAnimatorKey;
1029
1154
  }
1030
1155
 
1031
1156
  _enableLinkPreview = newViewProps.enableLinkPreview;
1157
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
1032
1158
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
1033
1159
  _contextMenuHidden = newViewProps.contextMenuHidden;
1034
1160
 
@@ -1185,6 +1311,9 @@ static char kENRMSegmentFadeAnimatorKey;
1185
1311
  _config = nil;
1186
1312
  _emitLinkLayout = NO;
1187
1313
  _lastLinkLayout = nil;
1314
+ _selectionReporter.enabled = NO;
1315
+ _selectionOwner = nil;
1316
+ [self clearActiveBlock];
1188
1317
  _md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
1189
1318
  _maxFontSizeMultiplier = 0;
1190
1319
  _allowTrailingMargin = NO;
@@ -1406,14 +1535,34 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1406
1535
  ENRMHandleTapOnTextView(textView, recognizer, ^(NSString *url) { [self emitLinkPress:url]; });
1407
1536
  }
1408
1537
 
1538
+ #if !TARGET_OS_OSX
1539
+ - (void)textViewDidChangeSelection:(UITextView *)textView
1540
+ {
1541
+ ENRMDismissSelectionEditMenuIfMoved(textView);
1542
+ if (textView.selectedRange.length > 0) {
1543
+ // Take ownership first so the previous owner's own "cleared" callback is ignored.
1544
+ ENRMPlatformTextView *previous = _selectionOwner;
1545
+ _selectionOwner = textView;
1546
+ [self clearActiveBlock];
1547
+ if (previous && previous != textView)
1548
+ previous.selectedRange = NSMakeRange(0, 0);
1549
+ [_selectionReporter schedule];
1550
+ } else if (textView == _selectionOwner) {
1551
+ [_selectionReporter selectionChanged:NO];
1552
+ }
1553
+ }
1554
+ #endif
1555
+
1409
1556
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
1410
1557
  #if !TARGET_OS_OSX
1411
1558
  - (UIMenu *)textView:(UITextView *)textView
1412
1559
  editMenuForTextInRange:(NSRange)range
1413
1560
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
1414
1561
  {
1562
+ // An empty menu presents nothing; nil would make UIKit show its default menu
1563
+ // (the selection and handles stay either way).
1415
1564
  if (_contextMenuHidden) {
1416
- return nil;
1565
+ return [UIMenu menuWithChildren:@[]];
1417
1566
  }
1418
1567
 
1419
1568
  __weak EnrichedMarkdown *weakSelf = self;
@@ -1448,6 +1597,31 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1448
1597
  customActions, _selectionMenuConfig, onCopyRequest);
1449
1598
  }
1450
1599
 
1600
+ // Attachments other than images (inline math) are text, not items: without
1601
+ // this a long press on one in an unfocused text view starts UIKit's text item
1602
+ // interaction, which presents nothing and leaves the view unable to select.
1603
+ - (BOOL)textView:(UITextView *)textView
1604
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
1605
+ inRange:(NSRange)characterRange
1606
+ interaction:(UITextItemInteraction)interaction
1607
+ {
1608
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
1609
+ return YES;
1610
+ }
1611
+ // A long press selects the attachment like a word, as it does once the view is focused.
1612
+ if (interaction == UITextItemInteractionPresentActions) {
1613
+ __weak UITextView *weakTextView = textView;
1614
+ dispatch_async(dispatch_get_main_queue(), ^{
1615
+ UITextView *strongTextView = weakTextView;
1616
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1617
+ return;
1618
+ }
1619
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1620
+ });
1621
+ }
1622
+ return NO;
1623
+ }
1624
+
1451
1625
  - (BOOL)textView:(UITextView *)textView
1452
1626
  shouldInteractWithURL:(NSURL *)URL
1453
1627
  inRange:(NSRange)characterRange
@@ -1459,6 +1633,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1459
1633
 
1460
1634
  NSString *urlString = linkURLAtRange(textView, characterRange);
1461
1635
 
1636
+ if (urlString && _linkLongPressSelects) {
1637
+ __weak UITextView *weakTextView = textView;
1638
+ dispatch_async(dispatch_get_main_queue(), ^{
1639
+ UITextView *strongTextView = weakTextView;
1640
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1641
+ return;
1642
+ }
1643
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1644
+ });
1645
+ return NO;
1646
+ }
1647
+
1462
1648
  if (!urlString || _enableLinkPreview) {
1463
1649
  return YES;
1464
1650
  }
@@ -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"
@@ -85,6 +87,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
85
87
  CGFloat _lastElementMarginBottom;
86
88
  BOOL _allowTrailingMargin;
87
89
  BOOL _enableLinkPreview;
90
+ BOOL _linkLongPressSelects;
88
91
  BOOL _enableTaskListItemToggle;
89
92
  BOOL _contextMenuHidden;
90
93
  BOOL _streamingAnimation;
@@ -115,6 +118,8 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
115
118
  BOOL _emitLinkLayout;
116
119
  NSArray<NSDictionary *> *_lastLinkLayout;
117
120
 
121
+ ENRMSelectionChangeReporter *_selectionReporter;
122
+
118
123
  NSLineBreakStrategy _lineBreakStrategy;
119
124
 
120
125
  ENRMWritingDirectionMode _writingDirectionMode;
@@ -272,11 +277,68 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
272
277
  };
273
278
 
274
279
  [self setupTextView];
280
+ [self setupSelectionReporter];
275
281
  }
276
282
 
277
283
  return self;
278
284
  }
279
285
 
286
+ - (void)setupSelectionReporter
287
+ {
288
+ __weak EnrichedMarkdownText *weakSelf = self;
289
+ _selectionReporter = [[ENRMSelectionChangeReporter alloc]
290
+ initWithSnapshot:^NSDictionary * {
291
+ EnrichedMarkdownText *strongSelf = weakSelf;
292
+ return strongSelf ? [strongSelf selectionPayload] : ENRMInactiveSelectionPayload();
293
+ }
294
+ emit:^(NSDictionary *payload) {
295
+ EnrichedMarkdownText *strongSelf = weakSelf;
296
+ if (!strongSelf)
297
+ return;
298
+ auto emitter = std::static_pointer_cast<EnrichedMarkdownTextEventEmitter const>(strongSelf->_eventEmitter);
299
+ if (emitter)
300
+ emitter->onSelectionChange(
301
+ ENRMSelectionChangeEventFromPayload<EnrichedMarkdownTextEventEmitter::OnSelectionChange>(payload));
302
+ }];
303
+ }
304
+
305
+ /// The current selection as an `onSelectionChange` payload. Markdown and
306
+ /// `whole` follow the same rules as the intercepted Copy (`onCopyRequest`).
307
+ - (NSDictionary *)selectionPayload
308
+ {
309
+ #if !TARGET_OS_OSX
310
+ NSDictionary *selection = ENRMTextSelectionInTextView(_textView, self);
311
+ if (!selection)
312
+ return ENRMInactiveSelectionPayload();
313
+
314
+ NSString *text = selection[ENRMSelectionTextKey];
315
+ NSString *markdown = markdownForRange(_textView.attributedText, _textView.selectedRange, _cachedMarkdown);
316
+ NSMutableDictionary *payload = [selection mutableCopy];
317
+ [payload removeObjectForKey:ENRMSelectionFullKey];
318
+ payload[ENRMSelectionActiveKey] = @YES;
319
+ payload[ENRMSelectionSourceKey] = @"selection";
320
+ payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
321
+ payload[ENRMSelectionWholeKey] = @([selection[ENRMSelectionFullKey] boolValue] && _cachedMarkdown.length > 0);
322
+ return payload;
323
+ #else
324
+ return ENRMInactiveSelectionPayload();
325
+ #endif
326
+ }
327
+
328
+ - (void)clearSelection
329
+ {
330
+ #if !TARGET_OS_OSX
331
+ _textView.selectedRange = NSMakeRange(0, 0);
332
+ [_textView resignFirstResponder];
333
+ #endif
334
+ [_selectionReporter flushNow];
335
+ }
336
+
337
+ - (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
338
+ {
339
+ RCTEnrichedMarkdownTextHandleCommand(self, commandName, args);
340
+ }
341
+
280
342
  - (void)setupTextView
281
343
  {
282
344
  #if !TARGET_OS_OSX
@@ -477,6 +539,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
477
539
  [self emitLinkLayoutIfNeeded];
478
540
  }
479
541
 
542
+ // New text drops the selection; setting attributedText doesn't call the delegate.
543
+ [_selectionReporter schedule];
544
+
480
545
  if (_textView.hidden) {
481
546
  dispatch_async(dispatch_get_main_queue(), ^{ self->_textView.hidden = NO; });
482
547
  }
@@ -539,6 +604,8 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
539
604
  [self emitLinkLayoutIfNeeded];
540
605
  }
541
606
 
607
+ _selectionReporter.enabled = newViewProps.emitSelectionChange;
608
+
542
609
  if (_textView.selectable != newViewProps.selectable) {
543
610
  _textView.selectable = newViewProps.selectable;
544
611
  }
@@ -587,6 +654,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
587
654
  }
588
655
 
589
656
  _enableLinkPreview = newViewProps.enableLinkPreview;
657
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
590
658
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
591
659
  _contextMenuHidden = newViewProps.contextMenuHidden;
592
660
 
@@ -717,6 +785,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
717
785
  _spoilerManager = nil;
718
786
  _emitLinkLayout = NO;
719
787
  _lastLinkLayout = nil;
788
+ _selectionReporter.enabled = NO;
720
789
  if (_textView != nil) {
721
790
  ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
722
791
  _textView.hidden = YES;
@@ -839,6 +908,31 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
839
908
  #pragma mark - UITextViewDelegate (Link Interaction)
840
909
 
841
910
  #if !TARGET_OS_OSX
911
+ // Attachments other than images (inline math) are text, not items: without
912
+ // this a long press on one in an unfocused text view starts UIKit's text item
913
+ // interaction, which presents nothing and leaves the view unable to select.
914
+ - (BOOL)textView:(UITextView *)textView
915
+ shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment
916
+ inRange:(NSRange)characterRange
917
+ interaction:(UITextItemInteraction)interaction
918
+ {
919
+ if ([textAttachment isKindOfClass:[ENRMImageAttachment class]]) {
920
+ return YES;
921
+ }
922
+ // A long press selects the attachment like a word, as it does once the view is focused.
923
+ if (interaction == UITextItemInteractionPresentActions) {
924
+ __weak UITextView *weakTextView = textView;
925
+ dispatch_async(dispatch_get_main_queue(), ^{
926
+ UITextView *strongTextView = weakTextView;
927
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
928
+ return;
929
+ }
930
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
931
+ });
932
+ }
933
+ return NO;
934
+ }
935
+
842
936
  - (BOOL)textView:(ENRMPlatformTextView *)textView
843
937
  shouldInteractWithURL:(NSURL *)URL
844
938
  inRange:(NSRange)characterRange
@@ -850,6 +944,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
850
944
 
851
945
  NSString *urlString = linkURLAtRange(textView, characterRange);
852
946
 
947
+ if (urlString && _linkLongPressSelects) {
948
+ __weak UITextView *weakTextView = textView;
949
+ dispatch_async(dispatch_get_main_queue(), ^{
950
+ UITextView *strongTextView = weakTextView;
951
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
952
+ return;
953
+ }
954
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
955
+ });
956
+ return NO;
957
+ }
958
+
853
959
  if (!urlString || _enableLinkPreview) {
854
960
  return YES;
855
961
  }
@@ -858,6 +964,14 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
858
964
  return NO;
859
965
  }
860
966
 
967
+ #pragma mark - UITextViewDelegate (Selection)
968
+
969
+ - (void)textViewDidChangeSelection:(UITextView *)textView
970
+ {
971
+ ENRMDismissSelectionEditMenuIfMoved(textView);
972
+ [_selectionReporter selectionChanged:textView.selectedRange.length > 0];
973
+ }
974
+
861
975
  #pragma mark - UITextViewDelegate (Edit Menu)
862
976
 
863
977
  // TODO: Remove API_AVAILABLE(ios(16.0)) guard when the minimum iOS deployment target in RN is bumped to 16.
@@ -865,8 +979,10 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
865
979
  editMenuForTextInRange:(NSRange)range
866
980
  suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0))
867
981
  {
982
+ // An empty menu presents nothing; nil would make UIKit show its default menu
983
+ // (the selection and handles stay either way).
868
984
  if (_contextMenuHidden) {
869
- return nil;
985
+ return [UIMenu menuWithChildren:@[]];
870
986
  }
871
987
 
872
988
  __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;