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

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 (50) hide show
  1. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +3 -0
  2. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +1 -0
  3. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +3 -0
  4. package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +1 -0
  5. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +2 -0
  6. package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +2 -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/EnrichedMarkdownManager.kt +8 -0
  10. package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +8 -0
  11. package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +1 -0
  12. package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +2 -0
  13. package/docs/API_REFERENCE.md +14 -2
  14. package/docs/WEB.md +1 -0
  15. package/ios/EnrichedMarkdown.mm +39 -2
  16. package/ios/EnrichedMarkdownText.mm +37 -2
  17. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +2 -0
  18. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +2 -0
  19. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +10 -0
  20. package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +2 -0
  21. package/ios/utils/ENRMLinkChip.m +25 -6
  22. package/ios/utils/ENRMSelectionChange.h +24 -0
  23. package/ios/utils/ENRMSelectionChange.m +78 -0
  24. package/ios/utils/EditMenuUtils.h +3 -0
  25. package/ios/utils/EditMenuUtils.m +116 -0
  26. package/lib/module/EnrichedMarkdownNativeComponent.ts +11 -0
  27. package/lib/module/EnrichedMarkdownTextNativeComponent.ts +11 -0
  28. package/lib/module/jest/index.js +1 -0
  29. package/lib/module/jest/index.js.map +1 -1
  30. package/lib/module/native/EnrichedMarkdownText.js +2 -0
  31. package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
  32. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +11 -0
  33. package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
  34. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +11 -0
  35. package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
  36. package/lib/typescript/src/jest/index.d.ts +1 -1
  37. package/lib/typescript/src/jest/index.d.ts.map +1 -1
  38. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +1 -1
  39. package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
  40. package/lib/typescript/src/types/MarkdownTextProps.d.ts +14 -2
  41. package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
  42. package/lib/typescript/src/types/events.d.ts +8 -0
  43. package/lib/typescript/src/types/events.d.ts.map +1 -1
  44. package/package.json +1 -1
  45. package/src/EnrichedMarkdownNativeComponent.ts +11 -0
  46. package/src/EnrichedMarkdownTextNativeComponent.ts +11 -0
  47. package/src/jest/index.tsx +1 -0
  48. package/src/native/EnrichedMarkdownText.tsx +2 -0
  49. package/src/types/MarkdownTextProps.ts +14 -2
  50. package/src/types/events.ts +8 -0
@@ -44,6 +44,9 @@ public class EnrichedMarkdownManagerDelegate<T extends View, U extends BaseViewM
44
44
  case "enableLinkPreview":
45
45
  mViewManager.setEnableLinkPreview(view, value == null ? true : (boolean) value);
46
46
  break;
47
+ case "linkLongPressSelects":
48
+ mViewManager.setLinkLongPressSelects(view, value == null ? false : (boolean) value);
49
+ break;
47
50
  case "selectable":
48
51
  mViewManager.setSelectable(view, value == null ? false : (boolean) value);
49
52
  break;
@@ -22,6 +22,7 @@ public interface EnrichedMarkdownManagerInterface<T extends View> extends ViewMa
22
22
  void setEmitSelectionChange(T view, boolean value);
23
23
  void setEnableTaskListItemToggle(T view, boolean value);
24
24
  void setEnableLinkPreview(T view, boolean value);
25
+ void setLinkLongPressSelects(T view, boolean value);
25
26
  void setSelectable(T view, boolean value);
26
27
  void setSelectionColor(T view, @Nullable Integer value);
27
28
  void setSelectionHandleColor(T view, @Nullable Integer value);
@@ -44,6 +44,9 @@ public class EnrichedMarkdownTextManagerDelegate<T extends View, U extends BaseV
44
44
  case "enableLinkPreview":
45
45
  mViewManager.setEnableLinkPreview(view, value == null ? true : (boolean) value);
46
46
  break;
47
+ case "linkLongPressSelects":
48
+ mViewManager.setLinkLongPressSelects(view, value == null ? false : (boolean) value);
49
+ break;
47
50
  case "selectable":
48
51
  mViewManager.setSelectable(view, value == null ? false : (boolean) value);
49
52
  break;
@@ -22,6 +22,7 @@ public interface EnrichedMarkdownTextManagerInterface<T extends View> extends Vi
22
22
  void setEmitSelectionChange(T view, boolean value);
23
23
  void setEnableTaskListItemToggle(T view, boolean value);
24
24
  void setEnableLinkPreview(T view, boolean value);
25
+ void setLinkLongPressSelects(T view, boolean value);
25
26
  void setSelectable(T view, boolean value);
26
27
  void setSelectionColor(T view, @Nullable Integer value);
27
28
  void setSelectionHandleColor(T view, @Nullable Integer value);
@@ -87,6 +87,7 @@ payload.setProperty(runtime, "whole", event.whole);
87
87
  endRect.setProperty(runtime, "height", event.endRect.height);
88
88
  payload.setProperty(runtime, "endRect", endRect);
89
89
  }
90
+ payload.setProperty(runtime, "adjusting", event.adjusting);
90
91
  return payload;
91
92
  });
92
93
  }
@@ -487,6 +488,7 @@ payload.setProperty(runtime, "whole", event.whole);
487
488
  endRect.setProperty(runtime, "height", event.endRect.height);
488
489
  payload.setProperty(runtime, "endRect", endRect);
489
490
  }
491
+ payload.setProperty(runtime, "adjusting", event.adjusting);
490
492
  return payload;
491
493
  });
492
494
  }
@@ -69,6 +69,7 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
69
69
  OnSelectionChangeRect rect;
70
70
  OnSelectionChangeStartRect startRect;
71
71
  OnSelectionChangeEndRect endRect;
72
+ bool adjusting;
72
73
  };
73
74
 
74
75
  struct OnTaskListItemPress {
@@ -395,6 +396,7 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
395
396
  OnSelectionChangeRect rect;
396
397
  OnSelectionChangeStartRect startRect;
397
398
  OnSelectionChangeEndRect endRect;
399
+ bool adjusting;
398
400
  };
399
401
 
400
402
  struct OnTaskListItemPress {
@@ -25,6 +25,7 @@ EnrichedMarkdownProps::EnrichedMarkdownProps(
25
25
  emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
26
26
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
27
27
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
28
+ linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
28
29
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
29
30
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
30
31
  selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
@@ -84,6 +85,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
84
85
  result["enableLinkPreview"] = enableLinkPreview;
85
86
  }
86
87
 
88
+ if (linkLongPressSelects != oldProps->linkLongPressSelects) {
89
+ result["linkLongPressSelects"] = linkLongPressSelects;
90
+ }
91
+
87
92
  if (selectable != oldProps->selectable) {
88
93
  result["selectable"] = selectable;
89
94
  }
@@ -318,6 +323,7 @@ EnrichedMarkdownTextProps::EnrichedMarkdownTextProps(
318
323
  emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
319
324
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
320
325
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
326
+ linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
321
327
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
322
328
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
323
329
  selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
@@ -377,6 +383,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
377
383
  result["enableLinkPreview"] = enableLinkPreview;
378
384
  }
379
385
 
386
+ if (linkLongPressSelects != oldProps->linkLongPressSelects) {
387
+ result["linkLongPressSelects"] = linkLongPressSelects;
388
+ }
389
+
380
390
  if (selectable != oldProps->selectable) {
381
391
  result["selectable"] = selectable;
382
392
  }
@@ -2890,6 +2890,7 @@ class EnrichedMarkdownProps final : public ViewProps {
2890
2890
  bool emitSelectionChange{false};
2891
2891
  bool enableTaskListItemToggle{true};
2892
2892
  bool enableLinkPreview{true};
2893
+ bool linkLongPressSelects{false};
2893
2894
  bool selectable{false};
2894
2895
  SharedColor selectionColor{};
2895
2896
  SharedColor selectionHandleColor{};
@@ -6935,6 +6936,7 @@ class EnrichedMarkdownTextProps final : public ViewProps {
6935
6936
  bool emitSelectionChange{false};
6936
6937
  bool enableTaskListItemToggle{true};
6937
6938
  bool enableLinkPreview{true};
6939
+ bool linkLongPressSelects{false};
6938
6940
  bool selectable{false};
6939
6941
  SharedColor selectionColor{};
6940
6942
  SharedColor selectionHandleColor{};
@@ -207,6 +207,14 @@ class EnrichedMarkdownManager :
207
207
  // No-op on Android — only used on iOS
208
208
  }
209
209
 
210
+ @ReactProp(name = "linkLongPressSelects", defaultBoolean = false)
211
+ override fun setLinkLongPressSelects(
212
+ view: EnrichedMarkdown?,
213
+ linkLongPressSelects: Boolean,
214
+ ) {
215
+ // No-op on Android — only used on iOS
216
+ }
217
+
210
218
  @ReactProp(name = "enableTaskListItemToggle", defaultBoolean = true)
211
219
  override fun setEnableTaskListItemToggle(
212
220
  view: EnrichedMarkdown?,
@@ -207,6 +207,14 @@ class EnrichedMarkdownTextManager :
207
207
  // No-op on Android — only used on iOS
208
208
  }
209
209
 
210
+ @ReactProp(name = "linkLongPressSelects", defaultBoolean = false)
211
+ override fun setLinkLongPressSelects(
212
+ view: EnrichedMarkdownText?,
213
+ linkLongPressSelects: Boolean,
214
+ ) {
215
+ // No-op on Android — only used on iOS
216
+ }
217
+
210
218
  @ReactProp(name = "enableTaskListItemToggle", defaultBoolean = true)
211
219
  override fun setEnableTaskListItemToggle(
212
220
  view: EnrichedMarkdownText?,
@@ -26,6 +26,7 @@ class SelectionChangeEvent(
26
26
  putMap("rect", snapshot.rect.toWritableMap())
27
27
  putMap("startRect", snapshot.startRect.toWritableMap())
28
28
  putMap("endRect", snapshot.endRect.toWritableMap())
29
+ putBoolean("adjusting", snapshot.adjusting)
29
30
  }
30
31
 
31
32
  private fun SelectionRect.toWritableMap(): WritableMap =
@@ -46,6 +46,8 @@ data class SelectionSnapshot(
46
46
  val rect: SelectionRect,
47
47
  val startRect: SelectionRect,
48
48
  val endRect: SelectionRect,
49
+ // Not tracked on Android: the selection is reported continuously while a handle is dragged.
50
+ val adjusting: Boolean = false,
49
51
  ) {
50
52
  companion object {
51
53
  val INACTIVE =
@@ -248,13 +248,15 @@ interface SelectionChangeEvent {
248
248
  /** Selected part of the first / last line — anchor a menu above the start or below the end. */
249
249
  startRect: SelectionRect;
250
250
  endRect: SelectionRect;
251
+ /** `true` while a selection handle is dragged (iOS; always `false` on Android). */
252
+ adjusting: boolean;
251
253
  }
252
254
  ```
253
255
 
254
256
  - Fires at most once per frame while the selection changes (payloads are deduplicated), and with `active: false` right away once it is gone — including when new `markdown` replaces the text.
255
257
  - Rects are in dp, relative to the component's native view: add the origin from `ref.current.measureInWindow` to get window coordinates. Re-measure after scrolling instead of waiting for a new event.
256
258
  - With `contextMenuHidden`, a long press on a table, math or code block (`flavor="github"`) fires the event with `source` set to the block kind, `whole: true` and every rect set to the block's frame. These blocks have no text selection, so without this they would offer no way to copy. Without `contextMenuHidden` the block menu shows as usual and no event fires.
257
- - There is no "dragging" flag. Debounce before showing your menu, so it doesn't flicker while a handle is dragged.
259
+ - On iOS the selection isn't reported while a handle is dragged: the event fires with `adjusting: true` (and the selection as it was) when the drag begins, and with `adjusting: false` and the new selection when the finger lifts. Hide your menu while `adjusting` is `true`. Android has no such flag (`adjusting` is always `false`) but reports every step of the drag, so debounce before showing the menu there too.
258
260
  - Within one component only one part holds a selection at a time: selecting in another text segment clears the previous one. To keep a single selection across several components, call `clearSelection()` on the previous one.
259
261
  - Ignored on macOS and web.
260
262
 
@@ -268,7 +270,7 @@ const ref = useRef<EnrichedMarkdownTextInstance>(null);
268
270
  markdown={content}
269
271
  contextMenuHidden
270
272
  onSelectionChange={(e) => {
271
- if (!e.active) return hideMenu();
273
+ if (!e.active || e.adjusting) return hideMenu();
272
274
  ref.current?.measureInWindow((x, y) =>
273
275
  showMenuDebounced({ x: x + e.startRect.x, y: y + e.startRect.y }, e.markdown)
274
276
  );
@@ -299,6 +301,16 @@ You can also control this behavior explicitly without providing a handler:
299
301
  />
300
302
  ```
301
303
 
304
+ ### `linkLongPressSelects`
305
+
306
+ Long-pressing a link selects its label and shows the selection edit menu (the same one a manual selection gets), instead of the system link preview (iOS only). Takes precedence over `enableLinkPreview` and `onLinkLongPress`.
307
+
308
+ | Type | Default Value | Platform |
309
+ | --------- | ------------- | -------- |
310
+ | `boolean` | `false` | iOS |
311
+
312
+ Only the link label is selected, not the chip padding; the selection handles can be dragged to extend it. Long-pressing inline math always behaves this way, without the prop.
313
+
302
314
  ### `selectable`
303
315
 
304
316
  Whether text can be selected.
package/docs/WEB.md CHANGED
@@ -37,6 +37,7 @@ The web implementation also exports `WebMarkdownTextProps` which extends `Enrich
37
37
  |---|---|
38
38
  | `flavor` | The web renderer always uses full GFM capabilities. On native, `flavor` controls whether a single `TextView` (CommonMark) or container-based renderer (GitHub) is used; the DOM has no such constraint. |
39
39
  | `enableLinkPreview` | iOS-only feature (native link preview on long press). |
40
+ | `linkLongPressSelects` | iOS-only feature (selects a link on long press instead of the link preview). |
40
41
  | `allowFontScaling` / `maxFontSizeMultiplier` | React Native text scaling props. Browsers handle font scaling natively via OS accessibility settings. |
41
42
  | `streamingAnimation` | Native-only tail fade-in animation. Not yet implemented on web. |
42
43
  | `streamingConfig` | Native-only streaming table configuration. Not yet implemented on web. |
@@ -107,6 +107,7 @@ static char kENRMSegmentFadeAnimatorKey;
107
107
  BOOL _allowTrailingMargin;
108
108
  BOOL _selectable;
109
109
  BOOL _enableLinkPreview;
110
+ BOOL _linkLongPressSelects;
110
111
  BOOL _enableTaskListItemToggle;
111
112
  BOOL _contextMenuHidden;
112
113
  BOOL _streamingAnimation;
@@ -130,6 +131,9 @@ static char kENRMSegmentFadeAnimatorKey;
130
131
  NSArray<NSDictionary *> *_lastLinkLayout;
131
132
 
132
133
  ENRMSelectionChangeReporter *_selectionReporter;
134
+ #if !TARGET_OS_OSX
135
+ ENRMSelectionGestureTracker *_selectionGestures;
136
+ #endif
133
137
  // At most one segment holds a selection; a new one clears the previous.
134
138
  __weak ENRMPlatformTextView *_selectionOwner;
135
139
  // A block long-pressed while contextMenuHidden is on, reported as the selection.
@@ -224,6 +228,20 @@ static char kENRMSegmentFadeAnimatorKey;
224
228
  emitter->onSelectionChange(
225
229
  ENRMSelectionChangeEventFromPayload<EnrichedMarkdownEventEmitter::OnSelectionChange>(payload));
226
230
  }];
231
+ #if !TARGET_OS_OSX
232
+ // A handle drag starting or ending is reported right away, even when the
233
+ // selection itself is unchanged.
234
+ _selectionGestures = [[ENRMSelectionGestureTracker alloc] initWithChange:^{
235
+ EnrichedMarkdown *strongSelf = weakSelf;
236
+ if (!strongSelf)
237
+ return;
238
+ UITextView *owner = strongSelf->_selectionOwner;
239
+ if (owner)
240
+ [strongSelf->_selectionGestures trackTextView:owner];
241
+ [strongSelf->_selectionReporter forgetLastReported];
242
+ [strongSelf->_selectionReporter flushNow];
243
+ }];
244
+ #endif
227
245
  }
228
246
  return self;
229
247
  }
@@ -261,6 +279,7 @@ static char kENRMSegmentFadeAnimatorKey;
261
279
  payload[ENRMSelectionSourceKey] = @"selection";
262
280
  payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
263
281
  payload[ENRMSelectionWholeKey] = @(whole);
282
+ payload[ENRMSelectionAdjustingKey] = @(_selectionGestures.adjusting);
264
283
  return payload;
265
284
  #else
266
285
  return ENRMInactiveSelectionPayload();
@@ -914,6 +933,7 @@ static char kENRMSegmentFadeAnimatorKey;
914
933
 
915
934
  #if !TARGET_OS_OSX
916
935
  view.textView.delegate = self;
936
+ [_selectionGestures trackTextView:view.textView];
917
937
  #else
918
938
  __weak EnrichedMarkdown *weakSelf = self;
919
939
  [view setContextMenuProvider:^NSMenu *_Nullable(NSMenu *baseMenu, NSTextView *textView) {
@@ -1153,6 +1173,7 @@ static char kENRMSegmentFadeAnimatorKey;
1153
1173
  }
1154
1174
 
1155
1175
  _enableLinkPreview = newViewProps.enableLinkPreview;
1176
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
1156
1177
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
1157
1178
  _contextMenuHidden = newViewProps.contextMenuHidden;
1158
1179
 
@@ -1310,6 +1331,9 @@ static char kENRMSegmentFadeAnimatorKey;
1310
1331
  _emitLinkLayout = NO;
1311
1332
  _lastLinkLayout = nil;
1312
1333
  _selectionReporter.enabled = NO;
1334
+ #if !TARGET_OS_OSX
1335
+ [_selectionGestures reset];
1336
+ #endif
1313
1337
  _selectionOwner = nil;
1314
1338
  [self clearActiveBlock];
1315
1339
  _md4cFlags = [EnrichedMarkdown flagsFromProps:resetProps->md4cFlags];
@@ -1536,6 +1560,8 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1536
1560
  #if !TARGET_OS_OSX
1537
1561
  - (void)textViewDidChangeSelection:(UITextView *)textView
1538
1562
  {
1563
+ [_selectionGestures trackTextView:textView];
1564
+ ENRMDismissSelectionEditMenuIfMoved(textView);
1539
1565
  if (textView.selectedRange.length > 0) {
1540
1566
  // Take ownership first so the previous owner's own "cleared" callback is ignored.
1541
1567
  ENRMPlatformTextView *previous = _selectionOwner;
@@ -1613,8 +1639,7 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1613
1639
  if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1614
1640
  return;
1615
1641
  }
1616
- [strongTextView becomeFirstResponder];
1617
- strongTextView.selectedRange = characterRange;
1642
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1618
1643
  });
1619
1644
  }
1620
1645
  return NO;
@@ -1631,6 +1656,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownCls(void)
1631
1656
 
1632
1657
  NSString *urlString = linkURLAtRange(textView, characterRange);
1633
1658
 
1659
+ if (urlString && _linkLongPressSelects) {
1660
+ __weak UITextView *weakTextView = textView;
1661
+ dispatch_async(dispatch_get_main_queue(), ^{
1662
+ UITextView *strongTextView = weakTextView;
1663
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
1664
+ return;
1665
+ }
1666
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
1667
+ });
1668
+ return NO;
1669
+ }
1670
+
1634
1671
  if (!urlString || _enableLinkPreview) {
1635
1672
  return YES;
1636
1673
  }
@@ -87,6 +87,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
87
87
  CGFloat _lastElementMarginBottom;
88
88
  BOOL _allowTrailingMargin;
89
89
  BOOL _enableLinkPreview;
90
+ BOOL _linkLongPressSelects;
90
91
  BOOL _enableTaskListItemToggle;
91
92
  BOOL _contextMenuHidden;
92
93
  BOOL _streamingAnimation;
@@ -118,6 +119,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
118
119
  NSArray<NSDictionary *> *_lastLinkLayout;
119
120
 
120
121
  ENRMSelectionChangeReporter *_selectionReporter;
122
+ #if !TARGET_OS_OSX
123
+ ENRMSelectionGestureTracker *_selectionGestures;
124
+ #endif
121
125
 
122
126
  NSLineBreakStrategy _lineBreakStrategy;
123
127
 
@@ -299,6 +303,19 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
299
303
  emitter->onSelectionChange(
300
304
  ENRMSelectionChangeEventFromPayload<EnrichedMarkdownTextEventEmitter::OnSelectionChange>(payload));
301
305
  }];
306
+ #if !TARGET_OS_OSX
307
+ // A handle drag starting or ending is reported right away, even when the
308
+ // selection itself is unchanged.
309
+ _selectionGestures = [[ENRMSelectionGestureTracker alloc] initWithChange:^{
310
+ EnrichedMarkdownText *strongSelf = weakSelf;
311
+ if (!strongSelf)
312
+ return;
313
+ [strongSelf->_selectionGestures trackTextView:strongSelf->_textView];
314
+ [strongSelf->_selectionReporter forgetLastReported];
315
+ [strongSelf->_selectionReporter flushNow];
316
+ }];
317
+ [_selectionGestures trackTextView:_textView];
318
+ #endif
302
319
  }
303
320
 
304
321
  /// The current selection as an `onSelectionChange` payload. Markdown and
@@ -318,6 +335,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
318
335
  payload[ENRMSelectionSourceKey] = @"selection";
319
336
  payload[ENRMSelectionMarkdownKey] = markdown.length > 0 ? markdown : text;
320
337
  payload[ENRMSelectionWholeKey] = @([selection[ENRMSelectionFullKey] boolValue] && _cachedMarkdown.length > 0);
338
+ payload[ENRMSelectionAdjustingKey] = @(_selectionGestures.adjusting);
321
339
  return payload;
322
340
  #else
323
341
  return ENRMInactiveSelectionPayload();
@@ -653,6 +671,7 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
653
671
  }
654
672
 
655
673
  _enableLinkPreview = newViewProps.enableLinkPreview;
674
+ _linkLongPressSelects = newViewProps.linkLongPressSelects;
656
675
  _enableTaskListItemToggle = newViewProps.enableTaskListItemToggle;
657
676
  _contextMenuHidden = newViewProps.contextMenuHidden;
658
677
 
@@ -784,6 +803,9 @@ typedef NS_OPTIONS(NSUInteger, ENRMDirtyFlags) {
784
803
  _emitLinkLayout = NO;
785
804
  _lastLinkLayout = nil;
786
805
  _selectionReporter.enabled = NO;
806
+ #if !TARGET_OS_OSX
807
+ [_selectionGestures reset];
808
+ #endif
787
809
  if (_textView != nil) {
788
810
  ENRMSetAttributedText(_textView, [[NSAttributedString alloc] initWithString:@""]);
789
811
  _textView.hidden = YES;
@@ -925,8 +947,7 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
925
947
  if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
926
948
  return;
927
949
  }
928
- [strongTextView becomeFirstResponder];
929
- strongTextView.selectedRange = characterRange;
950
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
930
951
  });
931
952
  }
932
953
  return NO;
@@ -943,6 +964,18 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
943
964
 
944
965
  NSString *urlString = linkURLAtRange(textView, characterRange);
945
966
 
967
+ if (urlString && _linkLongPressSelects) {
968
+ __weak UITextView *weakTextView = textView;
969
+ dispatch_async(dispatch_get_main_queue(), ^{
970
+ UITextView *strongTextView = weakTextView;
971
+ if (!strongTextView || NSMaxRange(characterRange) > strongTextView.attributedText.length) {
972
+ return;
973
+ }
974
+ ENRMSelectRangeAndPresentEditMenu(strongTextView, characterRange);
975
+ });
976
+ return NO;
977
+ }
978
+
946
979
  if (!urlString || _enableLinkPreview) {
947
980
  return YES;
948
981
  }
@@ -955,6 +988,8 @@ Class<RCTComponentViewProtocol> EnrichedMarkdownTextCls(void)
955
988
 
956
989
  - (void)textViewDidChangeSelection:(UITextView *)textView
957
990
  {
991
+ [_selectionGestures trackTextView:textView];
992
+ ENRMDismissSelectionEditMenuIfMoved(textView);
958
993
  [_selectionReporter selectionChanged:textView.selectedRange.length > 0];
959
994
  }
960
995
 
@@ -87,6 +87,7 @@ payload.setProperty(runtime, "whole", event.whole);
87
87
  endRect.setProperty(runtime, "height", event.endRect.height);
88
88
  payload.setProperty(runtime, "endRect", endRect);
89
89
  }
90
+ payload.setProperty(runtime, "adjusting", event.adjusting);
90
91
  return payload;
91
92
  });
92
93
  }
@@ -487,6 +488,7 @@ payload.setProperty(runtime, "whole", event.whole);
487
488
  endRect.setProperty(runtime, "height", event.endRect.height);
488
489
  payload.setProperty(runtime, "endRect", endRect);
489
490
  }
491
+ payload.setProperty(runtime, "adjusting", event.adjusting);
490
492
  return payload;
491
493
  });
492
494
  }
@@ -69,6 +69,7 @@ class EnrichedMarkdownEventEmitter : public ViewEventEmitter {
69
69
  OnSelectionChangeRect rect;
70
70
  OnSelectionChangeStartRect startRect;
71
71
  OnSelectionChangeEndRect endRect;
72
+ bool adjusting;
72
73
  };
73
74
 
74
75
  struct OnTaskListItemPress {
@@ -395,6 +396,7 @@ class EnrichedMarkdownTextEventEmitter : public ViewEventEmitter {
395
396
  OnSelectionChangeRect rect;
396
397
  OnSelectionChangeStartRect startRect;
397
398
  OnSelectionChangeEndRect endRect;
399
+ bool adjusting;
398
400
  };
399
401
 
400
402
  struct OnTaskListItemPress {
@@ -25,6 +25,7 @@ EnrichedMarkdownProps::EnrichedMarkdownProps(
25
25
  emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
26
26
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
27
27
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
28
+ linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
28
29
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
29
30
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
30
31
  selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
@@ -84,6 +85,10 @@ folly::dynamic EnrichedMarkdownProps::getDiffProps(
84
85
  result["enableLinkPreview"] = enableLinkPreview;
85
86
  }
86
87
 
88
+ if (linkLongPressSelects != oldProps->linkLongPressSelects) {
89
+ result["linkLongPressSelects"] = linkLongPressSelects;
90
+ }
91
+
87
92
  if (selectable != oldProps->selectable) {
88
93
  result["selectable"] = selectable;
89
94
  }
@@ -318,6 +323,7 @@ EnrichedMarkdownTextProps::EnrichedMarkdownTextProps(
318
323
  emitSelectionChange(convertRawProp(context, rawProps, "emitSelectionChange", sourceProps.emitSelectionChange, {false})),
319
324
  enableTaskListItemToggle(convertRawProp(context, rawProps, "enableTaskListItemToggle", sourceProps.enableTaskListItemToggle, {true})),
320
325
  enableLinkPreview(convertRawProp(context, rawProps, "enableLinkPreview", sourceProps.enableLinkPreview, {true})),
326
+ linkLongPressSelects(convertRawProp(context, rawProps, "linkLongPressSelects", sourceProps.linkLongPressSelects, {false})),
321
327
  selectable(convertRawProp(context, rawProps, "selectable", sourceProps.selectable, {false})),
322
328
  selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
323
329
  selectionHandleColor(convertRawProp(context, rawProps, "selectionHandleColor", sourceProps.selectionHandleColor, {})),
@@ -377,6 +383,10 @@ folly::dynamic EnrichedMarkdownTextProps::getDiffProps(
377
383
  result["enableLinkPreview"] = enableLinkPreview;
378
384
  }
379
385
 
386
+ if (linkLongPressSelects != oldProps->linkLongPressSelects) {
387
+ result["linkLongPressSelects"] = linkLongPressSelects;
388
+ }
389
+
380
390
  if (selectable != oldProps->selectable) {
381
391
  result["selectable"] = selectable;
382
392
  }
@@ -2890,6 +2890,7 @@ class EnrichedMarkdownProps final : public ViewProps {
2890
2890
  bool emitSelectionChange{false};
2891
2891
  bool enableTaskListItemToggle{true};
2892
2892
  bool enableLinkPreview{true};
2893
+ bool linkLongPressSelects{false};
2893
2894
  bool selectable{false};
2894
2895
  SharedColor selectionColor{};
2895
2896
  SharedColor selectionHandleColor{};
@@ -6935,6 +6936,7 @@ class EnrichedMarkdownTextProps final : public ViewProps {
6935
6936
  bool emitSelectionChange{false};
6936
6937
  bool enableTaskListItemToggle{true};
6937
6938
  bool enableLinkPreview{true};
6939
+ bool linkLongPressSelects{false};
6938
6940
  bool selectable{false};
6939
6941
  SharedColor selectionColor{};
6940
6942
  SharedColor selectionHandleColor{};
@@ -134,11 +134,32 @@ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContai
134
134
 
135
135
  CGRect textRect = [layoutManager boundingRectForGlyphRange:intersect
136
136
  inTextContainer:textContainer];
137
- CGFloat left = CGRectGetMinX(textRect);
138
- CGFloat right = CGRectGetMaxX(textRect);
137
+ NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
138
+ actualGlyphRange:NULL];
139
+ NSUInteger lineEnd = NSMaxRange(lineRange);
140
+ CGFloat left =
141
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:intersect.location].x;
142
+
143
+ NSUInteger visibleCharEnd = NSMaxRange(fragmentChars);
144
+ NSString *chars = storage.string;
145
+ NSCharacterSet *breakable = [NSCharacterSet whitespaceAndNewlineCharacterSet];
146
+ while (visibleCharEnd > fragmentChars.location &&
147
+ [chars characterAtIndex:visibleCharEnd - 1] != kSpacerChar &&
148
+ [breakable characterIsMember:[chars characterAtIndex:visibleCharEnd - 1]]) {
149
+ visibleCharEnd--;
150
+ }
151
+ NSUInteger visibleGlyphEnd = NSMaxRange(intersect);
152
+ if (visibleCharEnd < NSMaxRange(fragmentChars)) {
153
+ visibleGlyphEnd = [layoutManager glyphIndexForCharacterAtIndex:visibleCharEnd];
154
+ }
139
155
 
140
- // The trailing spacer's kern is its advance; don't rely on the
141
- // glyph bounds including it.
156
+ CGFloat right;
157
+ if (visibleGlyphEnd < lineEnd) {
158
+ right =
159
+ lineRect.origin.x + [layoutManager locationForGlyphAtIndex:visibleGlyphEnd].x;
160
+ } else {
161
+ right = CGRectGetMaxX(usedRect);
162
+ }
142
163
  if (isLast && paddingHorizontal > 0) {
143
164
  NSUInteger lastGlyph = NSMaxRange(intersect) - 1;
144
165
  CGFloat spacerX =
@@ -146,8 +167,6 @@ void ENRMEnumerateLinkChipFragments(NSLayoutManager *layoutManager, NSTextContai
146
167
  right = MAX(right, spacerX + paddingHorizontal);
147
168
  }
148
169
 
149
- NSRange fragmentChars = [layoutManager characterRangeForGlyphRange:intersect
150
- actualGlyphRange:NULL];
151
170
  UIFont *font = chipFontInRange(storage, fragmentChars);
152
171
  CGFloat top = CGRectGetMinY(textRect);
153
172
  CGFloat bottom = CGRectGetMaxY(textRect);
@@ -14,6 +14,7 @@ extern NSString *const ENRMSelectionWholeKey;
14
14
  extern NSString *const ENRMSelectionRectKey;
15
15
  extern NSString *const ENRMSelectionStartRectKey;
16
16
  extern NSString *const ENRMSelectionEndRectKey;
17
+ extern NSString *const ENRMSelectionAdjustingKey;
17
18
  /// Only in `ENRMTextSelectionInTextView` results: whether the range spans the
18
19
  /// whole text (same threshold `markdownForRange` uses for the cached markdown).
19
20
  extern NSString *const ENRMSelectionFullKey;
@@ -63,6 +64,28 @@ NSDictionary *_Nullable ENRMTextSelectionInTextView(UITextView *textView, UIView
63
64
 
64
65
  @end
65
66
 
67
+ #if !TARGET_OS_OSX
68
+ /// Tells when the user is dragging a selection handle (or holding the loupe).
69
+ /// `UITextView` doesn't call `textViewDidChangeSelection:` during the drag, so
70
+ /// this watches the text view's own selection gestures: every recognizer of
71
+ /// the view and its subviews except scroll, pinch and taps. UIKit adds the
72
+ /// handle recognizers lazily, so call `trackTextView:` again on each selection
73
+ /// change. `change` runs whenever `adjusting` flips.
74
+ @interface ENRMSelectionGestureTracker : NSObject
75
+
76
+ - (instancetype)initWithChange:(void (^)(void))change NS_DESIGNATED_INITIALIZER;
77
+ - (instancetype)init NS_UNAVAILABLE;
78
+
79
+ /// A tracked gesture is in `Began` / `Changed`.
80
+ @property (nonatomic, readonly) BOOL adjusting;
81
+
82
+ - (void)trackTextView:(UITextView *)textView;
83
+ /// Clears `adjusting` without calling `change` (view recycling).
84
+ - (void)reset;
85
+
86
+ @end
87
+ #endif
88
+
66
89
  #ifdef __cplusplus
67
90
  /// Converts a payload into a codegen `OnSelectionChange` struct. Templated
68
91
  /// because each component has its own (structurally identical) event struct.
@@ -74,6 +97,7 @@ template <typename Event> inline Event ENRMSelectionChangeEventFromPayload(NSDic
74
97
  event.text = std::string([payload[ENRMSelectionTextKey] UTF8String] ?: "");
75
98
  event.markdown = std::string([payload[ENRMSelectionMarkdownKey] UTF8String] ?: "");
76
99
  event.whole = [payload[ENRMSelectionWholeKey] boolValue];
100
+ event.adjusting = [payload[ENRMSelectionAdjustingKey] boolValue];
77
101
 
78
102
  auto assign = [](auto &target, NSValue *value) {
79
103
  #if TARGET_OS_OSX